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

641 lines
19 KiB
TypeScript

import * as React from "react";
import * as ReactDOM from "react-dom";
import { List, Map, Set } from "immutable";
import FileSize from "filesize";
import { RiFolder2Fill } from "@react-icons/all-files/ri/RiFolder2Fill";
import { RiHomeSmileFill } from "@react-icons/all-files/ri/RiHomeSmileFill";
import { RiFile2Fill } from "@react-icons/all-files/ri/RiFile2Fill";
import { alertMsg, confirmMsg } from "../common/env";
import { updater } from "./state_updater";
import { ICoreState, MsgProps, UIProps } from "./core_state";
import { LoginProps } from "./pane_login";
import { MetadataResp, roleVisitor, roleAdmin } from "../client";
import { Flexbox } from "./layout/flexbox";
import { Up } from "../worker/upload_mgr";
import { UploadEntry, UploadState } from "../worker/interface";
export interface Item {
name: string;
size: number;
modTime: string;
isDir: boolean;
selected: boolean;
sha1: string;
}
export interface FilesProps {
dirPath: List<string>;
isSharing: boolean;
items: List<MetadataResp>;
}
export interface Props {
filesInfo: FilesProps;
msg: MsgProps;
login: LoginProps;
ui: UIProps;
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
}
export function getItemPath(dirPath: string, itemName: string): string {
return dirPath.endsWith("/")
? `${dirPath}${itemName}`
: `${dirPath}/${itemName}`;
}
export interface State {
newFolderName: string;
selectedSrc: string;
selectedItems: Map<string, boolean>;
showDetail: Set<string>;
uploadFiles: string;
}
export class FilesPanel extends React.Component<Props, State, {}> {
private uploadInput: Element | Text;
private assignInput: (input: Element) => void;
private onClickUpload: () => void;
constructor(p: Props) {
super(p);
this.state = {
newFolderName: "",
selectedSrc: "",
selectedItems: Map<string, boolean>(),
showDetail: Set<string>(),
uploadFiles: "",
};
Up().setStatusCb(this.updateProgress);
this.uploadInput = undefined;
this.assignInput = (input) => {
this.uploadInput = ReactDOM.findDOMNode(input);
};
this.onClickUpload = () => {
const uploadInput = this.uploadInput as HTMLButtonElement;
uploadInput.click();
};
}
updateProgress = async (
infos: Map<string, UploadEntry>,
refresh: boolean
) => {
updater().setUploadings(infos);
let errCount = 0;
infos.valueSeq().forEach((entry: UploadEntry) => {
errCount += entry.state === UploadState.Error ? 1 : 0;
});
if (infos.size === 0 || infos.size === errCount) {
// refresh used space
updater()
.self()
.then(() => {
this.props.update(updater().updateLogin);
});
}
if (refresh) {
updater()
.setItems(this.props.filesInfo.dirPath)
.then(() => {
this.props.update(updater().updateFilesInfo);
});
} else {
this.props.update(updater().updateFilesInfo);
}
};
addUploads = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files.length > 1000) {
alertMsg(this.props.msg.pkg.get("err.tooManyUploads"));
return;
}
let fileList = List<File>();
for (let i = 0; i < event.target.files.length; i++) {
fileList = fileList.push(event.target.files[i]);
}
updater().addUploads(fileList);
this.props.update(updater().updateFilesInfo);
this.props.update(updater().updateSharingsInfo);
};
onNewFolderNameChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ newFolderName: ev.target.value });
};
onMkDir = () => {
if (this.state.newFolderName === "") {
alertMsg(this.props.msg.pkg.get("browser.folder.add.fail"));
return;
}
const dirPath = getItemPath(
this.props.filesInfo.dirPath.join("/"),
this.state.newFolderName
);
updater()
.mkDir(dirPath)
.then(() => {
this.setState({ newFolderName: "" });
return updater().setItems(this.props.filesInfo.dirPath);
})
.then(() => {
this.props.update(updater().updateFilesInfo);
this.props.update(updater().updateSharingsInfo);
});
};
delete = () => {
// TODO: selected should be cleaned after change the cwd
if (this.props.filesInfo.dirPath.join("/") !== this.state.selectedSrc) {
alertMsg(this.props.msg.pkg.get("browser.del.fail"));
this.setState({
selectedSrc: this.props.filesInfo.dirPath.join("/"),
selectedItems: Map<string, boolean>(),
});
return;
} else {
const filesToDel = this.state.selectedItems.keySeq().join(", ");
if (
!confirmMsg(
`${this.props.msg.pkg.get("op.confirm")} [${
this.state.selectedItems.size
}]: ${filesToDel}`
)
) {
return;
}
}
updater()
.delete(
this.props.filesInfo.dirPath,
this.props.filesInfo.items,
this.state.selectedItems
)
.then(() => {
return updater().self();
})
.then(() => {
this.props.update(updater().updateFilesInfo);
this.props.update(updater().updateSharingsInfo);
this.props.update(updater().updateLogin);
this.setState({
selectedSrc: "",
selectedItems: Map<string, boolean>(),
});
});
};
moveHere = () => {
const oldDir = this.state.selectedSrc;
const newDir = this.props.filesInfo.dirPath.join("/");
if (oldDir === newDir) {
alertMsg(this.props.msg.pkg.get("browser.move.fail"));
return;
}
updater()
.moveHere(
this.state.selectedSrc,
this.props.filesInfo.dirPath.join("/"),
this.state.selectedItems
)
.then(() => {
this.props.update(updater().updateFilesInfo);
this.props.update(updater().updateSharingsInfo);
this.setState({
selectedSrc: "",
selectedItems: Map<string, boolean>(),
});
});
};
gotoChild = async (childDirName: string) => {
return this.chdir(this.props.filesInfo.dirPath.push(childDirName));
};
chdir = async (dirPath: List<string>) => {
if (dirPath === this.props.filesInfo.dirPath) {
return;
} else if (this.props.login.userRole !== roleAdmin && dirPath.size <= 1) {
alertMsg(this.props.msg.pkg.get("unauthed"));
return;
}
return updater()
.setItems(dirPath)
.then(() => {
return updater().listSharings();
})
.then(() => {
return updater().isSharing(dirPath.join("/"));
})
.then(() => {
this.props.update(updater().updateFilesInfo);
this.props.update(updater().updateSharingsInfo);
});
};
select = (itemName: string) => {
const selectedItems = this.state.selectedItems.has(itemName)
? this.state.selectedItems.delete(itemName)
: this.state.selectedItems.set(itemName, true);
this.setState({
selectedSrc: this.props.filesInfo.dirPath.join("/"),
selectedItems: selectedItems,
});
};
selectAll = () => {
let newSelected = Map<string, boolean>();
const someSelected = this.state.selectedItems.size === 0 ? true : false;
if (someSelected) {
this.props.filesInfo.items.forEach((item) => {
newSelected = newSelected.set(item.name, true);
});
} else {
this.props.filesInfo.items.forEach((item) => {
newSelected = newSelected.delete(item.name);
});
}
this.setState({
selectedSrc: this.props.filesInfo.dirPath.join("/"),
selectedItems: newSelected,
});
};
toggleDetail = (name: string) => {
const showDetail = this.state.showDetail.has(name)
? this.state.showDetail.delete(name)
: this.state.showDetail.add(name);
this.setState({ showDetail });
};
generateHash = async (filePath: string): Promise<boolean> => {
alertMsg(this.props.msg.pkg.get("refresh-hint"));
return updater().generateHash(filePath);
};
addSharing = async () => {
return updater()
.addSharing()
.then((ok) => {
if (!ok) {
alertMsg(this.props.msg.pkg.get("browser.share.add.fail"));
} else {
updater().setSharing(true);
}
})
.then(() => {
this.props.update(updater().updateFilesInfo);
this.props.update(updater().updateSharingsInfo);
});
};
deleteSharing = async (dirPath: string) => {
return updater()
.deleteSharing(dirPath)
.then((ok) => {
if (!ok) {
alertMsg(this.props.msg.pkg.get("browser.share.del.fail"));
} else {
updater().setSharing(false);
}
})
.then(() => {
this.props.update(updater().updateFilesInfo);
this.props.update(updater().updateSharingsInfo);
});
};
render() {
const showOp = this.props.login.userRole === roleVisitor ? "hidden" : "";
const breadcrumb = this.props.filesInfo.dirPath.map(
(pathPart: string, key: number) => {
return (
<button
key={pathPart}
onClick={() =>
this.chdir(this.props.filesInfo.dirPath.slice(0, key + 1))
}
className="item"
>
{pathPart}
</button>
);
}
);
const nameWidthClass = `item-name item-name-${
this.props.ui.isVertical ? "vertical" : "horizontal"
} pointer`;
const ops = (
<div id="upload-op">
<div className="float">
<input
type="text"
onChange={this.onNewFolderNameChange}
value={this.state.newFolderName}
placeholder={this.props.msg.pkg.get("browser.folder.name")}
className="float"
/>
<button onClick={this.onMkDir} className="float">
{this.props.msg.pkg.get("browser.folder.add")}
</button>
</div>
<div className="float">
<button onClick={this.onClickUpload}>
{this.props.msg.pkg.get("browser.upload")}
</button>
<input
type="file"
onChange={this.addUploads}
multiple={true}
value={this.state.uploadFiles}
ref={this.assignInput}
className="hidden"
/>
</div>
</div>
);
const sortedItems = this.props.filesInfo.items.sort(
(item1: MetadataResp, item2: MetadataResp) => {
if (item1.isDir && !item2.isDir) {
return -1;
} else if (!item1.isDir && item2.isDir) {
return 1;
}
return 0;
}
);
const itemList = sortedItems.map((item: MetadataResp) => {
const isSelected = this.state.selectedItems.has(item.name);
const dirPath = this.props.filesInfo.dirPath.join("/");
const itemPath = dirPath.endsWith("/")
? `${dirPath}${item.name}`
: `${dirPath}/${item.name}`;
return item.isDir ? (
<Flexbox
key={item.name}
children={List([
<span className="padding-m">
<Flexbox
children={List([
<RiFolder2Fill
size="3rem"
className="yellow0-font margin-r-m"
/>,
<span className={`${nameWidthClass}`}>
<span
className="title-m"
onClick={() => this.gotoChild(item.name)}
>
{item.name}
</span>
<div className="desc-m grey0-font">
<span>
{item.modTime.slice(0, item.modTime.indexOf("T"))}
</span>
</div>
</span>,
])}
childrenStyles={List([
{ flex: "0 0 auto" },
{ flex: "0 0 auto" },
])}
/>
</span>,
<span className={`item-op padding-m ${showOp}`}>
<button
onClick={() => this.select(item.name)}
className={`${
isSelected ? "cyan0-bg white-font" : "grey2-bg grey3-font"
}`}
style={{ width: "8rem", display: "inline-block" }}
>
{isSelected
? this.props.msg.pkg.get("browser.deselect")
: this.props.msg.pkg.get("browser.select")}
</button>
</span>,
])}
childrenStyles={List([
{ flex: "0 0 auto", width: "60%" },
{ flex: "0 0 auto", justifyContent: "flex-end", width: "40%" },
])}
/>
) : (
<div key={item.name}>
<Flexbox
key={item.name}
children={List([
<Flexbox
children={List([
<RiFile2Fill
size="3rem"
className="cyan0-font margin-l-m margin-r-m"
/>,
<span className={`${nameWidthClass}`}>
<a
className="title-m"
href={`/v1/fs/files?fp=${itemPath}`}
target="_blank"
>
{item.name}
</a>
<div className="desc-m grey0-font">
<span>
{item.modTime.slice(0, item.modTime.indexOf("T"))}
</span>
&nbsp;/&nbsp;
<span>{FileSize(item.size, { round: 0 })}</span>
</div>
</span>,
])}
childrenStyles={List([
{ flex: "0 0 auto" },
{ flex: "0 0 auto" },
])}
/>,
<span className={`item-op padding-m ${showOp}`}>
<button
onClick={() => this.toggleDetail(item.name)}
style={{ width: "8rem" }}
className="float-input"
>
{this.props.msg.pkg.get("detail")}
</button>
<button
type="button"
onClick={() => this.select(item.name)}
className={`float-input ${
isSelected ? "cyan0-bg white-font " : "grey2-bg grey3-font "
}`}
style={{ width: "8rem" }}
>
{isSelected
? this.props.msg.pkg.get("browser.deselect")
: this.props.msg.pkg.get("browser.select")}
</button>
</span>,
])}
childrenStyles={List([
{ flex: "0 0 auto", width: "60%" },
{ flex: "0 0 auto", justifyContent: "flex-end", width: "40%" },
])}
/>
<div
className={`${
this.state.showDetail.has(item.name) ? "" : "hidden"
}`}
>
<Flexbox
children={List([
<span>
<b>SHA1:</b>
{` ${item.sha1}`}
</span>,
<button
onClick={() => this.generateHash(itemPath)}
className="black-bg white-font margin-l-m"
style={{ display: "inline-block" }}
>
{this.props.msg.pkg.get("refresh")}
</button>,
])}
className={`grey2-bg grey3-font detail margin-r-m`}
childrenStyles={List([{}, { justifyContent: "flex-end" }])}
/>
</div>
</div>
);
});
const usedSpace = FileSize(parseInt(this.props.login.usedSpace, 10), {
round: 0,
});
const spaceLimit = FileSize(
parseInt(this.props.login.quota.spaceLimit, 10),
{
round: 0,
}
);
const itemListPane = (
<div id="item-list">
<div className={`container ${showOp}`}>{ops}</div>
<div className="container">
<div id="browser-op" className={`${showOp}`}>
<Flexbox
children={List([
<span>
{this.props.filesInfo.isSharing ? (
<button
type="button"
onClick={() => {
this.deleteSharing(
this.props.filesInfo.dirPath.join("/")
);
}}
className="red-btn"
>
{this.props.msg.pkg.get("browser.share.del")}
</button>
) : (
<button
type="button"
onClick={this.addSharing}
className="cyan-btn"
>
{this.props.msg.pkg.get("browser.share.add")}
</button>
)}
</span>,
<span>
{this.state.selectedItems.size > 0 ? (
<span>
<button
type="button"
onClick={() => this.delete()}
className="red-btn"
>
{this.props.msg.pkg.get("browser.delete")}
</button>
<button type="button" onClick={() => this.moveHere()}>
{this.props.msg.pkg.get("browser.paste")}
</button>
</span>
) : null}
</span>,
<span>
<span
id="space-used"
className="desc-m grey0-font"
>{`${this.props.msg.pkg.get(
"browser.used"
)} ${usedSpace} / ${spaceLimit}`}</span>
</span>,
])}
childrenStyles={List([
{ flex: "0 0 auto" },
{ flex: "0 0 auto" },
{ justifyContent: "flex-end" },
])}
/>
</div>
<Flexbox
children={List([
<span id="breadcrumb">
<Flexbox
children={List([
<RiHomeSmileFill size="3rem" id="icon-home" />,
<Flexbox children={breadcrumb} />,
])}
childrenStyles={List([
{ flex: "0 0 auto" },
{ flex: "0 0 auto" },
])}
/>
</span>,
<span className={`${showOp}`}>
<button onClick={() => this.selectAll()} className="select-btn">
{this.props.msg.pkg.get("browser.selectAll")}
</button>
</span>,
])}
childrenStyles={List([{}, { justifyContent: "flex-end" }])}
/>
{itemList}
</div>
</div>
);
return <div id="browser">{itemListPane}</div>;
}
}