diff --git a/README.md b/README.md index e278e00..b34d06a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- [WORKING IN PROGRESS!!!] Quickshare + Quickshare

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 diff --git a/src/client/web/src/components/__test__/auth_pane.test.tsx b/src/client/web/src/components/__test__/pane_login.test.tsx similarity index 97% rename from src/client/web/src/components/__test__/auth_pane.test.tsx rename to src/client/web/src/components/__test__/pane_login.test.tsx index 26c5008..6315565 100644 --- a/src/client/web/src/components/__test__/auth_pane.test.tsx +++ b/src/client/web/src/components/__test__/pane_login.test.tsx @@ -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"; diff --git a/src/client/web/src/components/browser.tsx b/src/client/web/src/components/browser.tsx index dbc3767..94a441d 100644 --- a/src/client/web/src/components/browser.tsx +++ b/src/client/web/src/components/browser.tsx @@ -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; uploadValue: string; + isVertical: boolean; + update?: (updater: (prevState: ICoreState) => ICoreState) => void; } @@ -156,11 +158,6 @@ export class Updater { return Updater.setItems(List(dstDir.split("/"))); }; - static setPwd = async (oldPwd: string, newPwd: string): Promise => { - 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; - - show: boolean; - oldPwd: string; - newPwd1: string; - newPwd2: string; } export class Browser extends React.Component { @@ -201,10 +193,6 @@ export class Browser extends React.Component { inputValue: "", selectedSrc: "", selectedItems: Map(), - show: false, - oldPwd: "", - newPwd1: "", - newPwd2: "", }; this.uploadInput = undefined; @@ -227,18 +215,9 @@ export class Browser extends React.Component { }); } - showPane = () => { - this.setState({ show: !this.state.show }); - }; - changeOldPwd = (ev: React.ChangeEvent) => { - this.setState({ oldPwd: ev.target.value }); - }; - changeNewPwd1 = (ev: React.ChangeEvent) => { - this.setState({ newPwd1: ev.target.value }); - }; - changeNewPwd2 = (ev: React.ChangeEvent) => { - this.setState({ newPwd2: ev.target.value }); - }; + // showPane = () => { + // this.setState({ show: !this.state.show }); + // }; onInputChange = (ev: React.ChangeEvent) => { this.setState({ inputValue: ev.target.value }); }; @@ -352,31 +331,6 @@ export class Browser extends React.Component { }); }; - 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 { } ); + 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 = [ + , + , + + + + , + + + + , + ]; + const ops = ( -

-
- - - - - - - - - - - - - - - - - -
-
-
-

Update Password

