feat(fe): enable drag and drop to upload files
This commit is contained in:
parent
6034c56fe8
commit
badd5ce65b
12 changed files with 152 additions and 22 deletions
|
@ -59,7 +59,7 @@
|
|||
"react-icons": "4.3.1",
|
||||
"react-qr-code": "^2.0.3",
|
||||
"react-svg": "^8.0.6",
|
||||
"throttle-debounce": "^2.1.0",
|
||||
"throttle-debounce": "^4.0.1",
|
||||
"webpack-bundle-analyzer": "^4.4.2",
|
||||
"worker-loader": "^3.0.7"
|
||||
},
|
||||
|
|
|
@ -2,6 +2,7 @@ export const settingsTabsCtrl = "settingsTabs";
|
|||
export const settingsDialogCtrl = "settingsDialog";
|
||||
export const sharingCtrl = "sharingCtrl";
|
||||
export const filesViewCtrl = "filesView";
|
||||
export const dropAreaCtrl = "dropArea";
|
||||
export const ctrlHidden = "hidden";
|
||||
export const ctrlOn = "on";
|
||||
export const ctrlOff = "off";
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
ctrlOn,
|
||||
ctrlOff,
|
||||
loadingCtrl,
|
||||
dropAreaCtrl,
|
||||
} from "../common/controls";
|
||||
import { LoginProps } from "./pane_login";
|
||||
import { AdminProps } from "./pane_admin";
|
||||
|
@ -134,6 +135,7 @@ export function initState(): ICoreState {
|
|||
[sharingCtrl]: ctrlOff,
|
||||
[filesViewCtrl]: "rows",
|
||||
[loadingCtrl]: ctrlOff,
|
||||
[dropAreaCtrl]: ctrlOff,
|
||||
}),
|
||||
options: Map<string, Set<string>>({
|
||||
[panelTabs]: Set<string>([
|
||||
|
@ -146,6 +148,7 @@ export function initState(): ICoreState {
|
|||
[sharingCtrl]: Set<string>([ctrlOn, ctrlOff]),
|
||||
[filesViewCtrl]: Set<string>(["rows", "table"]),
|
||||
[loadingCtrl]: Set<string>([ctrlOn, ctrlOff]),
|
||||
[dropAreaCtrl]: Set<string>([ctrlOn, ctrlOff]),
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { List } from "immutable";
|
||||
import { throttle } from "throttle-debounce";
|
||||
|
||||
import { updater } from "./state_updater";
|
||||
import { ICoreState, MsgProps, UIProps } from "./core_state";
|
||||
|
@ -16,10 +16,12 @@ import {
|
|||
loadingCtrl,
|
||||
ctrlOn,
|
||||
ctrlHidden,
|
||||
dropAreaCtrl,
|
||||
} from "../common/controls";
|
||||
import { LoadingIcon } from "./visual/loading";
|
||||
import { Title } from "./visual/title";
|
||||
import { HotkeyHandler } from "../common/hotkeys";
|
||||
import { getIcon } from "./visual/icons";
|
||||
|
||||
export interface Props {
|
||||
filesInfo: FilesProps;
|
||||
|
@ -65,6 +67,10 @@ export class Layers extends React.Component<Props, State, {}> {
|
|||
(this.props.ui.control.controls.get(sharingCtrl) === ctrlOn &&
|
||||
this.props.filesInfo.isSharing);
|
||||
const loginPaneClass = hideLogin ? "hidden" : "";
|
||||
const dropAreaClass =
|
||||
this.props.ui.control.controls.get(dropAreaCtrl) === ctrlOn
|
||||
? ""
|
||||
: "hidden";
|
||||
|
||||
const showSettings =
|
||||
this.props.ui.control.controls.get(settingsDialogCtrl) === ctrlOn
|
||||
|
@ -82,15 +88,23 @@ export class Layers extends React.Component<Props, State, {}> {
|
|||
</div>
|
||||
|
||||
<div id="login-layer" className={`layer ${loginPaneClass}`}>
|
||||
{/* <div id="root-container"> */}
|
||||
<AuthPane
|
||||
login={this.props.login}
|
||||
ui={this.props.ui}
|
||||
update={this.props.update}
|
||||
msg={this.props.msg}
|
||||
enabled={!hideLogin}
|
||||
/>
|
||||
{/* </div> */}
|
||||
<AuthPane
|
||||
login={this.props.login}
|
||||
ui={this.props.ui}
|
||||
update={this.props.update}
|
||||
msg={this.props.msg}
|
||||
enabled={!hideLogin}
|
||||
/>
|
||||
</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 id="settings-layer" className={`layer ${showSettings}`}>
|
||||
|
|
|
@ -156,15 +156,15 @@ export class FilesPanel extends React.Component<Props, State, {}> {
|
|||
}
|
||||
};
|
||||
|
||||
addUploads = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files.length > 200) {
|
||||
addFileList = (originalFileList: FileList) => {
|
||||
if (originalFileList.length > 200) {
|
||||
Env().alertMsg(this.props.msg.pkg.get("err.tooManyUploads"));
|
||||
return;
|
||||
}
|
||||
|
||||
let fileList = List<File>();
|
||||
for (let i = 0; i < event.target.files.length; i++) {
|
||||
fileList = fileList.push(event.target.files[i]);
|
||||
for (let i = 0; i < originalFileList.length; i++) {
|
||||
fileList = fileList.push(originalFileList[i]);
|
||||
}
|
||||
|
||||
const status = updater().addUploads(fileList);
|
||||
|
@ -174,6 +174,10 @@ export class FilesPanel extends React.Component<Props, State, {}> {
|
|||
this.props.update(updater().updateUploadingsInfo);
|
||||
};
|
||||
|
||||
addUploads = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.addFileList(event.target.files);
|
||||
};
|
||||
|
||||
mkDirFromKb = async (
|
||||
event: React.KeyboardEvent<HTMLInputElement>
|
||||
): Promise<void> => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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 { FilesPanel, FilesProps } from "./panel_files";
|
||||
|
@ -13,8 +14,10 @@ import { AdminProps } from "./pane_admin";
|
|||
import { TopBar } from "./topbar";
|
||||
import { CronJobs } from "../common/cron";
|
||||
import { updater } from "./state_updater";
|
||||
import { dropAreaCtrl, ctrlOn, ctrlOff } from "../common/controls";
|
||||
|
||||
export const controlName = "panelTabs";
|
||||
const dragOverthrottlePeriod = 200;
|
||||
export interface Props {
|
||||
filesInfo: FilesProps;
|
||||
uploadingsInfo: UploadingsProps;
|
||||
|
@ -26,10 +29,16 @@ export interface Props {
|
|||
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||
}
|
||||
|
||||
export interface State {}
|
||||
export interface State {
|
||||
lastDragOverTime: number;
|
||||
}
|
||||
export class RootFrame extends React.Component<Props, State, {}> {
|
||||
private filesPanelRef: FilesPanel;
|
||||
constructor(p: Props) {
|
||||
super(p);
|
||||
this.state = {
|
||||
lastDragOverTime: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
|
@ -38,12 +47,22 @@ export class RootFrame extends React.Component<Props, State, {}> {
|
|||
args: [],
|
||||
delay: 60 * 1000,
|
||||
});
|
||||
|
||||
CronJobs().setInterval("endDrag", {
|
||||
func: this.endDrag,
|
||||
args: [],
|
||||
delay: dragOverthrottlePeriod * 2,
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
CronJobs().clearInterval("autoSwitchTheme");
|
||||
}
|
||||
|
||||
private setFilesPanelRef = (ref: FilesPanel) => {
|
||||
this.filesPanelRef = ref;
|
||||
};
|
||||
|
||||
makeBgStyle = (): Object => {
|
||||
if (this.props.ui.clientCfg.allowSetBg) {
|
||||
if (
|
||||
|
@ -78,6 +97,39 @@ export class RootFrame extends React.Component<Props, State, {}> {
|
|||
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() {
|
||||
const bgStyle = this.makeBgStyle();
|
||||
const autoTheme =
|
||||
|
@ -97,7 +149,12 @@ export class RootFrame extends React.Component<Props, State, {}> {
|
|||
const sharingsPanelClass = displaying === "sharingsPanel" ? "" : "hidden";
|
||||
|
||||
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="custom">
|
||||
<Layers
|
||||
|
@ -151,6 +208,7 @@ export class RootFrame extends React.Component<Props, State, {}> {
|
|||
ui={this.props.ui}
|
||||
enabled={displaying === "filesPanel"}
|
||||
update={this.props.update}
|
||||
ref={this.setFilesPanelRef}
|
||||
/>
|
||||
</span>
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import { BiSortUp } from "@react-icons/all-files/bi/BiSortUp";
|
|||
import { RiListSettingsFill } from "@react-icons/all-files/ri/RiListSettingsFill";
|
||||
import { RiHardDriveFill } from "@react-icons/all-files/ri/RiHardDriveFill";
|
||||
import { RiGridFill } from "@react-icons/all-files/ri/RiGridFill";
|
||||
import { RiFolderUploadFill } from "@react-icons/all-files/ri/RiFolderUploadFill";
|
||||
|
||||
import { colorClass } from "./colors";
|
||||
|
||||
|
@ -54,6 +55,7 @@ const icons = Map<string, IconType>({
|
|||
RiListSettingsFill: RiListSettingsFill,
|
||||
RiHardDriveFill: RiHardDriveFill,
|
||||
RiGridFill: RiGridFill,
|
||||
RiFolderUploadFill: RiFolderUploadFill,
|
||||
});
|
||||
|
||||
export function getIconWithProps(
|
||||
|
|
|
@ -153,4 +153,5 @@ export const msgs: Map<string, string> = Map({
|
|||
"autoTheme": "Enable auto theme switching",
|
||||
"term.enabled": "Enabled",
|
||||
"term.disabled": "Disabled",
|
||||
"term.dropAnywhere": "Drop files anywhere"
|
||||
});
|
||||
|
|
|
@ -150,4 +150,5 @@ export const msgs: Map<string, string> = Map({
|
|||
"autoTheme": "自动切换主题",
|
||||
"term.enabled": "启用",
|
||||
"term.disabled": "关闭",
|
||||
"term.dropAnywhere": "把文件在任意处释放"
|
||||
});
|
||||
|
|
|
@ -328,6 +328,22 @@
|
|||
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 */
|
||||
|
||||
.theme-dark .major-font {
|
||||
|
@ -354,6 +370,9 @@
|
|||
.theme-dark .focus-bg {
|
||||
background-color: #16a085;
|
||||
}
|
||||
.theme-dark .reverse-bg {
|
||||
background-color: #fff;
|
||||
}
|
||||
.theme-dark .minor-bg {
|
||||
background-color: #333;
|
||||
}
|
||||
|
|
|
@ -330,6 +330,30 @@
|
|||
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 */
|
||||
|
||||
.theme-default .minor-font {
|
||||
|
@ -360,6 +384,9 @@
|
|||
.theme-default .minor-bg {
|
||||
background-color: #ecf0f6;
|
||||
}
|
||||
.theme-default .reverse-bg {
|
||||
background-color: #000;
|
||||
}
|
||||
.theme-default ::placeholder {
|
||||
color: #95a5a6;
|
||||
}
|
||||
|
|
|
@ -4465,10 +4465,10 @@ throat@^6.0.1:
|
|||
resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375"
|
||||
integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==
|
||||
|
||||
throttle-debounce@^2.1.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2"
|
||||
integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==
|
||||
throttle-debounce@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-4.0.1.tgz#f86656fe9c8a6b8218952ef36c3bf225089b1baf"
|
||||
integrity sha512-s3PedbXdZtr8v3J5Sxd5T/GmWG80BcK5GVpwDdvgEaUXsaMqQe4zxgmC4TA7B8luSDCPxo3CeSBS3F9rF1CZwg==
|
||||
|
||||
tmpl@1.0.5:
|
||||
version "1.0.5"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue