import * as React from "react"; import {useEffect, useRef, useState} from "react"; import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events"; 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"; import {TransferStatus} from "tc-shared/ui/modal/transfer/FileDefinitions"; import {TransferInfoData, TransferInfoEvents} from "tc-shared/ui/modal/transfer/FileTransferInfoDefinitions"; const cssStyle = require("./FileTransferInfoRenderer.scss"); const iconArrow = require("./icon_double_arrow.svg"); const iconTransferUpload = require("./icon_transfer_upload.svg"); const iconTransferDownload = require("./icon_transfer_download.svg"); const ExpendState = (props: { extended: boolean, events: Registry }) => { const [expended, setExpended] = useState(props.extended); props.events.reactUse("action_toggle_expansion", event => setExpended(event.visible)); return (
props.events.fire("action_toggle_expansion", {visible: !expended})}> {iconArrow}
); }; const ToggleFinishedTransfersCheckbox = (props: { events: Registry }) => { const ref = useRef(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 ( props.events.fire("action_toggle_finished_transfers", {visible: state})} label={Show finished transfers}/> ); }; @ReactEventHandler(e => e.props.events) class RunningTransfersInfo extends React.Component<{ events: Registry }, { 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 ( ); } else if (this.state.state === "error") { return ( ); } else if (this.runningTransfers.length === 0) { return ( ); } 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 (
); } @EventHandler("query_transfers") private handleQueryTransfers() { this.setState({state: "querying"}); } @EventHandler("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("notify_transfer_registered") private handleTransferRegistered(event: TransferInfoEvents["notify_transfer_registered"]) { this.runningTransfers.push({transfer: event.transfer, progress: undefined}); this.forceUpdate(); } @EventHandler("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("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 }) => { const [extendedInfo, setExtendedInfo] = useState(false); props.events.reactUse("action_toggle_expansion", event => setExtendedInfo(event.visible)); return (
) }; const BottomBar = (props: { events: Registry }) => (
); const TransferEntry = (props: { transfer: TransferInfoData, events: Registry, finishedShown: boolean }) => { const [finishedShown, setFinishedShown] = useState(props.finishedShown); const [transferState, setTransferState] = useState(props.transfer.status); const [finishAnimationFinished, setFinishAnimationFinished] = useState(props.transfer.status === "finished" || props.transfer.status === "errored"); const progressBar = useRef(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
{props.transfer.direction === "upload" ? iconTransferUpload : iconTransferDownload}
}; @ReactEventHandler(e => e.props.events) class TransferList extends React.PureComponent<{ events: Registry }, { 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(); } else if (this.state.state === "loading") { entries.push(); } else { this.transfers.forEach(e => { entries.push(); }); entries.push(); } return (
{entries}
); } componentDidMount(): void { this.props.events.fire("query_transfers"); } @EventHandler("action_toggle_finished_transfers") private handleToggleFinishedTransfers(event: TransferInfoEvents["action_toggle_finished_transfers"]) { this.showFinishedTransfers = event.visible; } @EventHandler("action_remove_finished") private handleRemoveFinishedTransfers() { this.transfers = this.transfers.filter(e => e.status !== "finished" && e.status !== "errored"); this.forceUpdate(); } @EventHandler("query_transfers") private handleQueryTransfers() { this.setState({state: "loading"}); } @EventHandler("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("notify_transfer_registered") private handleTransferRegistered(event: TransferInfoEvents["notify_transfer_registered"]) { this.transfers.splice(0, 0, event.transfer); this.forceUpdate(); } @EventHandler("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("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 }) => { 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
{finishedShown ? File transfers : Running file transfers}
; }; export const FileTransferInfo = (props: { events: Registry }) => (
);