diff --git a/public/locales/en-US/application.json b/public/locales/en-US/application.json index e49a2e5..695b370 100644 --- a/public/locales/en-US/application.json +++ b/public/locales/en-US/application.json @@ -26,7 +26,7 @@ "forgetPassword": "Forgot password?", "2FA": "2FA Verification", "input2FACode": "Please enter the six-digit 2FA verification code", - "passwordNotMatch": "Those passwords didn’t match.", + "passwordNotMatch": "Those passwords didn't match.", "findMyPassword": "Find my password", "passwordReset": "Password has been reset.", "newPassword": "New password", @@ -363,7 +363,11 @@ "layout": "Layout", "thumbnails": "Thumbnails", "on": "On", - "off": "Off" + "off": "Off", + "viewSetting": "View setting", + "saved": "Saved", + "notSet": "Not set", + "deleteViewSetting": "Delete view setting" }, "modals": { "showFileName": "Show file name", @@ -465,6 +469,8 @@ "createShareLink": "Create share link", "privateShare": "Hide from public", "privateShareDes": "If selected, other people cannot see this share link on your homepage.", + "shareView": "Share view setting", + "shareViewDes": "If selected, other users can see your view setting (layout, sorting, etc.) saved on the server server when accessing this shared folder.", "expireAfterDownload": "Expire after being downloaded", "sharePassword": "Share password", "randomlyGenerate": "Random", @@ -666,6 +672,10 @@ "createdAt": "Created at: " }, "setting": { + "syncView": "View settings", + "syncViewDes": "Remember the view settings of each directory and synchronize to the server.", + "syncViewOn": "Sync to server", + "syncViewOff": "Do not sync", "noAuthenticator": "Add a passkey to sign in using fingerprint, face or USB key.", "neverUsed": "Never used", "usedAt": "Last used at <0>", diff --git a/public/locales/en-US/dashboard.json b/public/locales/en-US/dashboard.json index e90c270..f6c5754 100644 --- a/public/locales/en-US/dashboard.json +++ b/public/locales/en-US/dashboard.json @@ -621,7 +621,8 @@ "disable_2fa": "Disable 2FA", "add_passkey": "Add passkey", "remove_passkey": "Remove passkey", - "redeem_gift_code": "Redeem gift code" + "redeem_gift_code": "Redeem gift code", + "update_view": "Changed view setting" }, "server": "Server", "tempPath": "Temporary path", diff --git a/public/locales/ja-JP/application.json b/public/locales/ja-JP/application.json index 43b66fd..d71e4b7 100644 --- a/public/locales/ja-JP/application.json +++ b/public/locales/ja-JP/application.json @@ -183,6 +183,7 @@ "save": "保存", "noMoreImages": "現在のページには閲覧可能な画像がありません", "imageViewer": "画像ビューア", + "logUpdateView": "ビュー設定を更新", "logFileDeleteShare": "共有リンクを削除", "logFileEditShare": "共有リンクを編集", "deleteShareWarning": "この共有リンクを削除しますか?", @@ -363,7 +364,11 @@ "layout": "レイアウト", "thumbnails": "サムネイル", "on": "オン", - "off": "オフ" + "off": "オフ", + "viewSetting": "ビュー設定", + "saved": "保存", + "notSet": "未設定", + "deleteViewSetting": "ビュー設定を削除" }, "modals": { "showFileName": "ファイル名を表示", @@ -498,7 +503,9 @@ "skipSoftDelete": "ファイルを完全に削除", "skipSoftDeleteDes": "ゴミ箱をスキップしてファイルを完全に削除します", "unlinkOnly": "物理ファイルを保持", - "unlinkOnlyDes": "ファイル記録のみ削除、物理ファイルは削除されません" + "unlinkOnlyDes": "ファイル記録のみ削除、物理ファイルは削除されません", + "shareView": "共有ビュー設定", + "shareViewDes": "チェックを入れると、他のユーザーがこの共有フォルダにアクセスした際にあなたのビュー設定(レイアウト、ソートなど)を見ることができます。" }, "uploader": { "fileCopyName": "コピー_", @@ -666,6 +673,10 @@ "createdAt": "作成日:" }, "setting": { + "syncView": "ビュー設定", + "syncViewDes": "各ディレクトリのビュー設定を記憶し、サーバーに同期します。", + "syncViewOn": "サーバーに同期", + "syncViewOff": "同期しない", "noAuthenticator": "顔認証、指紋認証、またはUSBキーによるログインのために通行キーを追加", "neverUsed": "未使用", "usedAt": "最終使用日 <0>", diff --git a/public/locales/ja-JP/dashboard.json b/public/locales/ja-JP/dashboard.json index ab51e35..3da2614 100644 --- a/public/locales/ja-JP/dashboard.json +++ b/public/locales/ja-JP/dashboard.json @@ -617,7 +617,8 @@ "disable_2fa": "2FA無効化", "add_passkey": "パスキー追加", "remove_passkey": "パスキー削除", - "redeem_gift_code": "ギフトコード交換" + "redeem_gift_code": "ギフトコード交換", + "update_view": "ビュー設定変更" }, "server": "サーバー設定", "tempPath": "一時パス", diff --git a/public/locales/zh-CN/application.json b/public/locales/zh-CN/application.json index a0ea859..02f379a 100644 --- a/public/locales/zh-CN/application.json +++ b/public/locales/zh-CN/application.json @@ -363,7 +363,11 @@ "layout": "布局", "thumbnails": "缩略图", "on": "开启", - "off": "关闭" + "off": "关闭", + "viewSetting": "视图设置", + "saved": "已保存", + "notSet": "未设置", + "deleteViewSetting": "删除视图设置" }, "modals": { "showFileName": "显示文件名", @@ -498,7 +502,13 @@ "skipSoftDelete": "彻底删除文件", "skipSoftDeleteDes": "跳过回收站,直接删除文件", "unlinkOnly": "保留物理文件", - "unlinkOnlyDes": "仅删除文件记录,物理文件不会被删除" + "unlinkOnlyDes": "仅删除文件记录,物理文件不会被删除", + "shareView": "分享视图设置", + "shareViewDes": "勾选后,其他用户访问此共享文件夹时可以看到你保存在服务器的视图设置(布局、排序等)。", + "viewSetting": "视图设置", + "saved": "已保存", + "notSet": "未设置", + "deleteViewSetting": "删除视图设置" }, "uploader": { "fileCopyName": "副本_", @@ -666,6 +676,10 @@ "createdAt": "创建日期:" }, "setting": { + "syncView": "视图设置", + "syncViewDes": "是否记住各个目录的视图设置,并同步到服务器。", + "syncViewOn": "同步到服务器", + "syncViewOff": "不同步", "noAuthenticator": "添加通行密钥以使用人脸、指纹或 USB 密钥登录账号", "neverUsed": "从未使用过", "usedAt": "上次使用于 <0>", diff --git a/public/locales/zh-CN/dashboard.json b/public/locales/zh-CN/dashboard.json index b024747..ee90d1f 100644 --- a/public/locales/zh-CN/dashboard.json +++ b/public/locales/zh-CN/dashboard.json @@ -617,7 +617,8 @@ "disable_2fa": "禁用 2FA", "add_passkey": "添加通行密钥", "remove_passkey": "移除通行密钥", - "redeem_gift_code": "兑换礼品码" + "redeem_gift_code": "兑换礼品码", + "update_view": "更改视图设置" }, "server": "服务器设置", "tempPath": "临时路径", diff --git a/public/locales/zh-TW/application.json b/public/locales/zh-TW/application.json index 8f5957e..93631d1 100644 --- a/public/locales/zh-TW/application.json +++ b/public/locales/zh-TW/application.json @@ -498,7 +498,13 @@ "skipSoftDelete": "徹底刪除檔案", "skipSoftDeleteDes": "跳過回收站,直接刪除檔案", "unlinkOnly": "保留物理檔案", - "unlinkOnlyDes": "僅刪除檔案記錄,物理檔案不會被刪除" + "unlinkOnlyDes": "僅刪除檔案記錄,物理檔案不會被刪除", + "shareView": "分享視圖設定", + "shareViewDes": "勾選後,其他使用者存取此共享資料夾時可以看到你保存在服務器的視圖設定(佈局、排序等)。", + "viewSetting": "視圖設定", + "saved": "已保存", + "notSet": "未設定", + "deleteViewSetting": "刪除視圖設定" }, "uploader": { "fileCopyName": "副本_", @@ -666,6 +672,10 @@ "createdAt": "建立日期:" }, "setting": { + "syncView": "視圖設置", + "syncViewDes": "是否記住各個目錄的視圖設置,並同步到服務器。", + "syncViewOn": "同步到服務器", + "syncViewOff": "不同步", "noAuthenticator": "新增通行金鑰以使用人臉、指紋或 USB 金鑰登入賬號", "neverUsed": "從未使用過", "usedAt": "上次使用於 <0>", diff --git a/public/locales/zh-TW/dashboard.json b/public/locales/zh-TW/dashboard.json index 21bc148..ef4c01a 100644 --- a/public/locales/zh-TW/dashboard.json +++ b/public/locales/zh-TW/dashboard.json @@ -612,9 +612,10 @@ "change_password": "更改密碼", "enable_2fa": "啟用 2FA", "disable_2fa": "禁用 2FA", - "add_passkey": "新增通行金鑰", - "remove_passkey": "移除通行金鑰", - "redeem_gift_code": "兌換禮品碼" + "add_passkey": "新增通行密鑰", + "remove_passkey": "移除通行密鑰", + "redeem_gift_code": "兌換禮品碼", + "update_view": "更改檢視設定" }, "server": "伺服器設定", "tempPath": "臨時路徑", diff --git a/src/api/api.ts b/src/api/api.ts index 9d86869..b81dea9 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -55,6 +55,7 @@ import { MoveFileService, MultipleUriService, PatchMetadataService, + PatchViewSyncService, PinFileService, RenameFileService, Share, @@ -1987,3 +1988,18 @@ export function sendImport(req: ImportWorkflowService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/file/view`, + { method: "PATCH", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} diff --git a/src/api/explorer.ts b/src/api/explorer.ts index 28d57ec..ef6952a 100644 --- a/src/api/explorer.ts +++ b/src/api/explorer.ts @@ -1,3 +1,4 @@ +import { ListViewColumnSetting } from "../component/FileManager/Explorer/ListView/Column.tsx"; import { User } from "./user.ts"; export interface PaginationArgs { @@ -49,6 +50,7 @@ export interface ExtendedInfo { storage_used: number; shares?: Share[]; entities?: Entity[]; + view?: ExplorerView; } export interface Entity { @@ -65,6 +67,7 @@ export interface Share { name?: string; expires?: string; is_private?: boolean; + share_view?: boolean; remain_downloads?: number; created_at?: string; url: string; @@ -114,6 +117,16 @@ export interface NavigatorProps { order_direction_options: string[]; } +export interface ExplorerView { + page_size: number; + order?: string; + order_direction?: string; + view?: string; + thumbnail?: boolean; + columns?: ListViewColumnSetting[]; + gallery_width?: number; +} + export interface ListResponse { files: FileResponse[]; pagination: PaginationResults; @@ -124,6 +137,7 @@ export interface ListResponse { single_file_view?: boolean; parent?: FileResponse; storage_policy?: StoragePolicy; + view?: ExplorerView; } export const Metadata = { @@ -262,6 +276,7 @@ export interface ShareCreateService { downloads?: number; is_private?: boolean; expire?: number; + share_view?: boolean; } export interface CreateFileService { @@ -368,6 +383,7 @@ export const AuditLogType = { remove_passkey: 53, redeem_gift_code: 54, file_imported: 55, + update_view: 56, }; export interface MultipleUriService { @@ -477,3 +493,8 @@ export interface DirectLink { file_url: string; link: string; } + +export interface PatchViewSyncService { + uri: string; + view?: ExplorerView; +} diff --git a/src/api/user.ts b/src/api/user.ts index a5cd616..eb3f85b 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -22,6 +22,7 @@ export interface User { group?: Group; pined?: PinedFile[]; language?: string; + disable_view_sync?: boolean; } export interface Group { id: string; @@ -99,6 +100,7 @@ export interface UserSettings { passwordless: boolean; two_fa_enabled: boolean; passkeys?: Passkey[]; + disable_view_sync: boolean; } export interface PatchUserSetting { @@ -112,6 +114,7 @@ export interface PatchUserSetting { new_password?: string; two_fa_enabled?: boolean; two_fa_code?: string; + disable_view_sync?: boolean; } export interface PasskeyCredentialOption { diff --git a/src/component/Admin/Settings/Event/Events.tsx b/src/component/Admin/Settings/Event/Events.tsx index 042453f..965c439 100644 --- a/src/component/Admin/Settings/Event/Events.tsx +++ b/src/component/Admin/Settings/Event/Events.tsx @@ -63,6 +63,7 @@ export const eventCategories = { AuditLogType.move_to_trash, AuditLogType.update_metadata, AuditLogType.get_direct_link, + AuditLogType.update_view, ], }, share: { diff --git a/src/component/FileManager/Dialogs/Share/ShareDialog.tsx b/src/component/FileManager/Dialogs/Share/ShareDialog.tsx index 1dc515e..ab2c8db 100644 --- a/src/component/FileManager/Dialogs/Share/ShareDialog.tsx +++ b/src/component/FileManager/Dialogs/Share/ShareDialog.tsx @@ -1,20 +1,21 @@ -import { useTranslation } from "react-i18next"; import { Box, DialogContent, IconButton, Tooltip, useTheme } from "@mui/material"; -import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; +import dayjs from "dayjs"; +import { TFunction } from "i18next"; import React, { useCallback, useEffect, useState } from "react"; -import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; -import { closeShareLinkDialog } from "../../../../redux/globalStateSlice.ts"; -import AutoHeight from "../../../Common/AutoHeight.tsx"; -import ShareSettingContent, { downloadOptions, expireOptions, ShareSetting } from "./ShareSetting.tsx"; +import { useTranslation } from "react-i18next"; import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { Share as ShareModel } from "../../../../api/explorer.ts"; +import { closeShareLinkDialog } from "../../../../redux/globalStateSlice.ts"; +import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; import { createOrUpdateShareLink } from "../../../../redux/thunks/share.ts"; -import { FilledTextField } from "../../../Common/StyledComponents.tsx"; import { copyToClipboard, sendLink } from "../../../../util"; +import AutoHeight from "../../../Common/AutoHeight.tsx"; +import { FilledTextField } from "../../../Common/StyledComponents.tsx"; +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; import Share from "../../../Icons/Share.tsx"; import { FileManagerIndex } from "../../FileManager.tsx"; -import { TFunction } from "i18next"; -import { Share as ShareModel } from "../../../../api/explorer.ts"; -import dayjs from "dayjs"; +import ShareSettingContent, { downloadOptions, expireOptions, ShareSetting } from "./ShareSetting.tsx"; + const initialSetting: ShareSetting = { expires_val: expireOptions[2], @@ -24,6 +25,7 @@ const initialSetting: ShareSetting = { const shareToSetting = (share: ShareModel, t: TFunction): ShareSetting => { const res: ShareSetting = { is_private: share.is_private, + share_view: share.share_view, downloads: share.remain_downloads != undefined && share.remain_downloads > 0, expires_val: expireOptions[2], diff --git a/src/component/FileManager/Dialogs/Share/ShareSetting.tsx b/src/component/FileManager/Dialogs/Share/ShareSetting.tsx index e09c46b..b95973a 100644 --- a/src/component/FileManager/Dialogs/Share/ShareSetting.tsx +++ b/src/component/FileManager/Dialogs/Share/ShareSetting.tsx @@ -1,4 +1,3 @@ -import { useTranslation } from "react-i18next"; import { Autocomplete, Checkbox, @@ -13,14 +12,16 @@ import { TextField, Typography, } from "@mui/material"; -import { FileResponse, FileType } from "../../../../api/explorer.ts"; import MuiAccordion from "@mui/material/Accordion"; -import MuiAccordionSummary from "@mui/material/AccordionSummary"; import MuiAccordionDetails from "@mui/material/AccordionDetails"; +import MuiAccordionSummary from "@mui/material/AccordionSummary"; import { useState } from "react"; -import Eye from "../../../Icons/Eye.tsx"; -import Timer from "../../../Icons/Timer.tsx"; +import { useTranslation } from "react-i18next"; +import { FileResponse, FileType } from "../../../../api/explorer.ts"; import ClockArrowDownload from "../../../Icons/ClockArrowDownload.tsx"; +import Eye from "../../../Icons/Eye.tsx"; +import TableSettingsOutlined from "../../../Icons/TableSettings.tsx"; +import Timer from "../../../Icons/Timer.tsx"; const Accordion = styled(MuiAccordion)(() => ({ border: "0px solid rgba(0, 0, 0, .125)", @@ -69,6 +70,7 @@ const StyledListItemButton = styled(ListItemButton)(() => ({})); export interface ShareSetting { is_private?: boolean; + share_view?: boolean; downloads?: boolean; expires?: boolean; @@ -122,7 +124,7 @@ const ShareSettingContent = ({ setting, file, editing, onSettingChange }: ShareS setExpanded(isExpanded ? panel : undefined); }; - const handleCheck = (prop: "is_private" | "expires" | "downloads") => () => { + const handleCheck = (prop: "is_private" | "share_view" | "expires" | "downloads") => () => { if (!setting[prop]) { handleExpand(prop)(null, true); } @@ -150,6 +152,22 @@ const ShareSettingContent = ({ setting, file, editing, onSettingChange }: ShareS {t("application:modals.privateShareDes")} + {file?.type == FileType.folder && ( + + + + + + + + + + + + + {t("application:modals.shareViewDes")} + + )} diff --git a/src/component/FileManager/Explorer/ListView/ColumnSetting.tsx b/src/component/FileManager/Explorer/ListView/ColumnSetting.tsx index 3253012..65a9a2d 100644 --- a/src/component/FileManager/Explorer/ListView/ColumnSetting.tsx +++ b/src/component/FileManager/Explorer/ListView/ColumnSetting.tsx @@ -1,4 +1,3 @@ -import { useTranslation } from "react-i18next"; import { Box, DialogContent, @@ -10,21 +9,21 @@ import { TableHead, TableRow, } from "@mui/material"; -import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; +import { useSnackbar } from "notistack"; import { useCallback, useEffect, useState } from "react"; -import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; +import { useTranslation } from "react-i18next"; import { setListViewColumnSettingDialog } from "../../../../redux/globalStateSlice.ts"; +import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; +import { applyListColumns } from "../../../../redux/thunks/filemanager.ts"; import AutoHeight from "../../../Common/AutoHeight.tsx"; -import { FileManagerIndex } from "../../FileManager.tsx"; -import { getColumnTypeDefaults, ListViewColumnSetting } from "./Column.tsx"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx"; import { StyledTableContainerPaper } from "../../../Common/StyledComponents.tsx"; +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; import ArrowDown from "../../../Icons/ArrowDown.tsx"; import Dismiss from "../../../Icons/Dismiss.tsx"; -import { setListViewColumns } from "../../../../redux/fileManagerSlice.ts"; -import SessionManager, { UserSettings } from "../../../../session"; +import { FileManagerIndex } from "../../FileManager.tsx"; import AddColumn from "./AddColumn.tsx"; -import { useSnackbar } from "notistack"; -import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx"; +import { getColumnTypeDefaults, ListViewColumnSetting } from "./Column.tsx"; const ColumnSetting = () => { const { t } = useTranslation(); @@ -32,12 +31,8 @@ const ColumnSetting = () => { const { enqueueSnackbar } = useSnackbar(); const [columns, setColumns] = useState([]); - const open = useAppSelector( - (state) => state.globalState.listViewColumnSettingDialogOpen, - ); - const listViewColumns = useAppSelector( - (state) => state.fileManager[FileManagerIndex.main].listViewColumns, - ); + const open = useAppSelector((state) => state.globalState.listViewColumnSettingDialogOpen); + const listViewColumns = useAppSelector((state) => state.fileManager[FileManagerIndex.main].listViewColumns); useEffect(() => { if (open) { @@ -51,8 +46,7 @@ const ColumnSetting = () => { const onSubmitted = useCallback(() => { if (columns.length > 0) { - dispatch(setListViewColumns(columns)); - SessionManager.set(UserSettings.ListViewColumns, columns); + dispatch(applyListColumns(FileManagerIndex.main, columns)); } dispatch(setListViewColumnSettingDialog(false)); }, [dispatch, columns]); @@ -60,10 +54,7 @@ const ColumnSetting = () => { const onColumnAdded = useCallback( (column: ListViewColumnSetting) => { const existed = columns.find((c) => c.type === column.type); - if ( - !existed || - existed.props?.metadata_key != column.props?.metadata_key - ) { + if (!existed || existed.props?.metadata_key != column.props?.metadata_key) { setColumns((prev) => [...prev, column]); } else { enqueueSnackbar(t("application:fileManager.columnExisted"), { @@ -101,11 +92,7 @@ const ColumnSetting = () => { {columns.map((column, index) => ( - + {t(getColumnTypeDefaults(column).title)} diff --git a/src/component/FileManager/Explorer/ListView/ListHeader.tsx b/src/component/FileManager/Explorer/ListView/ListHeader.tsx index c0ea612..719ecf8 100644 --- a/src/component/FileManager/Explorer/ListView/ListHeader.tsx +++ b/src/component/FileManager/Explorer/ListView/ListHeader.tsx @@ -20,11 +20,7 @@ export interface ResizeProps { startX: number; } -const ListHeader = ({ - setColumns, - commitColumnSetting, - columns, -}: ListHeaderProps) => { +const ListHeader = ({ setColumns, commitColumnSetting, columns }: ListHeaderProps) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); const [showDivider, setShowDivider] = useState(false); @@ -44,14 +40,9 @@ const ListHeader = ({ const column = columns[resizeProps.current.index]; const currentWidth = column.width ?? column.defaults.width; const minWidth = column.defaults.minWidth ?? 100; - const newWidth = Math.max( - minWidth, - currentWidth + (e.clientX - resizeProps.current.startX), - ); + const newWidth = Math.max(minWidth, currentWidth + (e.clientX - resizeProps.current.startX)); setColumns((prev) => - prev.map((c, index) => - index === resizeProps.current?.index ? { ...c, width: newWidth } : c, - ), + prev.map((c, index) => (index === resizeProps.current?.index ? { ...c, width: newWidth } : c)), ); }, [columns, setColumns], @@ -65,16 +56,12 @@ const ListHeader = ({ }, [onMouseMove, commitColumnSetting]); const fmIndex = useContext(FmIndexContext); - const orderMethodOptions = useAppSelector( - (state) => state.fileManager[fmIndex].list?.props.order_by_options, - ); + const orderMethodOptions = useAppSelector((state) => state.fileManager[fmIndex].list?.props.order_by_options); const orderDirectionOption = useAppSelector( (state) => state.fileManager[fmIndex].list?.props.order_direction_options, ); const sortBy = useAppSelector((state) => state.fileManager[fmIndex].sortBy); - const sortDirection = useAppSelector( - (state) => state.fileManager[fmIndex].sortDirection, - ); + const sortDirection = useAppSelector((state) => state.fileManager[fmIndex].sortDirection); const allAvailableSortOptions = useMemo((): { [key: string]: boolean; @@ -83,10 +70,7 @@ const ListHeader = ({ const res: { [key: string]: boolean } = {}; orderMethodOptions.forEach((method) => { // make sure orderDirectionOption contains both asc and desc - if ( - orderDirectionOption.includes("asc") && - orderDirectionOption.includes("desc") - ) { + if (orderDirectionOption.includes("asc") && orderDirectionOption.includes("desc")) { res[method] = true; } }); @@ -96,8 +80,6 @@ const ListHeader = ({ const setSortBy = useCallback( (order_by: string, order_direction: string) => { dispatch(changeSortOption(fmIndex, order_by, order_direction)); - SessionManager.set(UserSettings.SortBy, order_by); - SessionManager.set(UserSettings.SortDirection, order_direction); }, [dispatch, fmIndex], ); @@ -119,15 +101,8 @@ const ListHeader = ({ key={index} column={column} setSortBy={setSortBy} - sortable={ - !!column.defaults.order_by && - allAvailableSortOptions[column.defaults.order_by] - } - sortDirection={ - sortBy && sortBy === column.defaults.order_by - ? sortDirection - : undefined - } + sortable={!!column.defaults.order_by && allAvailableSortOptions[column.defaults.order_by]} + sortDirection={sortBy && sortBy === column.defaults.order_by ? sortDirection : undefined} /> ))} @@ -139,11 +114,7 @@ const ListHeader = ({ }} > - dispatch(setListViewColumnSettingDialog(true))} - sx={{ ml: 1 }} - size={"small"} - > + dispatch(setListViewColumnSettingDialog(true))} sx={{ ml: 1 }} size={"small"}> state.fileManager[fmIndex].list?.recursion_limit_reached, - ); - const columnSetting = useAppSelector( - (state) => state.fileManager[fmIndex].listViewColumns, - ); + const recursion_limit_reached = useAppSelector((state) => state.fileManager[fmIndex].list?.recursion_limit_reached); + const columnSetting = useAppSelector((state) => state.fileManager[fmIndex].listViewColumns); const [columns, setColumns] = useState( columnSetting.map( @@ -64,10 +49,7 @@ const ListView = React.forwardRef( }, [columnSetting]); const totalWidth = useMemo(() => { - return columns.reduce( - (acc, column) => acc + (column.width ?? column.defaults.width), - 0, - ); + return columns.reduce((acc, column) => acc + (column.width ?? column.defaults.width), 0); }, [columns]); const commitColumnSetting = useCallback(() => { @@ -82,8 +64,7 @@ const ListView = React.forwardRef( return prev; }); if (settings.length > 0) { - dispatch(setListViewColumns(settings)); - SessionManager.set(UserSettings.ListViewColumns, settings); + dispatch(applyListColumns(fmIndex, settings)); } }, [dispatch, setColumns]); @@ -98,11 +79,7 @@ const ListView = React.forwardRef( flexDirection: "column", }} > - + {recursion_limit_reached && ( diff --git a/src/component/FileManager/Sidebar/BasicInfo.tsx b/src/component/FileManager/Sidebar/BasicInfo.tsx index 1a19cfc..c29d1fc 100644 --- a/src/component/FileManager/Sidebar/BasicInfo.tsx +++ b/src/component/FileManager/Sidebar/BasicInfo.tsx @@ -1,15 +1,16 @@ -import { FileResponse, FileType, FolderSummary, Metadata } from "../../../api/explorer.ts"; -import { useTranslation } from "react-i18next"; import { Link, Skeleton, Typography } from "@mui/material"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import InfoRow from "./InfoRow.tsx"; +import dayjs from "dayjs"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getFileInfo, sendPatchViewSync } from "../../../api/api.ts"; +import { ExplorerView, FileResponse, FileType, FolderSummary, Metadata } from "../../../api/explorer.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import SessionManager from "../../../session/index.ts"; import { sizeToString } from "../../../util"; -import FileBadge from "../FileBadge.tsx"; import CrUri from "../../../util/uri.ts"; import TimeBadge from "../../Common/TimeBadge.tsx"; -import { useAppDispatch } from "../../../redux/hooks.ts"; -import { getFileInfo } from "../../../api/api.ts"; -import dayjs from "dayjs"; +import FileBadge from "../FileBadge.tsx"; +import InfoRow from "./InfoRow.tsx"; export interface BasicInfoProps { target: FileResponse; @@ -25,6 +26,12 @@ const BasicInfo = ({ target }: BasicInfoProps) => { setFolderSummary(null); }, [target]); + const [viewSetting, setViewSetting] = useState(undefined); + + useEffect(() => { + setViewSetting(target?.extended_info?.view); + }, [target]); + const isSymbolicLink = useMemo(() => { return !!(target.metadata && target.metadata[Metadata.share_redirect]); }, [target.metadata]); @@ -68,6 +75,10 @@ const BasicInfo = ({ target }: BasicInfoProps) => { return new CrUri(target.path); }, [target]); + const viewSyncEnabled = useMemo(() => { + return !SessionManager.currentLoginOrNull()?.user?.disable_view_sync; + }, [target]); + const restoreParent = useMemo(() => { if (!target.metadata || !target.metadata[Metadata.restore_uri]) { return null; @@ -117,6 +128,16 @@ const BasicInfo = ({ target }: BasicInfoProps) => { }); }, [folderSummary, t]); + const handleDeleteViewSetting = useCallback(() => { + dispatch(sendPatchViewSync({ uri: target.path })) + .then(() => { + setViewSetting(undefined); + }) + .catch((error) => { + console.error("Failed to delete view setting:", error); + }); + }, [target.path, dispatch]); + return ( <> @@ -215,6 +236,30 @@ const BasicInfo = ({ target }: BasicInfoProps) => { title={t("application:fileManager.modifiedAt")} content={} /> + {target.type == FileType.folder && viewSyncEnabled && target.owned && !restoreParent && !isSymbolicLink && ( + + {t("application:fileManager.saved")}{" "} + { + e.preventDefault(); + handleDeleteViewSetting(); + }} + underline={"hover"} + > + {t("application:fileManager.deleteViewSetting")} + + + ) : ( + t("application:fileManager.notSet") + ) + } + /> + )} ); }; diff --git a/src/component/FileManager/TopBar/SortMethodMenu.tsx b/src/component/FileManager/TopBar/SortMethodMenu.tsx index 0776aeb..9fa3951 100644 --- a/src/component/FileManager/TopBar/SortMethodMenu.tsx +++ b/src/component/FileManager/TopBar/SortMethodMenu.tsx @@ -1,16 +1,9 @@ -import { - ListItemIcon, - ListItemText, - Menu, - MenuItem, - MenuProps, -} from "@mui/material"; +import { ListItemIcon, ListItemText, Menu, MenuItem, MenuProps } from "@mui/material"; +import { useContext, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; -import React, { useContext, useMemo } from "react"; -import Checkmark from "../../Icons/Checkmark.tsx"; import { changeSortOption } from "../../../redux/thunks/filemanager.ts"; -import SessionManager, { UserSettings } from "../../../session"; +import Checkmark from "../../Icons/Checkmark.tsx"; import { FmIndexContext } from "../FmIndexContext.tsx"; @@ -80,24 +73,17 @@ const SortMethodMenu = ({ onClose, ...rest }: MenuProps) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const fmIndex = useContext(FmIndexContext); - const orderMethodOptions = useAppSelector( - (state) => state.fileManager[fmIndex].list?.props.order_by_options, - ); + const orderMethodOptions = useAppSelector((state) => state.fileManager[fmIndex].list?.props.order_by_options); const orderDirectionOption = useAppSelector( (state) => state.fileManager[fmIndex].list?.props.order_direction_options, ); const sortBy = useAppSelector((state) => state.fileManager[fmIndex].sortBy); - const sortDirection = useAppSelector( - (state) => state.fileManager[fmIndex].sortDirection, - ); + const sortDirection = useAppSelector((state) => state.fileManager[fmIndex].sortDirection); const options = useMemo(() => { if (!orderMethodOptions || !orderDirectionOption) return []; const res: sortOption[] = []; - const selectedVal = - !sortBy || !sortDirection - ? "created_at_asc" - : `${sortBy}_${sortDirection}`; + const selectedVal = !sortBy || !sortDirection ? "created_at_asc" : `${sortBy}_${sortDirection}`; orderMethodOptions.forEach((method) => { orderDirectionOption.forEach((direction) => { const key = `${method}_${direction}`; @@ -110,25 +96,15 @@ const SortMethodMenu = ({ onClose, ...rest }: MenuProps) => { }, [orderMethodOptions, orderDirectionOption, sortBy, sortDirection]); const selectOption = (option: sortOption) => { - dispatch( - changeSortOption(fmIndex, option.order_by, option.order_direction), - ); - SessionManager.set(UserSettings.SortBy, option.order_by); - SessionManager.set(UserSettings.SortDirection, option.order_direction); + dispatch(changeSortOption(fmIndex, option.order_by, option.order_direction)); onClose && onClose({}, "escapeKeyDown"); }; return ( {options.map((option) => ( - selectOption(option)} - > - {!option.selected && ( - {t(option.label)} - )} + selectOption(option)}> + {!option.selected && {t(option.label)}} {option.selected && ( <> diff --git a/src/component/FileManager/TopBar/ViewOptionPopover.tsx b/src/component/FileManager/TopBar/ViewOptionPopover.tsx index 41001be..6140226 100644 --- a/src/component/FileManager/TopBar/ViewOptionPopover.tsx +++ b/src/component/FileManager/TopBar/ViewOptionPopover.tsx @@ -9,30 +9,29 @@ import { ToggleButtonGroup, Typography, } from "@mui/material"; +import React, { useContext, useEffect } from "react"; import { useTranslation } from "react-i18next"; +import { Layouts } from "../../../redux/fileManagerSlice.ts"; import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; -import AppsListOutlined from "../../Icons/AppsListOutlined.tsx"; -import GridOutlined from "../../Icons/GridOutlined.tsx"; -import Grid from "../../Icons/Grid.tsx"; -import AppsList from "../../Icons/AppsList.tsx"; -import NavIconTransition from "../../Frame/NavBar/NavIconTransition.tsx"; -import React, { useContext } from "react"; -import SessionManager, { UserSettings } from "../../../session"; import { - Layouts, - setGalleryWidth, - setLayout, - setShowThumb, -} from "../../../redux/fileManagerSlice.ts"; -import ImageOutlined from "../../Icons/ImageOutlined.tsx"; -import ImageOffOutlined from "../../Icons/ImageOffOutlined.tsx"; -import { changePageSize } from "../../../redux/thunks/filemanager.ts"; + applyGalleryWidth, + changePageSize, + setLayoutSetting, + setThumbToggle, +} from "../../../redux/thunks/filemanager.ts"; +import NavIconTransition from "../../Frame/NavBar/NavIconTransition.tsx"; +import AppsList from "../../Icons/AppsList.tsx"; +import AppsListOutlined from "../../Icons/AppsListOutlined.tsx"; +import Grid from "../../Icons/Grid.tsx"; +import GridOutlined from "../../Icons/GridOutlined.tsx"; import ImageCopy from "../../Icons/ImageCopy.tsx"; import ImageCopyOutlined from "../../Icons/ImageCopyOutlined.tsx"; +import ImageOffOutlined from "../../Icons/ImageOffOutlined.tsx"; +import ImageOutlined from "../../Icons/ImageOutlined.tsx"; -import { FmIndexContext } from "../FmIndexContext.tsx"; -import Setting from "../../Icons/Setting.tsx"; import { setListViewColumnSettingDialog } from "../../../redux/globalStateSlice.ts"; +import Setting from "../../Icons/Setting.tsx"; +import { FmIndexContext } from "../FmIndexContext.tsx"; const layoutOptions: { label: string; @@ -80,71 +79,44 @@ const ViewOptionPopover = ({ ...rest }: PopoverProps) => { const dispatch = useAppDispatch(); const fmIndex = useContext(FmIndexContext); const layout = useAppSelector((state) => state.fileManager[fmIndex].layout); - const showThumb = useAppSelector( - (state) => state.fileManager[fmIndex].showThumb, - ); - const pageSize = useAppSelector( - (state) => state.fileManager[fmIndex].pageSize, - ); - const pageSizeMax = useAppSelector( - (state) => state.fileManager[fmIndex].list?.props.max_page_size, - ); - const galleryWidth = useAppSelector( - (state) => state.fileManager[fmIndex].galleryWidth, - ); + const showThumb = useAppSelector((state) => state.fileManager[fmIndex].showThumb); + const pageSize = useAppSelector((state) => state.fileManager[fmIndex].pageSize); + const pageSizeMax = useAppSelector((state) => state.fileManager[fmIndex].list?.props.max_page_size); + const galleryWidth = useAppSelector((state) => state.fileManager[fmIndex].galleryWidth); const [desiredPageSize, setDesiredPageSize] = React.useState(pageSize); const pageSizeMaxSafe = pageSizeMax ?? desiredPageSize; const step = pageSizeMaxSafe - MinPageSize <= 100 ? 1 : 10; - const [desiredImageWidth, setDesiredImageWidth] = - React.useState(galleryWidth); + const [desiredImageWidth, setDesiredImageWidth] = React.useState(galleryWidth); - const handleLayoutChange = ( - _event: React.MouseEvent, - newMode: string, - ) => { + useEffect(() => { + setDesiredPageSize(pageSize); + }, [pageSize]); + + const handleLayoutChange = (_event: React.MouseEvent, newMode: string) => { if (newMode) { - dispatch(setLayout({ index: fmIndex, value: newMode })); - SessionManager.set(UserSettings.Layout, newMode); + dispatch(setLayoutSetting(fmIndex, newMode)); } }; - const handleThumbChange = ( - _event: React.MouseEvent, - newMode: boolean, - ) => { - dispatch(setShowThumb({ index: fmIndex, value: newMode })); - SessionManager.set(UserSettings.ShowThumb, newMode); + const handleThumbChange = (_event: React.MouseEvent, newMode: boolean) => { + dispatch(setThumbToggle(fmIndex, newMode)); }; - const handlePageSlideChange = ( - _event: Event, - newValue: number | number[], - ) => { + const handlePageSlideChange = (_event: Event, newValue: number | number[]) => { setDesiredPageSize(newValue as number); }; - const commitPageSize = ( - _event: React.SyntheticEvent | Event, - newValue: number | number[], - ) => { + const commitPageSize = (_event: React.SyntheticEvent | Event, newValue: number | number[]) => { const pageSize = Math.max(MinPageSize, newValue as number); - SessionManager.set(UserSettings.PageSize, pageSize); dispatch(changePageSize(fmIndex, pageSize)); }; - const handleImageSizeChange = ( - _event: Event, - newValue: number | number[], - ) => { + const handleImageSizeChange = (_event: Event, newValue: number | number[]) => { setDesiredImageWidth(newValue as number); }; - const commitImageSize = ( - _event: React.SyntheticEvent | Event, - newValue: number | number[], - ) => { - SessionManager.set(UserSettings.GalleryWidth, newValue as number); - dispatch(setGalleryWidth({ index: fmIndex, value: newValue as number })); + const commitImageSize = (_event: React.SyntheticEvent | Event, newValue: number | number[]) => { + dispatch(applyGalleryWidth(fmIndex, newValue as number)); }; return ( @@ -161,11 +133,7 @@ const ViewOptionPopover = ({ ...rest }: PopoverProps) => { > - + {t("application:fileManager.layout")} { - + {t("application:fileManager.thumbnails")} { > {thumbOptions.map((option) => ( - + {t(option.label)} ))} - + {t("application:fileManager.listColumnSetting")} - - dispatch(setListViewColumnSettingDialog(true))} - > + + dispatch(setListViewColumnSettingDialog(true))}> {t("application:fileManager.listColumnSetting")} - + {t("application:fileManager.imageSize")} @@ -269,11 +213,7 @@ const ViewOptionPopover = ({ ...rest }: PopoverProps) => { - + {t("application:fileManager.paginationSize")} diff --git a/src/component/Icons/RectangleLandscapeSync.tsx b/src/component/Icons/RectangleLandscapeSync.tsx new file mode 100644 index 0000000..c21b4b6 --- /dev/null +++ b/src/component/Icons/RectangleLandscapeSync.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function RectangleLandscapeSync(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/RectangleLandscapeSyncOff.tsx b/src/component/Icons/RectangleLandscapeSyncOff.tsx new file mode 100644 index 0000000..c2e0d69 --- /dev/null +++ b/src/component/Icons/RectangleLandscapeSyncOff.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function RectangleLandscapeSync(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Pages/Setting/PreferenceSetting.tsx b/src/component/Pages/Setting/PreferenceSetting.tsx index a38bb30..79484ea 100644 --- a/src/component/Pages/Setting/PreferenceSetting.tsx +++ b/src/component/Pages/Setting/PreferenceSetting.tsx @@ -9,6 +9,8 @@ import { ListItemText, Stack, styled, + ToggleButton, + ToggleButtonGroup, Typography, useMediaQuery, useTheme, @@ -21,6 +23,7 @@ import { UserSettings as UserSettingsType } from "../../../api/user.ts"; import { languages } from "../../../i18n.ts"; import { setPreferredTheme } from "../../../redux/globalStateSlice.ts"; import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { clearLocalCustomView } from "../../../redux/thunks/filemanager.ts"; import { selectLanguage } from "../../../redux/thunks/settings.ts"; import SessionManager, { UserSettings } from "../../../session"; import { refreshTimeZone, timeZone } from "../../../util/datetime.ts"; @@ -33,6 +36,8 @@ import { import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx"; import { ColorCircle, SelectorBox } from "../../FileManager/FileInfo/ColorCircle/CircleColorSelector.tsx"; import { SwitchPopover } from "../../Frame/NavBar/DarkThemeSwitcher.tsx"; +import RectangleLandscapeSync from "../../Icons/RectangleLandscapeSync.tsx"; +import RectangleLandscapeSyncOff from "../../Icons/RectangleLandscapeSyncOff.tsx"; import SettingForm from "./SettingForm.tsx"; export interface PreferenceSettingProps { @@ -141,6 +146,30 @@ const PreferenceSetting = ({ setting, setSetting }: PreferenceSettingProps) => { }); }; + const onDisableViewSyncChange = (e: React.MouseEvent, enabled: boolean) => { + setSetting({ ...setting, disable_view_sync: !enabled }); + setLoading(true); + dispatch( + sendUpdateUserSetting({ + disable_view_sync: !enabled, + }), + ) + .then(() => { + const user = SessionManager.currentLoginOrNull(); + if (user?.user) { + SessionManager.updateUserIfExist({ + ...user?.user, + disable_view_sync: !enabled, + }); + } + clearLocalCustomView(); + setLoading(false); + }) + .finally(() => { + setLoading(false); + }); + }; + return ( @@ -276,6 +305,27 @@ const PreferenceSetting = ({ setting, setSetting }: PreferenceSettingProps) => { + + + + + {t("setting.syncViewOn")} + + + + {t("setting.syncViewOff")} + + + {t("setting.syncViewDes")} + ); }; diff --git a/src/redux/thunks/filemanager.ts b/src/redux/thunks/filemanager.ts index 23f10f5..5681501 100644 --- a/src/redux/thunks/filemanager.ts +++ b/src/redux/thunks/filemanager.ts @@ -1,14 +1,15 @@ import dayjs from "dayjs"; -import { getFileInfo, getFileList, getUserCapacity } from "../../api/api.ts"; -import { FileResponse, ListResponse, Metadata } from "../../api/explorer.ts"; +import { getFileInfo, getFileList, getUserCapacity, sendPatchViewSync } from "../../api/api.ts"; +import { ExplorerView, FileResponse, ListResponse, Metadata } from "../../api/explorer.ts"; import { getActionOpt } from "../../component/FileManager/ContextMenu/useActionDisplayOpt.ts"; +import { ListViewColumnSetting } from "../../component/FileManager/Explorer/ListView/Column.tsx"; import { FileManagerIndex } from "../../component/FileManager/FileManager.tsx"; import { Condition, ConditionType } from "../../component/FileManager/Search/AdvanceSearch/ConditionBox.tsx"; import { MinPageSize } from "../../component/FileManager/TopBar/ViewOptionPopover.tsx"; import { SelectType } from "../../component/Uploader/core"; import { defaultPath } from "../../hooks/useNavigation.tsx"; import { router } from "../../router"; -import SessionManager from "../../session"; +import SessionManager, { UserSettings } from "../../session"; import { getFileLinkedUri, sleep } from "../../util"; import CrUri, { Filesystem, SearchParam, UriQuery } from "../../util/uri.ts"; import { @@ -19,16 +20,21 @@ import { clearSelected, closeContextMenu, ContextMenuTypes, + Layouts, resetFileManager, setCapacity, setContextMenu, setFmError, setFmLoading, + setGalleryWidth, + setLayout, + setListViewColumns, setMultiSelectHovered, setPage, setPageSize, setPathProps, setSelected, + setShowThumb, setSortOption, SingleManager, } from "../fileManagerSlice.ts"; @@ -82,6 +88,7 @@ export function setTargetPath(index: number, path: string): AppThunk { let generation = 0; export interface NavigateReconcileOptions { next_page?: boolean; + sync_view?: boolean; } const pageSize = (fm: SingleManager) => { @@ -110,7 +117,7 @@ export function navigateReconcile(index: number, opt?: NavigateReconcileOptions) fileManager, globalState: { sidebarOpen, imageViewer }, } = getState(); - const { path, list } = fileManager[index]; + const { path, list, pure_path } = fileManager[index]; if (!path) { return; } @@ -121,16 +128,30 @@ export function navigateReconcile(index: number, opt?: NavigateReconcileOptions) } dispatch(setFmError({ index, value: undefined })); + if (opt?.sync_view) { + try { + await dispatch(syncViewSettings(index)); + } catch (e) {} + } + + const currentLogin = SessionManager.currentLoginOrNull(); + const currentView = localCustomView[pure_path ?? ""]; + let useCustomView = currentLogin?.user.disable_view_sync || currentView; + let listRes: ListResponse | null = null; try { listRes = await dispatch( getFileList({ next_page_token: opt && opt.next_page ? list?.pagination.next_token : undefined, - page_size: pageSize(fileManager[index]), uri: path, - order_by: fileManager[index].sortBy, - order_direction: fileManager[index].sortDirection, page: list?.pagination.page ?? undefined, + ...(useCustomView + ? { + page_size: currentView?.page_size ?? pageSize(fileManager[index]), + order_by: currentView?.order ?? fileManager[index].sortBy, + order_direction: currentView?.order_direction ?? fileManager[index].sortDirection, + } + : {}), }), ); } catch (e) { @@ -163,6 +184,42 @@ export function navigateReconcile(index: number, opt?: NavigateReconcileOptions) }), ); + if (listRes.view) { + // Apply view setting from cloud + dispatch(setPageSize({ index, value: listRes.view.page_size })); + dispatch( + setSortOption({ index, value: [listRes.view.order ?? "created_at", listRes.view.order_direction ?? "asc"] }), + ); + if (!currentView) { + dispatch(setShowThumb({ index, value: !!listRes.view.thumbnail })); + dispatch(setLayout({ index, value: listRes.view.view ?? Layouts.grid })); + dispatch( + setListViewColumns(listRes.view.columns ?? SessionManager.getWithFallback(UserSettings.ListViewColumns)), + ); + dispatch( + setGalleryWidth({ + index, + value: listRes.view.gallery_width ?? SessionManager.getWithFallback(UserSettings.GalleryWidth), + }), + ); + } + } + + if (currentView) { + // Apply view setting from local cache + dispatch(setShowThumb({ index, value: !!currentView.thumbnail })); + dispatch(setLayout({ index, value: currentView.view ?? Layouts.grid })); + dispatch( + setListViewColumns(currentView.columns ?? SessionManager.getWithFallback(UserSettings.ListViewColumns)), + ); + dispatch( + setGalleryWidth({ + index, + value: currentView.gallery_width ?? SessionManager.getWithFallback(UserSettings.GalleryWidth), + }), + ); + } + if (opt && opt.next_page) { dispatch(appendListResponse({ index, value: listRes })); } else { @@ -226,8 +283,9 @@ export function loadChild(index: number, path: string, beforeLoad?: () => void): export function changePageSize(index: number, pageSize: number): AppThunk { return async (dispatch, _getState) => { + SessionManager.set(UserSettings.PageSize, pageSize); dispatch(setPageSize({ index, value: pageSize })); - dispatch(navigateReconcile(index)); + dispatch(navigateReconcile(index, { sync_view: true })); }; } @@ -241,7 +299,9 @@ export function changePage(index: number, page: number): AppThunk { export function changeSortOption(index: number, sortBy: string, sortDirection: string): AppThunk { return async (dispatch, _getState) => { dispatch(setSortOption({ index, value: [sortBy, sortDirection] })); - dispatch(navigateReconcile(index)); + SessionManager.set(UserSettings.SortBy, sortBy); + SessionManager.set(UserSettings.SortDirection, sortDirection); + dispatch(navigateReconcile(index, { sync_view: true })); }; } @@ -562,3 +622,89 @@ export function openContextUrlFromUri(index: number, uri: string, e: React.Mouse dispatch(openFileContextMenu(index, file, true, e, ContextMenuTypes.file, false)); }; } + +export function setThumbToggle(index: number, value: boolean): AppThunk { + return async (dispatch, _getState) => { + dispatch(setFmLoading({ index, value: true })); + await dispatch(syncViewSettings(index, undefined, undefined, value)); + dispatch(setShowThumb({ index: index, value: value })); + SessionManager.set(UserSettings.ShowThumb, value); + dispatch(setFmLoading({ index, value: false })); + }; +} + +export function setLayoutSetting(index: number, value: string): AppThunk { + return async (dispatch, _getState) => { + dispatch(setFmLoading({ index, value: true })); + dispatch(setLayout({ index: index, value: value })); + SessionManager.set(UserSettings.Layout, value); + await dispatch(syncViewSettings(index)); + dispatch(setFmLoading({ index, value: false })); + }; +} + +let localCustomView: Record = {}; + +export const clearLocalCustomView = () => { + localCustomView = {}; +}; + +export function syncViewSettings( + index: number, + columns?: ListViewColumnSetting[], + galleryWidth?: number, + thumbOff?: boolean, +): AppThunk { + return async (dispatch, getState) => { + const fm = getState().fileManager[index]; + const currentLogin = SessionManager.currentLoginOrNull(); + if (!fm.list || !fm.pure_path) { + return; + } + const parent = fm.list.parent; + const crUri = new CrUri(fm.pure_path); + const shouldUpdatedView = + currentLogin && + !currentLogin.user.disable_view_sync && + (parent?.owned || crUri.fs() == Filesystem.trash || crUri.fs() == Filesystem.shared_with_me); + + const currentView: ExplorerView = { + page_size: pageSize(fm), + order: fm.sortBy ?? "created_at", + order_direction: fm.sortDirection ?? "asc", + view: fm.layout ?? Layouts.grid, + thumbnail: thumbOff ?? fm.showThumb, + columns: columns ?? fm.listViewColumns, + gallery_width: galleryWidth ?? fm.galleryWidth ?? 110, + }; + + if (shouldUpdatedView) { + await dispatch( + sendPatchViewSync({ + uri: fm.pure_path, + view: currentView, + }), + ); + } else { + localCustomView[fm.pure_path] = currentView; + } + }; +} + +export function applyListColumns(index: number, columns: ListViewColumnSetting[]): AppThunk { + return async (dispatch, _getState) => { + dispatch(setListViewColumns(columns)); + SessionManager.set(UserSettings.ListViewColumns, columns); + dispatch(syncViewSettings(index, columns)); + }; +} + +export function applyGalleryWidth(index: number, width: number): AppThunk { + return async (dispatch, _getState) => { + dispatch(setFmLoading({ index, value: true })); + await dispatch(syncViewSettings(index, undefined, width)); + dispatch(setGalleryWidth({ index, value: width })); + SessionManager.set(UserSettings.GalleryWidth, width); + dispatch(setFmLoading({ index, value: false })); + }; +} diff --git a/src/redux/thunks/share.ts b/src/redux/thunks/share.ts index a027607..003d0d2 100644 --- a/src/redux/thunks/share.ts +++ b/src/redux/thunks/share.ts @@ -20,6 +20,7 @@ export function createOrUpdateShareLink( const req: ShareCreateService = { uri: file.path, is_private: setting.is_private, + share_view: setting.share_view, downloads: setting.downloads && setting.downloads_val.value > 0 ? setting.downloads_val.value : undefined, expire: setting.expires && setting.expires_val.value > 0 ? setting.expires_val.value : undefined, };