Dev cleanup (#32)
* fix(client): fix layout issues in mobile devices * feat(client/panes): add panes, settings, login pane * fix(client/dialog): move return button back to dialog * fix(client/panes): fix style issues * fix(client/browser): fix table style issues * fix(client): refresh list after login * chore(readme): update reademe
This commit is contained in:
parent
31e4850344
commit
1ff1e2024e
12 changed files with 653 additions and 331 deletions
45
README.md
45
README.md
|
@ -1,5 +1,5 @@
|
|||
<h1 align="center">
|
||||
[WORKING IN PROGRESS!!!] Quickshare
|
||||
Quickshare
|
||||
</h1>
|
||||
<p align="center">
|
||||
Simple file sharing server built with Go/Golang, Typescript, Gin, React, Boltdb, etc.
|
||||
|
@ -24,50 +24,21 @@ Choose Language: English | [简体中文](./docs/README_zh-cn.md)
|
|||
|
||||
Visit [Release Page](https://github.com/ihexxa/quickshare/releases) to get Linux | Mac | Windows distribution(s).
|
||||
|
||||
## Features
|
||||
## Main Features
|
||||
|
||||
- Upload and download in browser, no client
|
||||
- Share files among desktop and mobile devices
|
||||
- Portable software
|
||||
- Add files from local
|
||||
- Add download limit for resource
|
||||
- Download from interrupted point
|
||||
- Uploading and downloading in browser without client
|
||||
- Be compatible with Linux, Mac and Windows
|
||||
- Sharing files among different devices (desktop & mobile)
|
||||
- Stopping and resuming uploading/downloading
|
||||
|
||||
## Installation
|
||||
|
||||
2 steps are needed to start a quickshare: unzip it and start it.
|
||||
|
||||
The first step, unzip and start quickshare
|
||||
|
||||
### Linux
|
||||
|
||||
- Unzip the package: `unzip [package].` (`[package]` could be `quickshare_0.0.8_linux_x86_6 4.zip`)
|
||||
- Start quickshare `./quickshare`
|
||||
|
||||
### Mac
|
||||
|
||||
- Unzip the package: `unzip [package].` (`[package]` could be `quickshare_0.0.8_macos_x86_64.zip`)
|
||||
- Start quickshare `./quickshare`
|
||||
|
||||
### Windows
|
||||
|
||||
- Unzip the package
|
||||
- Go into folder and click `quickshare.exe`
|
||||
|
||||
Last step, meet quickshare in browser
|
||||
|
||||
- Quickshare will start and show `quickshare starts @ [URL]` in terminal (e.g. `URL` could be `192.168.0.1:8888`)
|
||||
- Open `URL` in browser and login with `admin` and `quicksh@re`
|
||||
- Enjoy (But don't forget to change password according to [FAQ document](./docs/FAQ_en-us.md))
|
||||
Coming soon.
|
||||
|
||||
### FAQ
|
||||
|
||||
Please refer [FAQ document](./docs/FAQ_en-us.md)
|
||||
|
||||
### Configuration
|
||||
|
||||
Please refer [Configuration document](./docs/CONFIG_en-us.md)
|
||||
|
||||
### Contribution
|
||||
|
||||
Will add it soon...
|
||||
TODO
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { mock, instance } from "ts-mockito";
|
||||
|
||||
import { initWithWorker } from "../core_state";
|
||||
import { Updater } from "../auth_pane";
|
||||
import { Updater } from "../pane_login";
|
||||
import { MockUsersClient } from "../../client/users_mock";
|
||||
import { Response } from "../../client";
|
||||
import { MockWorker } from "../../worker/interface";
|
|
@ -3,6 +3,7 @@ import * as ReactDOM from "react-dom";
|
|||
import { List, Map } from "immutable";
|
||||
import FileSize from "filesize";
|
||||
|
||||
import { Layouter } from "./layouter";
|
||||
import { ICoreState } from "./core_state";
|
||||
import {
|
||||
IUsersClient,
|
||||
|
@ -14,7 +15,6 @@ import { FilesClient } from "../client/files";
|
|||
import { UsersClient } from "../client/users";
|
||||
import { UploadMgr } from "../worker/upload_mgr";
|
||||
import { UploadEntry } from "../worker/interface";
|
||||
// import { FileUploader } from "../worker/uploader";
|
||||
|
||||
export const uploadCheckCycle = 1000;
|
||||
|
||||
|
@ -34,6 +34,8 @@ export interface Props {
|
|||
uploadFiles: List<File>;
|
||||
uploadValue: string;
|
||||
|
||||
isVertical: boolean;
|
||||
|
||||
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||
}
|
||||
|
||||
|
@ -156,11 +158,6 @@ export class Updater {
|
|||
return Updater.setItems(List<string>(dstDir.split("/")));
|
||||
};
|
||||
|
||||
static setPwd = async (oldPwd: string, newPwd: string): Promise<boolean> => {
|
||||
const resp = await Updater.usersClient.setPwd(oldPwd, newPwd);
|
||||
return resp.status === 200;
|
||||
};
|
||||
|
||||
static addUploadFiles = (fileList: FileList, len: number) => {
|
||||
for (let i = 0; i < len; i++) {
|
||||
// do not wait for the promise
|
||||
|
@ -179,11 +176,6 @@ export interface State {
|
|||
inputValue: string;
|
||||
selectedSrc: string;
|
||||
selectedItems: Map<string, boolean>;
|
||||
|
||||
show: boolean;
|
||||
oldPwd: string;
|
||||
newPwd1: string;
|
||||
newPwd2: string;
|
||||
}
|
||||
|
||||
export class Browser extends React.Component<Props, State, {}> {
|
||||
|
@ -201,10 +193,6 @@ export class Browser extends React.Component<Props, State, {}> {
|
|||
inputValue: "",
|
||||
selectedSrc: "",
|
||||
selectedItems: Map<string, boolean>(),
|
||||
show: false,
|
||||
oldPwd: "",
|
||||
newPwd1: "",
|
||||
newPwd2: "",
|
||||
};
|
||||
|
||||
this.uploadInput = undefined;
|
||||
|
@ -227,18 +215,9 @@ export class Browser extends React.Component<Props, State, {}> {
|
|||
});
|
||||
}
|
||||
|
||||
showPane = () => {
|
||||
this.setState({ show: !this.state.show });
|
||||
};
|
||||
changeOldPwd = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ oldPwd: ev.target.value });
|
||||
};
|
||||
changeNewPwd1 = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ newPwd1: ev.target.value });
|
||||
};
|
||||
changeNewPwd2 = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ newPwd2: ev.target.value });
|
||||
};
|
||||
// showPane = () => {
|
||||
// this.setState({ show: !this.state.show });
|
||||
// };
|
||||
onInputChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ inputValue: ev.target.value });
|
||||
};
|
||||
|
@ -352,31 +331,6 @@ export class Browser extends React.Component<Props, State, {}> {
|
|||
});
|
||||
};
|
||||
|
||||
setPwd = () => {
|
||||
if (this.state.newPwd1 !== this.state.newPwd2) {
|
||||
alert("new passwords are not same");
|
||||
} else if (this.state.newPwd1 == "") {
|
||||
alert("new passwords can not be empty");
|
||||
} else if (this.state.oldPwd == this.state.newPwd1) {
|
||||
alert("old and new passwords are same");
|
||||
} else {
|
||||
Updater.setPwd(this.state.oldPwd, this.state.newPwd1).then(
|
||||
(ok: boolean) => {
|
||||
if (ok) {
|
||||
alert("Password is updated");
|
||||
} else {
|
||||
alert("fail to update password");
|
||||
}
|
||||
this.setState({
|
||||
oldPwd: "",
|
||||
newPwd1: "",
|
||||
newPwd2: "",
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const breadcrumb = this.props.dirPath.map(
|
||||
(pathPart: string, key: number) => {
|
||||
|
@ -395,96 +349,56 @@ export class Browser extends React.Component<Props, State, {}> {
|
|||
}
|
||||
);
|
||||
|
||||
const nameCellClass = `item-name item-name-${
|
||||
this.props.isVertical ? "vertical" : "horizontal"
|
||||
} pointer`;
|
||||
const sizeCellClass = this.props.isVertical ? `hidden margin-s` : ``;
|
||||
const modTimeCellClass = this.props.isVertical ? `hidden margin-s` : ``;
|
||||
|
||||
const layoutChildren = [
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => this.delete()}
|
||||
className="red0-bg white-font margin-t-m margin-b-m"
|
||||
>
|
||||
Delete Selected
|
||||
</button>,
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => this.moveHere()}
|
||||
className="grey1-bg white-font margin-t-m margin-b-m"
|
||||
>
|
||||
Paste
|
||||
</button>,
|
||||
<span className="inline-block margin-t-m margin-b-m">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.onInputChange}
|
||||
value={this.state.inputValue}
|
||||
className="black0-font margin-r-m"
|
||||
placeholder="folder name"
|
||||
/>
|
||||
<button onClick={this.onMkDir} className="grey1-bg white-font">
|
||||
Create Folder
|
||||
</button>
|
||||
</span>,
|
||||
<span className="inline-block margin-t-m margin-b-m">
|
||||
<button onClick={this.onClickUpload} className="green0-bg white-font">
|
||||
Upload Files
|
||||
</button>
|
||||
<input
|
||||
type="file"
|
||||
onChange={this.addUploadFile}
|
||||
multiple={true}
|
||||
value={this.props.uploadValue}
|
||||
ref={this.assignInput}
|
||||
className="black0-font hidden"
|
||||
/>
|
||||
</span>,
|
||||
];
|
||||
|
||||
const ops = (
|
||||
<div>
|
||||
<div className="grey0-font">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => this.delete()}
|
||||
className="red0-bg white-font margin-m"
|
||||
>
|
||||
Delete Selected
|
||||
</button>
|
||||
<span className="margin-s">-</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => this.moveHere()}
|
||||
className="grey1-bg white-font margin-m"
|
||||
>
|
||||
Paste
|
||||
</button>
|
||||
<span className="margin-s">-</span>
|
||||
<button
|
||||
onClick={this.onClickUpload}
|
||||
className="green0-bg white-font margin-m"
|
||||
>
|
||||
Upload Files
|
||||
</button>
|
||||
<span className="margin-s">-</span>
|
||||
<span className="margin-m">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.onInputChange}
|
||||
value={this.state.inputValue}
|
||||
className="margin-r-m black0-font"
|
||||
placeholder="folder name"
|
||||
/>
|
||||
<button onClick={this.onMkDir} className="grey1-bg white-font">
|
||||
Create Folder
|
||||
</button>
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
onChange={this.addUploadFile}
|
||||
multiple={true}
|
||||
value={this.props.uploadValue}
|
||||
ref={this.assignInput}
|
||||
className="black0-font hidden margin-m"
|
||||
/>
|
||||
<span className="margin-s">-</span>
|
||||
<button
|
||||
onClick={this.showPane}
|
||||
className="grey1-bg white-font margin-m"
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{ display: this.state.show ? "inherit" : "none" }}
|
||||
className="margin-t-m"
|
||||
>
|
||||
<h3 className="padding-l-s grey0-font">Update Password</h3>
|
||||
<input
|
||||
name="old_pwd"
|
||||
type="password"
|
||||
onChange={this.changeOldPwd}
|
||||
value={this.state.oldPwd}
|
||||
className="margin-m black0-font"
|
||||
placeholder="old password"
|
||||
/>
|
||||
<input
|
||||
name="new_pwd1"
|
||||
type="password"
|
||||
onChange={this.changeNewPwd1}
|
||||
value={this.state.newPwd1}
|
||||
className="margin-m black0-font"
|
||||
placeholder="new password"
|
||||
/>
|
||||
<input
|
||||
name="new_pwd2"
|
||||
type="password"
|
||||
onChange={this.changeNewPwd2}
|
||||
value={this.state.newPwd2}
|
||||
className="margin-m black0-font"
|
||||
placeholder="new password again"
|
||||
/>
|
||||
<button onClick={this.setPwd} className="grey1-bg white-font">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Layouter isHorizontal={false} elements={layoutChildren}></Layouter>
|
||||
);
|
||||
|
||||
const itemList = this.props.items.map((item: MetadataResp) => {
|
||||
|
@ -497,59 +411,67 @@ export class Browser extends React.Component<Props, State, {}> {
|
|||
return item.isDir ? (
|
||||
<tr
|
||||
key={item.name}
|
||||
className={`${isSelected ? "white0-bg selected" : ""}`}
|
||||
// className={`${isSelected ? "white0-bg selected" : ""}`}
|
||||
>
|
||||
<td className="padding-l-l" style={{ width: "3rem" }}>
|
||||
<td>
|
||||
<span className="dot yellow0-bg"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className="item-name pointer"
|
||||
className={nameCellClass}
|
||||
onClick={() => this.gotoChild(item.name)}
|
||||
>
|
||||
{item.name}
|
||||
</span>
|
||||
</td>
|
||||
<td>--</td>
|
||||
<td>{item.modTime.slice(0, item.modTime.indexOf("T"))}</td>
|
||||
|
||||
<td className={sizeCellClass}>--</td>
|
||||
<td className={modTimeCellClass}>
|
||||
{item.modTime.slice(0, item.modTime.indexOf("T"))}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={() => this.select(item.name)}
|
||||
className="white-font margin-t-m margin-b-m"
|
||||
>
|
||||
{isSelected ? "Unselect" : "Select"}
|
||||
</button>
|
||||
<span className="item-op">
|
||||
<button
|
||||
onClick={() => this.select(item.name)}
|
||||
className={`white-font ${isSelected ? "blue0-bg" : ""}`}
|
||||
style={{width: "8rem", display: "inline-block"}}
|
||||
>
|
||||
{isSelected ? "Deselect" : "Select"}
|
||||
</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
<tr
|
||||
key={item.name}
|
||||
className={`${isSelected ? "white0-bg selected" : ""}`}
|
||||
// className={`${isSelected ? "white0-bg selected" : ""}`}
|
||||
>
|
||||
<td className="padding-l-l" style={{ width: "3rem" }}>
|
||||
<td>
|
||||
<span className="dot green0-bg"></span>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
className="item-name"
|
||||
className={nameCellClass}
|
||||
href={`/v1/fs/files?fp=${itemPath}`}
|
||||
target="_blank"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
</td>
|
||||
<td>{FileSize(item.size, { round: 0 })}</td>
|
||||
<td>{item.modTime.slice(0, item.modTime.indexOf("T"))}</td>
|
||||
|
||||
<td className={sizeCellClass}>{FileSize(item.size, { round: 0 })}</td>
|
||||
<td className={modTimeCellClass}>
|
||||
{item.modTime.slice(0, item.modTime.indexOf("T"))}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => this.select(item.name)}
|
||||
className="white-font margin-t-m margin-b-m"
|
||||
>
|
||||
{isSelected ? "Unselect" : "Select"}
|
||||
</button>
|
||||
<span className="item-op">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => this.select(item.name)}
|
||||
className={`white-font ${isSelected ? "blue0-bg" : ""}`}
|
||||
style={{width: "8rem", display: "inline-block"}}
|
||||
>
|
||||
{isSelected ? "Deselect" : "Select"}
|
||||
</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
@ -561,28 +483,28 @@ export class Browser extends React.Component<Props, State, {}> {
|
|||
|
||||
return (
|
||||
<tr key={fileName}>
|
||||
<td className="padding-l-l" style={{ width: "3rem" }}>
|
||||
<td>
|
||||
<span className="dot blue0-bg"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="item-name pointer">{fileName}</span>
|
||||
<div className={nameCellClass}>{fileName}</div>
|
||||
<div className="item-op">
|
||||
<button
|
||||
onClick={() => this.stopUploading(uploading.realFilePath)}
|
||||
className="white-font margin-r-m"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
onClick={() => this.deleteUploading(uploading.realFilePath)}
|
||||
className="white-font"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>{FileSize(uploading.uploaded, { round: 0 })}</td>
|
||||
<td>{FileSize(uploading.size, { round: 0 })}</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={() => this.stopUploading(uploading.realFilePath)}
|
||||
className="white-font margin-m"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
onClick={() => this.deleteUploading(uploading.realFilePath)}
|
||||
className="white-font margin-m"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
@ -593,56 +515,60 @@ export class Browser extends React.Component<Props, State, {}> {
|
|||
<div className="margin-l-m margin-r-m">{ops}</div>
|
||||
</div>
|
||||
|
||||
<div id="item-list" className="">
|
||||
<div id="item-list">
|
||||
<div className="margin-b-l">{breadcrumb}</div>
|
||||
|
||||
<table>
|
||||
<thead style={{ fontWeight: "bold" }}>
|
||||
<tr>
|
||||
<td className="padding-l-l" style={{ width: "3rem" }}>
|
||||
<span className="dot black-bg"></span>
|
||||
</td>
|
||||
<td>Name</td>
|
||||
<td>Uploaded</td>
|
||||
<td>Size</td>
|
||||
<td>Action</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{uploadingList}</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
{this.props.uploadings.size === 0 ? null : (
|
||||
<div className="container">
|
||||
<table>
|
||||
<thead style={{ fontWeight: "bold" }}>
|
||||
<tr>
|
||||
<td>
|
||||
<span className="dot black-bg"></span>
|
||||
</td>
|
||||
<td>Name</td>
|
||||
<td className={sizeCellClass}>Uploaded</td>
|
||||
<td className={modTimeCellClass}>Size</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{uploadingList}</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td className={sizeCellClass}></td>
|
||||
<td className={modTimeCellClass}></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<table>
|
||||
<thead style={{ fontWeight: "bold" }}>
|
||||
<tr>
|
||||
<td className="padding-l-l" style={{ width: "3rem" }}>
|
||||
<span className="dot black-bg"></span>
|
||||
</td>
|
||||
<td>Name</td>
|
||||
<td>File Size</td>
|
||||
<td>Mod Time</td>
|
||||
<td>Edit</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{itemList}</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<div className="container">
|
||||
<table>
|
||||
<thead style={{ fontWeight: "bold" }}>
|
||||
<tr>
|
||||
<td>
|
||||
<span className="dot black-bg"></span>
|
||||
</td>
|
||||
<td>Name</td>
|
||||
<td className={sizeCellClass}>File Size</td>
|
||||
<td className={modTimeCellClass}>Mod Time</td>
|
||||
<td>Edit</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{itemList}</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td className={sizeCellClass}></td>
|
||||
<td className={modTimeCellClass}></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { List } from "immutable";
|
||||
import { List, Set } from "immutable";
|
||||
|
||||
import BgWorker from "../worker/upload.bg.worker";
|
||||
import { FgWorker } from "../worker/upload.fgworker";
|
||||
|
@ -15,6 +15,7 @@ export interface IContext {
|
|||
export interface ICoreState {
|
||||
ctx: IContext;
|
||||
panel: PanelProps;
|
||||
isVertical: boolean;
|
||||
}
|
||||
|
||||
export function initWithWorker(worker: IWorker): ICoreState {
|
||||
|
@ -33,21 +34,34 @@ export function init(): ICoreState {
|
|||
return initState();
|
||||
}
|
||||
|
||||
export function isVertical(): boolean {
|
||||
return window.innerWidth <= window.innerHeight;
|
||||
}
|
||||
|
||||
export function initState(): ICoreState {
|
||||
return {
|
||||
ctx: null,
|
||||
isVertical: isVertical(),
|
||||
panel: {
|
||||
displaying: "browser",
|
||||
authPane: {
|
||||
authed: false,
|
||||
},
|
||||
browser: {
|
||||
isVertical: isVertical(),
|
||||
dirPath: List<string>(["."]),
|
||||
items: List<Item>([]),
|
||||
uploadings: List<UploadInfo>([]),
|
||||
uploadValue: "",
|
||||
uploadFiles: List<File>([]),
|
||||
},
|
||||
panes: {
|
||||
displaying: "",
|
||||
paneNames: Set<string>(["settings", "login"]),
|
||||
login: {
|
||||
authed: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
38
src/client/web/src/components/layouter.tsx
Normal file
38
src/client/web/src/components/layouter.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import * as React from "react";
|
||||
|
||||
export interface Props {
|
||||
isHorizontal: boolean;
|
||||
elements: Array<JSX.Element>;
|
||||
}
|
||||
|
||||
export interface State {}
|
||||
export class Layouter extends React.Component<Props, State, {}> {
|
||||
constructor(p: Props) {
|
||||
super(p);
|
||||
}
|
||||
|
||||
horizontalLayout = (children: Array<JSX.Element>): Array<JSX.Element> => {
|
||||
return children.map((child: JSX.Element, idx: number) => {
|
||||
// if (idx === 0) {
|
||||
// return <span key={`layout=${idx}`}>{child}</span>;
|
||||
// }
|
||||
return (
|
||||
<span key={`layout=${idx}`}>
|
||||
{child}
|
||||
<span className="margin-s"></span>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
verticalLayout = (children: Array<JSX.Element>): Array<JSX.Element> => {
|
||||
return this.horizontalLayout(children);
|
||||
};
|
||||
|
||||
render() {
|
||||
const elements = this.props.isHorizontal
|
||||
? this.horizontalLayout(this.props.elements)
|
||||
: this.verticalLayout(this.props.elements);
|
||||
return <div className="layouter">{elements}</div>;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,12 @@
|
|||
import * as React from "react";
|
||||
import { List } from "immutable";
|
||||
|
||||
import { ICoreState } from "./core_state";
|
||||
import { IUsersClient } from "../client";
|
||||
import { UsersClient } from "../client/users";
|
||||
import { Updater as PanesUpdater } from "./panes";
|
||||
import { Updater as BrowserUpdater } from "./browser";
|
||||
import { Layouter } from "./layouter";
|
||||
|
||||
export interface Props {
|
||||
authed: boolean;
|
||||
|
@ -90,15 +94,30 @@ export class AuthPane extends React.Component<Props, State, {}> {
|
|||
};
|
||||
|
||||
login = () => {
|
||||
Updater.login(this.state.user, this.state.pwd).then((ok: boolean) => {
|
||||
if (ok) {
|
||||
this.update(Updater.setAuthPane);
|
||||
this.setState({ user: "", pwd: "" });
|
||||
} else {
|
||||
this.setState({ user: "", pwd: "" });
|
||||
alert("Failed to login.");
|
||||
}
|
||||
});
|
||||
Updater.login(this.state.user, this.state.pwd)
|
||||
.then((ok: boolean) => {
|
||||
if (ok) {
|
||||
this.update(Updater.setAuthPane);
|
||||
this.setState({ user: "", pwd: "" });
|
||||
// close all the panes
|
||||
PanesUpdater.displayPane("");
|
||||
this.update(PanesUpdater.updateState);
|
||||
|
||||
// refresh
|
||||
return BrowserUpdater.setItems(
|
||||
List<string>(["."])
|
||||
);
|
||||
} else {
|
||||
this.setState({ user: "", pwd: "" });
|
||||
alert("Failed to login.");
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
return BrowserUpdater.refreshUploadings();
|
||||
})
|
||||
.then((_: boolean) => {
|
||||
this.update(BrowserUpdater.setBrowser);
|
||||
});
|
||||
};
|
||||
|
||||
logout = () => {
|
||||
|
@ -112,30 +131,38 @@ export class AuthPane extends React.Component<Props, State, {}> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const elements: Array<JSX.Element> = [
|
||||
<input
|
||||
name="user"
|
||||
type="text"
|
||||
onChange={this.changeUser}
|
||||
value={this.state.user}
|
||||
className="black0-font margin-t-m margin-b-m"
|
||||
// style={{ width: "80%" }}
|
||||
placeholder="user name"
|
||||
/>,
|
||||
<input
|
||||
name="pwd"
|
||||
type="password"
|
||||
onChange={this.changePwd}
|
||||
value={this.state.pwd}
|
||||
className="black0-font margin-t-m margin-b-m"
|
||||
// style={{ width: "80%" }}
|
||||
placeholder="password"
|
||||
/>,
|
||||
<button
|
||||
onClick={this.login}
|
||||
className="green0-bg white-font margin-t-m margin-b-m"
|
||||
>
|
||||
Log in
|
||||
</button>,
|
||||
];
|
||||
|
||||
return (
|
||||
<span>
|
||||
<span style={{ display: this.props.authed ? "none" : "inherit" }}>
|
||||
<input
|
||||
name="user"
|
||||
type="text"
|
||||
onChange={this.changeUser}
|
||||
value={this.state.user}
|
||||
className="margin-r-m black0-font"
|
||||
style={{ width: "6rem" }}
|
||||
placeholder="user name"
|
||||
/>
|
||||
<input
|
||||
name="pwd"
|
||||
type="password"
|
||||
onChange={this.changePwd}
|
||||
value={this.state.pwd}
|
||||
className="margin-r-m black0-font"
|
||||
style={{ width: "6rem" }}
|
||||
placeholder="password"
|
||||
/>
|
||||
<button onClick={this.login} className="green0-bg white-font">
|
||||
Log in
|
||||
</button>
|
||||
<h5 className="grey0-font">Login</h5>
|
||||
<Layouter isHorizontal={false} elements={elements} />
|
||||
</span>
|
||||
<span style={{ display: this.props.authed ? "inherit" : "none" }}>
|
||||
<button
|
132
src/client/web/src/components/pane_settings.tsx
Normal file
132
src/client/web/src/components/pane_settings.tsx
Normal file
|
@ -0,0 +1,132 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { ICoreState } from "./core_state";
|
||||
import { IUsersClient } from "../client";
|
||||
import { AuthPane, Props as LoginProps } from "./pane_login";
|
||||
import { Layouter } from "./layouter";
|
||||
import { UsersClient } from "../client/users";
|
||||
|
||||
export interface Props {
|
||||
login: LoginProps;
|
||||
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||
}
|
||||
|
||||
export class Updater {
|
||||
private static props: Props;
|
||||
private static usersClient: IUsersClient;
|
||||
|
||||
static init = (props: Props) => (Updater.props = { ...props });
|
||||
static setClient(usersClient: IUsersClient) {
|
||||
Updater.usersClient = usersClient;
|
||||
}
|
||||
|
||||
static setPwd = async (oldPwd: string, newPwd: string): Promise<boolean> => {
|
||||
const resp = await Updater.usersClient.setPwd(oldPwd, newPwd);
|
||||
return resp.status === 200;
|
||||
};
|
||||
|
||||
static updateState = (prevState: ICoreState): ICoreState => {
|
||||
return {
|
||||
...prevState,
|
||||
panel: { ...prevState.panel, ...Updater.props },
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface State {
|
||||
oldPwd: string;
|
||||
newPwd1: string;
|
||||
newPwd2: string;
|
||||
}
|
||||
|
||||
export class PaneSettings extends React.Component<Props, State, {}> {
|
||||
private update: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||
changeOldPwd = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ oldPwd: ev.target.value });
|
||||
};
|
||||
changeNewPwd1 = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ newPwd1: ev.target.value });
|
||||
};
|
||||
changeNewPwd2 = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ newPwd2: ev.target.value });
|
||||
};
|
||||
|
||||
constructor(p: Props) {
|
||||
super(p);
|
||||
Updater.init(p);
|
||||
Updater.setClient(new UsersClient(""));
|
||||
this.update = p.update;
|
||||
this.state = {
|
||||
oldPwd: "",
|
||||
newPwd1: "",
|
||||
newPwd2: "",
|
||||
};
|
||||
}
|
||||
|
||||
setPwd = () => {
|
||||
if (this.state.newPwd1 !== this.state.newPwd2) {
|
||||
alert("new passwords are not same");
|
||||
} else if (this.state.newPwd1 == "") {
|
||||
alert("new passwords can not be empty");
|
||||
} else if (this.state.oldPwd == this.state.newPwd1) {
|
||||
alert("old and new passwords are same");
|
||||
} else {
|
||||
Updater.setPwd(this.state.oldPwd, this.state.newPwd1).then(
|
||||
(ok: boolean) => {
|
||||
if (ok) {
|
||||
alert("Password is updated");
|
||||
} else {
|
||||
alert("fail to update password");
|
||||
}
|
||||
this.setState({
|
||||
oldPwd: "",
|
||||
newPwd1: "",
|
||||
newPwd2: "",
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const inputs: Array<JSX.Element> = [
|
||||
<input
|
||||
name="old_pwd"
|
||||
type="password"
|
||||
onChange={this.changeOldPwd}
|
||||
value={this.state.oldPwd}
|
||||
className="black0-font margin-t-m margin-b-m"
|
||||
placeholder="old password"
|
||||
/>,
|
||||
<input
|
||||
name="new_pwd1"
|
||||
type="password"
|
||||
onChange={this.changeNewPwd1}
|
||||
value={this.state.newPwd1}
|
||||
className="black0-font margin-t-m margin-b-m"
|
||||
placeholder="new password"
|
||||
/>,
|
||||
<input
|
||||
name="new_pwd2"
|
||||
type="password"
|
||||
onChange={this.changeNewPwd2}
|
||||
value={this.state.newPwd2}
|
||||
className="black0-font margin-t-m margin-b-m"
|
||||
placeholder="new password again"
|
||||
/>,
|
||||
<button onClick={this.setPwd} className="grey1-bg white-font">
|
||||
Update
|
||||
</button>,
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h5 className="grey0-font">Update Password</h5>
|
||||
<Layouter isHorizontal={false} elements={inputs}></Layouter>
|
||||
<div className="hr"></div>
|
||||
|
||||
<AuthPane authed={this.props.login.authed} update={this.update} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,12 +2,14 @@ import * as React from "react";
|
|||
|
||||
import { ICoreState } from "./core_state";
|
||||
import { Browser, Props as BrowserProps } from "./browser";
|
||||
import { AuthPane, Props as AuthPaneProps } from "./auth_pane";
|
||||
import { AuthPane, Props as AuthPaneProps } from "./pane_login";
|
||||
import { Panes, Props as PanesProps, Updater as PanesUpdater } from "./panes";
|
||||
|
||||
export interface Props {
|
||||
displaying: string;
|
||||
browser: BrowserProps;
|
||||
authPane: AuthPaneProps;
|
||||
panes: PanesProps;
|
||||
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||
}
|
||||
|
||||
|
@ -33,26 +35,45 @@ export class Panel extends React.Component<Props, State, {}> {
|
|||
this.update = p.update;
|
||||
}
|
||||
|
||||
showSettings = () => {
|
||||
PanesUpdater.displayPane("settings");
|
||||
this.update(PanesUpdater.updateState);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="theme-white desktop">
|
||||
<div id="bg" className="bg bg-img font-m">
|
||||
<Panes
|
||||
displaying={this.props.panes.displaying}
|
||||
paneNames={this.props.panes.paneNames}
|
||||
login={this.props.authPane}
|
||||
update={this.update}
|
||||
/>
|
||||
|
||||
<div
|
||||
id="top-bar"
|
||||
className="top-bar cyan1-font padding-t-m padding-b-m padding-l-l padding-r-l"
|
||||
>
|
||||
<div className="flex-2col-parent">
|
||||
<a href="https://github.com/ihexxa/quickshare" className="flex-13col h5">Quickshare</a>
|
||||
<a
|
||||
href="https://github.com/ihexxa/quickshare"
|
||||
className="flex-13col h5"
|
||||
>
|
||||
Quickshare
|
||||
</a>
|
||||
<span className="flex-23col text-right">
|
||||
<AuthPane
|
||||
authed={this.props.authPane.authed}
|
||||
update={this.update}
|
||||
/>
|
||||
<button
|
||||
onClick={this.showSettings}
|
||||
className="grey1-bg white-font margin-r-m"
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="container-center">
|
||||
<div className="container-center">
|
||||
<Browser
|
||||
dirPath={this.props.browser.dirPath}
|
||||
items={this.props.browser.items}
|
||||
|
@ -60,8 +81,13 @@ export class Panel extends React.Component<Props, State, {}> {
|
|||
update={this.update}
|
||||
uploadFiles={this.props.browser.uploadFiles}
|
||||
uploadValue={this.props.browser.uploadValue}
|
||||
isVertical={this.props.browser.isVertical}
|
||||
/>
|
||||
</div>
|
||||
<div className="container-center black0-font tail margin-t-xl margin-b-xl">
|
||||
<a href="https://github.com/ihexxa/quickshare">Quickshare</a> -
|
||||
sharing in simple way.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
101
src/client/web/src/components/panes.tsx
Normal file
101
src/client/web/src/components/panes.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
import * as React from "react";
|
||||
import { Set, Map } from "immutable";
|
||||
|
||||
import { ICoreState } from "./core_state";
|
||||
import { PaneSettings } from "./pane_settings";
|
||||
import { AuthPane, Props as AuthPaneProps } from "./pane_login";
|
||||
|
||||
export interface Props {
|
||||
displaying: string;
|
||||
paneNames: Set<string>;
|
||||
login: AuthPaneProps;
|
||||
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||
}
|
||||
|
||||
export class Updater {
|
||||
private static props: Props;
|
||||
|
||||
static init = (props: Props) => (Updater.props = { ...props });
|
||||
|
||||
static displayPane = (paneName: string) => {
|
||||
if (paneName === "") {
|
||||
// hide all panes
|
||||
Updater.props.displaying = "";
|
||||
} else {
|
||||
const pane = Updater.props.paneNames.get(paneName);
|
||||
if (pane != null) {
|
||||
Updater.props.displaying = paneName;
|
||||
} else {
|
||||
alert(`dialgos: pane (${paneName}) not found`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static updateState = (prevState: ICoreState): ICoreState => {
|
||||
return {
|
||||
...prevState,
|
||||
panel: {
|
||||
...prevState.panel,
|
||||
panes: { ...prevState.panel.panes, ...Updater.props },
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface State {}
|
||||
export class Panes extends React.Component<Props, State, {}> {
|
||||
private update: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||
constructor(p: Props) {
|
||||
super(p);
|
||||
Updater.init(p);
|
||||
this.update = p.update;
|
||||
}
|
||||
|
||||
closePane = () => {
|
||||
if (this.props.displaying !== "login") {
|
||||
Updater.displayPane("");
|
||||
this.update(Updater.updateState);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let displaying = this.props.displaying;
|
||||
if (!this.props.login.authed) {
|
||||
// TODO: use constant instead
|
||||
displaying = "login";
|
||||
}
|
||||
|
||||
const panesMap: Map<string, JSX.Element> = Map({
|
||||
settings: <PaneSettings login={this.props.login} update={this.update} />,
|
||||
login: <AuthPane authed={this.props.login.authed} update={this.update} />,
|
||||
});
|
||||
|
||||
const panes = panesMap.keySeq().map(
|
||||
(paneName: string): JSX.Element => {
|
||||
const isDisplay = displaying === paneName ? "" : "hidden";
|
||||
return (
|
||||
<div key={paneName} className={`${isDisplay}`}>
|
||||
{panesMap.get(paneName)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const btnClass = displaying === "login" ? "hidden" : "";
|
||||
return (
|
||||
<div id="panes" className={displaying === "" ? "hidden" : ""}>
|
||||
<div className="container">
|
||||
<div className="padding-l">
|
||||
<div className={btnClass}>
|
||||
<button onClick={this.closePane} className="black0-bg white-font">
|
||||
Return
|
||||
</button>
|
||||
<div className="hr"></div>
|
||||
</div>
|
||||
{panes}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -26,12 +26,15 @@ export class StateMgr extends React.Component<Props, State, {}> {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<Panel
|
||||
authPane = {this.state.panel.authPane}
|
||||
displaying={this.state.panel.displaying}
|
||||
update={this.update}
|
||||
browser={this.state.panel.browser}
|
||||
/>
|
||||
<div>
|
||||
<Panel
|
||||
authPane = {this.state.panel.authPane}
|
||||
displaying={this.state.panel.displaying}
|
||||
update={this.update}
|
||||
browser={this.state.panel.browser}
|
||||
panes={this.state.panel.panes}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,58 +9,127 @@
|
|||
|
||||
#top-bar {
|
||||
line-height: 3rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#container-center {
|
||||
margin: 5rem auto auto auto;
|
||||
.container-center {
|
||||
margin: 2rem auto auto auto;
|
||||
width: 96%;
|
||||
max-width: 960px;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.layouter .vcell {
|
||||
text-align: left;
|
||||
padding: 0.5rem 2rem;
|
||||
border-bottom: solid 1px #e8e8e8;
|
||||
}
|
||||
|
||||
#panes {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
z-index: 100;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#panes .container {
|
||||
max-width: 960px;
|
||||
width: 96%;
|
||||
background-color: white;
|
||||
z-index: 101;
|
||||
text-align: left;
|
||||
margin: 3rem auto 8rem auto;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
|
||||
#panes .return-btn {
|
||||
position: fixed;
|
||||
max-width: 960px;
|
||||
width: 96%;
|
||||
margin: auto;
|
||||
z-index: 101;
|
||||
text-align: center;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
|
||||
#op-bar {
|
||||
width: 96%;
|
||||
max-width: 960px;
|
||||
/* width: 96%; */
|
||||
/* max-width: 960px; */
|
||||
|
||||
position: fixed;
|
||||
/* position: fixed;
|
||||
top: 6rem;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
left: auto; */
|
||||
line-height: 3rem;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
|
||||
#item-list {
|
||||
margin-top: 12rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
#item-list .container {
|
||||
width: 100%;
|
||||
color: #34495e;
|
||||
font-size: 1.4rem;
|
||||
line-height: 5rem;
|
||||
background-color: white;
|
||||
border-radius: 0.8rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
#item-list table {
|
||||
width: 100%;
|
||||
color: #34495e;
|
||||
font-size: 1.4rem;
|
||||
line-height: 4rem;
|
||||
background-color: white;
|
||||
border-radius: 0.8rem;
|
||||
margin-bottom: 8rem;
|
||||
}
|
||||
|
||||
#item-list tr {
|
||||
border-top: solid 1px transparent;
|
||||
}
|
||||
|
||||
#item-list .dot {
|
||||
overflow: hidden;
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
#item-list .item-name-cell {
|
||||
max-width: 30%;
|
||||
}
|
||||
|
||||
#item-list .item-name-vertical {
|
||||
width: 14rem;
|
||||
}
|
||||
|
||||
#item-list .item-name-horizontal {
|
||||
width: 48rem;
|
||||
}
|
||||
|
||||
#item-list .item-name {
|
||||
display: block;
|
||||
max-width: 8rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: break-word;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#item-list tr button {
|
||||
#item-list .item-op {
|
||||
line-height: 4rem;
|
||||
}
|
||||
|
||||
tr button {
|
||||
background-color: #95a5a6;
|
||||
}
|
||||
|
||||
#item-list tr.selected button {
|
||||
#item-list tr.selected button {
|
||||
background-color: #e74c3c;
|
||||
}
|
||||
|
||||
|
@ -92,7 +161,7 @@
|
|||
bottom: auto;
|
||||
left: 0;
|
||||
|
||||
font-size: 2.0rem;
|
||||
font-size: 2rem;
|
||||
line-height: 3.9rem;
|
||||
}
|
||||
|
||||
|
@ -496,3 +565,18 @@ div.hr {
|
|||
text-align: left;
|
||||
padding: 1rem 0;
|
||||
} */
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div.hr {
|
||||
height: 1px;
|
||||
background-color: #95a5a6;
|
||||
margin: 1rem auto 1rem auto;
|
||||
}
|
||||
|
||||
.tail {
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@
|
|||
-webkit-backdrop-filter: blur( 9.5px );
|
||||
}
|
||||
|
||||
.theme-white div.hr {
|
||||
/* .theme-white div.hr {
|
||||
background-color: white;
|
||||
}
|
||||
} */
|
||||
|
||||
.theme-white .op-bar {
|
||||
background: rgba( 255, 255, 255, 0.9 );
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue