mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-25 19:52:48 +00:00
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:
parent
beb21a6dbc
commit
30290d774f
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@
|
|||
"clickToRefresh": "CAPTCHAを再読み込み",
|
||||
"switchLanguage": "言語を切り替え"
|
||||
},
|
||||
"resetThumbnail": "壊れたサムネイルをリセット",
|
||||
"resetThumbnailRequested": "サムネイルのリセットをリクエストしました。",
|
||||
"noFileCanResetThumbnail": "サムネイルをリセットできるファイルがありません。",
|
||||
"navbar": {
|
||||
"notBefore": "~より前",
|
||||
"notAfter": "~より後",
|
||||
|
|
|
|||
|
|
@ -88,6 +88,9 @@
|
|||
"photos": "사진",
|
||||
"music": "음악",
|
||||
"documents": "문서",
|
||||
"resetThumbnail": "깨진 썸네일 재설정",
|
||||
"resetThumbnailRequested": "썸네일 재설정이 요청되었습니다.",
|
||||
"noFileCanResetThumbnail": "썸네일을 재설정할 수 있는 파일이 없습니다.",
|
||||
"addATag": "태그 추가...",
|
||||
"addTagDialog": {
|
||||
"selectFolder": "폴더 선택",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@
|
|||
"notNameOpOr": "Должны содержаться все ключевые слова",
|
||||
"caseFolding": "Игнорировать регистр",
|
||||
"keywords": "Ключевые слова",
|
||||
"resetThumbnail": "Сбросить повреждённую миниатюру",
|
||||
"resetThumbnailRequested": "Запрошен сброс миниатюры.",
|
||||
"noFileCanResetThumbnail": "Нет файлов для сброса миниатюры.",
|
||||
"fileNameKeywordsHelp": "Нажмите Enter для добавления ключевого слова",
|
||||
"advancedSearch": "Расширенный поиск",
|
||||
"searchFilesTitle": "Поиск файлов",
|
||||
|
|
|
|||
|
|
@ -284,6 +284,9 @@
|
|||
"moreActions": "更多操作",
|
||||
"refresh": "刷新",
|
||||
"createArchive": "创建压缩文件",
|
||||
"resetThumbnail": "重置失败的缩略图",
|
||||
"resetThumbnailRequested": "已请求重置缩略图。",
|
||||
"noFileCanResetThumbnail": "没有可重置缩略图的文件。",
|
||||
"newFolder": "创建文件夹",
|
||||
"newFile": "创建文件",
|
||||
"showFullPath": "显示路径",
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@
|
|||
"notBefore": "不早於",
|
||||
"notAfter": "不晚於",
|
||||
"minimum": "最小",
|
||||
"resetThumbnail": "重設失敗的縮圖",
|
||||
"resetThumbnailRequested": "已請求重設縮圖。",
|
||||
"noFileCanResetThumbnail": "沒有可重設縮圖的檔案。",
|
||||
"maximum": "最大",
|
||||
"fileSize": "檔案大小",
|
||||
"searchBase": "搜尋路徑",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export interface SiteConfig {
|
|||
custom_props?: CustomProps[];
|
||||
custom_nav_items?: CustomNavItem[];
|
||||
custom_html?: CustomHTML;
|
||||
thumb_exts?: string[];
|
||||
}
|
||||
|
||||
export interface CaptchaResponse {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ const initialState: SiteConfigSlice = {
|
|||
loaded: ConfigLoadState.NotLoaded,
|
||||
config: {},
|
||||
},
|
||||
thumb: {
|
||||
loaded: ConfigLoadState.NotLoaded,
|
||||
config: {},
|
||||
},
|
||||
};
|
||||
|
||||
export let Viewers: ExpandedViewerSetting = {};
|
||||
|
|
|
|||
|
|
@ -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>> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue