From badd5ce65b3599ec53d511428e10a6b5128dccf3 Mon Sep 17 00:00:00 2001 From: hexxa Date: Sat, 30 Apr 2022 21:40:45 +0800 Subject: [PATCH] feat(fe): enable drag and drop to upload files --- src/client/web/package.json | 2 +- src/client/web/src/common/controls.ts | 1 + src/client/web/src/components/core_state.ts | 3 + src/client/web/src/components/layers.tsx | 34 +++++++--- src/client/web/src/components/panel_files.tsx | 12 ++-- src/client/web/src/components/root_frame.tsx | 64 ++++++++++++++++++- .../web/src/components/visual/icons.tsx | 2 + src/client/web/src/i18n/en_US.ts | 1 + src/client/web/src/i18n/zh_CN.ts | 1 + static/public/css/dark.css | 19 ++++++ static/public/css/white.css | 27 ++++++++ yarn.lock | 8 +-- 12 files changed, 152 insertions(+), 22 deletions(-) diff --git a/src/client/web/package.json b/src/client/web/package.json index a2f7589..3aad055 100644 --- a/src/client/web/package.json +++ b/src/client/web/package.json @@ -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" }, diff --git a/src/client/web/src/common/controls.ts b/src/client/web/src/common/controls.ts index fde24a6..219c4e9 100644 --- a/src/client/web/src/common/controls.ts +++ b/src/client/web/src/common/controls.ts @@ -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"; diff --git a/src/client/web/src/components/core_state.ts b/src/client/web/src/components/core_state.ts index b730692..bc1dc24 100644 --- a/src/client/web/src/components/core_state.ts +++ b/src/client/web/src/components/core_state.ts @@ -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>({ [panelTabs]: Set([ @@ -146,6 +148,7 @@ export function initState(): ICoreState { [sharingCtrl]: Set([ctrlOn, ctrlOff]), [filesViewCtrl]: Set(["rows", "table"]), [loadingCtrl]: Set([ctrlOn, ctrlOff]), + [dropAreaCtrl]: Set([ctrlOn, ctrlOff]), }), }, }, diff --git a/src/client/web/src/components/layers.tsx b/src/client/web/src/components/layers.tsx index a4056b2..a10f01d 100644 --- a/src/client/web/src/components/layers.tsx +++ b/src/client/web/src/components/layers.tsx @@ -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 { (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 {
- {/*
*/} - - {/*
*/} + +
+ + {/* ${dropAreaClass} */} +
+
+
+
{getIcon("RiFolderUploadFill", "4rem", "focus")}
+ {this.props.msg.pkg.get("term.dropAnywhere")} +
+
diff --git a/src/client/web/src/components/panel_files.tsx b/src/client/web/src/components/panel_files.tsx index 8d0b5a1..44805f8 100644 --- a/src/client/web/src/components/panel_files.tsx +++ b/src/client/web/src/components/panel_files.tsx @@ -156,15 +156,15 @@ export class FilesPanel extends React.Component { } }; - addUploads = (event: React.ChangeEvent) => { - 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(); - 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 { this.props.update(updater().updateUploadingsInfo); }; + addUploads = (event: React.ChangeEvent) => { + this.addFileList(event.target.files); + }; + mkDirFromKb = async ( event: React.KeyboardEvent ): Promise => { diff --git a/src/client/web/src/components/root_frame.tsx b/src/client/web/src/components/root_frame.tsx index 8aa4007..2145a5d 100644 --- a/src/client/web/src/components/root_frame.tsx +++ b/src/client/web/src/components/root_frame.tsx @@ -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 { + private filesPanelRef: FilesPanel; constructor(p: Props) { super(p); + this.state = { + lastDragOverTime: 0, + }; } componentDidMount(): void { @@ -38,12 +47,22 @@ export class RootFrame extends React.Component { 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 { return {}; }; + onDragOver = (ev: React.DragEvent) => { + 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) => { + 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 { const sharingsPanelClass = displaying === "sharingsPanel" ? "" : "hidden"; return ( -
+
{ ui={this.props.ui} enabled={displaying === "filesPanel"} update={this.props.update} + ref={this.setFilesPanelRef} /> diff --git a/src/client/web/src/components/visual/icons.tsx b/src/client/web/src/components/visual/icons.tsx index ea64eab..7123aae 100644 --- a/src/client/web/src/components/visual/icons.tsx +++ b/src/client/web/src/components/visual/icons.tsx @@ -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({ RiListSettingsFill: RiListSettingsFill, RiHardDriveFill: RiHardDriveFill, RiGridFill: RiGridFill, + RiFolderUploadFill: RiFolderUploadFill, }); export function getIconWithProps( diff --git a/src/client/web/src/i18n/en_US.ts b/src/client/web/src/i18n/en_US.ts index 8bca28e..3efab24 100644 --- a/src/client/web/src/i18n/en_US.ts +++ b/src/client/web/src/i18n/en_US.ts @@ -153,4 +153,5 @@ export const msgs: Map = Map({ "autoTheme": "Enable auto theme switching", "term.enabled": "Enabled", "term.disabled": "Disabled", + "term.dropAnywhere": "Drop files anywhere" }); diff --git a/src/client/web/src/i18n/zh_CN.ts b/src/client/web/src/i18n/zh_CN.ts index 278a76c..d34810f 100644 --- a/src/client/web/src/i18n/zh_CN.ts +++ b/src/client/web/src/i18n/zh_CN.ts @@ -150,4 +150,5 @@ export const msgs: Map = Map({ "autoTheme": "自动切换主题", "term.enabled": "启用", "term.disabled": "关闭", + "term.dropAnywhere": "把文件在任意处释放" }); diff --git a/static/public/css/dark.css b/static/public/css/dark.css index 8e856c4..f4a6ec7 100644 --- a/static/public/css/dark.css +++ b/static/public/css/dark.css @@ -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; } diff --git a/static/public/css/white.css b/static/public/css/white.css index f18e1d6..a51e781 100644 --- a/static/public/css/white.css +++ b/static/public/css/white.css @@ -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; } diff --git a/yarn.lock b/yarn.lock index bce240c..10ca22d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"