feat(remote download): unified UI for torrent and URL download, improve path selection experience

This commit is contained in:
HFO4 2022-10-31 20:56:34 +08:00
parent a00408f333
commit 5ff53fe47f
8 changed files with 345 additions and 245 deletions

View File

@ -197,7 +197,7 @@
"newRemoteDownloadTitle": "New remote download task",
"remoteDownloadURL": "Download target URL",
"remoteDownloadURLDescription": "Paste the download URL, one URL per line, support HTTP(s) / FTP / Magnet link",
"remoteDownloadDst": "Select a download destination",
"remoteDownloadDst": "Download to",
"createTask": "Creat task",
"downloadTo": "Download to <0>{{name}}</0>",
"decompressTo": "Decompress to",

View File

@ -195,9 +195,9 @@
"deleteOneDescription": "确定要删除 <0>{{name}}</0> 吗?",
"deleteMultipleDescription": "确定要删除这 {{num}} 个对象吗?",
"newRemoteDownloadTitle": "新建离线下载任务",
"remoteDownloadURL": "文件地址",
"remoteDownloadURL": "下载链接",
"remoteDownloadURLDescription": "输入文件下载地址,一行一个,支持 HTTP(s) / FTP / 磁力链",
"remoteDownloadDst": "选择存储位置",
"remoteDownloadDst": "下载至",
"createTask": "创建任务",
"downloadTo": "下载至 <0>{{name}}</0>",
"decompressTo": "解压缩至",

View File

