mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-25 19:52:48 +00:00
update reset thumbnail feature
This commit is contained in:
parent
ddfacc1c31
commit
f7088df677
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@
|
|||
"loggedOut": "ログアウトしました",
|
||||
"clickToRefresh": "CAPTCHAを再読み込み"
|
||||
},
|
||||
"resetThumbnail": "サムネイルをリセット",
|
||||
"resetThumbnailRequested": "サムネイルのリセットをリクエストしました。",
|
||||
"noFileCanResetThumbnail": "サムネイルをリセットできるファイルがありません。",
|
||||
"navbar": {
|
||||
"notBefore": "~より前",
|
||||
"notAfter": "~より後",
|
||||
|
|
|
|||
|
|
@ -87,6 +87,9 @@
|
|||
"photos": "사진",
|
||||
"music": "음악",
|
||||
"documents": "문서",
|
||||
"resetThumbnail": "썸네일 재설정",
|
||||
"resetThumbnailRequested": "썸네일 재설정이 요청되었습니다.",
|
||||
"noFileCanResetThumbnail": "썸네일을 재설정할 수 있는 파일이 없습니다.",
|
||||
"addATag": "태그 추가...",
|
||||
"addTagDialog": {
|
||||
"selectFolder": "폴더 선택",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -67,6 +67,9 @@
|
|||
"notNameOpOr": "Должны содержаться все ключевые слова",
|
||||
"caseFolding": "Игнорировать регистр",
|
||||
"keywords": "Ключевые слова",
|
||||
"resetThumbnail": "Сбросить миниатюру",
|
||||
"resetThumbnailRequested": "Запрошен сброс миниатюры.",
|
||||
"noFileCanResetThumbnail": "Нет файлов для сброса миниатюры.",
|
||||
"fileNameKeywordsHelp": "Нажмите Enter для добавления ключевого слова",
|
||||
"advancedSearch": "Расширенный поиск",
|
||||
"searchFilesTitle": "Поиск файлов",
|
||||
|
|
|
|||
|
|
@ -281,6 +281,9 @@
|
|||
"moreActions": "更多操作",
|
||||
"refresh": "刷新",
|
||||
"createArchive": "创建压缩文件",
|
||||
"resetThumbnail": "重置缩略图",
|
||||
"resetThumbnailRequested": "已请求重置缩略图。",
|
||||
"noFileCanResetThumbnail": "没有可重置缩略图的文件。",
|
||||
"newFolder": "创建文件夹",
|
||||
"newFile": "创建文件",
|
||||
"showFullPath": "显示路径",
|
||||
|
|
|
|||
|
|
@ -57,6 +57,9 @@
|
|||
"notBefore": "不早於",
|
||||
"notAfter": "不晚於",
|
||||
"minimum": "最小",
|
||||
"resetThumbnail": "重設縮圖",
|
||||
"resetThumbnailRequested": "已請求重設縮圖。",
|
||||
"noFileCanResetThumbnail": "沒有可重設縮圖的檔案。",
|
||||
"maximum": "最大",
|
||||
"fileSize": "檔案大小",
|
||||
"searchBase": "搜尋路徑",
|
||||
|
|
|
|||
131
src/api/api.ts
131
src/api/api.ts
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue