update reset thumbnail feature

This commit is contained in:
MasonDye 2025-08-29 12:03:02 +08:00
parent ddfacc1c31
commit f7088df677
17 changed files with 264 additions and 4 deletions

View File

@ -315,6 +315,9 @@
"moreActions": "Weitere Aktionen",
"refresh": "Aktualisieren",
"createArchive": "Archiv erstellen",
"resetThumbnail": "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

@ -281,6 +281,9 @@
"moreActions": "More actions",
"refresh": "Refresh",
"createArchive": "Create archive file",
"resetThumbnail": "Reset thumbnail",
"resetThumbnailRequested": "Thumbnail reset requested.",
"noFileCanResetThumbnail": "No files available for thumbnail reset.",
"newFolder": "New folder",
"newFile": "New file",
"showFullPath": "Show full path",

View File

@ -315,6 +315,9 @@
"moreActions": "Más acciones",
"refresh": "Actualizar",
"createArchive": "Crear archivo comprimido",
"resetThumbnail": "Restablecer miniatura",
"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

@ -18,7 +18,9 @@
"email": "E-mail",
"password": "Mot de passe",
"captcha": "CAPTCHA",
"captchaError": "Impossible de charger le CAPTCHA : {{message}}",
"resetThumbnail": "Réinitialiser la miniature",
"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

@ -53,6 +53,9 @@
"loggedOut": "Sei stato disconnesso",
"clickToRefresh": "Clicca per aggiornare il CAPTCHA"
},
"resetThumbnail": "Reimposta miniatura",
"resetThumbnailRequested": "Reimpostazione della miniatura richiesta.",
"noFileCanResetThumbnail": "Nessun file può reimpostare la miniatura.",
"navbar": {
"notBefore": "Non prima di",
"notAfter": "Non dopo",

View File

@ -53,6 +53,9 @@
"loggedOut": "ログアウトしました",
"clickToRefresh": "CAPTCHAを再読み込み"
},
"resetThumbnail": "サムネイルをリセット",
"resetThumbnailRequested": "サムネイルのリセットをリクエストしました。",
"noFileCanResetThumbnail": "サムネイルをリセットできるファイルがありません。",
"navbar": {
"notBefore": "~より前",
"notAfter": "~より後",

View File

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

View File

@ -74,6 +74,9 @@
"recentlyViewed": "Visualizados recentemente",
"searchFiles": "Buscar arquivos...",
"showMore": "Mais",
"resetThumbnail": "Redefinir miniatura",
"resetThumbnailRequested": "Redefinição de miniatura solicitada.",
"noFileCanResetThumbnail": "Nenhum arquivo pode redefinir a miniatura.",
"myFiles": "Meus arquivos",
"hisFiles": "Arquivos dele/dela",
"trash": "Lixeira",

View File

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

View File

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

View File

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

View File

@ -292,6 +292,137 @@ export function getFileThumb(path: string, contextHint?: string): ThunkResponse<
};
}
// Reset thumbnails for given file URIs by clearing thumb:disabled metadata
export interface ResetThumbRequest {
uris: string[];
}
export function sendResetFileThumbs(req: ResetThumbRequest): ThunkResponse<void> {
return async (dispatch, _getState) => {
return await dispatch(
send(
"/file/thumb/reset",
{
data: req,
method: "POST",
},
{
...defaultOpts,
},
),
);
};
}
// Get supported thumbnail file extensions by reading enabled generators' settings
export function getSupportedThumbExts(): ThunkResponse<string[]> {
return async (dispatch, _getState) => {
// Try backend endpoint first if available
try {
const remote = await dispatch(
send<{ exts?: string[] }>(
"/file/thumb/exts",
{
method: "GET",
},
{
...defaultOpts,
},
),
);
if (remote && Array.isArray(remote.exts)) {
return remote.exts;
}
} catch (_e) {
// Fallback to compute from settings below
}
const keys = {
keys: [
"thumb_builtin_enabled",
"thumb_vips_enabled",
"thumb_vips_exts",
"thumb_ffmpeg_enabled",
"thumb_ffmpeg_exts",
"thumb_libreoffice_enabled",
"thumb_libreoffice_exts",
"thumb_music_cover_enabled",
"thumb_music_cover_exts",
"thumb_libraw_enabled",
"thumb_libraw_exts",
],
};
const settings = await dispatch(getSettings(keys));
const enabled = (k: string) => {
const v = (settings?.[k] ?? "").toString().toLowerCase();
return v === "1" || v === "true";
};
const parseExts = (v?: string) =>
(v || "")
.split(",")
.map((s) => s.trim().toLowerCase())
.filter((s) => !!s);
const exts = new Set<string>();
if (enabled("thumb_vips_enabled")) {
parseExts(settings?.["thumb_vips_exts"]).forEach((e) => exts.add(e));
}
if (enabled("thumb_ffmpeg_enabled")) {
parseExts(settings?.["thumb_ffmpeg_exts"]).forEach((e) => exts.add(e));
}
if (enabled("thumb_libreoffice_enabled")) {
parseExts(settings?.["thumb_libreoffice_exts"]).forEach((e) => exts.add(e));
}
if (enabled("thumb_music_cover_enabled")) {
parseExts(settings?.["thumb_music_cover_exts"]).forEach((e) => exts.add(e));
}
if (enabled("thumb_libraw_enabled")) {
parseExts(settings?.["thumb_libraw_exts"]).forEach((e) => exts.add(e));
}
// Note: builtin generator does not expose explicit extensions list in settings
// so we do not add extra exts for it to keep behavior consistent with backend.
return Array.from(exts);
};
}
// --- Cached supported thumbnail extensions helpers ---
let __thumbExtsCache: Set<string> | null | undefined = undefined; // undefined: not fetched, null: unknown/fallback
// Prime cache once per page. Safe to call multiple times.
export function primeThumbExtsCache(): ThunkResponse<void> {
return async (dispatch, _getState) => {
if (__thumbExtsCache !== undefined) return;
try {
const exts = await dispatch(getSupportedThumbExts());
__thumbExtsCache = new Set(exts.map((e) => e.toLowerCase()));
} catch (_e) {
// Mark as unknown to fall back to legacy behavior
__thumbExtsCache = null;
}
};
}
export function getCachedThumbExts(): Set<string> | null | undefined {
return __thumbExtsCache;
}
// Check if a file name is likely supported based on cached exts
// Returns undefined if cache is not ready (treat as supported by caller).
export function isThumbExtSupportedSync(fileName: string): boolean | undefined {
const cache = __thumbExtsCache;
if (cache === undefined) return undefined;
if (cache === null) return true; // unknown => allow
const idx = fileName.lastIndexOf(".");
const ext = idx >= 0 ? fileName.substring(idx + 1).toLowerCase() : "";
if (!ext) return false;
return cache.has(ext);
}
export function getUserInfo(uid: string): ThunkResponse<User> {
return async (dispatch, _getState) => {
return await dispatch(

View File

@ -1,5 +1,5 @@
import { Box, Divider, ListItemIcon, ListItemText, Menu, MenuItem, styled, Typography, useTheme } from "@mui/material";
import { useCallback, useMemo } from "react";
import { useCallback, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { closeContextMenu } from "../../../redux/fileManagerSlice.ts";
import { CreateNewDialogType } from "../../../redux/globalStateSlice.ts";
@ -22,6 +22,7 @@ import {
} from "../../../redux/thunks/file.ts";
import { refreshFileList, uploadClicked, uploadFromClipboard } from "../../../redux/thunks/filemanager.ts";
import { openViewers } from "../../../redux/thunks/viewer.ts";
import { primeThumbExtsCache } from "../../../api/api.ts";
import AppFolder from "../../Icons/AppFolder.tsx";
import ArchiveArrow from "../../Icons/ArchiveArrow.tsx";
import ArrowSync from "../../Icons/ArrowSync.tsx";
@ -107,6 +108,13 @@ const ContextMenu = ({ fmIndex = 0 }: ContextMenuProps) => {
dispatch(closeContextMenu({ index: fmIndex, value: undefined }));
}, [dispatch]);
// Ensure supported thumbnail extensions are primed when menu opens
useEffect(() => {
if (contextMenuOpen) {
dispatch(primeThumbExtsCache());
}
}, [contextMenuOpen, dispatch]);
const showOpenWithCascading = displayOpt.showOpenWithCascading && displayOpt.showOpenWithCascading();
const showOpenWith = displayOpt.showOpenWith && displayOpt.showOpenWith();
let part1 =

View File

@ -10,11 +10,13 @@ import {
} from "../../../redux/globalStateSlice.ts";
import { useAppDispatch } from "../../../redux/hooks.ts";
import Archive from "../../Icons/Archive.tsx";
import RectangleLandscapeSync from "../../Icons/RectangleLandscapeSync.tsx";
import BranchForkLink from "../../Icons/BranchForkLink.tsx";
import HistoryOutlined from "../../Icons/HistoryOutlined.tsx";
import LinkSetting from "../../Icons/LinkSetting.tsx";
import { CascadingContext, CascadingMenuItem } from "./CascadingMenu.tsx";
import { SubMenuItemsProps } from "./OrganizeMenuItems.tsx";
import { resetThumbnails } from "../../../redux/thunks/file.ts";
const MoreMenuItems = ({ displayOpt, targets }: SubMenuItemsProps) => {
const { rootPopupState } = useContext(CascadingContext);
@ -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>
<RectangleLandscapeSync fontSize="small" />
</ListItemIcon>
<ListItemText>{t("application:fileManager.resetThumbnail")}</ListItemText>
</CascadingMenuItem>
)}
</>
);
};

