Skip to content

Commit badd5ce

Browse files
committed
feat(fe): enable drag and drop to upload files
1 parent 6034c56 commit badd5ce

File tree

12 files changed

+152
-22
lines changed

12 files changed

+152
-22
lines changed

src/client/web/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
"react-icons": "4.3.1",
6060
"react-qr-code": "^2.0.3",
6161
"react-svg": "^8.0.6",
62-
"throttle-debounce": "^2.1.0",
62+
"throttle-debounce": "^4.0.1",
6363
"webpack-bundle-analyzer": "^4.4.2",
6464
"worker-loader": "^3.0.7"
6565
},

src/client/web/src/common/controls.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const settingsTabsCtrl = "settingsTabs";
22
export const settingsDialogCtrl = "settingsDialog";
33
export const sharingCtrl = "sharingCtrl";
44
export const filesViewCtrl = "filesView";
5+
export const dropAreaCtrl = "dropArea";
56
export const ctrlHidden = "hidden";
67
export const ctrlOn = "on";
78
export const ctrlOff = "off";

src/client/web/src/components/core_state.ts

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
ctrlOn,
1616
ctrlOff,
1717
loadingCtrl,
18+
dropAreaCtrl,
1819
} from "../common/controls";
1920
import { LoginProps } from "./pane_login";
2021
import { AdminProps } from "./pane_admin";
@@ -134,6 +135,7 @@ export function initState(): ICoreState {
134135
[sharingCtrl]: ctrlOff,
135136
[filesViewCtrl]: "rows",
136137
[loadingCtrl]: ctrlOff,
138+
[dropAreaCtrl]: ctrlOff,
137139
}),
138140
options: Map<string, Set<string>>({
139141
[panelTabs]: Set<string>([
@@ -146,6 +148,7 @@ export function initState(): ICoreState {
146148
[sharingCtrl]: Set<string>([ctrlOn, ctrlOff]),
147149
[filesViewCtrl]: Set<string>(["rows", "table"]),
148150
[loadingCtrl]: Set<string>([ctrlOn, ctrlOff]),
151+
[dropAreaCtrl]: Set<string>([ctrlOn, ctrlOff]),
149152
}),
150153
},
151154
},

src/client/web/src/components/layers.tsx

+24-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from "react";
2-
import { List } from "immutable";
2+
import { throttle } from "throttle-debounce";
33

44
import { updater } from "./state_updater";
55
import { ICoreState, MsgProps, UIProps } from "./core_state";
@@ -16,10 +16,12 @@ import {
1616
loadingCtrl,
1717
ctrlOn,
1818
ctrlHidden,
19+
dropAreaCtrl,
1920
} from "../common/controls";
2021
import { LoadingIcon } from "./visual/loading";
2122
import { Title } from "./visual/title";
2223
import { HotkeyHandler } from "../common/hotkeys";
24+
import { getIcon } from "./visual/icons";
2325

2426
export interface Props {
2527
filesInfo: FilesProps;
@@ -65,6 +67,10 @@ export class Layers extends React.Component<Props, State, {}> {
6567
(this.props.ui.control.controls.get(sharingCtrl) === ctrlOn &&
6668
this.props.filesInfo.isSharing);
6769
const loginPaneClass = hideLogin ? "hidden" : "";
70+
const dropAreaClass =
71+
this.props.ui.control.controls.get(dropAreaCtrl) === ctrlOn
72+
? ""
73+
: "hidden";
6874

6975
const showSettings =
7076
this.props.ui.control.controls.get(settingsDialogCtrl) === ctrlOn
@@ -82,15 +88,23 @@ export class Layers extends React.Component<Props, State, {}> {
8288
</div>
8389

8490
<div id="login-layer" className={`layer ${loginPaneClass}`}>
85-
{/* <div id="root-container"> */}
86-
<AuthPane
87-
login={this.props.login}
88-
ui={this.props.ui}
89-
update={this.props.update}
90-
msg={this.props.msg}
91-
enabled={!hideLogin}
92-
/>
93-
{/* </div> */}
91+
<AuthPane
92+
login={this.props.login}
93+
ui={this.props.ui}
94+
update={this.props.update}
95+
msg={this.props.msg}
96+
enabled={!hideLogin}
97+
/>
98+
</div>
99+
100+
{/* ${dropAreaClass} */}
101+
<div id="drop-area-layer" className={`${dropAreaClass}`}>
102+
<div className="drop-area-container">
103+
<div className="drop-area major-bg focus-font">
104+
<div>{getIcon("RiFolderUploadFill", "4rem", "focus")}</div>
105+
<span>{this.props.msg.pkg.get("term.dropAnywhere")}</span>
106+
</div>
107+
</div>
94108
</div>
95109

96110
<div id="settings-layer" className={`layer ${showSettings}`}>

src/client/web/src/components/panel_files.tsx

+8-4
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,15 @@ export class FilesPanel extends React.Component<Props, State, {}> {
156156
}
157157
};
158158

159-
addUploads = (event: React.ChangeEvent<HTMLInputElement>) => {
160-
if (event.target.files.length > 200) {
159+
addFileList = (originalFileList: FileList) => {
160+
if (originalFileList.length > 200) {
161161
Env().alertMsg(this.props.msg.pkg.get("err.tooManyUploads"));
162162
return;
163163
}
164164

165165
let fileList = List<File>();
166-
for (let i = 0; i < event.target.files.length; i++) {
167-
fileList = fileList.push(event.target.files[i]);
166+
for (let i = 0; i < originalFileList.length; i++) {
167+
fileList = fileList.push(originalFileList[i]);
168168
}
169169

170170
const status = updater().addUploads(fileList);
@@ -174,6 +174,10 @@ export class FilesPanel extends React.Component<Props, State, {}> {
174174
this.props.update(updater().updateUploadingsInfo);
175175
};
176176

177+
addUploads = (event: React.ChangeEvent<HTMLInputElement>) => {
178+
this.addFileList(event.target.files);
179+
};
180+
177181
mkDirFromKb = async (
178182
event: React.KeyboardEvent<HTMLInputElement>
179183
): Promise<void> => {

src/client/web/src/components/root_frame.tsx

+61-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from "react";
2-
import { Map } from "immutable";
2+
import { Map, List } from "immutable";
3+
import { throttle } from "throttle-debounce";
34

45
import { ICoreState, MsgProps, UIProps } from "./core_state";
56
import { FilesPanel, FilesProps } from "./panel_files";
@@ -13,8 +14,10 @@ import { AdminProps } from "./pane_admin";
1314
import { TopBar } from "./topbar";
1415
import { CronJobs } from "../common/cron";
1516
import { updater } from "./state_updater";
17+
import { dropAreaCtrl, ctrlOn, ctrlOff } from "../common/controls";
1618

1719
export const controlName = "panelTabs";
20+
const dragOverthrottlePeriod = 200;
1821
export interface Props {
1922
filesInfo: FilesProps;
2023
uploadingsInfo: UploadingsProps;
@@ -26,10 +29,16 @@ export interface Props {
2629
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
2730
}
2831

29-
export interface State {}
32+
export interface State {
33+
lastDragOverTime: number;
34+
}
3035
export class RootFrame extends React.Component<Props, State, {}> {
36+
private filesPanelRef: FilesPanel;
3137
constructor(p: Props) {
3238
super(p);
39+
this.state = {
40+
lastDragOverTime: 0,
41+
};
3342
}
3443

3544
componentDidMount(): void {
@@ -38,12 +47,22 @@ export class RootFrame extends React.Component<Props, State, {}> {
3847
args: [],
3948
delay: 60 * 1000,
4049
});
50+
51+
CronJobs().setInterval("endDrag", {
52+
func: this.endDrag,
53+
args: [],
54+
delay: dragOverthrottlePeriod * 2,
55+
});
4156
}
4257

4358
componentWillUnmount() {
4459
CronJobs().clearInterval("autoSwitchTheme");
4560
}
4661

62+
private setFilesPanelRef = (ref: FilesPanel) => {
63+
this.filesPanelRef = ref;
64+
};
65+
4766
makeBgStyle = (): Object => {
4867
if (this.props.ui.clientCfg.allowSetBg) {
4968
if (
@@ -78,6 +97,39 @@ export class RootFrame extends React.Component<Props, State, {}> {
7897
return {};
7998
};
8099

100+
onDragOver = (ev: React.DragEvent<HTMLDivElement>) => {
101+
this.onDragOverImp();
102+
ev.preventDefault();
103+
};
104+
105+
onDragOverImp = throttle(dragOverthrottlePeriod, () => {
106+
updater().setControlOption(dropAreaCtrl, ctrlOn);
107+
this.props.update(updater().updateUI);
108+
this.setState({ lastDragOverTime: Date.now() });
109+
});
110+
111+
onDrop = (ev: React.DragEvent<HTMLDivElement>) => {
112+
if (ev.dataTransfer?.files?.length > 0) {
113+
this.filesPanelRef.addFileList(ev.dataTransfer.files);
114+
}
115+
ev.preventDefault();
116+
};
117+
118+
endDrag = () => {
119+
const now = Date.now();
120+
const isDragOverOff =
121+
this.props.ui.control.controls.get(dropAreaCtrl) === ctrlOff;
122+
if (
123+
now - this.state.lastDragOverTime < dragOverthrottlePeriod * 1.5 ||
124+
isDragOverOff
125+
) {
126+
return;
127+
}
128+
129+
updater().setControlOption(dropAreaCtrl, ctrlOff);
130+
this.props.update(updater().updateUI);
131+
};
132+
81133
render() {
82134
const bgStyle = this.makeBgStyle();
83135
const autoTheme =
@@ -97,7 +149,12 @@ export class RootFrame extends React.Component<Props, State, {}> {
97149
const sharingsPanelClass = displaying === "sharingsPanel" ? "" : "hidden";
98150

99151
return (
100-
<div id="root-frame" className={`${theme} ${fontSizeClass}`}>
152+
<div
153+
id="root-frame"
154+
className={`${theme} ${fontSizeClass}`}
155+
onDragOver={this.onDragOver}
156+
onDrop={this.onDrop}
157+
>
101158
<div id="bg" style={bgStyle}>
102159
<div id="custom">
103160
<Layers
@@ -151,6 +208,7 @@ export class RootFrame extends React.Component<Props, State, {}> {
151208
ui={this.props.ui}
152209
enabled={displaying === "filesPanel"}
153210
update={this.props.update}
211+
ref={this.setFilesPanelRef}
154212
/>
155213
</span>
156214

src/client/web/src/components/visual/icons.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { BiSortUp } from "@react-icons/all-files/bi/BiSortUp";
2323
import { RiListSettingsFill } from "@react-icons/all-files/ri/RiListSettingsFill";
2424
import { RiHardDriveFill } from "@react-icons/all-files/ri/RiHardDriveFill";
2525
import { RiGridFill } from "@react-icons/all-files/ri/RiGridFill";
26+
import { RiFolderUploadFill } from "@react-icons/all-files/ri/RiFolderUploadFill";
2627

2728
import { colorClass } from "./colors";
2829

@@ -54,6 +55,7 @@ const icons = Map<string, IconType>({
5455
RiListSettingsFill: RiListSettingsFill,
5556
RiHardDriveFill: RiHardDriveFill,
5657
RiGridFill: RiGridFill,
58+
RiFolderUploadFill: RiFolderUploadFill,
5759
});
5860

5961
export function getIconWithProps(

src/client/web/src/i18n/en_US.ts

+1
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,5 @@ export const msgs: Map<string, string> = Map({
153153
"autoTheme": "Enable auto theme switching",
154154
"term.enabled": "Enabled",
155155
"term.disabled": "Disabled",
156+
"term.dropAnywhere": "Drop files anywhere"
156157
});

src/client/web/src/i18n/zh_CN.ts

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

static/public/css/dark.css

+19
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,22 @@
328328
background-color: #333;
329329
}
330330

331+
.theme-dark .drop-area-container {
332+
position: relative;
333+
width: 100%;
334+
padding-top: 20rem;
335+
}
336+
337+
.theme-dark .drop-area {
338+
opacity: 0.8;
339+
backdrop-filter: blur(9.5px);
340+
text-align: center;
341+
border-radius: 0.8rem;
342+
padding: 2rem 0;
343+
margin: auto;
344+
width: 25rem;
345+
}
346+
331347
/* +colors */
332348

333349
.theme-dark .major-font {
@@ -354,6 +370,9 @@
354370
.theme-dark .focus-bg {
355371
background-color: #16a085;
356372
}
373+
.theme-dark .reverse-bg {
374+
background-color: #fff;
375+
}
357376
.theme-dark .minor-bg {
358377
background-color: #333;
359378
}

static/public/css/white.css

+27
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,30 @@
330330
background-color: #ecf0f1;
331331
}
332332

333+
.theme-default .drop-area-container {
334+
position: relative;
335+
width: 100%;
336+
padding-top: 20rem;
337+
}
338+
339+
.theme-default .drop-area {
340+
opacity: 0.8;
341+
backdrop-filter: blur(9.5px);
342+
text-align: center;
343+
border-radius: 0.8rem;
344+
padding: 2rem 0;
345+
margin: auto;
346+
width: 25rem;
347+
}
348+
349+
.theme-default #login-layer {
350+
z-index: 200;
351+
}
352+
353+
.theme-default #drop-area-layer {
354+
z-index: 4;
355+
}
356+
333357
/* +colors */
334358

335359
.theme-default .minor-font {
@@ -360,6 +384,9 @@
360384
.theme-default .minor-bg {
361385
background-color: #ecf0f6;
362386
}
387+
.theme-default .reverse-bg {
388+
background-color: #000;
389+
}
363390
.theme-default ::placeholder {
364391
color: #95a5a6;
365392
}

yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -4465,10 +4465,10 @@ throat@^6.0.1:
44654465
resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375"
44664466
integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==
44674467

4468-
throttle-debounce@^2.1.0:
4469-
version "2.3.0"
4470-
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2"
4471-
integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==
4468+
throttle-debounce@^4.0.1:
4469+
version "4.0.1"
4470+
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-4.0.1.tgz#f86656fe9c8a6b8218952ef36c3bf225089b1baf"
4471+
integrity sha512-s3PedbXdZtr8v3J5Sxd5T/GmWG80BcK5GVpwDdvgEaUXsaMqQe4zxgmC4TA7B8luSDCPxo3CeSBS3F9rF1CZwg==
44724472

44734473
44744474
version "1.0.5"

0 commit comments

Comments
 (0)