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-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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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]),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,7 +88,6 @@ 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}
|
||||||
|
@ -90,7 +95,16 @@ export class Layers extends React.Component<Props, State, {}> {
|
||||||
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}`}>
|
||||||
|
|
|
@ -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> => {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"
|
||||||
});
|
});
|
||||||
|
|
|
@ -150,4 +150,5 @@ export const msgs: Map<string, string> = Map({
|
||||||
"autoTheme": "自动切换主题",
|
"autoTheme": "自动切换主题",
|
||||||
"term.enabled": "启用",
|
"term.enabled": "启用",
|
||||||
"term.disabled": "关闭",
|
"term.disabled": "关闭",
|
||||||
|
"term.dropAnywhere": "把文件在任意处释放"
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue