Spaces:
Sleeping
Sleeping
| import { useEffect, useMemo, useState } from "react"; | |
| import { | |
| App as AntdApp, | |
| Button, | |
| Form, | |
| Input, | |
| Layout, | |
| message, | |
| Modal, | |
| Progress, | |
| Space, | |
| Table, | |
| Tabs, | |
| Tag, | |
| Typography, | |
| Upload, | |
| } from "antd"; | |
| import { api, clearToken, getToken, setToken } from "../api/client"; | |
| const { Header, Content } = Layout; | |
| const { Text } = Typography; | |
| const taskColor = { | |
| queued: "default", | |
| running: "processing", | |
| success: "success", | |
| failed: "error", | |
| }; | |
| export function App() { | |
| const [token, setTokenState] = useState(getToken()); | |
| const [pwd, setPwd] = useState(""); | |
| const [currentPath, setCurrentPath] = useState("."); | |
| const [entries, setEntries] = useState([]); | |
| const [tasks, setTasks] = useState([]); | |
| const [loading, setLoading] = useState(false); | |
| const [downloadOpen, setDownloadOpen] = useState(false); | |
| const [extractOpen, setExtractOpen] = useState(false); | |
| const [apiMsg, contextHolder] = message.useMessage(); | |
| async function loadFiles(path = currentPath) { | |
| setLoading(true); | |
| try { | |
| const data = await api(`/api/fs/list?path=${encodeURIComponent(path)}`); | |
| setEntries(data.entries || []); | |
| setCurrentPath(path); | |
| } catch (err) { | |
| apiMsg.error(String(err.message || err)); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| async function loadTasks() { | |
| try { | |
| const data = await api("/api/tasks?limit=100"); | |
| setTasks(data.tasks || []); | |
| } catch (err) { | |
| if (String(err.message || "").includes("401")) { | |
| clearToken(); | |
| setTokenState(""); | |
| } | |
| } | |
| } | |
| useEffect(() => { | |
| if (!token) return; | |
| loadFiles("."); | |
| loadTasks(); | |
| const timer = setInterval(loadTasks, 2000); | |
| return () => clearInterval(timer); | |
| }, [token]); | |
| const columns = useMemo( | |
| () => [ | |
| { | |
| title: "Name", | |
| dataIndex: "name", | |
| render: (_, row) => | |
| row.isDir ? ( | |
| <a onClick={() => loadFiles(row.path)}>{row.name}</a> | |
| ) : ( | |
| <span>{row.name}</span> | |
| ), | |
| }, | |
| { | |
| title: "Type", | |
| dataIndex: "isDir", | |
| render: (v) => (v ? "Directory" : "File"), | |
| }, | |
| { | |
| title: "Size", | |
| dataIndex: "size", | |
| }, | |
| { | |
| title: "Action", | |
| render: (_, row) => ( | |
| <Space> | |
| <Button | |
| danger | |
| size="small" | |
| onClick={async () => { | |
| await api("/api/fs/delete", { | |
| method: "POST", | |
| body: JSON.stringify({ path: row.path }), | |
| }); | |
| apiMsg.success("Deleted"); | |
| loadFiles(currentPath); | |
| }} | |
| > | |
| Delete | |
| </Button> | |
| {!row.isDir && ( | |
| <Button | |
| size="small" | |
| onClick={() => | |
| window.open( | |
| `/api/fs/download?path=${encodeURIComponent(row.path)}`, | |
| "_blank" | |
| ) | |
| } | |
| > | |
| Download | |
| </Button> | |
| )} | |
| </Space> | |
| ), | |
| }, | |
| ], | |
| [currentPath] | |
| ); | |
| if (!token) { | |
| return ( | |
| <AntdApp> | |
| {contextHolder} | |
| <div className="center-card"> | |
| <h2>FastFileViewer Login</h2> | |
| <Space.Compact style={{ width: "100%" }}> | |
| <Input.Password | |
| placeholder="Admin password" | |
| value={pwd} | |
| onChange={(e) => setPwd(e.target.value)} | |
| onPressEnter={async () => { | |
| try { | |
| const data = await api("/api/login", { | |
| method: "POST", | |
| body: JSON.stringify({ password: pwd }), | |
| }); | |
| setToken(data.token); | |
| setTokenState(data.token); | |
| setPwd(""); | |
| } catch (err) { | |
| apiMsg.error("Login failed"); | |
| } | |
| }} | |
| /> | |
| <Button | |
| type="primary" | |
| onClick={async () => { | |
| try { | |
| const data = await api("/api/login", { | |
| method: "POST", | |
| body: JSON.stringify({ password: pwd }), | |
| }); | |
| setToken(data.token); | |
| setTokenState(data.token); | |
| setPwd(""); | |
| } catch (err) { | |
| apiMsg.error("Login failed"); | |
| } | |
| }} | |
| > | |
| Login | |
| </Button> | |
| </Space.Compact> | |
| </div> | |
| </AntdApp> | |
| ); | |
| } | |
| return ( | |
| <AntdApp> | |
| {contextHolder} | |
| <Layout style={{ minHeight: "100vh" }}> | |
| <Header className="header"> | |
| <Text strong style={{ color: "#fff" }}> | |
| FastFileViewer | |
| </Text> | |
| <Button | |
| onClick={() => { | |
| clearToken(); | |
| setTokenState(""); | |
| }} | |
| > | |
| Logout | |
| </Button> | |
| </Header> | |
| <Content className="content"> | |
| <Tabs | |
| items={[ | |
| { | |
| key: "files", | |
| label: "Files", | |
| children: ( | |
| <> | |
| <Space style={{ marginBottom: 12 }}> | |
| <Text>Current: {currentPath}</Text> | |
| <Button onClick={() => loadFiles(".")}>Root</Button> | |
| <Button onClick={() => loadFiles(currentPath)}>Refresh</Button> | |
| <Button onClick={() => setDownloadOpen(true)}> | |
| URL Download | |
| </Button> | |
| <Button onClick={() => setExtractOpen(true)}>Extract</Button> | |
| <Upload | |
| showUploadList={false} | |
| customRequest={async ({ file, onSuccess, onError }) => { | |
| try { | |
| const form = new FormData(); | |
| form.append("dir", currentPath); | |
| form.append("file", file); | |
| await api("/api/fs/upload", { | |
| method: "POST", | |
| body: form, | |
| }); | |
| onSuccess?.("ok"); | |
| loadFiles(currentPath); | |
| } catch (err) { | |
| onError?.(err); | |
| } | |
| }} | |
| > | |
| <Button>Upload</Button> | |
| </Upload> | |
| </Space> | |
| <Table | |
| rowKey={(row) => row.path} | |
| columns={columns} | |
| dataSource={entries} | |
| loading={loading} | |
| pagination={false} | |
| /> | |
| </> | |
| ), | |
| }, | |
| { | |
| key: "tasks", | |
| label: "Tasks", | |
| children: ( | |
| <Table | |
| rowKey={(row) => row.id} | |
| dataSource={tasks} | |
| pagination={false} | |
| columns={[ | |
| { title: "ID", dataIndex: "id" }, | |
| { title: "Type", dataIndex: "type" }, | |
| { | |
| title: "Status", | |
| dataIndex: "status", | |
| render: (v) => <Tag color={taskColor[v]}>{v}</Tag>, | |
| }, | |
| { | |
| title: "Progress", | |
| dataIndex: "progress", | |
| render: (v) => <Progress percent={v} size="small" />, | |
| }, | |
| { title: "Source", dataIndex: "source" }, | |
| { title: "Target", dataIndex: "targetPath" }, | |
| { title: "Error", dataIndex: "error" }, | |
| ]} | |
| /> | |
| ), | |
| }, | |
| ]} | |
| /> | |
| </Content> | |
| </Layout> | |
| <Modal | |
| open={downloadOpen} | |
| title="Create Download Task" | |
| onCancel={() => setDownloadOpen(false)} | |
| footer={null} | |
| > | |
| <Form | |
| layout="vertical" | |
| onFinish={async (values) => { | |
| await api("/api/tasks/download", { | |
| method: "POST", | |
| body: JSON.stringify(values), | |
| }); | |
| setDownloadOpen(false); | |
| loadTasks(); | |
| }} | |
| > | |
| <Form.Item | |
| label="URL" | |
| name="url" | |
| rules={[{ required: true, message: "Please input URL" }]} | |
| > | |
| <Input placeholder="https://example.com/file.zip" /> | |
| </Form.Item> | |
| <Form.Item | |
| label="Target Path" | |
| name="targetPath" | |
| initialValue={`${currentPath}/download.bin`} | |
| rules={[{ required: true, message: "Please input target path" }]} | |
| > | |
| <Input /> | |
| </Form.Item> | |
| <Button type="primary" htmlType="submit"> | |
| Submit | |
| </Button> | |
| </Form> | |
| </Modal> | |
| <Modal | |
| open={extractOpen} | |
| title="Create Extract Task" | |
| onCancel={() => setExtractOpen(false)} | |
| footer={null} | |
| > | |
| <Form | |
| layout="vertical" | |
| onFinish={async (values) => { | |
| await api("/api/tasks/extract", { | |
| method: "POST", | |
| body: JSON.stringify(values), | |
| }); | |
| setExtractOpen(false); | |
| loadTasks(); | |
| }} | |
| > | |
| <Form.Item | |
| label="Archive Path" | |
| name="sourcePath" | |
| rules={[{ required: true, message: "Please input archive path" }]} | |
| > | |
| <Input placeholder="path/to/file.zip" /> | |
| </Form.Item> | |
| <Form.Item | |
| label="Target Dir" | |
| name="targetPath" | |
| initialValue={currentPath} | |
| rules={[{ required: true, message: "Please input target dir" }]} | |
| > | |
| <Input /> | |
| </Form.Item> | |
| <Button type="primary" htmlType="submit"> | |
| Submit | |
| </Button> | |
| </Form> | |
| </Modal> | |
| </AntdApp> | |
| ); | |
| } | |