Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions public/locales/de-DE/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions public/locales/en-US/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions public/locales/es-ES/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions public/locales/fr-FR/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
"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",
Expand Down
3 changes: 3 additions & 0 deletions public/locales/it-IT/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions public/locales/ja-JP/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
"loggedOut": "ログアウトしました",
"clickToRefresh": "CAPTCHAを再読み込み"
},
"resetThumbnail": "サムネイルをリセット",
"resetThumbnailRequested": "サムネイルのリセットをリクエストしました。",
"noFileCanResetThumbnail": "サムネイルをリセットできるファイルがありません。",
"navbar": {
"notBefore": "~より前",
"notAfter": "~より後",
Expand Down
3 changes: 3 additions & 0 deletions public/locales/ko-KR/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@
"photos": "사진",
"music": "음악",
"documents": "문서",
"resetThumbnail": "썸네일 재설정",
"resetThumbnailRequested": "썸네일 재설정이 요청되었습니다.",
"noFileCanResetThumbnail": "썸네일을 재설정할 수 있는 파일이 없습니다.",
"addATag": "태그 추가...",
"addTagDialog": {
"selectFolder": "폴더 선택",
Expand Down
3 changes: 3 additions & 0 deletions public/locales/pt-BR/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions public/locales/ru-RU/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
"notNameOpOr": "Должны содержаться все ключевые слова",
"caseFolding": "Игнорировать регистр",
"keywords": "Ключевые слова",
"resetThumbnail": "Сбросить миниатюру",
"resetThumbnailRequested": "Запрошен сброс миниатюры.",
"noFileCanResetThumbnail": "Нет файлов для сброса миниатюры.",
"fileNameKeywordsHelp": "Нажмите Enter для добавления ключевого слова",
"advancedSearch": "Расширенный поиск",
"searchFilesTitle": "Поиск файлов",
Expand Down
3 changes: 3 additions & 0 deletions public/locales/zh-CN/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@
"moreActions": "更多操作",
"refresh": "刷新",
"createArchive": "创建压缩文件",
"resetThumbnail": "重置缩略图",
"resetThumbnailRequested": "已请求重置缩略图。",
"noFileCanResetThumbnail": "没有可重置缩略图的文件。",
"newFolder": "创建文件夹",
"newFile": "创建文件",
"showFullPath": "显示路径",
Expand Down
3 changes: 3 additions & 0 deletions public/locales/zh-TW/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
"notBefore": "不早於",
"notAfter": "不晚於",
"minimum": "最小",
"resetThumbnail": "重設縮圖",
"resetThumbnailRequested": "已請求重設縮圖。",
"noFileCanResetThumbnail": "沒有可重設縮圖的檔案。",
"maximum": "最大",
"fileSize": "檔案大小",
"searchBase": "搜尋路徑",
Expand Down
18 changes: 18 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,24 @@ export function getFileThumb(path: string, contextHint?: string): ThunkResponse<
};
}

// Thin wrapper to query supported thumbnail extensions from backend
export function getThumbExts(): ThunkResponse<{ thumb_exts?: string[] }> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please reuse

export function loadSiteConfig(section: string): AppThunk {

return async (dispatch, _getState) => {
return await dispatch(
send<{ thumb_exts?: string[] }>(
"/site/config/thumb",
{
method: "GET",
},
{
...defaultOpts,
noCredential: true,
},
),
);
};
}

export function getUserInfo(uid: string): ThunkResponse<User> {
return async (dispatch, _getState) => {
return await dispatch(
Expand Down
10 changes: 9 additions & 1 deletion src/component/FileManager/ContextMenu/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 "../../../redux/thunks/thumb.ts";
import AppFolder from "../../Icons/AppFolder.tsx";
import ArchiveArrow from "../../Icons/ArchiveArrow.tsx";
import ArrowSync from "../../Icons/ArrowSync.tsx";
Expand Down Expand Up @@ -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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait... does this mean adding an extra request for whenever a new context menu is open for the first time?
I don't think it is worth the cost since this new feature is not commonly used. We need to revisit the design to avoid introducing new requests.
Two options:

  1. Just ignore the supported thumbnail extension, show this option for all file with thumbnail disabled. I understand it will allow this context menu option visable for files does not support thumbnails, but that's OK.
  2. Still ignore the supported thumbnail extension and show the option. But when user actually clicked the option, request supported extensions from backend and check, popup an error/warning for files that are not supported.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the second option is better, i will modify it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not resolved. The extra request is still sent every time a user opens the explorer page.

}
}, [contextMenuOpen, dispatch]);