View File

@ -1,5 +1,6 @@
import { useMemo } from "react";
import { FileResponse, FileType, Metadata, NavigatorCapability } from "../../../api/explorer.ts";
import { getCachedThumbExts } from "../../../api/api.ts";
import { GroupPermission } from "../../../api/user.ts";
import { defaultPath } from "../../../hooks/useNavigation.tsx";
import { ContextMenuTypes } from "../../../redux/fileManagerSlice.ts";
@ -77,6 +78,7 @@ export interface DisplayOption {
showDirectLinkManagement?: boolean;
showManageShares?: boolean;
showCreateArchive?: boolean;
showResetThumb?: boolean;
andCapability?: Boolset;
orCapability?: Boolset;
@ -291,11 +293,23 @@ export const getActionOpt = (
display.orCapability &&
display.orCapability.enabled(NavigatorCapability.download_file);
// Reset thumbnail is available when at least one file is selected and
// current capability allows generating thumbnails
// Show only when at least one selected file has a supported extension,
// based on cached supported thumbnail extensions.
const cache = getCachedThumbExts();
const anySupported =
cache instanceof Set
? targets.some((f) => f.type == FileType.file && cache.has((fileExtension(f.name) || "").toLowerCase()))
: false;
display.showResetThumb = display.hasFile && anySupported;
display.showMore =
display.showVersionControl ||
display.showManageShares ||
display.showCreateArchive ||
display.showDirectLinkManagement;
display.showDirectLinkManagement ||
display.showResetThumb;
return display;
};

View File

@ -6,6 +6,7 @@ import { clearSelected } from "../../redux/fileManagerSlice.ts";
import { resetDialogs } from "../../redux/globalStateSlice.ts";
import { useAppDispatch } from "../../redux/hooks.ts";
import { resetFm, selectAll, shortCutDelete } from "../../redux/thunks/filemanager.ts";
import { primeThumbExtsCache } from "../../api/api.ts";
import ImageViewer from "../Viewers/ImageViewer/ImageViewer.tsx";
import Explorer from "./Explorer/Explorer.tsx";
import { FmIndexContext } from "./FmIndexContext.tsx";
@ -37,6 +38,8 @@ export const FileManager = ({ index = 0, initialPath, skipRender }: FileManagerP
useEffect(() => {
if (index == FileManagerIndex.main) {
dispatch(resetDialogs());
// Prime supported thumbnail extension cache once per page
dispatch(primeThumbExtsCache());
return () => {
dispatch(resetFm(index));
};

View File

@ -6,6 +6,8 @@ import {
getFileEntityUrl,
getFileList,
getFileThumb,
getCachedThumbExts,
sendResetFileThumbs,
sendCreateFile,
sendDeleteFiles,
sendMetadataPatch,
@ -39,7 +41,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";
@ -1154,6 +1156,66 @@ export function batchGetDirectLinks(index: number, files: FileResponse[]): AppTh
};
}
export function resetThumbnails(files: FileResponse[]): AppThunk {
return async (dispatch, _getState) => {
const cache = getCachedThumbExts();
const uris = files
.filter((f) => f.type == FileType.file)
.filter((f) =>
cache === undefined || cache === null ? true : cache.has((fileExtension(f.name) || "").toLowerCase()),
)
.map((f) => getFileLinkedUri(f));
if (uris.length === 0) {
enqueueSnackbar({
message: i18next.t("application:fileManager.noFileCanResetThumbnail"),
preventDuplicate: true,
variant: "warning",
action: DefaultCloseAction,
});
return;
}
try {
await dispatch(
sendResetFileThumbs({
uris,
}),
);
enqueueSnackbar({
message: i18next.t("application:fileManager.resetThumbnailRequested"),
variant: "success",
action: DefaultCloseAction,
});
} catch (_e) {
// Error snackbar is handled in send()
} finally {
// Clear cached thumbnails so they will be reloaded next time
files
.filter((f) => f.type == FileType.file)
.forEach((f) =>
dispatch(
fileUpdated({
index: 0,
value: [
{
file: f,
oldPath: f.path,
},
],
}),
),
);
// Use the same refresh approach as uploader: refresh after a short delay
setTimeout(() => {
dispatch(refreshFileList(0));
}, 1000);
}
};
}
// 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>> {