feat: update reset thumbnail feature (#306)

* update reset thumbnail feature

* fix the Translation issues

* centralize thumbnail ext logic and use site config API

* drop reset API; use PATCH metadata and reload only selected thumbnails

* Improve handling of resetting thumbnails

* Remove unused code

---------

Co-authored-by: Aaron Liu <abslant.liu@gmail.com>
This commit is contained in:
Mason Liu 2025-09-23 11:24:33 +08:00 committed by GitHub
parent beb21a6dbc
commit 30290d774f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 165 additions and 18 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -54,6 +54,9 @@
"clickToRefresh": "CAPTCHAを再読み込み",
"switchLanguage": "言語を切り替え"
},
"resetThumbnail": "壊れたサムネイルをリセット",
"resetThumbnailRequested": "サムネイルのリセットをリクエストしました。",
"noFileCanResetThumbnail": "サムネイルをリセットできるファイルがありません。",
"navbar": {
"notBefore": "~より前",
"notAfter": "~より後",

View File

@ -88,6 +88,9 @@
"photos": "사진",
"music": "음악",
"documents": "문서",
"resetThumbnail": "깨진 썸네일 재설정",
"resetThumbnailRequested": "썸네일 재설정이 요청되었습니다.",
"noFileCanResetThumbnail": "썸네일을 재설정할 수 있는 파일이 없습니다.",
"addATag": "태그 추가...",
"addTagDialog": {
"selectFolder": "폴더 선택",

View File

@ -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",

View File

@ -68,6 +68,9 @@
"notNameOpOr": "Должны содержаться все ключевые слова",
"caseFolding": "Игнорировать регистр",
"keywords": "Ключевые слова",
"resetThumbnail": "Сбросить повреждённую миниатюру",
"resetThumbnailRequested": "Запрошен сброс миниатюры.",
"noFileCanResetThumbnail": "Нет файлов для сброса миниатюры.",
"fileNameKeywordsHelp": "Нажмите Enter для добавления ключевого слова",
"advancedSearch": "Расширенный поиск",
"searchFilesTitle": "Поиск файлов",

View File

@ -284,6 +284,9 @@
"moreActions": "更多操作",
"refresh": "刷新",
"createArchive": "创建压缩文件",
"resetThumbnail": "重置失败的缩略图",
"resetThumbnailRequested": "已请求重置缩略图。",
"noFileCanResetThumbnail": "没有可重置缩略图的文件。",
"newFolder": "创建文件夹",
"newFile": "创建文件",
"showFullPath": "显示路径",

View File

@ -58,6 +58,9 @@
"notBefore": "不早於",
"notAfter": "不晚於",
"minimum": "最小",
"resetThumbnail": "重設失敗的縮圖",
"resetThumbnailRequested": "已請求重設縮圖。",
"noFileCanResetThumbnail": "沒有可重設縮圖的檔案。",
"maximum": "最大",
"fileSize": "檔案大小",
"searchBase": "搜尋路徑",

View File

@ -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<User> {
},
{
...defaultOpts,
bypassSnackbar: (_e) => skipSnackbar,
bypassSnackbar: (_e) => true,
},
),
);

View File

@ -44,6 +44,7 @@ export interface SiteConfig {
custom_props?: CustomProps[];
custom_nav_items?: CustomNavItem[];
custom_html?: CustomHTML;
thumb_exts?: string[];
}
export interface CaptchaResponse {

View File

@ -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) => {
<ListItemText>{t("application:fileManager.createArchive")}</ListItemText>
</CascadingMenuItem>
)}
{displayOpt.showResetThumb && (
<CascadingMenuItem onClick={onClick(() => dispatch(resetThumbnails(targets)))}>
<ListItemIcon>
<ImageArrowCounterclockwise fontSize="small" />
</ListItemIcon>
<ListItemText>{t("application:fileManager.resetThumbnail")}</ListItemText>
</CascadingMenuItem>
)}
</>
);
};

View File

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

View File

@ -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;

View File

@ -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";

View File

@ -0,0 +1,9 @@
import { SvgIcon, SvgIconProps } from "@mui/material";
export default function ImageArrowCounterclockwise(props: SvgIconProps) {
return (
<SvgIcon {...props}>
<path d="M12 6.5a5.5 5.5 0 1 0-11 0a5.5 5.5 0 0 0 11 0m-8-3v.551a3.5 3.5 0 1 1-.187 4.691C3.55 8.427 3.811 8 4.221 8c.176 0 .339.085.46.213A2.5 2.5 0 1 0 4.5 5h1a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 1 0m14.75 2h-5.826a6.5 6.5 0 0 0-.422-1.5h6.248A3.25 3.25 0 0 1 22 7.25v11.5A3.25 3.25 0 0 1 18.75 22H7.25A3.25 3.25 0 0 1 4 18.75v-6.248c.474.198.977.34 1.5.422v5.826q.001.313.103.594l5.823-5.701a2.25 2.25 0 0 1 3.02-.116l.128.116l5.822 5.702q.102-.28.104-.595V7.25a1.75 1.75 0 0 0-1.75-1.75m.58 14.901l-5.805-5.686a.75.75 0 0 0-.966-.071l-.084.07l-5.807 5.687q.274.097.582.099h11.5c.203 0 .399-.035.58-.099M16.253 7.5a2.252 2.252 0 1 1 0 4.504a2.252 2.252 0 0 1 0-4.504m0 1.5a.752.752 0 1 0 0 1.504a.752.752 0 0 0 0-1.504" />
</SvgIcon>
);
}

View File

@ -127,6 +127,11 @@ export const fileManagerSlice = createSlice({
state[index].listViewColumns = action.payload;
});
},
removeThumbCache: (state, action: PayloadAction<FileManagerArgsBase<string[]>>) => {
action.payload.value.forEach((path) => {
state[action.payload.index].tree[path].thumb = undefined;
});
},
resetFileManager: (state, action: PayloadAction<number>) => {
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,

View File

@ -47,6 +47,10 @@ const initialState: SiteConfigSlice = {
loaded: ConfigLoadState.NotLoaded,
config: {},
},
thumb: {
loaded: ConfigLoadState.NotLoaded,
config: {},
},
};
export let Viewers: ExpandedViewerSetting = {};

View File

@ -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<Promi
dispatch(setThumbCache({ index, value: [file.path, thumb] }));
return thumb.url;
} catch (e) {
dispatch(
fileUpdated({
index,
value: [
{
file: { ...file, metadata: { ...file.metadata, [Metadata.thumbDisabled]: "" } },
oldPath: file.path,
includeMetadata: true,
},
],
}),
);
console.warn("Failed to load thumb", e);
return null;
}
@ -1160,6 +1174,62 @@ export function batchGetDirectLinks(index: number, files: FileResponse[]): AppTh
};
}
export function resetThumbnails(files: FileResponse[]): AppThunk {
return async (dispatch, getState) => {
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<Promise<FileResponse>> {