From b49d215145a5c3f39eb8e66cf3ace425f805e88b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20Vasconcellos?= Date: Sat, 22 Apr 2023 06:43:08 +0000 Subject: [PATCH] [ENG-489] FileThumb has incorrect dimensions for some thumbnails (#731) * WIP Fix thumb incorrect size * Fix sidebar thumb incorrect size - Fix ext pill incorrect positioning on first load * Improve `Thumb` overall style behavior - Fix incorrect position of the video extension pill - Simplify `MediaView`'s `Thumb` usage by internalizing the `mediaAspectSquare` style - Change `QuickPreview` to use `cover` and size 0 for `Thumb` * Remove unused import * Allow modifing the internal img className * lint * Apply PR feedback - Remove superfluous true value in component property - Rename ObjectKinds to ObjectKindKey * Fix video ext pill not being properly positioned in MediaView(non square thumbnail) in prod build * Fix video ext pill incorrect position (now for sure) - Replace img size logic with ResizeObserver - Add error handling when thumbnails fails to load * Shorten variable names * Fix IndexerRuleEditor isKeyOf --------- Co-authored-by: Jamie Pine <32987599+jamiepine@users.noreply.github.com> --- .../app/$libraryId/Explorer/File/Thumb.tsx | 239 ++++++++++++------ .../app/$libraryId/Explorer/GridView.tsx | 2 +- .../$libraryId/Explorer/Inspector/index.tsx | 2 +- .../app/$libraryId/Explorer/MediaView.tsx | 16 +- .../app/$libraryId/Explorer/QuickPreview.tsx | 2 +- interface/app/$libraryId/Explorer/util.ts | 4 +- .../library/locations/IndexerRuleEditor.tsx | 2 +- packages/client/src/utils/index.ts | 2 +- packages/client/src/utils/objectKind.ts | 50 ++-- 9 files changed, 192 insertions(+), 127 deletions(-) diff --git a/interface/app/$libraryId/Explorer/File/Thumb.tsx b/interface/app/$libraryId/Explorer/File/Thumb.tsx index df16a2dd7..928eec3cb 100644 --- a/interface/app/$libraryId/Explorer/File/Thumb.tsx +++ b/interface/app/$libraryId/Explorer/File/Thumb.tsx @@ -1,111 +1,184 @@ import * as icons from '@sd/assets/icons'; import clsx from 'clsx'; -import { CSSProperties, useEffect, useState } from 'react'; -import { ExplorerItem, useLibraryContext } from '@sd/client'; +import { memo, useLayoutEffect, useRef, useState } from 'react'; +import { ExplorerItem, isKeyOf, useLibraryContext } from '@sd/client'; import { useExplorerStore } from '~/hooks/useExplorerStore'; import { useIsDark, usePlatform } from '~/util/Platform'; import { getExplorerItemData } from '../util'; import classes from './Thumb.module.scss'; -interface Props { - data: ExplorerItem; - size: number; - loadOriginal?: boolean; - className?: string; - forceShowExtension?: boolean; - extensionClassName?: string; +export const getIcon = ( + isDir: boolean, + isDark: boolean, + kind: string, + extension?: string | null +) => { + if (isDir) return icons[isDark ? 'Folder' : 'Folder_Light']; + + let document: Extract = 'Document'; + if (extension) extension = `${kind}_${extension.toLowerCase()}`; + if (!isDark) { + kind = kind + '_Light'; + document = 'Document_Light'; + if (extension) extension = extension + '_Light'; + } + + return icons[ + extension && isKeyOf(icons, extension) ? extension : isKeyOf(icons, kind) ? kind : document + ]; +}; + +interface VideoThumbSize { + width: number; + height: number; } -export default function Thumb(props: Props) { - const { cas_id, isDir, kind, hasThumbnail, extension } = getExplorerItemData(props.data); - const store = useExplorerStore(); - const platform = usePlatform(); - const { library } = useLibraryContext(); +export interface ThumbProps { + data: ExplorerItem; + size: null | number; + cover?: boolean; + className?: string; + loadOriginal?: boolean; +} + +function Thumb({ size, cover, ...props }: ThumbProps) { const isDark = useIsDark(); + const platform = usePlatform(); + const thumbImg = useRef(null); + const [thumbSize, setThumbSize] = useState(null); + const { library } = useLibraryContext(); + const [thumbLoaded, setThumbLoaded] = useState(false); + const { locationId } = useExplorerStore(); + const { cas_id, isDir, kind, hasThumbnail, extension } = getExplorerItemData(props.data); - const [fullPreviewUrl, setFullPreviewUrl] = useState(null); + // Allows disabling thumbnails when they fail to load + const [useThumb, setUseThumb] = useState(hasThumbnail); - useEffect(() => { - if (props.loadOriginal && hasThumbnail) { - const url = platform.getFileUrl(library.uuid, store.locationId!, props.data.item.id); - if (url) setFullPreviewUrl(url); - } - }, [ - props.data.item.id, - hasThumbnail, - library.uuid, - props.loadOriginal, - platform, - store.locationId - ]); + useLayoutEffect(() => { + const img = thumbImg.current; + if (cover || kind !== 'Video' || !img || !thumbLoaded) return; - const videoBarsHeight = Math.floor(props.size / 10); - const videoHeight = Math.floor((props.size * 9) / 16) + videoBarsHeight * 2; + const resizeObserver = new ResizeObserver(() => { + const { width, height } = img; + setThumbSize(width && height ? { width, height } : null); + }); - const imgStyle: CSSProperties = - kind === 'Video' - ? { - borderTopWidth: videoBarsHeight, - borderBottomWidth: videoBarsHeight, - width: props.size, - height: videoHeight - } - : {}; + resizeObserver.observe(img); + return () => resizeObserver.disconnect(); + }, [kind, cover, thumbImg, thumbLoaded]); - let icon = icons['Document']; - if (isDir) { - icon = icons['Folder']; - } else if ( - kind && - extension && - icons[`${kind}_${extension.toLowerCase()}` as keyof typeof icons] - ) { - icon = icons[`${kind}_${extension.toLowerCase()}` as keyof typeof icons]; - } else if (kind !== 'Unknown' && kind && icons[kind as keyof typeof icons]) { - icon = icons[kind as keyof typeof icons]; - } - - if (!hasThumbnail || !cas_id) { - if (!isDark) { - icon = icon?.substring(0, icon.length - 4) + '_Light' + '.png'; - } - return ; + // Only Videos and Images can show the original file + const loadOriginal = (kind === 'Video' || kind === 'Image') && props.loadOriginal; + const src = useThumb + ? loadOriginal && locationId + ? platform.getFileUrl(library.uuid, locationId, props.data.item.id) + : cas_id && platform.getThumbnailUrlById(cas_id) + : null; + + let style = {}; + if (size && kind === 'Video') { + const videoBarsHeight = Math.floor(size / 10); + style = { + borderTopWidth: videoBarsHeight, + borderBottomWidth: videoBarsHeight + }; } + const childClassName = 'max-h-full max-w-full object-contain'; return (
- 60 && 'border-2 border-app-line', - kind === 'Video' && 'rounded border-x-0 !border-black', - props.className - )} - src={fullPreviewUrl || platform.getThumbnailUrlById(cas_id)} - /> - {extension && - kind === 'Video' && - hasThumbnail && - (props.size > 80 || props.forceShowExtension) && ( -
{ + const video = e.target as HTMLVideoElement; + // Why not use the element's attribute? Because React... + // https://github.com/facebook/react/issues/10389 + video.loop = true; + video.muted = true; + }} + style={style} + autoPlay className={clsx( - 'absolute bottom-[13%] right-[5%] rounded bg-black/60 py-0.5 px-1 text-[9px] font-semibold uppercase opacity-70', - props.extensionClassName + childClassName, + size && 'rounded border-x-0 border-black', + props.className )} - > - {extension} -
- )} + playsInline + /> + ) : ( + <> + { + setUseThumb(true); + setThumbLoaded(true); + }} + onError={() => { + setUseThumb(false); + setThumbSize(null); + setThumbLoaded(false); + }} + decoding="async" + className={clsx( + cover + ? 'min-h-full min-w-full object-cover object-center' + : childClassName, + 'shadow shadow-black/30', + kind === 'Video' ? 'rounded' : 'rounded-sm', + size && + (kind === 'Video' + ? 'border-x-0 border-black' + : size > 60 && 'border-2 border-app-line'), + props.className + )} + /> + {kind === 'Video' && (!size || size > 80) && ( +
+ {extension} +
+ )} + + ) + ) : ( + + )}
); } + +export default memo(Thumb); diff --git a/interface/app/$libraryId/Explorer/GridView.tsx b/interface/app/$libraryId/Explorer/GridView.tsx index a14c46021..b9c8b34aa 100644 --- a/interface/app/$libraryId/Explorer/GridView.tsx +++ b/interface/app/$libraryId/Explorer/GridView.tsx @@ -33,7 +33,7 @@ const GridViewItem = memo(({ data, selected, index, ...props }: GridViewItemProp height: explorerStore.gridItemSize }} className={clsx( - 'mb-1 rounded-lg border-2 border-transparent text-center active:translate-y-[1px]', + 'mb-1 flex items-center justify-center justify-items-center rounded-lg border-2 border-transparent text-center active:translate-y-[1px]', { 'bg-app-selected/20': selected } diff --git a/interface/app/$libraryId/Explorer/Inspector/index.tsx b/interface/app/$libraryId/Explorer/Inspector/index.tsx index 00effab21..9b940b909 100644 --- a/interface/app/$libraryId/Explorer/Inspector/index.tsx +++ b/interface/app/$libraryId/Explorer/Inspector/index.tsx @@ -79,7 +79,7 @@ export const Inspector = ({ data, context, ...elementProps }: Props) => { {explorerStore.layoutMode !== 'media' && (
diff --git a/interface/app/$libraryId/Explorer/MediaView.tsx b/interface/app/$libraryId/Explorer/MediaView.tsx index 6aa5858ea..803890096 100644 --- a/interface/app/$libraryId/Explorer/MediaView.tsx +++ b/interface/app/$libraryId/Explorer/MediaView.tsx @@ -35,20 +35,10 @@ const MediaViewItem = memo(({ data, index }: MediaViewItemProps) => { )} >