feat(share): download-all action on share pages (#3397)

Adds a "Download all" entry to the share page's more-actions menu. It
walks every page of the listing (the visible list is only the first
page) and feeds the whole set into the existing multi-download flow.
Gated on the parent navigator's download_file capability, so
preview-only and upload-only shares do not offer it.

Authored By: TDvorak <info@tdvorak.dev>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
pull/3582/head
Tomas Dvorak 2 weeks ago
parent 649f361385
commit 8feb55fb82

@ -470,7 +470,8 @@
"ftsStatus": "Full-text index",
"ftsIndexed": "Indexed",
"ftsNotIndexed": "Not indexed",
"lineEnding": "Line ending"
"lineEnding": "Line ending",
"downloadAll": "Download all"
},
"modals": {
"includePasswordInShareLink": "Include password in share link",

@ -470,7 +470,8 @@
"ftsStatus": "全文索引",
"ftsIndexed": "已索引",
"ftsNotIndexed": "未索引",
"lineEnding": "换行符"
"lineEnding": "换行符",
"downloadAll": "全部下载"
},
"modals": {
"includePasswordInShareLink": "在链接中包含密码",

@ -4,15 +4,19 @@ import { useCallback, useContext } from "react";
import { useTranslation } from "react-i18next";
import { clearSelected } from "../../../redux/fileManagerSlice.ts";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
import { NavigatorCapability } from "../../../api/explorer.ts";
import { downloadAll } from "../../../redux/thunks/download.ts";
import { createShareShortcut, isMacbook } from "../../../redux/thunks/file.ts";
import { inverseSelection, pinCurrentView, refreshFileList, selectAll } from "../../../redux/thunks/filemanager.ts";
import SessionManager from "../../../session";
import Boolset from "../../../util/boolset.ts";
import { Filesystem } from "../../../util/uri.ts";
import { KeyIndicator } from "../../Frame/NavBar/SearchBar.tsx";
import ArrowSync from "../../Icons/ArrowSync.tsx";
import Border from "../../Icons/Border.tsx";
import BorderAll from "../../Icons/BorderAll.tsx";
import BorderInside from "../../Icons/BorderInside.tsx";
import CloudDownloadOutlined from "../../Icons/CloudDownloadOutlined.tsx";
import FolderLink from "../../Icons/FolderLink.tsx";
import PinOutlined from "../../Icons/PinOutlined.tsx";
import { DenseDivider, SquareMenu, SquareMenuItem } from "../ContextMenu/ContextMenu.tsx";
@ -22,6 +26,8 @@ const MoreActionMenu = ({ onClose, ...rest }: MenuProps) => {
const { t } = useTranslation();
const fmIndex = useContext(FmIndexContext);
const fs = useAppSelector((state) => state.fileManager[fmIndex].current_fs);
const parentCapability = useAppSelector((state) => state.fileManager[fmIndex].list?.parent?.capability);
const canDownloadAll = new Boolset(parentCapability).enabled(NavigatorCapability.download_file);
const dispatch = useAppDispatch();
const isLogin = !!SessionManager.currentLoginOrNull();
const theme = useTheme();
@ -62,6 +68,11 @@ const MoreActionMenu = ({ onClose, ...rest }: MenuProps) => {
dispatch(refreshFileList(fmIndex));
}, [dispatch, onClose, fmIndex]);
const onDownloadAllClicked = useCallback(() => {
onClose && onClose({}, "escapeKeyDown");
dispatch(downloadAll(fmIndex));
}, [dispatch, onClose, fmIndex]);
return (
<SquareMenu
MenuListProps={{
@ -104,6 +115,14 @@ const MoreActionMenu = ({ onClose, ...rest }: MenuProps) => {
<ListItemText>{t("application:fileManager.saveShortcut")}</ListItemText>
</SquareMenuItem>
)}
{fs == Filesystem.share && canDownloadAll && (
<SquareMenuItem onClick={onDownloadAllClicked}>
<ListItemIcon>
<CloudDownloadOutlined fontSize="small" />
</ListItemIcon>
<ListItemText>{t("application:fileManager.downloadAll")}</ListItemText>
</SquareMenuItem>
)}
{isLogin && <DenseDivider />}
<SquareMenuItem onClick={onSelectAllClicked}>
<ListItemIcon>

@ -2,8 +2,8 @@ import dayjs from "dayjs";
import i18next from "i18next";
import { closeSnackbar, enqueueSnackbar } from "notistack";
import streamSaver from "streamsaver";
import { getFileEntityUrl } from "../../api/api.ts";
import { FileResponse, FileType, Metadata } from "../../api/explorer.ts";
import { getFileEntityUrl, getFileList } from "../../api/api.ts";
import { FileResponse, FileType, ListResponse, Metadata } from "../../api/explorer.ts";
import { GroupPermission } from "../../api/user.ts";
import {
DefaultCloseAction,
@ -51,6 +51,32 @@ export function downloadFiles(index: number, files: FileResponse[]): AppThunk {
};
}
// downloadAll fetches every page of the current directory listing and
// downloads the whole set — the visible list may be only the first page.
export function downloadAll(index: number): AppThunk {
return async (dispatch, getState) => {
const path = getState().fileManager[index]?.path;
if (!path) {
return;
}
const files: FileResponse[] = [];
let token: string | undefined = undefined;
do {
const res: ListResponse = await dispatch(
getFileList({ uri: path, next_page_token: token, page_size: 1000 }),
);
files.push(...(res.files ?? []));
token = res.pagination?.next_token;
} while (token);
if (files.length == 0) {
return;
}
await dispatch(downloadFiles(index, files));
};
}
export function downloadMultipleFiles(files: FileResponse[]): AppThunk {
return async (dispatch, _getState) => {
// Prepare download options

Loading…
Cancel
Save