- - - - -
-
-
+ ); const itemList = this.props.items.map((item: MetadataResp) => { @@ -497,59 +411,67 @@ export class Browser extends React.Component { return item.isDir ? ( - + this.gotoChild(item.name)} > {item.name} - -- - {item.modTime.slice(0, item.modTime.indexOf("T"))} - + -- + + {item.modTime.slice(0, item.modTime.indexOf("T"))} + - + + + ) : ( - + {item.name} - {FileSize(item.size, { round: 0 })} - {item.modTime.slice(0, item.modTime.indexOf("T"))} - + {FileSize(item.size, { round: 0 })} + + {item.modTime.slice(0, item.modTime.indexOf("T"))} + - + + + ); @@ -561,28 +483,28 @@ export class Browser extends React.Component { return ( - + - {fileName} +
{fileName}
+
+ + +
{FileSize(uploading.uploaded, { round: 0 })} {FileSize(uploading.size, { round: 0 })} - - - - ); }); @@ -593,56 +515,60 @@ export class Browser extends React.Component {
{ops}
-
+
{breadcrumb}
- - - - - - - - - - - {uploadingList} - - - - - - - - - -
- - NameUploadedSizeAction
+ {this.props.uploadings.size === 0 ? null : ( +
+ + + + + + + + + + {uploadingList} + + + + + + + + +
+ + NameUploadedSize
+
+ )} - - - - - - - - - - - {itemList} - - - - - - - - - -
- - NameFile SizeMod TimeEdit
+
+ + + + + + + + + + + {itemList} + + + + + + + + + +
+ + NameFile SizeMod TimeEdit
+
); diff --git a/src/client/web/src/components/core_state.ts b/src/client/web/src/components/core_state.ts index 22fbc30..58a65fc 100644 --- a/src/client/web/src/components/core_state.ts +++ b/src/client/web/src/components/core_state.ts @@ -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(["."]), items: List([]), uploadings: List([]), uploadValue: "", uploadFiles: List([]), }, + panes: { + displaying: "", + paneNames: Set(["settings", "login"]), + login: { + authed: false, + }, + }, }, }; } diff --git a/src/client/web/src/components/layouter.tsx b/src/client/web/src/components/layouter.tsx new file mode 100644 index 0000000..5001a2d --- /dev/null +++ b/src/client/web/src/components/layouter.tsx @@ -0,0 +1,38 @@ +import * as React from "react"; + +export interface Props { + isHorizontal: boolean; + elements: Array; +} + +export interface State {} +export class Layouter extends React.Component { + constructor(p: Props) { + super(p); + } + + horizontalLayout = (children: Array): Array => { + return children.map((child: JSX.Element, idx: number) => { + // if (idx === 0) { + // return {child}; + // } + return ( + + {child} + + + ); + }); + }; + + verticalLayout = (children: Array): Array => { + return this.horizontalLayout(children); + }; + + render() { + const elements = this.props.isHorizontal + ? this.horizontalLayout(this.props.elements) + : this.verticalLayout(this.props.elements); + return
{elements}
; + } +} diff --git a/src/client/web/src/components/auth_pane.tsx b/src/client/web/src/components/pane_login.tsx similarity index 62% rename from src/client/web/src/components/auth_pane.tsx rename to src/client/web/src/components/pane_login.tsx index a8f12c4..c8ab867 100644 --- a/src/client/web/src/components/auth_pane.tsx +++ b/src/client/web/src/components/pane_login.tsx @@ -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 { }; 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(["."]) + ); + } 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 { }; render() { + const elements: Array = [ + , + , + , + ]; + return ( - - - +
Login
+
, + ]; + + return ( +
+
Update Password
+ +
+ + +
+ ); + } +} diff --git a/src/client/web/src/components/panel.tsx b/src/client/web/src/components/panel.tsx index e2d70a4..b08ede0 100644 --- a/src/client/web/src/components/panel.tsx +++ b/src/client/web/src/components/panel.tsx @@ -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 { this.update = p.update; } + showSettings = () => { + PanesUpdater.displayPane("settings"); + this.update(PanesUpdater.updateState); + }; + render() { return (
+ +
- Quickshare + + Quickshare + - +
-
+
{ update={this.update} uploadFiles={this.props.browser.uploadFiles} uploadValue={this.props.browser.uploadValue} + isVertical={this.props.browser.isVertical} />
+
+ Quickshare - + sharing in simple way. +
); diff --git a/src/client/web/src/components/panes.tsx b/src/client/web/src/components/panes.tsx new file mode 100644 index 0000000..152a11c --- /dev/null +++ b/src/client/web/src/components/panes.tsx @@ -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; + 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 { + 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 = Map({ + settings: , + login: , + }); + + const panes = panesMap.keySeq().map( + (paneName: string): JSX.Element => { + const isDisplay = displaying === paneName ? "" : "hidden"; + return ( +
+ {panesMap.get(paneName)} +
+ ); + } + ); + + const btnClass = displaying === "login" ? "hidden" : ""; + return ( +
+
+
+
+ +
+
+ {panes} +
+
+
+ ); + } +} diff --git a/src/client/web/src/components/state_mgr.tsx b/src/client/web/src/components/state_mgr.tsx index ea991e3..d5646af 100644 --- a/src/client/web/src/components/state_mgr.tsx +++ b/src/client/web/src/components/state_mgr.tsx @@ -26,12 +26,15 @@ export class StateMgr extends React.Component { render() { return ( - +
+ +
); } } diff --git a/src/client/web/src/theme/style.css b/src/client/web/src/theme/style.css index 55a30d0..c6d783f 100644 --- a/src/client/web/src/theme/style.css +++ b/src/client/web/src/theme/style.css @@ -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; +} diff --git a/src/client/web/src/theme/white.css b/src/client/web/src/theme/white.css index 0c593fa..26fc3dc 100644 --- a/src/client/web/src/theme/white.css +++ b/src/client/web/src/theme/white.css @@ -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 );