const showOpenWithCascading = displayOpt.showOpenWithCascading && displayOpt.showOpenWithCascading();
const showOpenWith = displayOpt.showOpenWith && displayOpt.showOpenWith();
let part1 =
Expand Down
10 changes: 10 additions & 0 deletions src/component/FileManager/ContextMenu/MoreMenuItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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>
)}
</>
);
};
Expand Down
16 changes: 15 additions & 1 deletion src/component/FileManager/ContextMenu/useActionDisplayOpt.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import { FileResponse, FileType, Metadata, NavigatorCapability } from "../../../api/explorer.ts";
import { getCachedThumbExts } from "../../../redux/thunks/thumb.ts";
import { GroupPermission } from "../../../api/user.ts";
import { defaultPath } from "../../../hooks/useNavigation.tsx";
import { ContextMenuTypes } from "../../../redux/fileManagerSlice.ts";
Expand Down Expand Up @@ -77,6 +78,7 @@ export interface DisplayOption {
showDirectLinkManagement?: boolean;
showManageShares?: boolean;
showCreateArchive?: boolean;
showResetThumb?: boolean;

andCapability?: Boolset;
orCapability?: Boolset;
Expand Down Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Load thumb exts only after user clicked the context menu option.
Here you just need something like

 display.showResetThumb = display.hasFile  && has update permission && has thumb disabled key;


display.showMore =
display.showVersionControl ||
display.showManageShares ||
display.showCreateArchive ||
display.showDirectLinkManagement;
display.showDirectLinkManagement ||
display.showResetThumb;
return display;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,11 @@ const GalleryImage = memo((props: FileBlockProps) => {
return;
}

// Reset to loading state before reloading thumb (e.g., after reset)
setImageLoading(true);
setThumbSrc(undefined);
tryLoadThumbSrc();
}, [inView]);
}, [inView, file, file.metadata?.[Metadata.thumbDisabled]]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will trigger a reload of the thumbnail after reset. But you already loaded once in https://github.com/cloudreve/frontend/pull/306/files#diff-acc4829d8b9ac4ca5b1f91063828fb9acd9704b8d210258944946090996786e1R1201

So two identical requests will be made?


const onIconClick = useCallback(
(e: React.MouseEvent<HTMLElement>) => {
Expand Down
5 changes: 4 additions & 1 deletion src/component/FileManager/Explorer/GridView/GridFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,11 @@ const GridFile = memo((props: FileBlockProps) => {
return;
}

// Reset to loading state before reloading thumb (e.g., after reset)
setImageLoading(true);
setThumbSrc(undefined);
tryLoadThumbSrc();
}, [inView]);
}, [inView, file, file.metadata?.[Metadata.thumbDisabled]]);

const hoverProps = bindDelayedHover(popupState, 800);
const { open: thumbPopoverOpen, ...rest } = bindPopover(popupState);
Expand Down
3 changes: 3 additions & 0 deletions src/component/FileManager/FileManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 "../../redux/thunks/thumb.ts";
import ImageViewer from "../Viewers/ImageViewer/ImageViewer.tsx";
import Explorer from "./Explorer/Explorer.tsx";
import { FmIndexContext } from "./FmIndexContext.tsx";
Expand Down Expand Up @@ -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));
};
Expand Down
61 changes: 60 additions & 1 deletion src/redux/thunks/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
sendUnlockFiles,
setCurrentVersion,
} from "../../api/api.ts";
import { getCachedThumbExts } from "./thumb.ts";
import {
ConflictDetail,
DirectLink,
Expand All @@ -39,7 +40,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";
Expand Down Expand Up @@ -1154,6 +1155,64 @@ 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 {
// Re-enable thumbnails by removing the disable mark, and update local metadata/cache.
const targetFiles = files
.filter((f) => f.type == FileType.file)
.filter((f) =>
cache === undefined || cache === null ? true : cache.has((fileExtension(f.name) || "").toLowerCase()),
);

await dispatch(
patchFileMetadata(FileManagerIndex.main, targetFiles, [
{
key: Metadata.thumbDisabled,
remove: true,
},
]),
);

// 预取:立即为所选文件请求缩略图(不依赖列表刷新或滚动触发)
const fm = getState().fileManager[FileManagerIndex.main];
const toPrefetch = targetFiles
.map((f) => fm.list?.files.find((ff) => ff.path === f.path) || f)
.filter((f): f is FileResponse => !!f);
// 并发触发 GET /file/thumb
await Promise.allSettled(toPrefetch.map((f) => dispatch(loadFileThumb(FileManagerIndex.main, f))));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/file/thumb is expensive. Consider not loading it proactively. Just reset the thumbnail status, and replying on user scroll in view to trigger the loading.


// 成功信息
enqueueSnackbar({
message: i18next.t("application:fileManager.resetThumbnailRequested"),
variant: "success",
action: DefaultCloseAction,
});
// 不再刷新文件列表;组件会基于metadata变化自动重新请求所选文件的缩略图
} 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>> {
Expand Down
Loading