@ -34,12 +34,12 @@ import pathHelper from "../../utils/page";
import RefreshIcon from "@material-ui/icons/Refresh";
import {
batchGetSource,
openPreview,
openPreview, openTorrentDownload,
setSelectedTarget,
startBatchDownload,
startDirectoryDownload,
startDownload,
toggleObjectInfoSidebar,
toggleObjectInfoSidebar
} from "../../redux/explorer/action";
import {
changeContextMenu,
@ -56,7 +56,6 @@ import {
openRemoveDialog,
openRenameDialog,
openShareDialog,
openTorrentDownloadDialog,
refreshFileList,
setNavigatorLoadingStatus,
showImgPreivew,
@ -142,7 +141,7 @@ const mapDispatchToProps = (dispatch) => {
dispatch(openRemoteDownloadDialog());
},
openTorrentDownloadDialog: () => {
dispatch(openTorrentDownloadDialog());
dispatch(openTorrentDownload());
},
openCopyDialog: () => {
dispatch(openCopyDialog());

View File

@ -32,6 +32,7 @@ import {
import OptionSelector from "../Modals/OptionSelector";
import { getDownloadURL } from "../../services/file";
import { Trans, withTranslation } from "react-i18next";
import RemoteDownload from "../Modals/RemoteDownload";
const styles = (theme) => ({
wrapper: {
@ -100,8 +101,6 @@ class ModalsCompoment extends Component {
secretShare: false,
sharePwd: "",
shareUrl: "",
downloadURL: "",
remoteDownloadPathSelect: false,
purchaseCallback: null,
};
@ -413,83 +412,6 @@ class ModalsCompoment extends Component {
//this.props.toggleSnackbar();
};
submitTorrentDownload = (e) => {
e.preventDefault();
this.props.setModalsLoading(true);
API.post("/aria2/torrent/" + this.props.selected[0].id, {
dst:
this.state.selectedPath === "//"
? "/"
: this.state.selectedPath,
})
.then(() => {
this.props.toggleSnackbar(
"top",
"right",
this.props.t("modals.taskCreated"),
"success"
);
this.onClose();
this.props.setModalsLoading(false);
})
.catch((error) => {
this.props.toggleSnackbar(
"top",
"right",
error.message,
"error"
);
this.props.setModalsLoading(false);
});
};
submitDownload = (e) => {
e.preventDefault();
this.props.setModalsLoading(true);
API.post("/aria2/url", {
url: this.state.downloadURL.split("\n"),
dst:
this.state.selectedPath === "//"
? "/"
: this.state.selectedPath,
})
.then((response) => {
const failed = response.data
.filter((r) => r.code !== 0)
.map((r) => new AppError(r.msg, r.code, r.error).message);
if (failed.length > 0) {
this.props.toggleSnackbar(
"top",
"right",
this.props.t("modals.taskCreateFailed", {
failed: failed.length,
details: failed.join(","),
}),
"warning"
);
} else {
this.props.toggleSnackbar(
"top",
"right",
this.props.t("modals.taskCreated"),
"success"
);
}
this.onClose();
this.props.setModalsLoading(false);
})
.catch((error) => {
this.props.toggleSnackbar(
"top",
"right",
error.message,
"error"
);
this.props.setModalsLoading(false);
});
};
setMoveTarget = (folder) => {
const path =
folder.path === "/"
@ -501,13 +423,6 @@ class ModalsCompoment extends Component {
});
};
remoteDownloadNext = () => {
this.props.closeAllModals();
this.setState({
remoteDownloadPathSelect: true,
});
};
onClose = () => {
this.setState({
newFolderName: "",
@ -517,9 +432,7 @@ class ModalsCompoment extends Component {
selectedPathName: "",
secretShare: false,
sharePwd: "",
downloadURL: "",
shareUrl: "",
remoteDownloadPathSelect: false,
});
this.newNameSuffix = "";
this.props.closeAllModals();
@ -848,150 +761,14 @@ class ModalsCompoment extends Component {
setModalsLoading={this.props.setModalsLoading}
selected={this.props.selected}
/>
<Dialog
<RemoteDownload
open={this.props.modalsStatus.remoteDownload}
onClose={this.onClose}
aria-labelledby="form-dialog-title"
fullWidth
>
<DialogTitle id="form-dialog-title">
{t("modals.newRemoteDownloadTitle")}
</DialogTitle>
<DialogContent>
<DialogContentText>
<TextField
label={t("modals.remoteDownloadURL")}
autoFocus
fullWidth
multiline
id="downloadURL"
onChange={this.handleInputChange}
placeholder={t(
"modals.remoteDownloadURLDescription"
)}
/>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={this.onClose}>
{t("cancel", { ns: "common" })}
</Button>
<Button
onClick={this.remoteDownloadNext}
color="primary"
disabled={
this.props.modalsLoading ||
this.state.downloadURL === ""
}
>
{t("ok", { ns: "common" })}
</Button>
</DialogActions>
</Dialog>
<Dialog
open={this.state.remoteDownloadPathSelect}
onClose={this.onClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">
{t("modals.remoteDownloadDst")}
</DialogTitle>
<PathSelector
presentPath={this.props.path}
selected={this.props.selected}
onSelect={this.setMoveTarget}
/>
{this.state.selectedPath !== "" && (
<DialogContent className={classes.contentFix}>
<DialogContentText>
<Trans
i18nKey="modals.downloadTo"
values={{
name: this.state.selectedPathName,
}}
components={[<strong key={0} />]}
/>
</DialogContentText>
</DialogContent>
)}
<DialogActions>
<Button onClick={this.onClose}>
{t("cancel", { ns: "common" })}
</Button>
<div className={classes.wrapper}>
<Button
onClick={this.submitDownload}
color="primary"
disabled={
this.state.selectedPath === "" ||
this.props.modalsLoading
}
>
{t("modals.createTask")}
{this.props.modalsLoading && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</div>
</DialogActions>
</Dialog>
<Dialog
open={this.props.modalsStatus.torrentDownload}
onClose={this.onClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">
{t("modals.remoteDownloadDst")}
</DialogTitle>
<PathSelector
presentPath={this.props.path}
selected={this.props.selected}
onSelect={this.setMoveTarget}
/>
{this.state.selectedPath !== "" && (
<DialogContent className={classes.contentFix}>
<DialogContentText>
<Trans
i18nKey="modals.downloadTo"
values={{
name: this.state.selectedPathName,
}}
components={[<strong key={0} />]}
/>
</DialogContentText>
</DialogContent>
)}
<DialogActions>
<Button onClick={this.onClose}>
{t("cancel", { ns: "common" })}
</Button>
<div className={classes.wrapper}>
<Button
onClick={this.submitTorrentDownload}
color="primary"
disabled={
this.state.selectedPath === "" ||
this.props.modalsLoading
}
>
{t("modals.createTask")}
{this.props.modalsLoading && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</div>
</DialogActions>
</Dialog>
modalsLoading={this.props.modalsLoading}
setModalsLoading={this.props.setModalsLoading}
presentPath={this.props.path}
torrent={this.props.modalsStatus.remoteDownloadTorrent}
/>
<DecompressDialog
open={this.props.modalsStatus.decompress}
onClose={this.onClose}

View File

@ -0,0 +1,312 @@
import React, { useCallback, useEffect, useState } from "react";
import {
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
makeStyles, TextField
} from "@material-ui/core";
import PathSelector from "../FileManager/PathSelector";
import { useDispatch } from "react-redux";
import API, { AppError } from "../../middleware/Api";
import {
refreshFileList,
setModalsLoading,
toggleSnackbar,
} from "../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
import { FolderOpenOutlined } from "@material-ui/icons";
import { pathBack } from "../../utils";
import InputAdornment from "@material-ui/core/InputAdornment";
import { AccountCircle } from "mdi-material-ui";
import { useTheme } from "@material-ui/core/styles";
import useMediaQuery from "@material-ui/core/useMediaQuery";
import LinkIcon from "@material-ui/icons/Link";
import Chip from "@material-ui/core/Chip";
const useStyles = makeStyles((theme) => ({
contentFix: {
padding: "10px 24px 0px 24px",
},
wrapper: {
margin: theme.spacing(1),
position: "relative",
},
buttonProgress: {
color: theme.palette.secondary.light,
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
formGroup: {
display: "flex",
marginBottom: theme.spacing(3),
},
forumInput: {
flexGrow: 1,
}
}));
export default function RemoteDownload(props) {
const { t } = useTranslation();
const [selectPathOpen,setSelectPathOpen] = useState(false);
const [selectedPath, setSelectedPath] = useState("");
const [selectedPathName, setSelectedPathName] = useState("");
const [downloadTo, setDownloadTo] = useState("");
const [url, setUrl] = useState("");
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
useEffect(()=>{
if (props.open){
setDownloadTo(props.presentPath)
}
},[props.open])
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const SetModalsLoading = useCallback(
(status) => {
dispatch(setModalsLoading(status));
},
[dispatch]
);
const setDownloadToPath = (folder) => {
const path =
folder.path === "/"
? folder.path + folder.name
: folder.path + "/" + folder.name;
setSelectedPath(path);
setSelectedPathName(folder.name);
};
const selectPath = () => {
setDownloadTo(selectedPath === "//" ? "/" : selectedPath);
setSelectPathOpen(false);
};
const submitTorrentDownload = (e) => {
e.preventDefault();
props.setModalsLoading(true);
API.post("/aria2/torrent/" + props.torrent.id, {
dst:
downloadTo === "//"
? "/"
: downloadTo,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
t("modals.taskCreated"),
"success"
);
props.onClose();
props.setModalsLoading(false);
})
.catch((error) => {
ToggleSnackbar(
"top",
"right",
error.message,
"error"
);
props.setModalsLoading(false);
});
};
const submitDownload = (e) => {
e.preventDefault();
props.setModalsLoading(true);
API.post("/aria2/url", {
url: url.split("\n"),
dst:
downloadTo === "//"
? "/"
: downloadTo,
})
.then((response) => {
const failed = response.data
.filter((r) => r.code !== 0)
.map((r) => new AppError(r.msg, r.code, r.error).message);
if (failed.length > 0) {
ToggleSnackbar(
"top",
"right",
t("modals.taskCreateFailed", {
failed: failed.length,
details: failed.join(","),
}),
"warning"
);
} else {
ToggleSnackbar(
"top",
"right",
t("modals.taskCreated"),
"success"
);
}
props.onClose();
props.setModalsLoading(false);
})
.catch((error) => {
ToggleSnackbar(
"top",
"right",
error.message,
"error"
);
props.setModalsLoading(false);
});
};
const classes = useStyles();
return (
<>
<Dialog
open={props.open}
onClose={props.onClose}
aria-labelledby="form-dialog-title"
fullWidth
>
<DialogTitle id="form-dialog-title">
{t("modals.newRemoteDownloadTitle")}
</DialogTitle>
<DialogContent>
<DialogContentText>
<div className={classes.formGroup}>
<div className={classes.forumInput}>
<TextField
variant={"outlined"}
label={t("modals.remoteDownloadURL")}
autoFocus
fullWidth
disabled={props.torrent}
multiline
value={props.torrent?props.torrent.name:url}
onChange={(e) => setUrl(e.target.value)}
placeholder={t(
"modals.remoteDownloadURLDescription"
)}
InputProps={{
startAdornment: !isMobile&&(
<InputAdornment position="start">
<LinkIcon />
</InputAdornment>
),
}}
/>
</div>
</div>
<div className={classes.formGroup}>
<div className={classes.forumInput}>
<TextField
variant={"outlined"}
fullWidth
value={downloadTo}
onChange={(e) => setDownloadTo(e.target.value)}
className={classes.input}
label={t("modals.remoteDownloadDst")}
InputProps={{
startAdornment: !isMobile&&(
<InputAdornment position="start">
<FolderOpenOutlined />
</InputAdornment>
),
endAdornment:(
<InputAdornment position="end">
<Button
className={classes.button}
color="primary"
onClick={() => setSelectPathOpen(true)}
>
{t("navbar.addTagDialog.selectFolder")}
</Button>
</InputAdornment>
)
}}
/>
<br />
</div>
</div>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose}>
{t("cancel", { ns: "common" })}
</Button>
<div className={classes.wrapper}>
<Button
onClick={props.torrent?submitTorrentDownload:submitDownload}
color="primary"
disabled={(url === "" && props.torrent===null) || downloadTo==="" || props.modalsLoading}
>
{t("modals.createTask")}
{props.modalsLoading && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</div>
</DialogActions>
</Dialog>
<Dialog
open={selectPathOpen}
onClose={() => setSelectPathOpen(false)}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">
{t("modals.remoteDownloadDst")}
</DialogTitle>
<PathSelector
presentPath={pathBack(props.presentPath)}
selected={[]}
onSelect={setDownloadToPath}
/>
{selectedPathName !== "" && (
<DialogContent className={classes.contentFix}>
<DialogContentText>
<Trans
i18nKey="modals.downloadTo"
values={{
name: selectedPathName,
}}
components={[<strong key={0} />]}
/>
</DialogContentText>
</DialogContent>
)}
<DialogActions>
<Button onClick={() => setSelectPathOpen(false)}>
{t("cancel", { ns: "common" })}
</Button>
<Button
onClick={selectPath}
color="primary"
disabled={selectedPath === ""}
>
{t("ok", { ns: "common" })}
</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@ -19,10 +19,10 @@ import {
closeAllModals,
openDirectoryDownloadDialog,
openGetSourceDialog,
openLoadingDialog,
openLoadingDialog, openTorrentDownloadDialog,
showAudioPreview,
showImgPreivew,
toggleSnackbar,
toggleSnackbar
} from "./index";
import { getDownloadURL } from "../../services/file";
import i18next from "../../i18n";
@ -1094,3 +1094,13 @@ export const batchGetSource = (): ThunkAction<any, any, any, any> => {
});
};
};
export const openTorrentDownload = (): ThunkAction<any, any, any, any> => {
return (dispatch, getState): void => {
const {
explorer: { selected },
} = getState();
dispatch(openTorrentDownloadDialog(selected[0]));
};
};

View File

@ -168,9 +168,10 @@ export const openRemoteDownloadDialog = () => {
type: "OPEN_REMOTE_DOWNLOAD_DIALOG",
};
};
export const openTorrentDownloadDialog = () => {
export const openTorrentDownloadDialog = (selected) => {
return {
type: "OPEN_TORRENT_DOWNLOAD_DIALOG",
selected:selected,
};
};
export const openDecompressDialog = () => {

View File

@ -1,6 +1,6 @@
import { AnyAction } from "redux";
import Auth from "../../middleware/Auth";
import { SortMethod } from "../../types";
import { CloudreveFile, SortMethod } from "../../types";
export interface ViewUpdateState {
isLogin: boolean;
@ -27,7 +27,7 @@ export interface ViewUpdateState {
share: boolean;
music: boolean;
remoteDownload: boolean;
torrentDownload: boolean;
remoteDownloadTorrent: CloudreveFile | null;
getSource: string;
copy: boolean;
resave: boolean;
@ -91,7 +91,7 @@ export const initState: ViewUpdateState = {
share: false,
music: false,
remoteDownload: false,
torrentDownload: false,
remoteDownloadTorrent: null,
getSource: "",
copy: false,
resave: false,
@ -212,7 +212,8 @@ const viewUpdate = (state: ViewUpdateState = initState, action: AnyAction) => {
case "OPEN_TORRENT_DOWNLOAD_DIALOG":
return Object.assign({}, state, {
modals: Object.assign({}, state.modals, {
torrentDownload: true,
remoteDownload: true,
remoteDownloadTorrent: action.selected,
}),
contextOpen: false,
});
@ -276,7 +277,7 @@ const viewUpdate = (state: ViewUpdateState = initState, action: AnyAction) => {
share: false,
music: false,
remoteDownload: false,
torrentDownload: false,
remoteDownloadTorrent: null,
getSource: "",
resave: false,
copy: false,