feat(fe): enable drag and drop to upload files

This commit is contained in:
hexxa 2022-04-30 21:40:45 +08:00 committed by Hexxa
parent 6034c56fe8
commit badd5ce65b
12 changed files with 152 additions and 22 deletions

View file

@ -59,7 +59,7 @@
"react-icons": "4.3.1", "react-icons": "4.3.1",
"react-qr-code": "^2.0.3", "react-qr-code": "^2.0.3",
"react-svg": "^8.0.6", "react-svg": "^8.0.6",
"throttle-debounce": "^2.1.0", "throttle-debounce": "^4.0.1",
"webpack-bundle-analyzer": "^4.4.2", "webpack-bundle-analyzer": "^4.4.2",
"worker-loader": "^3.0.7" "worker-loader": "^3.0.7"
}, },

View file

@ -2,6 +2,7 @@ export const settingsTabsCtrl = "settingsTabs";
export const settingsDialogCtrl = "settingsDialog"; export const settingsDialogCtrl = "settingsDialog";
export const sharingCtrl = "sharingCtrl"; export const sharingCtrl = "sharingCtrl";
export const filesViewCtrl = "filesView"; export const filesViewCtrl = "filesView";
export const dropAreaCtrl = "dropArea";
export const ctrlHidden = "hidden"; export const ctrlHidden = "hidden";
export const ctrlOn = "on"; export const ctrlOn = "on";
export const ctrlOff = "off"; export const ctrlOff = "off";

View file

