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, }; };