diff --git a/public/locales/de-DE/application.json b/public/locales/de-DE/application.json index fd5587c..1e96ba2 100644 --- a/public/locales/de-DE/application.json +++ b/public/locales/de-DE/application.json @@ -318,6 +318,9 @@ "moreActions": "Weitere Aktionen", "refresh": "Aktualisieren", "createArchive": "Archiv erstellen", + "resetThumbnail": "Beschädigte Miniaturansicht zurücksetzen", + "resetThumbnailRequested": "Zurücksetzen der Miniaturansicht angefordert.", + "noFileCanResetThumbnail": "Keine Dateien zum Zurücksetzen der Miniaturansicht verfügbar.", "newFolder": "Neuen Ordner erstellen", "newFile": "Neue Datei erstellen", "showFullPath": "Pfad anzeigen", diff --git a/public/locales/en-US/application.json b/public/locales/en-US/application.json index 7c7dc71..7e0ad98 100644 --- a/public/locales/en-US/application.json +++ b/public/locales/en-US/application.json @@ -284,6 +284,9 @@ "moreActions": "More actions", "refresh": "Refresh", "createArchive": "Create archive file", + "resetThumbnail": "Reset broken thumbnail", + "resetThumbnailRequested": "Thumbnail reset requested.", + "noFileCanResetThumbnail": "No files available for thumbnail reset.", "newFolder": "New folder", "newFile": "New file", "showFullPath": "Show full path", diff --git a/public/locales/es-ES/application.json b/public/locales/es-ES/application.json index d00eb6b..44dadf3 100644 --- a/public/locales/es-ES/application.json +++ b/public/locales/es-ES/application.json @@ -318,6 +318,9 @@ "moreActions": "Más acciones", "refresh": "Actualizar", "createArchive": "Crear archivo comprimido", + "resetThumbnail": "Restablecer miniatura dañada", + "resetThumbnailRequested": "Se solicitó restablecer la miniatura.", + "noFileCanResetThumbnail": "No hay archivos disponibles para restablecer la miniatura.", "newFolder": "Nueva carpeta", "newFile": "Nuevo archivo", "showFullPath": "Mostrar ruta completa", diff --git a/public/locales/fr-FR/application.json b/public/locales/fr-FR/application.json index 14ad43e..dc08870 100644 --- a/public/locales/fr-FR/application.json +++ b/public/locales/fr-FR/application.json @@ -19,6 +19,9 @@ "password": "Mot de passe", "captcha": "CAPTCHA", "captchaError": "Impossible de charger le CAPTCHA : {{message}}", + "resetThumbnail": "Réinitialiser la miniature cassée", + "resetThumbnailRequested": "Réinitialisation de la miniature demandée.", + "noFileCanResetThumbnail": "Aucun fichier pouvant réinitialiser la miniature.", "signIn": "Se connecter", "signUp": "S'inscrire", "signUpAccount": "S'inscrire", diff --git a/public/locales/it-IT/application.json b/public/locales/it-IT/application.json index e1213dc..472d8d7 100644 --- a/public/locales/it-IT/application.json +++ b/public/locales/it-IT/application.json @@ -54,6 +54,9 @@ "clickToRefresh": "Clicca per aggiornare il CAPTCHA", "switchLanguage": "Cambia lingua" }, + "resetThumbnail": "Reimposta miniatura danneggiata", + "resetThumbnailRequested": "Reimpostazione della miniatura richiesta.", + "noFileCanResetThumbnail": "Nessun file può reimpostare la miniatura.", "navbar": { "notBefore": "Non prima di", "notAfter": "Non dopo", diff --git a/public/locales/ja-JP/application.json b/public/locales/ja-JP/application.json index 34f51e1..ae5e7a1 100644 --- a/public/locales/ja-JP/application.json +++ b/public/locales/ja-JP/application.json @@ -54,6 +54,9 @@ "clickToRefresh": "CAPTCHAを再読み込み", "switchLanguage": "言語を切り替え" }, + "resetThumbnail": "壊れたサムネイルをリセット", + "resetThumbnailRequested": "サムネイルのリセットをリクエストしました。", + "noFileCanResetThumbnail": "サムネイルをリセットできるファイルがありません。", "navbar": { "notBefore": "~より前", "notAfter": "~より後", diff --git a/public/locales/ko-KR/application.json b/public/locales/ko-KR/application.json index 9c0986c..72fe7e9 100644 --- a/public/locales/ko-KR/application.json +++ b/public/locales/ko-KR/application.json @@ -88,6 +88,9 @@ "photos": "사진", "music": "음악", "documents": "문서", + "resetThumbnail": "깨진 썸네일 재설정", + "resetThumbnailRequested": "썸네일 재설정이 요청되었습니다.", + "noFileCanResetThumbnail": "썸네일을 재설정할 수 있는 파일이 없습니다.", "addATag": "태그 추가...", "addTagDialog": { "selectFolder": "폴더 선택", diff --git a/public/locales/pt-BR/application.json b/public/locales/pt-BR/application.json index a441719..bc912c9 100644 --- a/public/locales/pt-BR/application.json +++ b/public/locales/pt-BR/application.json @@ -75,6 +75,9 @@ "recentlyViewed": "Visualizados recentemente", "searchFiles": "Buscar arquivos...", "showMore": "Mais", + "resetThumbnail": "Redefinir miniatura quebrada", + "resetThumbnailRequested": "Redefinição de miniatura solicitada.", + "noFileCanResetThumbnail": "Nenhum arquivo pode redefinir a miniatura.", "myFiles": "Meus arquivos", "hisFiles": "Arquivos dele/dela", "trash": "Lixeira", diff --git a/public/locales/ru-RU/application.json b/public/locales/ru-RU/application.json index 126f8bb..d323821 100644 --- a/public/locales/ru-RU/application.json +++ b/public/locales/ru-RU/application.json @@ -68,6 +68,9 @@ "notNameOpOr": "Должны содержаться все ключевые слова", "caseFolding": "Игнорировать регистр", "keywords": "Ключевые слова", + "resetThumbnail": "Сбросить повреждённую миниатюру", + "resetThumbnailRequested": "Запрошен сброс миниатюры.", + "noFileCanResetThumbnail": "Нет файлов для сброса миниатюры.", "fileNameKeywordsHelp": "Нажмите Enter для добавления ключевого слова", "advancedSearch": "Расширенный поиск", "searchFilesTitle": "Поиск файлов", diff --git a/public/locales/zh-CN/application.json b/public/locales/zh-CN/application.json index 462cda4..220ac3d 100644 --- a/public/locales/zh-CN/application.json +++ b/public/locales/zh-CN/application.json @@ -284,6 +284,9 @@ "moreActions": "更多操作", "refresh": "刷新", "createArchive": "创建压缩文件", + "resetThumbnail": "重置失败的缩略图", + "resetThumbnailRequested": "已请求重置缩略图。", + "noFileCanResetThumbnail": "没有可重置缩略图的文件。", "newFolder": "创建文件夹", "newFile": "创建文件", "showFullPath": "显示路径", diff --git a/public/locales/zh-TW/application.json b/public/locales/zh-TW/application.json index 3a10f15..7208852 100644 --- a/public/locales/zh-TW/application.json +++ b/public/locales/zh-TW/application.json @@ -58,6 +58,9 @@ "notBefore": "不早於", "notAfter": "不晚於", "minimum": "最小", + "resetThumbnail": "重設失敗的縮圖", + "resetThumbnailRequested": "已請求重設縮圖。", + "noFileCanResetThumbnail": "沒有可重設縮圖的檔案。", "maximum": "最大", "fileSize": "檔案大小", "searchBase": "搜尋路徑", diff --git a/src/api/api.ts b/src/api/api.ts index efaff02..df87986 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -3,6 +3,8 @@ import i18n from "../i18n.ts"; import { AdminListGroupResponse, AdminListService, + ListShareResponse as AdminListShareResponse, + StoragePolicy as AdminStoragePolicy, BatchIDService, CleanupTaskService, CreateStoragePolicyCorsService, @@ -17,7 +19,6 @@ import { ListEntityResponse, ListFileResponse, ListNodeResponse, - ListShareResponse as AdminListShareResponse, ListStoragePolicyResponse, ListTaskResponse, ListUserResponse, @@ -26,7 +27,6 @@ import { QueueMetric, SetSettingService, Share as ShareEnt, - StoragePolicy as AdminStoragePolicy, Task, TestNodeDownloaderService, TestNodeService, @@ -78,7 +78,6 @@ import { Capacity, FinishPasskeyLoginService, FinishPasskeyRegistrationService, - Group, LoginResponse, Passkey, PasskeyCredentialOption, @@ -304,7 +303,7 @@ export function getUserInfo(uid: string): ThunkResponse { }, { ...defaultOpts, - bypassSnackbar: (_e) => skipSnackbar, + bypassSnackbar: (_e) => true, }, ), ); diff --git a/src/api/site.ts b/src/api/site.ts index d3ecb89..d9638f8 100644 --- a/src/api/site.ts +++ b/src/api/site.ts @@ -44,6 +44,7 @@ export interface SiteConfig { custom_props?: CustomProps[]; custom_nav_items?: CustomNavItem[]; custom_html?: CustomHTML; + thumb_exts?: string[]; } export interface CaptchaResponse { diff --git a/src/component/FileManager/ContextMenu/MoreMenuItems.tsx b/src/component/FileManager/ContextMenu/MoreMenuItems.tsx index 448ed8d..75b4909 100644 --- a/src/component/FileManager/ContextMenu/MoreMenuItems.tsx +++ b/src/component/FileManager/ContextMenu/MoreMenuItems.tsx @@ -9,9 +9,11 @@ import { setVersionControlDialog, } from "../../../redux/globalStateSlice.ts"; import { useAppDispatch } from "../../../redux/hooks.ts"; +import { resetThumbnails } from "../../../redux/thunks/file.ts"; import Archive from "../../Icons/Archive.tsx"; import BranchForkLink from "../../Icons/BranchForkLink.tsx"; import HistoryOutlined from "../../Icons/HistoryOutlined.tsx"; +import ImageArrowCounterclockwise from "../../Icons/ImageAarowCounterclockwise.tsx"; import LinkSetting from "../../Icons/LinkSetting.tsx"; import { CascadingContext, CascadingMenuItem } from "./CascadingMenu.tsx"; import { SubMenuItemsProps } from "./OrganizeMenuItems.tsx"; @@ -105,6 +107,14 @@ const MoreMenuItems = ({ displayOpt, targets }: SubMenuItemsProps) => { {t("application:fileManager.createArchive")} )} + {displayOpt.showResetThumb && ( + dispatch(resetThumbnails(targets)))}> + + + + {t("application:fileManager.resetThumbnail")} + + )} ); }; diff --git a/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts b/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts index 9e29a02..a2e1e62 100644 --- a/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts +++ b/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts @@ -45,6 +45,7 @@ export interface DisplayOption { hasFile?: boolean; hasFolder?: boolean; hasOwned?: boolean; + hasFailedThumb?: boolean; showEnter?: boolean; showOpen?: boolean; @@ -77,6 +78,7 @@ export interface DisplayOption { showDirectLinkManagement?: boolean; showManageShares?: boolean; showCreateArchive?: boolean; + showResetThumb?: boolean; andCapability?: Boolset; orCapability?: Boolset; @@ -154,8 +156,13 @@ export const getActionOpt = ( display.hasUpdatable = true; } - if (target.metadata && target.metadata[Metadata.restore_uri]) { - display.hasTrashFile = true; + if (target.metadata) { + if (target.metadata[Metadata.restore_uri]) { + display.hasTrashFile = true; + } + if (target.metadata[Metadata.thumbDisabled] !== undefined) { + display.hasFailedThumb = true; + } } if (target.type == FileType.file) { @@ -290,12 +297,20 @@ export const getActionOpt = ( groupBs.enabled(GroupPermission.archive_task) && display.orCapability && display.orCapability.enabled(NavigatorCapability.download_file); + display.showResetThumb = + display.hasFile && + !display.hasFolder && + display.hasFailedThumb && + display.allUpdatable && + display.orCapability && + display.orCapability.enabled(NavigatorCapability.update_metadata); display.showMore = display.showVersionControl || display.showManageShares || display.showCreateArchive || - display.showDirectLinkManagement; + display.showDirectLinkManagement || + display.showResetThumb; return display; }; diff --git a/src/component/FileManager/Dnd/DndWrappedFile.tsx b/src/component/FileManager/Dnd/DndWrappedFile.tsx index b87b9e2..a976fca 100644 --- a/src/component/FileManager/Dnd/DndWrappedFile.tsx +++ b/src/component/FileManager/Dnd/DndWrappedFile.tsx @@ -1,18 +1,18 @@ -import { useDrag, useDrop } from "react-dnd"; import { memo, useCallback, useContext, useEffect } from "react"; -import { FileResponse, FileType } from "../../../api/explorer.ts"; +import { useDrag, useDrop } from "react-dnd"; import { getEmptyImage } from "react-dnd-html5-backend"; -import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { FileResponse, FileType } from "../../../api/explorer.ts"; import { setDragging } from "../../../redux/globalStateSlice.ts"; -import { getFileLinkedUri, mergeRefs } from "../../../util"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; import { processDnd } from "../../../redux/thunks/file.ts"; +import { getFileLinkedUri, mergeRefs } from "../../../util"; -import { FmIndexContext } from "../FmIndexContext.tsx"; -import { FileManagerIndex } from "../FileManager.tsx"; -import { FileBlockProps } from "../Explorer/Explorer.tsx"; import { useTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; import CrUri, { Filesystem } from "../../../util/uri.ts"; +import { FileBlockProps } from "../Explorer/Explorer.tsx"; +import { FileManagerIndex } from "../FileManager.tsx"; +import { FmIndexContext } from "../FmIndexContext.tsx"; export interface DragItem { target: FileResponse; diff --git a/src/component/FileManager/Explorer/GridView/GridView.tsx b/src/component/FileManager/Explorer/GridView/GridView.tsx index 6229c24..e1fed98 100644 --- a/src/component/FileManager/Explorer/GridView/GridView.tsx +++ b/src/component/FileManager/Explorer/GridView/GridView.tsx @@ -1,8 +1,8 @@ +import { Box, Grid, Stack, styled, Typography } from "@mui/material"; import React, { useContext, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { Box, Grid, Stack, styled, Typography } from "@mui/material"; -import { useAppSelector } from "../../../../redux/hooks.ts"; import { FileResponse, FileType } from "../../../../api/explorer.ts"; +import { useAppSelector } from "../../../../redux/hooks.ts"; import DndWrappedFile from "../../Dnd/DndWrappedFile.tsx"; import { FmIndexContext } from "../../FmIndexContext.tsx"; diff --git a/src/component/Icons/ImageAarowCounterclockwise.tsx b/src/component/Icons/ImageAarowCounterclockwise.tsx new file mode 100644 index 0000000..850f620 --- /dev/null +++ b/src/component/Icons/ImageAarowCounterclockwise.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ImageArrowCounterclockwise(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/redux/fileManagerSlice.ts b/src/redux/fileManagerSlice.ts index de931f0..d759c29 100644 --- a/src/redux/fileManagerSlice.ts +++ b/src/redux/fileManagerSlice.ts @@ -127,6 +127,11 @@ export const fileManagerSlice = createSlice({ state[index].listViewColumns = action.payload; }); }, + removeThumbCache: (state, action: PayloadAction>) => { + action.payload.value.forEach((path) => { + state[action.payload.index].tree[path].thumb = undefined; + }); + }, resetFileManager: (state, action: PayloadAction) => { state[action.payload].path = undefined; state[action.payload].previous_path = undefined; @@ -498,6 +503,7 @@ export const fileManagerSlice = createSlice({ export default fileManagerSlice.reducer; export const { + removeThumbCache, clearMultiSelectHovered, setGalleryWidth, setListViewColumns, diff --git a/src/redux/siteConfigSlice.ts b/src/redux/siteConfigSlice.ts index ddb6170..5cd8050 100644 --- a/src/redux/siteConfigSlice.ts +++ b/src/redux/siteConfigSlice.ts @@ -47,6 +47,10 @@ const initialState: SiteConfigSlice = { loaded: ConfigLoadState.NotLoaded, config: {}, }, + thumb: { + loaded: ConfigLoadState.NotLoaded, + config: {}, + }, }; export let Viewers: ExpandedViewerSetting = {}; diff --git a/src/redux/thunks/file.ts b/src/redux/thunks/file.ts index bc753ea..21aabbc 100644 --- a/src/redux/thunks/file.ts +++ b/src/redux/thunks/file.ts @@ -39,7 +39,7 @@ import { loadingDebounceMs } from "../../constants"; import { defaultPath } from "../../hooks/useNavigation.tsx"; import SessionManager, { UserSettings } from "../../session"; import { addRecentUsedColor, addUsedTags } from "../../session/utils.ts"; -import { getFileLinkedUri } from "../../util"; +import { fileExtension, getFileLinkedUri } from "../../util"; import Boolset from "../../util/boolset.ts"; import { canCopyMoveTo } from "../../util/permission.ts"; import CrUri, { Filesystem } from "../../util/uri.ts"; @@ -52,6 +52,7 @@ import { ContextMenuTypes, fileUpdated, removeSelected, + removeThumbCache, removeTreeCache, setContextMenu, setFileDeleteModal, @@ -71,12 +72,13 @@ import { setSidebar, updateLockConflicts, } from "../globalStateSlice.ts"; -import { Viewers } from "../siteConfigSlice.ts"; +import { ConfigLoadState, Viewers } from "../siteConfigSlice.ts"; import { AppThunk } from "../store.ts"; import { confirmOperation, deleteConfirmation, renameForm, requestCreateNew, selectPath } from "./dialog.ts"; import { downloadSingleFile } from "./download.ts"; import { navigateToPath, refreshFileList, updateUserCapacity } from "./filemanager.ts"; import { queueLoadShareInfo } from "./share.ts"; +import { loadSiteConfig } from "./site.ts"; import { openViewer, openViewers } from "./viewer.ts"; const contextMenuCloseAnimationDelay = 250; @@ -114,6 +116,18 @@ export function loadFileThumb(index: number, file: FileResponse): AppThunk { + const thumbConfigLoaded = getState().siteConfig.thumb.loaded; + if (thumbConfigLoaded == ConfigLoadState.NotLoaded) { + await dispatch(loadSiteConfig("thumb")); + } + + const thumbExts = getState().siteConfig.thumb.config.thumb_exts ?? []; + const targetFiles = files + .filter((f) => f.type == FileType.file) + .filter( + (f) => f.metadata?.[Metadata.thumbDisabled] !== undefined && thumbExts.includes(fileExtension(f.name) ?? ""), + ); + + if (targetFiles.length === 0) { + enqueueSnackbar({ + message: i18next.t("application:fileManager.noFileCanResetThumbnail"), + preventDuplicate: true, + variant: "warning", + action: DefaultCloseAction, + }); + return; + } + + try { + // Re-enable thumbnails by removing the disable mark, and update local metadata/cache. + await dispatch( + patchFileMetadata(FileManagerIndex.main, targetFiles, [ + { + key: Metadata.thumbDisabled, + remove: true, + }, + ]), + ); + + dispatch( + removeThumbCache({ + index: FileManagerIndex.main, + value: targetFiles.map((file) => file.path), + }), + ); + // refresh file list + dispatch(refreshFileList(FileManagerIndex.main)); + + // 成功信息 + enqueueSnackbar({ + message: i18next.t("application:fileManager.resetThumbnailRequested"), + variant: "success", + action: DefaultCloseAction, + }); + } catch (_e) { + // Error snackbar is handled in send() + } + }; +} + // Single file symbolic links might be invalid if original file is renamed by its owner, // we need to refresh the symbolic links by getting the latest file list export function refreshSingleFileSymbolicLinks(file: FileResponse): AppThunk> {