@ -15,6 +15,7 @@ import {
ctrlOn, ctrlOn,
ctrlOff, ctrlOff,
loadingCtrl, loadingCtrl,
dropAreaCtrl,
} from "../common/controls"; } from "../common/controls";
import { LoginProps } from "./pane_login"; import { LoginProps } from "./pane_login";
import { AdminProps } from "./pane_admin"; import { AdminProps } from "./pane_admin";
@ -134,6 +135,7 @@ export function initState(): ICoreState {
[sharingCtrl]: ctrlOff, [sharingCtrl]: ctrlOff,
[filesViewCtrl]: "rows", [filesViewCtrl]: "rows",
[loadingCtrl]: ctrlOff, [loadingCtrl]: ctrlOff,
[dropAreaCtrl]: ctrlOff,
}), }),
options: Map<string, Set<string>>({ options: Map<string, Set<string>>({
[panelTabs]: Set<string>([ [panelTabs]: Set<string>([
@ -146,6 +148,7 @@ export function initState(): ICoreState {
[sharingCtrl]: Set<string>([ctrlOn, ctrlOff]), [sharingCtrl]: Set<string>([ctrlOn, ctrlOff]),
[filesViewCtrl]: Set<string>(["rows", "table"]), [filesViewCtrl]: Set<string>(["rows", "table"]),
[loadingCtrl]: Set<string>([ctrlOn, ctrlOff]), [loadingCtrl]: Set<string>([ctrlOn, ctrlOff]),
[dropAreaCtrl]: Set<string>([ctrlOn, ctrlOff]),
}), }),
}, },
}, },

View file

@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import { List } from "immutable"; import { throttle } from "throttle-debounce";
import { updater } from "./state_updater"; import { updater } from "./state_updater";
import { ICoreState, MsgProps, UIProps } from "./core_state"; import { ICoreState, MsgProps, UIProps } from "./core_state";
@ -16,10 +16,12 @@ import {
loadingCtrl, loadingCtrl,
ctrlOn, ctrlOn,
ctrlHidden, ctrlHidden,
dropAreaCtrl,
} from "../common/controls"; } from "../common/controls";
import { LoadingIcon } from "./visual/loading"; import { LoadingIcon } from "./visual/loading";
import { Title } from "./visual/title"; import { Title } from "./visual/title";
import { HotkeyHandler } from "../common/hotkeys"; import { HotkeyHandler } from "../common/hotkeys";
import { getIcon } from "./visual/icons";
export interface Props { export interface Props {
filesInfo: FilesProps; filesInfo: FilesProps;
@ -65,6 +67,10 @@ export class Layers extends React.Component<Props, State, {}> {
(this.props.ui.control.controls.get(sharingCtrl) === ctrlOn && (this.props.ui.control.controls.get(sharingCtrl) === ctrlOn &&
this.props.filesInfo.isSharing); this.props.filesInfo.isSharing);
const loginPaneClass = hideLogin ? "hidden" : ""; const loginPaneClass = hideLogin ? "hidden" : "";
const dropAreaClass =
this.props.ui.control.controls.get(dropAreaCtrl) === ctrlOn
? ""
: "hidden";
const showSettings = const showSettings =
this.props.ui.control.controls.get(settingsDialogCtrl) === ctrlOn this.props.ui.control.controls.get(settingsDialogCtrl) === ctrlOn
@ -82,15 +88,23 @@ export class Layers extends React.Component<Props, State, {}> {
</div> </div>
<div id="login-layer" className={`layer ${loginPaneClass}`}> <div id="login-layer" className={`layer ${loginPaneClass}`}>
{/* <div id="root-container"> */} <AuthPane
<AuthPane login={this.props.login}
login={this.props.login} ui={this.props.ui}
ui={this.props.ui} update={this.props.update}
update={this.props.update} msg={this.props.msg}
msg={this.props.msg} enabled={!hideLogin}
enabled={!hideLogin} />
/> </div>
{/* </div> */}
{/* ${dropAreaClass} */}
<div id="drop-area-layer" className={`${dropAreaClass}`}>
<div className="drop-area-container">
<div className="drop-area major-bg focus-font">
<div>{getIcon("RiFolderUploadFill", "4rem", "focus")}</div>
<span>{this.props.msg.pkg.get("term.dropAnywhere")}</span>
</div>
</div>
</div> </div>
<div id="settings-layer" className={`layer ${showSettings}`}> <div id="settings-layer" className={`layer ${showSettings}`}>

View file

@ -156,15 +156,15 @@ export class FilesPanel extends React.Component<Props, State, {}> {
} }
}; };
addUploads = (event: React.ChangeEvent<HTMLInputElement>) => { addFileList = (originalFileList: FileList) => {
if (event.target.files.length > 200) { if (originalFileList.length > 200) {
Env().alertMsg(this.props.msg.pkg.get("err.tooManyUploads")); Env().alertMsg(this.props.msg.pkg.get("err.tooManyUploads"));
return; return;
} }
let fileList = List<File>(); let fileList = List<File>();
for (let i = 0; i < event.target.files.length; i++) { for (let i = 0; i < originalFileList.length; i++) {
fileList = fileList.push(event.target.files[i]); fileList = fileList.push(originalFileList[i]);
} }
const status = updater().addUploads(fileList); const status = updater().addUploads(fileList);
@ -174,6 +174,10 @@ export class FilesPanel extends React.Component<Props, State, {}> {
this.props.update(updater().updateUploadingsInfo); this.props.update(updater().updateUploadingsInfo);
}; };
addUploads = (event: React.ChangeEvent<HTMLInputElement>) => {
this.addFileList(event.target.files);
};
mkDirFromKb = async ( mkDirFromKb = async (
event: React.KeyboardEvent<HTMLInputElement> event: React.KeyboardEvent<HTMLInputElement>
): Promise<void> => { ): Promise<void> => {

View file

@ -1,5 +1,6 @@
import * as React from "react"; import * as React from "react";
import { Map } from "immutable"; import { Map, List } from "immutable";
import { throttle } from "throttle-debounce";
import { ICoreState, MsgProps, UIProps } from "./core_state"; import { ICoreState, MsgProps, UIProps } from "./core_state";
import { FilesPanel, FilesProps } from "./panel_files"; import { FilesPanel, FilesProps } from "./panel_files";
@ -13,8 +14,10 @@ import { AdminProps } from "./pane_admin";
import { TopBar } from "./topbar"; import { TopBar } from "./topbar";
import { CronJobs } from "../common/cron"; import { CronJobs } from "../common/cron";
import { updater } from "./state_updater"; import { updater } from "./state_updater";
import { dropAreaCtrl, ctrlOn, ctrlOff } from "../common/controls";
export const controlName = "panelTabs"; export const controlName = "panelTabs";
const dragOverthrottlePeriod = 200;
export interface Props { export interface Props {
filesInfo: FilesProps; filesInfo: FilesProps;
uploadingsInfo: UploadingsProps; uploadingsInfo: UploadingsProps;
@ -26,10 +29,16 @@ export interface Props {
update?: (updater: (prevState: ICoreState) => ICoreState) => void; update?: (updater: (prevState: ICoreState) => ICoreState) => void;
} }
export interface State {} export interface State {
lastDragOverTime: number;
}
export class RootFrame extends React.Component<Props, State, {}> { export class RootFrame extends React.Component<Props, State, {}> {
private filesPanelRef: FilesPanel;
constructor(p: Props) { constructor(p: Props) {
super(p); super(p);
this.state = {
lastDragOverTime: 0,
};
} }
componentDidMount(): void { componentDidMount(): void {
@ -38,12 +47,22 @@ export class RootFrame extends React.Component<Props, State, {}> {
args: [], args: [],
delay: 60 * 1000, delay: 60 * 1000,
}); });
CronJobs().setInterval("endDrag", {
func: this.endDrag,
args: [],
delay: dragOverthrottlePeriod * 2,
});
} }
componentWillUnmount() { componentWillUnmount() {
CronJobs().clearInterval("autoSwitchTheme"); CronJobs().clearInterval("autoSwitchTheme");
} }
private setFilesPanelRef = (ref: FilesPanel) => {
this.filesPanelRef = ref;
};
makeBgStyle = (): Object => { makeBgStyle = (): Object => {
if (this.props.ui.clientCfg.allowSetBg) { if (this.props.ui.clientCfg.allowSetBg) {
if ( if (
@ -78,6 +97,39 @@ export class RootFrame extends React.Component<Props, State, {}> {
return {}; return {};
}; };
onDragOver = (ev: React.DragEvent<HTMLDivElement>) => {
this.onDragOverImp();
ev.preventDefault();
};
onDragOverImp = throttle(dragOverthrottlePeriod, () => {
updater().setControlOption(dropAreaCtrl, ctrlOn);
this.props.update(updater().updateUI);
this.setState({ lastDragOverTime: Date.now() });
});
onDrop = (ev: React.DragEvent<HTMLDivElement>) => {
if (ev.dataTransfer?.files?.length > 0) {
this.filesPanelRef.addFileList(ev.dataTransfer.files);
}
ev.preventDefault();
};
endDrag = () => {
const now = Date.now();
const isDragOverOff =
this.props.ui.control.controls.get(dropAreaCtrl) === ctrlOff;
if (
now - this.state.lastDragOverTime < dragOverthrottlePeriod * 1.5 ||
isDragOverOff
) {
return;
}
updater().setControlOption(dropAreaCtrl, ctrlOff);
this.props.update(updater().updateUI);
};
render() { render() {
const bgStyle = this.makeBgStyle(); const bgStyle = this.makeBgStyle();
const autoTheme = const autoTheme =
@ -97,7 +149,12 @@ export class RootFrame extends React.Component<Props, State, {}> {
const sharingsPanelClass = displaying === "sharingsPanel" ? "" : "hidden"; const sharingsPanelClass = displaying === "sharingsPanel" ? "" : "hidden";
return ( return (
<div id="root-frame" className={`${theme} ${fontSizeClass}`}> <div
id="root-frame"
className={`${theme} ${fontSizeClass}`}
onDragOver={this.onDragOver}
onDrop={this.onDrop}
>
<div id="bg" style={bgStyle}> <div id="bg" style={bgStyle}>
<div id="custom"> <div id="custom">
<Layers <Layers
@ -151,6 +208,7 @@ export class RootFrame extends React.Component<Props, State, {}> {
ui={this.props.ui} ui={this.props.ui}
enabled={displaying === "filesPanel"} enabled={displaying === "filesPanel"}
update={this.props.update} update={this.props.update}
ref={this.setFilesPanelRef}
/> />
</span> </span>

View file

@ -23,6 +23,7 @@ import { BiSortUp } from "@react-icons/all-files/bi/BiSortUp";
import { RiListSettingsFill } from "@react-icons/all-files/ri/RiListSettingsFill"; import { RiListSettingsFill } from "@react-icons/all-files/ri/RiListSettingsFill";
import { RiHardDriveFill } from "@react-icons/all-files/ri/RiHardDriveFill"; import { RiHardDriveFill } from "@react-icons/all-files/ri/RiHardDriveFill";
import { RiGridFill } from "@react-icons/all-files/ri/RiGridFill"; import { RiGridFill } from "@react-icons/all-files/ri/RiGridFill";
import { RiFolderUploadFill } from "@react-icons/all-files/ri/RiFolderUploadFill";
import { colorClass } from "./colors"; import { colorClass } from "./colors";
@ -54,6 +55,7 @@ const icons = Map<string, IconType>({
RiListSettingsFill: RiListSettingsFill, RiListSettingsFill: RiListSettingsFill,
RiHardDriveFill: RiHardDriveFill, RiHardDriveFill: RiHardDriveFill,
RiGridFill: RiGridFill, RiGridFill: RiGridFill,
RiFolderUploadFill: RiFolderUploadFill,
}); });
export function getIconWithProps( export function getIconWithProps(

View file

@ -153,4 +153,5 @@ export const msgs: Map<string, string> = Map({
"autoTheme": "Enable auto theme switching", "autoTheme": "Enable auto theme switching",
"term.enabled": "Enabled", "term.enabled": "Enabled",
"term.disabled": "Disabled", "term.disabled": "Disabled",
"term.dropAnywhere": "Drop files anywhere"
}); });

View file

@ -150,4 +150,5 @@ export const msgs: Map<string, string> = Map({
"autoTheme": "自动切换主题", "autoTheme": "自动切换主题",
"term.enabled": "启用", "term.enabled": "启用",
"term.disabled": "关闭", "term.disabled": "关闭",
"term.dropAnywhere": "把文件在任意处释放"
}); });

View file

@ -328,6 +328,22 @@
background-color: #333; background-color: #333;
} }
.theme-dark .drop-area-container {
position: relative;
width: 100%;
padding-top: 20rem;
}
.theme-dark .drop-area {
opacity: 0.8;
backdrop-filter: blur(9.5px);
text-align: center;
border-radius: 0.8rem;
padding: 2rem 0;
margin: auto;
width: 25rem;
}
/* +colors */ /* +colors */
.theme-dark .major-font { .theme-dark .major-font {
@ -354,6 +370,9 @@
.theme-dark .focus-bg { .theme-dark .focus-bg {
background-color: #16a085; background-color: #16a085;
} }
.theme-dark .reverse-bg {
background-color: #fff;
}
.theme-dark .minor-bg { .theme-dark .minor-bg {
background-color: #333; background-color: #333;
} }

View file

@ -330,6 +330,30 @@
background-color: #ecf0f1; background-color: #ecf0f1;
} }
.theme-default .drop-area-container {
position: relative;
width: 100%;
padding-top: 20rem;
}
.theme-default .drop-area {
opacity: 0.8;
backdrop-filter: blur(9.5px);
text-align: center;
border-radius: 0.8rem;
padding: 2rem 0;
margin: auto;
width: 25rem;
}
.theme-default #login-layer {
z-index: 200;
}
.theme-default #drop-area-layer {
z-index: 4;
}
/* +colors */ /* +colors */
.theme-default .minor-font { .theme-default .minor-font {
@ -360,6 +384,9 @@
.theme-default .minor-bg { .theme-default .minor-bg {
background-color: #ecf0f6; background-color: #ecf0f6;
} }
.theme-default .reverse-bg {
background-color: #000;
}
.theme-default ::placeholder { .theme-default ::placeholder {
color: #95a5a6; color: #95a5a6;
} }

View file

@ -4465,10 +4465,10 @@ throat@^6.0.1:
resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375"
integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==
throttle-debounce@^2.1.0: throttle-debounce@^4.0.1:
version "2.3.0" version "4.0.1"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2" resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-4.0.1.tgz#f86656fe9c8a6b8218952ef36c3bf225089b1baf"
integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ== integrity sha512-s3PedbXdZtr8v3J5Sxd5T/GmWG80BcK5GVpwDdvgEaUXsaMqQe4zxgmC4TA7B8luSDCPxo3CeSBS3F9rF1CZwg==
tmpl@1.0.5: tmpl@1.0.5:
version "1.0.5" version "1.0.5"