478 lines
No EOL
19 KiB
TypeScript
478 lines
No EOL
19 KiB
TypeScript
import * as React from "react";
|
|
import {useEffect, useRef, useState} from "react";
|
|
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
|
|
import {
|
|
TransferStatus
|
|
} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
|
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
|
import {HTMLRenderer} from "tc-shared/ui/react-elements/HTMLRenderer";
|
|
import {ProgressBar} from "tc-shared/ui/react-elements/ProgressBar";
|
|
import {
|
|
TransferProgress,
|
|
} from "tc-shared/file/Transfer";
|
|
import {tra} from "tc-shared/i18n/localize";
|
|
import {format_time, network} from "tc-shared/ui/frames/chat";
|
|
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
|
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
|
import {Button} from "tc-shared/ui/react-elements/Button";
|
|
|
|
const cssStyle = require("./TransferInfo.scss");
|
|
const iconArrow = require("./icon_double_arrow.svg");
|
|
const iconTransferUpload = require("./icon_transfer_upload.svg");
|
|
const iconTransferDownload = require("./icon_transfer_download.svg");
|
|
|
|
export interface TransferInfoEvents {
|
|
query_transfers: {},
|
|
query_transfer_result: {
|
|
status: "success" | "error" | "timeout";
|
|
|
|
error?: string;
|
|
transfers?: TransferInfoData[],
|
|
showFinished?: boolean
|
|
}
|
|
|
|
action_toggle_expansion: { visible: boolean },
|
|
action_toggle_finished_transfers: { visible: boolean },
|
|
action_remove_finished: {},
|
|
|
|
notify_transfer_registered: { transfer: TransferInfoData },
|
|
notify_transfer_status: {
|
|
id: number,
|
|
status: TransferStatus,
|
|
error?: string
|
|
},
|
|
notify_transfer_progress: {
|
|
id: number;
|
|
status: TransferStatus,
|
|
progress: TransferProgress
|
|
},
|
|
|
|
notify_modal_closed: {}
|
|
}
|
|
|
|
export interface TransferInfoData {
|
|
id: number;
|
|
|
|
direction: "upload" | "download";
|
|
status: TransferStatus;
|
|
|
|
name: string;
|
|
path: string;
|
|
|
|
progress: number;
|
|
error?: string;
|
|
|
|
timestampRegistered: number;
|
|
timestampBegin: number;
|
|
timestampEnd: number;
|
|
|
|
transferredBytes: number;
|
|
}
|
|
|
|
const ExpendState = (props: { extended: boolean, events: Registry<TransferInfoEvents>}) => {
|
|
const [expended, setExpended] = useState(props.extended);
|
|
|
|
props.events.reactUse("action_toggle_expansion", event => setExpended(event.visible));
|
|
return <div className={cssStyle.expansionContainer + (expended ? " " + cssStyle.expended : "")} onClick={() => props.events.fire("action_toggle_expansion", { visible: !expended })}>
|
|
<HTMLRenderer purify={false}>{iconArrow}</HTMLRenderer>
|
|
</div>;
|
|
};
|
|
|
|
const ToggleFinishedTransfersCheckbox = (props: { events: Registry<TransferInfoEvents> }) => {
|
|
const ref = useRef<Checkbox>(null);
|
|
const [state, setState] = useState({ disabled: true, checked: false });
|
|
props.events.reactUse("action_toggle_finished_transfers", event => {
|
|
setState({
|
|
checked: event.visible,
|
|
disabled: false
|
|
});
|
|
ref.current?.setState({
|
|
checked: event.visible,
|
|
disabled: false
|
|
});
|
|
});
|
|
|
|
props.events.reactUse("query_transfer_result", event => {
|
|
if(event.status !== "success")
|
|
return;
|
|
|
|
setState({
|
|
checked: event.showFinished,
|
|
disabled: false
|
|
});
|
|
ref.current?.setState({
|
|
checked: event.showFinished,
|
|
disabled: false
|
|
});
|
|
});
|
|
|
|
return (
|
|
<Checkbox
|
|
ref={ref}
|
|
initialValue={state.checked}
|
|
disabled={state.disabled}
|
|
onChange={state => props.events.fire("action_toggle_finished_transfers", { visible: state })}
|
|
label={<Translatable>Show finished transfers</Translatable>} />
|
|
);
|
|
};
|
|
|
|
@ReactEventHandler<RunningTransfersInfo>(e => e.props.events)
|
|
class RunningTransfersInfo extends React.Component<{ events: Registry<TransferInfoEvents> }, { state: "error" | "querying" | "normal" }> {
|
|
private runningTransfers: { transfer: TransferInfoData, progress: TransferProgress | undefined }[] = [];
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
state: "querying"
|
|
};
|
|
}
|
|
|
|
private currentStatistic() {
|
|
const progress = this.runningTransfers.map(e => e.progress).filter(e => !!e);
|
|
return {
|
|
totalBytes: progress.map(e => e.file_total_size).reduce((a, b) => a + b, 0),
|
|
currentOffset: progress.map(e => e.file_current_offset).reduce((a, b) => a + b, 0),
|
|
speed: progress.map(e => e.network_current_speed).reduce((a, b) => a + b, 0)
|
|
}
|
|
}
|
|
|
|
render() {
|
|
if(this.state.state === "querying") {
|
|
return (
|
|
<div key={"querying"} className={cssStyle.overlay + " " + cssStyle.querying}>
|
|
<a><Translatable>loading</Translatable> <LoadingDots maxDots={3} /></a>
|
|
</div>
|
|
);
|
|
} else if(this.state.state === "error") {
|
|
return (
|
|
<div key={"query-error"} className={cssStyle.overlay + " " + cssStyle.error}>
|
|
<a><Translatable>query error</Translatable></a>
|
|
</div>
|
|
);
|
|
} else if(this.runningTransfers.length === 0) {
|
|
return (
|
|
<div key={"no-transfers"} className={cssStyle.overlay + " " + cssStyle.noTransfers}>
|
|
<a><Translatable>No running transfers</Translatable></a>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const stats = this.currentStatistic();
|
|
const totalBytes = network.format_bytes(stats.totalBytes, { unit: "B", time: "", exact: false });
|
|
const currentOffset = network.format_bytes(stats.currentOffset, { unit: "B", time: "", exact: false });
|
|
const speed = network.format_bytes(stats.speed, { unit: "B", time: "second", exact: false });
|
|
|
|
return (
|
|
<div key={"running-transfers"} className={cssStyle.overlay + " " + cssStyle.runningTransfers}>
|
|
<ProgressBar value={stats.currentOffset * 100 / stats.totalBytes} type={"normal"} text={tra("Transferred {0} out of {1} total bytes ({2})", currentOffset, totalBytes, speed)} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
@EventHandler<TransferInfoEvents>("query_transfers")
|
|
private handleQueryTransfers() {
|
|
this.setState({ state: "querying" });
|
|
}
|
|
|
|
@EventHandler<TransferInfoEvents>("query_transfer_result")
|
|
private handleQueryTransferResult(event: TransferInfoEvents["query_transfer_result"]) {
|
|
this.setState({
|
|
state: event.status !== "success" ? "error" : "normal"
|
|
});
|
|
|
|
this.runningTransfers = (event.transfers || []).filter(e => e.status !== "finished" && e.status !== "errored").map(e => {
|
|
return {
|
|
progress: undefined,
|
|
transfer: e
|
|
}
|
|
});
|
|
}
|
|
|
|
@EventHandler<TransferInfoEvents>("notify_transfer_registered")
|
|
private handleTransferRegistered(event: TransferInfoEvents["notify_transfer_registered"]) {
|
|
this.runningTransfers.push({ transfer: event.transfer, progress: undefined });
|
|
this.forceUpdate();
|
|
}
|
|
|
|
@EventHandler<TransferInfoEvents>("notify_transfer_status")
|
|
private handleTransferStatus(event: TransferInfoEvents["notify_transfer_status"]) {
|
|
const index = this.runningTransfers.findIndex(e => e.transfer.id === event.id);
|
|
if(index === -1) return;
|
|
|
|
if(event.status === "finished" || event.status === "errored")
|
|
this.runningTransfers.splice(index, 1);
|
|
this.forceUpdate();
|
|
}
|
|
|
|
@EventHandler<TransferInfoEvents>("notify_transfer_progress")
|
|
private handleTransferProgress(event: TransferInfoEvents["notify_transfer_progress"]) {
|
|
const index = this.runningTransfers.findIndex(e => e.transfer.id === event.id);
|
|
if(index === -1) return;
|
|
|
|
this.runningTransfers[index].progress = event.progress;
|
|
this.forceUpdate();
|
|
}
|
|
}
|
|
|
|
const BottomTransferInfo = (props: { events: Registry<TransferInfoEvents> }) => {
|
|
const [extendedInfo, setExtendedInfo] = useState(false);
|
|
|
|
props.events.reactUse("action_toggle_expansion", event => setExtendedInfo(event.visible));
|
|
|
|
return (
|
|
<div className={cssStyle.info}>
|
|
<RunningTransfersInfo events={props.events} />
|
|
<div className={cssStyle.overlay + (extendedInfo ? "" : " " + cssStyle.hidden) + " " + cssStyle.extended} >
|
|
<ToggleFinishedTransfersCheckbox events={props.events} />
|
|
</div>
|
|
</div>
|
|
)
|
|
};
|
|
|
|
const BottomBar = (props: { events: Registry<TransferInfoEvents> }) => (
|
|
<div className={cssStyle.bottomContainer}>
|
|
<BottomTransferInfo events={props.events} />
|
|
<ExpendState extended={false} events={props.events} />
|
|
</div>
|
|
);
|
|
|
|
const TransferEntry = (props: { transfer: TransferInfoData, events: Registry<TransferInfoEvents>, finishedShown: boolean }) => {
|
|
const [finishedShown, setFinishedShown] = useState(props.finishedShown);
|
|
const [transferState, setTransferState] = useState<TransferStatus>(props.transfer.status);
|
|
const [finishAnimationFinished, setFinishAnimationFinished] = useState(props.transfer.status === "finished" || props.transfer.status === "errored");
|
|
|
|
const progressBar = useRef<ProgressBar>(null);
|
|
|
|
const progressBarText = (status: TransferStatus, info?: TransferProgress) => {
|
|
switch (status) {
|
|
case "errored":
|
|
return props.transfer.error ? tr("file transfer failed: ") + props.transfer.error : tr("file transfer failed");
|
|
|
|
case "finished":
|
|
const neededTime = format_time(props.transfer.timestampEnd - props.transfer.timestampBegin, tr("less than a second"));
|
|
const totalBytes = network.format_bytes(props.transfer.transferredBytes, { unit: "B", time: "", exact: false });
|
|
const speed = network.format_bytes(props.transfer.transferredBytes * 1000 / Math.max(props.transfer.timestampEnd - props.transfer.timestampBegin, 1000), { unit: "B", time: "second", exact: false });
|
|
return tra("transferred {0} in {1} ({2})", totalBytes, format_time(props.transfer.timestampEnd - props.transfer.timestampBegin, neededTime), speed);
|
|
|
|
case "pending":
|
|
return tr("pending");
|
|
|
|
case "none":
|
|
return tr("invalid state!");
|
|
|
|
case "transferring": {
|
|
if(!info) {
|
|
return tr("awaiting info");
|
|
}
|
|
|
|
const currentBytes = network.format_bytes(info.file_current_offset, { unit: "B", time: "", exact: false });
|
|
const totalBytes = network.format_bytes(info.file_total_size, { unit: "B", time: "", exact: false });
|
|
const speed = network.format_bytes(info.network_current_speed, { unit: "B", time: "second", exact: false });
|
|
|
|
return tra("transferred {0} out of {1} ({2})", currentBytes, totalBytes, speed);
|
|
}
|
|
}
|
|
return "";
|
|
};
|
|
|
|
const progressBarMode = (status: TransferStatus) => {
|
|
switch (status) {
|
|
case "errored":
|
|
return "error";
|
|
case "finished":
|
|
return "success";
|
|
default:
|
|
return "normal";
|
|
}
|
|
};
|
|
|
|
props.events.reactUse("notify_transfer_status", event => {
|
|
if(event.id !== props.transfer.id)
|
|
return;
|
|
|
|
setTransferState(event.status);
|
|
if(!progressBar.current)
|
|
return;
|
|
|
|
const pbState = {
|
|
text: progressBarText(event.status),
|
|
type: progressBarMode(event.status)
|
|
} as any;
|
|
|
|
if(event.status === "errored" || event.status === "finished") {
|
|
pbState.value = 100;
|
|
|
|
} else if(event.status === "none" || event.status === "pending")
|
|
pbState.value = 0;
|
|
|
|
progressBar.current.setState(pbState);
|
|
});
|
|
|
|
props.events.reactUse("notify_transfer_progress", event => {
|
|
if(event.id !== props.transfer.id || !progressBar.current)
|
|
return;
|
|
|
|
const pb = progressBar.current;
|
|
pb.setState({
|
|
text: progressBarText(event.status, event.progress),
|
|
type: progressBarMode(event.status),
|
|
value: event.status === "errored" || event.status === "finished" ? 100 : event.status === "pending" || event.status === "none" ? 0 : (event.progress.file_current_offset / event.progress.file_total_size) * 100
|
|
});
|
|
});
|
|
|
|
props.events.reactUse("action_toggle_finished_transfers", event => setFinishedShown(event.visible));
|
|
|
|
useEffect(() => {
|
|
if(finishAnimationFinished)
|
|
return;
|
|
if(transferState !== "finished" && transferState !== "errored")
|
|
return;
|
|
|
|
const id = setTimeout(() => setFinishAnimationFinished(true), 1500);
|
|
return () => clearTimeout(id);
|
|
});
|
|
|
|
let hidden = transferState === "finished" || transferState === "errored" ? !finishedShown && finishAnimationFinished : false;
|
|
return <div className={cssStyle.transferEntryContainer + (hidden ? " " + cssStyle.hidden : "")}>
|
|
<div className={cssStyle.transferEntry}>
|
|
<div className={cssStyle.image}>
|
|
<HTMLRenderer purify={false}>{props.transfer.direction === "upload" ? iconTransferUpload : iconTransferDownload}</HTMLRenderer>
|
|
</div>
|
|
<div className={cssStyle.info}>
|
|
<a className={cssStyle.name}>{props.transfer.name}</a>
|
|
<a className={cssStyle.path}>{props.transfer.path}</a>
|
|
<div className={cssStyle.status}>
|
|
<ProgressBar ref={progressBar} value={props.transfer.progress * 100} type={progressBarMode(transferState)} text={progressBarText(transferState)} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
};
|
|
|
|
@ReactEventHandler<TransferList>(e => e.props.events)
|
|
class TransferList extends React.PureComponent<{ events: Registry<TransferInfoEvents> }, { state: "loading" | "error" | "normal", error?: string }> {
|
|
private transfers: TransferInfoData[] = [];
|
|
private showFinishedTransfers: boolean = true;
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
state: "loading"
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const entries = [];
|
|
|
|
if(this.state.state === "error") {
|
|
entries.push(<div key={"query-error"} className={cssStyle.queryError}><a><Translatable>Failed to query the file transfers:</Translatable><br/>{this.state.error}</a></div>);
|
|
} else if(this.state.state === "loading") {
|
|
entries.push(<div key={"loading"} className={cssStyle.querying}><a><Translatable>loading</Translatable> <LoadingDots maxDots={3}/></a></div>);
|
|
} else {
|
|
this.transfers.forEach(e => {
|
|
entries.push(<TransferEntry finishedShown={this.showFinishedTransfers} key={"transfer-" + e.id} transfer={e} events={this.props.events} />);
|
|
});
|
|
entries.push(<div key={"no-transfers"} className={cssStyle.noTransfers}><a><Translatable>No transfers</Translatable></a></div>);
|
|
}
|
|
|
|
return (
|
|
<div className={cssStyle.list}>{entries}</div>
|
|
);
|
|
}
|
|
|
|
componentDidMount(): void {
|
|
this.props.events.fire("query_transfers");
|
|
}
|
|
|
|
@EventHandler<TransferInfoEvents>("action_toggle_finished_transfers")
|
|
private handleToggleFinishedTransfers(event: TransferInfoEvents["action_toggle_finished_transfers"]) {
|
|
this.showFinishedTransfers = event.visible;
|
|
}
|
|
|
|
|
|
@EventHandler<TransferInfoEvents>("action_remove_finished")
|
|
private handleRemoveFinishedTransfers() {
|
|
this.transfers = this.transfers.filter(e => e.status !== "finished" && e.status !== "errored");
|
|
this.forceUpdate();
|
|
}
|
|
|
|
@EventHandler<TransferInfoEvents>("query_transfers")
|
|
private handleQueryTransfers() {
|
|
this.setState({ state: "loading" });
|
|
}
|
|
|
|
@EventHandler<TransferInfoEvents>("query_transfer_result")
|
|
private handleQueryTransferResult(event: TransferInfoEvents["query_transfer_result"]) {
|
|
this.setState({
|
|
state: event.status === "success" ? "normal" : "error",
|
|
error: event.status === "timeout" ? tr("Request timed out") : event.error || tr("unknown error")
|
|
});
|
|
if(event.status === "success")
|
|
this.showFinishedTransfers = event.showFinished;
|
|
|
|
this.transfers = event.transfers || [];
|
|
this.transfers.sort((a, b) => b.timestampRegistered - a.timestampRegistered);
|
|
}
|
|
|
|
@EventHandler<TransferInfoEvents>("notify_transfer_registered")
|
|
private handleTransferRegistered(event: TransferInfoEvents["notify_transfer_registered"]) {
|
|
this.transfers.splice(0, 0, event.transfer);
|
|
this.forceUpdate();
|
|
}
|
|
|
|
@EventHandler<TransferInfoEvents>("notify_transfer_status")
|
|
private handleTransferStatus(event: TransferInfoEvents["notify_transfer_status"]) {
|
|
const transfer = this.transfers.find(e => e.id === event.id);
|
|
if(!transfer) return;
|
|
|
|
switch (event.status) {
|
|
case "finished":
|
|
case "errored":
|
|
case "none":
|
|
transfer.timestampEnd = Date.now();
|
|
break;
|
|
|
|
case "transferring":
|
|
if(transfer.timestampBegin === 0)
|
|
transfer.timestampBegin = Date.now();
|
|
}
|
|
transfer.status = event.status;
|
|
transfer.error = event.error;
|
|
}
|
|
|
|
@EventHandler<TransferInfoEvents>("notify_transfer_progress")
|
|
private handleTransferProgress(event: TransferInfoEvents["notify_transfer_progress"]) {
|
|
const transfer = this.transfers.find(e => e.id === event.id);
|
|
if(!transfer) return;
|
|
|
|
transfer.progress = event.progress.file_current_offset / event.progress.file_total_size;
|
|
transfer.status = event.status;
|
|
transfer.transferredBytes = event.progress.file_bytes_transferred;
|
|
}
|
|
}
|
|
|
|
const ExtendedInfo = (props: { events: Registry<TransferInfoEvents> }) => {
|
|
const [expended, setExpended] = useState(false);
|
|
const [finishedShown, setFinishedShown] = useState(true);
|
|
|
|
props.events.reactUse("action_toggle_expansion", event => setExpended(event.visible));
|
|
props.events.reactUse("action_toggle_finished_transfers", event => setFinishedShown(event.visible));
|
|
props.events.reactUse("query_transfer_result", event => event.status === "success" && setFinishedShown(event.showFinished));
|
|
|
|
return <div className={cssStyle.expendedContainer + (expended ? "" : " " + cssStyle.hidden)} >
|
|
<div className={cssStyle.header}>
|
|
<a>{finishedShown ? <Translatable key={"file-transfers"}>File transfers</Translatable> : <Translatable key={"running-file-transfers"}>Running file transfers</Translatable>}</a>
|
|
<Button disabled={!finishedShown} color={"blue"} onClick={() => props.events.fire("action_remove_finished")}><Translatable>Remove finished</Translatable></Button>
|
|
</div>
|
|
<TransferList events={props.events} />
|
|
</div>;
|
|
};
|
|
|
|
export const TransferInfo = (props: { events: Registry<TransferInfoEvents> }) => (
|
|
<div className={cssStyle.container} >
|
|
<ExtendedInfo events={props.events} />
|
|
<BottomBar events={props.events} />
|
|
</div>
|
|
); |