feat: server transfer rotation

This commit is contained in:
naiba
2026-05-25 10:15:24 +00:00
parent 142fc19469
commit b23a6c0be1
60 changed files with 4488 additions and 1359 deletions
+83 -72
View File
@@ -37,7 +37,7 @@ import {
import { ColumnDef } from "@tanstack/react-table"
import { Row, flexRender } from "@tanstack/react-table"
import { File, Folder } from "lucide-react"
import { HTMLAttributes, JSX, useEffect, useRef, useState } from "react"
import { HTMLAttributes, JSX, useCallback, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@@ -59,6 +59,10 @@ interface FMProps {
wsUrl: string
}
type VirtualizedTableRowProps = HTMLAttributes<HTMLTableRowElement> & {
"data-index"?: number | string
}
const arraysEqual = (a: Uint8Array, b: Uint8Array) => {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
@@ -67,16 +71,12 @@ const arraysEqual = (a: Uint8Array, b: Uint8Array) => {
return true
}
const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl, ...props }) => {
export const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl, ...props }) => {
const { t } = useTranslation()
const fmRef = useRef<HTMLDivElement>(null)
const wsRef = useRef<WebSocket | null>(null)
useEffect(() => {
return () => {
wsRef.current?.close()
}
}, [])
const tRef = useRef(t)
tRef.current = t
const [dOpen, setdOpen] = useState(false)
const [uOpen, setuOpen] = useState(false)
@@ -120,9 +120,8 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
]
const tableRowComponent = (rows: Row<FMEntry>[]) =>
function getTableRow(props: HTMLAttributes<HTMLTableRowElement>) {
// @ts-expect-error data-index is a valid attribute
const index = props["data-index"]
function getTableRow(props: VirtualizedTableRowProps) {
const index = Number(props["data-index"])
const row = rows[index]
if (!row) return null
@@ -160,6 +159,65 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
}
}
useEffect(() => {
worker.onmessage = async (event: MessageEvent<FMWorkerData>) => {
switch (event.data.type) {
case FMWorkerOpcode.Error: {
console.error("Error from worker", event.data.error)
break
}
case FMWorkerOpcode.Progress: {
handleReady.current = true
break
}
case FMWorkerOpcode.Result: {
handleReady.current = false
if (event.data.blob && event.data.fileName) {
const url = URL.createObjectURL(event.data.blob)
const anchor = document.createElement("a")
anchor.href = url
anchor.download = event.data.fileName
anchor.click()
URL.revokeObjectURL(url)
}
firstChunk.current = true
if (dOpen) setdOpen(false)
break
}
}
}
const handleBeforeUnload = () => {
worker.postMessage({
operation: 3,
})
}
window.addEventListener("beforeunload", handleBeforeUnload)
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload)
}
}, [dOpen])
const [currentPath, setPath] = useState("")
const currentPathRef = useRef(currentPath)
currentPathRef.current = currentPath
const listFile = useCallback(() => {
const prefix = new Int8Array([FMOpcode.List])
const pathMsg = new TextEncoder().encode(currentPathRef.current)
const msg = new Int8Array(prefix.length + pathMsg.length)
msg.set(prefix)
msg.set(pathMsg, prefix.length)
wsRef.current?.send(msg)
}, [])
// The WebSocket initialization must not depend on listFile or currentPath,
// otherwise navigating directories triggers a disconnect and reconnect.
useEffect(() => {
const url = new URL(wsUrl, window.location.origin)
url.protocol = url.protocol.replace("http", "ws")
@@ -174,8 +232,8 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
}
ws.onerror = (e) => {
console.error(e)
toast("Websocket" + " " + t("Error"), {
description: t("Results.UnExpectedError"),
toast("Websocket" + " " + tRef.current("Error"), {
description: tRef.current("Results.UnExpectedError"),
})
}
ws.onmessage = async (e: MessageEvent<ArrayBufferLike>) => {
@@ -186,7 +244,6 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
const errMsg = new TextDecoder("utf-8").decode(errBytes)
throw new Error(errMsg)
}
if (firstChunk.current) {
if (arraysEqual(identifier, FMIdentifier.file)) {
worker.postMessage({
@@ -204,7 +261,7 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
setuOpen(false)
listFile()
} else {
throw new Error(t("Results.UnknownIdentifier"))
throw new Error(tRef.current("Results.UnknownIdentifier"))
}
} else {
await waitForHandleReady()
@@ -216,74 +273,27 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
}
} catch (error) {
console.error("Error processing received data:", error)
toast("FM" + " " + t("Error"), {
description: t("Results.UnExpectedError"),
toast("FM" + " " + tRef.current("Error"), {
description: tRef.current("Results.UnExpectedError"),
})
setdOpen(false)
setuOpen(false)
}
}
}, [wsUrl])
useEffect(() => {
worker.onmessage = async (event: MessageEvent<FMWorkerData>) => {
switch (event.data.type) {
case FMWorkerOpcode.Error: {
console.error("Error from worker", event.data.error)
break
}
case FMWorkerOpcode.Progress: {
handleReady.current = true
break
}
case FMWorkerOpcode.Result: {
handleReady.current = false
if (event.data.blob && event.data.fileName) {
const url = URL.createObjectURL(event.data.blob)
const anchor = document.createElement("a")
anchor.href = url
anchor.download = event.data.fileName
anchor.click()
URL.revokeObjectURL(url)
}
firstChunk.current = true
if (dOpen) setdOpen(false)
break
}
return () => {
ws.close()
if (wsRef.current === ws) {
wsRef.current = null
}
}
}, [listFile, wsUrl])
const handleBeforeUnload = () => {
worker.postMessage({
operation: 3,
})
}
window.addEventListener("beforeunload", handleBeforeUnload)
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload)
}
}, [worker, dOpen])
const [currentPath, setPath] = useState("")
useEffect(() => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
listFile()
}
}, [wsRef.current, currentPath])
const listFile = () => {
const prefix = new Int8Array([FMOpcode.List])
const pathMsg = new TextEncoder().encode(currentPath)
const msg = new Int8Array(prefix.length + pathMsg.length)
msg.set(prefix)
msg.set(pathMsg, prefix.length)
wsRef.current?.send(msg)
}
}, [currentPath, listFile])
const downloadFile = (basename: string) => {
currentBasename.current = basename
@@ -331,9 +341,10 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
onClick={async () => {
try {
await copyToClipboard(formatPath(currentPath))
} catch (error: any) {
} catch (error) {
const description = error instanceof Error ? error.message : t("Results.UnExpectedError")
toast("FM" + " " + t("Error"), {
description: error.message,
description,
})
console.error("copy error: ", error)
}