import React, { HTMLProps, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Nullable, Undefinable } from 'tsdef';
import { ChonkyActions } from '../../action-definitions/index';
import { selectThumbnailGenerator } from '../../redux/selectors';
import { thunkRequestFileAction } from '../../redux/thunks/dispatchers.thunks';
import { DndEntryState } from '../../types/file-list.types';
import { FileData } from '../../types/file.types';
import { ChonkyIconName } from '../../types/icons.types';
import { ChonkyDispatch } from '../../types/redux.types';
import { FileHelper } from '../../util/file-helper';
import { ChonkyIconContext, ColorsDark, ColorsLight, useIconData } from '../../util/icon-helper';
import { Logger } from '../../util/logger';
import { TextPlaceholder } from '../external/TextPlaceholder';
import { KeyboardClickEvent, MouseClickEvent } from '../internal/ClickableWrapper';
import { FileEntryState } from './GridEntryPreview';
export const useFileEntryHtmlProps = (file: Nullable): HTMLProps => {
return useMemo(() => {
const dataProps: { [prop: string]: Undefinable } = {
'data-test-id': 'file-entry',
'data-chonky-file-id': file ? file.id : undefined,
};
return {
role: 'listitem',
...dataProps,
};
}, [file]);
};
export const useFileEntryState = (file: Nullable, selected: boolean, focused: boolean) => {
const iconData = useIconData(file);
const { thumbnailUrl, thumbnailLoading } = useThumbnailUrl(file);
return useMemo(() => {
const fileColor = thumbnailUrl ? ColorsDark[iconData.colorCode] : ColorsLight[iconData.colorCode];
const iconSpin = thumbnailLoading || !file;
const icon = thumbnailLoading ? ChonkyIconName.loading : iconData.icon;
return {
childrenCount: FileHelper.getChildrenCount(file),
icon: file && file.icon !== undefined ? file.icon : icon,
iconSpin: iconSpin,
thumbnailUrl: thumbnailUrl,
color: file && file.color !== undefined ? file.color : fileColor,
selected: selected,
focused: !!focused,
};
}, [file, focused, iconData, selected, thumbnailLoading, thumbnailUrl]);
};
export const useDndIcon = (dndState: DndEntryState) => {
let dndIconName: Nullable = null;
if (dndState.dndIsOver) {
const showDropIcon = dndState.dndCanDrop;
dndIconName = showDropIcon ? ChonkyIconName.dndCanDrop : ChonkyIconName.dndCannotDrop;
} else if (dndState.dndIsDragging) {
dndIconName = ChonkyIconName.dndDragging;
}
return dndIconName;
};
export const useModifierIconComponents = (file: Nullable) => {
const modifierIcons: ChonkyIconName[] = useMemo(() => {
const modifierIcons: ChonkyIconName[] = [];
if (FileHelper.isHidden(file)) modifierIcons.push(ChonkyIconName.hidden);
if (FileHelper.isSymlink(file)) modifierIcons.push(ChonkyIconName.archive);
if (FileHelper.isEncrypted(file)) modifierIcons.push(ChonkyIconName.lock);
return modifierIcons;
}, [file]);
const ChonkyIcon = useContext(ChonkyIconContext);
const modifierIconComponents = useMemo(
() => modifierIcons.map((icon, index) => ),
// For some reason ESLint marks `ChonkyIcon` as an unnecessary dependency,
// but we expect it can change at runtime so we disable the check.
// eslint-disable-next-line react-hooks/exhaustive-deps
[ChonkyIcon, modifierIcons],
);
return modifierIconComponents;
};
const _extname = (fileName: string) => {
const parts = fileName.split('.');
if (parts.length) {
return `.${parts[parts.length - 1]}`;
}
return '';
};
export const useFileNameComponent = (file: Nullable, shortenFileName: boolean) => {
return useMemo(() => {
if (!file) return ;
let name;
let ext = null;
const isDir = FileHelper.isDirectory(file as FileData);
name = file.name;
ext = file.ext;
let addEllipsis = false;
if(name.length > 23 && shortenFileName) addEllipsis = true;
if(addEllipsis) name = name.substring(0, 23)+"...";
if(addEllipsis) ext = "";
return (
<>
{name ? name: null}
{!isDir ? ext && {ext}:null}
>
);
}, [file]);
};
export const useLabelComponent = (file: Nullable) => {
return useMemo(() => {
if (!file) return ;
let labels = [];
labels = file.labels;
if(!labels) return null;
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const labelStyles: React.CSSProperties = {
display: 'inline-flex',
pointerEvents: 'none',
backgroundColor: prefersDarkMode ? 'rgba(51,51,51,.9)' : 'rgba(241, 241, 241, .9)',
color: prefersDarkMode ? '#f1f1f1': '#333',
padding: '0.15rem 0.4rem',
borderRadius: '9999px',
fontSize: '0.7rem',
};
return (
{labels.map((label:string)=>{label})}
);
}, [file]);
};
export const useThumbnailUrl = (file: Nullable) => {
const thumbnailGenerator = useSelector(selectThumbnailGenerator);
const [thumbnailUrl, setThumbnailUrl] = useState>(null);
const [thumbnailLoading, setThumbnailLoading] = useState(false);
const loadingAttempts = useRef(0);
useEffect(() => {
let loadingCancelled = false;
if (file) {
if (thumbnailGenerator) {
if (loadingAttempts.current === 0) {
setThumbnailLoading(true);
}
loadingAttempts.current++;
Promise.resolve()
.then(() => thumbnailGenerator(file))
.then((thumbnailUrl: any) => {
if (loadingCancelled) return;
setThumbnailLoading(false);
if (thumbnailUrl && typeof thumbnailUrl === 'string') {
setThumbnailUrl(thumbnailUrl);
}
})
.catch((error) => {
if (!loadingCancelled) setThumbnailLoading(false);
Logger.error(`User-defined "thumbnailGenerator" handler threw an error: ${error.message}`);
});
} else if (file.thumbnailUrl) {
setThumbnailUrl(file.thumbnailUrl);
}
}
return () => {
loadingCancelled = true;
};
}, [file, setThumbnailUrl, setThumbnailLoading, thumbnailGenerator]);
return { thumbnailUrl, thumbnailLoading };
};
export const useFileClickHandlers = (file: Nullable, displayIndex: number) => {
const dispatch: ChonkyDispatch = useDispatch();
// Prepare base handlers
const onMouseClick = useCallback(
(event: MouseClickEvent, clickType: 'single' | 'double') => {
if (!file) return;
dispatch(
thunkRequestFileAction(ChonkyActions.MouseClickFile, {
clickType,
file,
fileDisplayIndex: displayIndex,
altKey: event.altKey,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
}),
);
},
[dispatch, file, displayIndex],
);
const onKeyboardClick = useCallback(
(event: KeyboardClickEvent) => {
if (!file) return;
dispatch(
thunkRequestFileAction(ChonkyActions.KeyboardClickFile, {
file,
fileDisplayIndex: displayIndex,
enterKey: event.enterKey,
spaceKey: event.spaceKey,
altKey: event.altKey,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
}),
);
},
[dispatch, file, displayIndex],
);
// Prepare single/double click handlers
const onSingleClick = useCallback((event: MouseClickEvent) => onMouseClick(event, 'single'), [onMouseClick]);
const onDoubleClick = useCallback((event: MouseClickEvent) => onMouseClick(event, 'double'), [onMouseClick]);
return {
onSingleClick,
onDoubleClick,
onKeyboardClick,
};
};