From 75fcca7ce349fd0002a1289d90dfd8a71f24a739 Mon Sep 17 00:00:00 2001
From: zwzzrl <66906196+zwzzrl@users.noreply.github.com>
Date: Tue, 10 Mar 2026 22:21:18 +0800
Subject: [PATCH 01/12] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=A6=BB=E7=BA=BF?=
=?UTF-8?q?=E4=B8=8B=E8=BD=BD=E4=BB=BB=E5=8A=A1=E5=88=97=E8=A1=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/ModalInput.tsx | 2 +
src/lang/en/tasks.json | 4 +-
.../home/offlinedownload/TaskProgress.tsx | 163 ++++++++++++
src/pages/home/offlinedownload/task.ts | 64 +++++
src/pages/home/toolbar/OfflineDownload.tsx | 235 ++++++++++++------
5 files changed, 387 insertions(+), 81 deletions(-)
create mode 100644 src/pages/home/offlinedownload/TaskProgress.tsx
create mode 100644 src/pages/home/offlinedownload/task.ts
diff --git a/src/components/ModalInput.tsx b/src/components/ModalInput.tsx
index fd6ede9b4..6312d38ba 100644
--- a/src/components/ModalInput.tsx
+++ b/src/components/ModalInput.tsx
@@ -25,6 +25,7 @@ export type ModalInputProps = {
title: string
isRenamingFile?: boolean
onSubmit?: (text: string) => void
+ onSubmitWithValue?: (text: string,setValue: (value: string) => void) => void
type?: string
defaultValue?: string
loading?: boolean
@@ -92,6 +93,7 @@ export const ModalInput = (props: ModalInputProps) => {
}
}
props.onSubmit?.(value())
+ props.onSubmitWithValue?.(value(),setValue)
}
const handleInput = (newValue: string) => {
diff --git a/src/lang/en/tasks.json b/src/lang/en/tasks.json
index a96897f90..e48e703f4 100644
--- a/src/lang/en/tasks.json
+++ b/src/lang/en/tasks.json
@@ -50,7 +50,9 @@
"path": "Destination Path",
"transfer_src": "Source Path",
"transfer_src_local": "Source Path (Local)",
- "transfer_dst": "Destination Path"
+ "transfer_dst": "Destination Path",
+ "list_title": "Offline Download Tasks",
+ "no_tasks": "No ongoing tasks"
},
"decompress": {
"src": "Source Path",
diff --git a/src/pages/home/offlinedownload/TaskProgress.tsx b/src/pages/home/offlinedownload/TaskProgress.tsx
new file mode 100644
index 000000000..72bec1b1f
--- /dev/null
+++ b/src/pages/home/offlinedownload/TaskProgress.tsx
@@ -0,0 +1,163 @@
+// src/pages/home/toolbar/TaskProgress.tsx
+import {
+ VStack,
+ HStack,
+ Text,
+ Badge,
+ Progress,
+ ProgressIndicator,
+ Box,
+} from "@hope-ui/solid";
+import { TaskInfo } from "./task";
+import { getFileSize } from "~/utils";
+import { Show, createSignal, JSX } from "solid-js";
+import { useT } from "~/hooks";
+import { me } from "~/store";
+
+// 复制 helper.tsx 中的 getPath 函数(用于生成路径链接)
+const getPath = (
+ device: string,
+ path: string,
+ asLink: boolean = true,
+): JSX.Element => {
+ const fullPath = (device === "/" ? "" : device) + path;
+ const prefix = me().base_path === "/" ? "" : me().base_path;
+ const accessible = fullPath.startsWith(prefix);
+ const [underline, setUnderline] = createSignal(false);
+ return accessible && asLink ? (
+ setUnderline(true)}
+ onMouseOut={() => setUnderline(false)}
+ href={fullPath.slice(prefix.length)}
+ >
+ {fullPath}
+
+ ) : (
+
{fullPath}
+ );
+};
+
+// 解析任务名称,返回文件名和路径信息
+const parseTaskName = (name: string) => {
+ // download 类型:download 文件名 to (路径)
+ let match = name.match(/^download (.+) to \((.+)\)$/);
+ if (match) {
+ return {
+ type: "download" as const,
+ fileName: match[1],
+ path: match[2],
+ };
+ }
+ // transfer/upload 类型:transfer [设备](路径) to [目标设备](目标路径) 或 upload [文件名](URL) to [目标设备](目标路径)
+ match = name.match(
+ /^(transfer|upload) \[(.*?)\]\((.*?)\) to \[(.*?)\]\((.*?)\)$/,
+ );
+ if (match) {
+ const type = match[1] as "transfer" | "upload";
+ const bracketContent = match[2]; // 方括号内:transfer 为设备,upload 为文件名
+ const urlOrPath = match[3]; // 圆括号内:transfer 为路径,upload 为 URL
+ const dstDevice = match[4];
+ const dstPath = match[5];
+
+ if (type === "transfer") {
+ // 从路径中提取文件名(最后一段,去除参数)
+ const fileName = urlOrPath.split("/").pop()?.split("?")[0] || "未知文件";
+ return {
+ type,
+ fileName,
+ srcDevice: bracketContent,
+ srcPath: urlOrPath,
+ dstDevice,
+ dstPath,
+ };
+ } else {
+ // upload 类型:文件名直接取自方括号
+ return {
+ type,
+ fileName: bracketContent,
+ srcDevice: "",
+ srcPath: urlOrPath,
+ dstDevice,
+ dstPath,
+ };
+ }
+ }
+ return null;
+};
+
+export const StatusColor = {
+ 0: "neutral",
+ 1: "info",
+ 2: "warning",
+ 3: "danger",
+ 4: "success",
+ 5: "info",
+} as const;
+
+export const TaskItem = (props: TaskInfo) => {
+ const t = useT();
+ const parsed = parseTaskName(props.name);
+
+ return (
+
+ {parsed ? (
+ <>
+ {parsed.fileName}
+ {parsed.type === "download" && parsed.path && (
+
+ {t("tasks.attr.offline_download.path")}:{" "}
+ {getPath("", parsed.path)}
+
+ )}
+ {parsed.type === "transfer" && parsed.dstPath && (
+
+ {t("tasks.attr.offline_download.transfer_dst")}:{" "}
+ {getPath(parsed.dstDevice, parsed.dstPath)}
+
+ )}
+ {parsed.type === "upload" && parsed.dstPath && (
+
+ {t("tasks.attr.offline_download.path")}:{" "}
+ {getPath(parsed.dstDevice, parsed.dstPath)}
+
+ )}
+ >
+ ) : (
+ {props.name}
+ )}
+
+
+ {t("tasks.state." + props.state)}
+
+ {getFileSize(props.total_bytes)}
+
+
+
+
+ {props.error}
+
+
+
+ );
+};
+
+export default TaskItem;
diff --git a/src/pages/home/offlinedownload/task.ts b/src/pages/home/offlinedownload/task.ts
new file mode 100644
index 000000000..6892330a3
--- /dev/null
+++ b/src/pages/home/offlinedownload/task.ts
@@ -0,0 +1,64 @@
+// src/store/task.ts
+import { createSignal } from "solid-js";
+import { createStore } from "solid-js/store";
+import { r } from "~/utils";
+
+export interface TaskInfo {
+ id: string;
+ name: string;
+ creator: string;
+ creator_role: number;
+ state: number;
+ status: string;
+ progress: number;
+ start_time: string | null;
+ end_time: string | null;
+ total_bytes: number;
+ error: string;
+}
+
+const [tasks, setTasks] = createStore([]);
+const [loading, setLoading] = createSignal(false);
+
+export const fetchTasks = async (showLoading = true) => {
+ if (showLoading) setLoading(true);
+ try {
+ const [respOld, respNew] = await Promise.all([
+ r.get("/task/offline_download/undone").catch(() => ({ data: [] })),
+ r
+ .get("/task/offline_download_transfer/undone")
+ .catch(() => ({ data: [] })),
+ ]);
+
+ const taskMap = new Map();
+
+ const oldTasks = respOld.data || [];
+ oldTasks.forEach((item: any) => {
+ if (!item.state) item.state = 0;
+ taskMap.set(item.id, item);
+ });
+
+ const newTasks = respNew.data || [];
+ newTasks.forEach((item: any) => {
+ taskMap.set(item.id, item);
+ });
+
+ const mergedTasks = Array.from(taskMap.values());
+
+ // 按 start_time 降序排序(最新的在前),null 值视为最旧,排在后面
+ mergedTasks.sort((a, b) => {
+ if (!a.start_time && !b.start_time) return 0;
+ if (!a.start_time) return 1;
+ if (!b.start_time) return -1;
+ return b.start_time.localeCompare(a.start_time); // 字符串降序比较
+ });
+
+ setTasks(mergedTasks);
+ } catch (e) {
+ console.error("Failed to fetch tasks:", e);
+ } finally {
+ if (showLoading) setLoading(false);
+ }
+};
+
+export const useTasks = () => ({ tasks, loading, fetchTasks });
diff --git a/src/pages/home/toolbar/OfflineDownload.tsx b/src/pages/home/toolbar/OfflineDownload.tsx
index 7eaa6793e..a206ccc84 100644
--- a/src/pages/home/toolbar/OfflineDownload.tsx
+++ b/src/pages/home/toolbar/OfflineDownload.tsx
@@ -1,17 +1,35 @@
-import { Box, createDisclosure } from "@hope-ui/solid"
-import { ModalInput, SelectWrapper } from "~/components"
-import { useFetch, useRouter, useT } from "~/hooks"
+import {
+ Box,
+ createDisclosure,
+ HStack,
+ Heading,
+ Button,
+ VStack,
+ Text,
+} from "@hope-ui/solid";
+import { ModalInput, SelectWrapper } from "~/components";
+import { useFetch, useRouter, useT } from "~/hooks";
import {
offlineDownload,
bus,
handleRespWithNotifySuccess,
r,
handleResp,
-} from "~/utils"
-import { createSignal, onCleanup, onMount } from "solid-js"
-import { PResp } from "~/types"
-import bencode from "bencode"
-import crypto from "crypto-js"
+} from "~/utils";
+import {
+ createSignal,
+ onCleanup,
+ onMount,
+ createEffect,
+ For,
+ Show,
+} from "solid-js";
+import { PResp } from "~/types";
+import bencode from "bencode";
+import crypto from "crypto-js";
+import { useTasks } from "../offlinedownload/task";
+import TaskItem from "../offlinedownload/TaskProgress";
+import { FullLoading } from "~/components/FullLoading";
const deletePolicies = [
"upload_download_stream",
@@ -19,99 +37,139 @@ const deletePolicies = [
"delete_on_upload_failed",
"delete_never",
"delete_always",
-] as const
+] as const;
-type DeletePolicy = (typeof deletePolicies)[number]
+type DeletePolicy = (typeof deletePolicies)[number];
function utf8Decode(data: Uint8Array): string {
- return crypto.enc.Utf8.stringify(crypto.lib.WordArray.create(data))
+ return crypto.enc.Utf8.stringify(crypto.lib.WordArray.create(data));
}
function toMagnetUrl(torrentBuffer: Uint8Array) {
- const data = bencode.decode(torrentBuffer as any)
- const infoEncode = bencode.encode(data.info) as unknown as Uint8Array
+ const data = bencode.decode(torrentBuffer as any);
+ const infoEncode = bencode.encode(data.info) as unknown as Uint8Array;
const infoHash = crypto
.SHA1(crypto.lib.WordArray.create(infoEncode))
- .toString()
- let params = {} as any
+ .toString();
+ let params = {} as any;
if (Number.isInteger(data?.info?.length)) {
- params.xl = data.info.length
+ params.xl = data.info.length;
}
if (data.info.name) {
- params.dn = utf8Decode(data.info.name)
+ params.dn = utf8Decode(data.info.name);
}
if (data.announce) {
- params.tr = utf8Decode(data.announce)
+ params.tr = utf8Decode(data.announce);
}
return `magnet:?xt=urn:btih:${infoHash}&${new URLSearchParams(
params,
- ).toString()}`
+ ).toString()}`;
}
export const OfflineDownload = () => {
- const t = useT()
- const [tools, setTools] = createSignal([] as string[])
+ const t = useT();
+ const [tools, setTools] = createSignal([] as string[]);
const [toolsLoading, reqTool] = useFetch((): PResp => {
- return r.get("/public/offline_download_tools")
- })
- const [tool, setTool] = createSignal("")
+ return r.get("/public/offline_download_tools");
+ });
+ const [tool, setTool] = createSignal("");
const [deletePolicy, setDeletePolicy] = createSignal(
"upload_download_stream",
- )
+ );
onMount(async () => {
- const resp = await reqTool()
+ const resp = await reqTool();
handleResp(resp, (data) => {
- setTools(data)
- setTool(data[0])
- })
- })
-
- const { isOpen, onOpen, onClose } = createDisclosure()
- const [loading, ok] = useFetch(offlineDownload)
- const { pathname } = useRouter()
+ setTools(data);
+ setTool(data[0]);
+ });
+ });
+
+ const { isOpen, onOpen, onClose } = createDisclosure();
+ const [loading, ok] = useFetch(offlineDownload);
+ const { pathname } = useRouter();
+
+ // 监听工具栏事件
const handler = (name: string) => {
if (name === "offline_download") {
- onOpen()
+ onOpen();
+ setShowTasks(true);
}
- }
- bus.on("tool", handler)
+ };
+ bus.on("tool", handler);
onCleanup(() => {
- bus.off("tool", handler)
- })
+ bus.off("tool", handler);
+ });
+
+ const { tasks, loading: tasksLoading, fetchTasks } = useTasks();
+ const [showTasks, setShowTasks] = createSignal(false);
+
+ const handleSubmit = async (
+ urls: string,
+ setValue: (value: string) => void,
+ ) => {
+ const resp = await ok(pathname(), urls.split("\n"), tool(), deletePolicy());
+ handleRespWithNotifySuccess(resp, () => {
+ fetchTasks(false); // 不显示 loading
+ setValue("");
+ });
+ };
+
+ // 定时刷新任务进度(仅当显示任务列表时)
+ let timer: number | undefined;
+ createEffect(() => {
+ if (showTasks()) {
+ // 打开时立即获取一次最新数据,不显示 loading
+ fetchTasks(false);
+ // 启动定时器,每隔3秒刷新(也不显示 loading)
+ timer = setInterval(() => fetchTasks(false), 3000);
+ } else {
+ clearInterval(timer);
+ timer = undefined;
+ }
+ });
- // convert torrent file to magnet link
+ onCleanup(() => {
+ clearInterval(timer);
+ });
+ // 拖拽种子文件转换为磁力链接
const handleTorrentFileDrop = async (
e: DragEvent,
setValue: (value: string) => void,
) => {
- e.preventDefault()
+ e.preventDefault();
if (e.dataTransfer?.files.length) {
- const values = []
+ const values = [];
for (const file of e.dataTransfer.files) {
if (file.name.toLowerCase().endsWith(".torrent")) {
try {
- const buffer = await file.arrayBuffer()
- values.push(toMagnetUrl(new Uint8Array(buffer)))
+ const buffer = await file.arrayBuffer();
+ values.push(toMagnetUrl(new Uint8Array(buffer)));
} catch (err) {
- console.error("Failed to convert torrent file to magnet link:", err)
+ console.error(
+ "Failed to convert torrent file to magnet link:",
+ err,
+ );
}
}
}
if (values.length) {
- setValue(values.join("\n"))
+ setValue(values.join("\n"));
}
}
- }
+ };
return (
{
+ onClose();
+ setShowTasks(false);
+ }}
loading={toolsLoading() || loading()}
tips={t("home.toolbar.offline_download-tips")}
onDrop={handleTorrentFileDrop}
@@ -124,47 +182,64 @@ export const OfflineDownload = () => {
v !== "SimpleHttp" &&
deletePolicy() === "upload_download_stream"
) {
- setDeletePolicy("delete_on_upload_succeed")
+ setDeletePolicy("delete_on_upload_succeed");
}
- setTool(v)
+ setTool(v);
}}
options={tools().map((tool) => {
- return { value: tool, label: tool }
+ return { value: tool, label: tool };
})}
/>
}
bottomSlot={
-
- setDeletePolicy(v as DeletePolicy)}
- options={deletePolicies
- .filter((policy) =>
- policy == "upload_download_stream"
- ? tool() === "SimpleHttp"
- : true,
- )
- .map((policy) => {
- return {
+
+
+ setDeletePolicy(v as DeletePolicy)}
+ options={deletePolicies
+ .filter((policy) =>
+ policy === "upload_download_stream"
+ ? tool() === "SimpleHttp"
+ : true,
+ )
+ .map((policy) => ({
value: policy,
label: t(`home.toolbar.delete_policy.${policy}`),
- }
- })}
- />
-
+ }))}
+ />
+
+
+ {/* 任务列表 */}
+
+
+
+
+ {t("tasks.attr.offline_download.list_title")}
+
+
+ }>
+
+ {(task) => }
+
+
+ {t("tasks.attr.offline_download.no_tasks")}
+
+
+
+
+
+
+
}
- onSubmit={async (urls) => {
- const resp = await ok(
- pathname(),
- urls.split("\n"),
- tool(),
- deletePolicy(),
- )
- handleRespWithNotifySuccess(resp, () => {
- onClose()
- })
- }}
+ onSubmitWithValue={handleSubmit}
/>
- )
-}
+ );
+};
From a0395eeb9c7fe70bb842a0594a1b12893f7270c2 Mon Sep 17 00:00:00 2001
From: zwzzrl <66906196+zwzzrl@users.noreply.github.com>
Date: Tue, 10 Mar 2026 22:30:42 +0800
Subject: [PATCH 02/12] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=A6=BB=E7=BA=BF?=
=?UTF-8?q?=E4=B8=8B=E8=BD=BD=E4=BB=BB=E5=8A=A1=E5=88=97=E8=A1=A8=E4=BA=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../home/offlinedownload/TaskProgress.tsx | 77 ++++----
src/pages/home/offlinedownload/task.ts | 76 ++++----
src/pages/home/toolbar/OfflineDownload.tsx | 165 +++++++++---------
3 files changed, 157 insertions(+), 161 deletions(-)
diff --git a/src/pages/home/offlinedownload/TaskProgress.tsx b/src/pages/home/offlinedownload/TaskProgress.tsx
index 72bec1b1f..173d98318 100644
--- a/src/pages/home/offlinedownload/TaskProgress.tsx
+++ b/src/pages/home/offlinedownload/TaskProgress.tsx
@@ -5,25 +5,24 @@ import {
Text,
Badge,
Progress,
- ProgressIndicator,
- Box,
-} from "@hope-ui/solid";
-import { TaskInfo } from "./task";
-import { getFileSize } from "~/utils";
-import { Show, createSignal, JSX } from "solid-js";
-import { useT } from "~/hooks";
-import { me } from "~/store";
+ ProgressIndicator
+} from "@hope-ui/solid"
+import { TaskInfo } from "./task"
+import { getFileSize } from "~/utils"
+import { Show, createSignal, JSX } from "solid-js"
+import { useT } from "~/hooks"
+import { me } from "~/store"
// 复制 helper.tsx 中的 getPath 函数(用于生成路径链接)
const getPath = (
device: string,
path: string,
- asLink: boolean = true,
+ asLink: boolean = true
): JSX.Element => {
- const fullPath = (device === "/" ? "" : device) + path;
- const prefix = me().base_path === "/" ? "" : me().base_path;
- const accessible = fullPath.startsWith(prefix);
- const [underline, setUnderline] = createSignal(false);
+ const fullPath = (device === "/" ? "" : device) + path
+ const prefix = me().base_path === "/" ? "" : me().base_path
+ const accessible = fullPath.startsWith(prefix)
+ const [underline, setUnderline] = createSignal(false)
return accessible && asLink ? (
) : (
{fullPath}
- );
-};
+ )
+}
// 解析任务名称,返回文件名和路径信息
const parseTaskName = (name: string) => {
// download 类型:download 文件名 to (路径)
- let match = name.match(/^download (.+) to \((.+)\)$/);
+ let match = name.match(/^download (.+) to \((.+)\)$/)
if (match) {
return {
type: "download" as const,
fileName: match[1],
- path: match[2],
- };
+ path: match[2]
+ }
}
// transfer/upload 类型:transfer [设备](路径) to [目标设备](目标路径) 或 upload [文件名](URL) to [目标设备](目标路径)
match = name.match(
- /^(transfer|upload) \[(.*?)\]\((.*?)\) to \[(.*?)\]\((.*?)\)$/,
- );
+ /^(transfer|upload) \[(.*?)\]\((.*?)\) to \[(.*?)\]\((.*?)\)$/
+ )
if (match) {
- const type = match[1] as "transfer" | "upload";
- const bracketContent = match[2]; // 方括号内:transfer 为设备,upload 为文件名
- const urlOrPath = match[3]; // 圆括号内:transfer 为路径,upload 为 URL
- const dstDevice = match[4];
- const dstPath = match[5];
+ const type = match[1] as "transfer" | "upload"
+ const bracketContent = match[2] // 方括号内:transfer 为设备,upload 为文件名
+ const urlOrPath = match[3] // 圆括号内:transfer 为路径,upload 为 URL
+ const dstDevice = match[4]
+ const dstPath = match[5]
if (type === "transfer") {
// 从路径中提取文件名(最后一段,去除参数)
- const fileName = urlOrPath.split("/").pop()?.split("?")[0] || "未知文件";
+ const fileName = urlOrPath.split("/").pop()?.split("?")[0] || "未知文件"
return {
type,
fileName,
srcDevice: bracketContent,
srcPath: urlOrPath,
dstDevice,
- dstPath,
- };
+ dstPath
+ }
} else {
// upload 类型:文件名直接取自方括号
return {
@@ -79,12 +78,12 @@ const parseTaskName = (name: string) => {
srcDevice: "",
srcPath: urlOrPath,
dstDevice,
- dstPath,
- };
+ dstPath
+ }
}
}
- return null;
-};
+ return null
+}
export const StatusColor = {
0: "neutral",
@@ -92,12 +91,12 @@ export const StatusColor = {
2: "warning",
3: "danger",
4: "success",
- 5: "info",
-} as const;
+ 5: "info"
+} as const
export const TaskItem = (props: TaskInfo) => {
- const t = useT();
- const parsed = parseTaskName(props.name);
+ const t = useT()
+ const parsed = parseTaskName(props.name)
return (
{
- );
-};
+ )
+}
-export default TaskItem;
+export default TaskItem
diff --git a/src/pages/home/offlinedownload/task.ts b/src/pages/home/offlinedownload/task.ts
index 6892330a3..6fe0f7d26 100644
--- a/src/pages/home/offlinedownload/task.ts
+++ b/src/pages/home/offlinedownload/task.ts
@@ -1,64 +1,64 @@
// src/store/task.ts
-import { createSignal } from "solid-js";
-import { createStore } from "solid-js/store";
-import { r } from "~/utils";
+import { createSignal } from "solid-js"
+import { createStore } from "solid-js/store"
+import { r } from "~/utils"
export interface TaskInfo {
- id: string;
- name: string;
- creator: string;
- creator_role: number;
- state: number;
- status: string;
- progress: number;
- start_time: string | null;
- end_time: string | null;
- total_bytes: number;
- error: string;
+ id: string
+ name: string
+ creator: string
+ creator_role: number
+ state: number
+ status: string
+ progress: number
+ start_time: string | null
+ end_time: string | null
+ total_bytes: number
+ error: string
}
-const [tasks, setTasks] = createStore([]);
-const [loading, setLoading] = createSignal(false);
+const [tasks, setTasks] = createStore([])
+const [loading, setLoading] = createSignal(false)
export const fetchTasks = async (showLoading = true) => {
- if (showLoading) setLoading(true);
+ if (showLoading) setLoading(true)
try {
const [respOld, respNew] = await Promise.all([
r.get("/task/offline_download/undone").catch(() => ({ data: [] })),
r
.get("/task/offline_download_transfer/undone")
- .catch(() => ({ data: [] })),
- ]);
+ .catch(() => ({ data: [] }))
+ ])
- const taskMap = new Map();
+ const taskMap = new Map()
- const oldTasks = respOld.data || [];
+ const oldTasks = respOld.data || []
oldTasks.forEach((item: any) => {
- if (!item.state) item.state = 0;
- taskMap.set(item.id, item);
- });
+ if (!item.state) item.state = 0
+ taskMap.set(item.id, item)
+ })
- const newTasks = respNew.data || [];
+ const newTasks = respNew.data || []
newTasks.forEach((item: any) => {
- taskMap.set(item.id, item);
- });
+ taskMap.set(item.id, item)
+ })
- const mergedTasks = Array.from(taskMap.values());
+ const mergedTasks = Array.from(taskMap.values())
// 按 start_time 降序排序(最新的在前),null 值视为最旧,排在后面
mergedTasks.sort((a, b) => {
- if (!a.start_time && !b.start_time) return 0;
- if (!a.start_time) return 1;
- if (!b.start_time) return -1;
- return b.start_time.localeCompare(a.start_time); // 字符串降序比较
- });
+ if (!a.start_time && !b.start_time) return 0
+ if (!a.start_time) return 1
+ if (!b.start_time) return -1
+ return b.start_time.localeCompare(a.start_time) // 字符串降序比较
+ })
- setTasks(mergedTasks);
+ setTasks(mergedTasks)
} catch (e) {
- console.error("Failed to fetch tasks:", e);
+ console.error("Failed to fetch tasks:", e)
} finally {
- if (showLoading) setLoading(false);
+ if (showLoading) setLoading(false)
}
-};
+}
-export const useTasks = () => ({ tasks, loading, fetchTasks });
+export const useTasks = () => ({ tasks, loading, fetchTasks })
diff --git a/src/pages/home/toolbar/OfflineDownload.tsx b/src/pages/home/toolbar/OfflineDownload.tsx
index a206ccc84..5fb10ae24 100644
--- a/src/pages/home/toolbar/OfflineDownload.tsx
+++ b/src/pages/home/toolbar/OfflineDownload.tsx
@@ -5,161 +5,158 @@ import {
Heading,
Button,
VStack,
- Text,
-} from "@hope-ui/solid";
-import { ModalInput, SelectWrapper } from "~/components";
-import { useFetch, useRouter, useT } from "~/hooks";
+ Text
+} from "@hope-ui/solid"
+import { ModalInput, SelectWrapper } from "~/components"
+import { useFetch, useRouter, useT } from "~/hooks"
import {
offlineDownload,
bus,
handleRespWithNotifySuccess,
r,
- handleResp,
-} from "~/utils";
+ handleResp
+} from "~/utils"
import {
createSignal,
onCleanup,
onMount,
createEffect,
For,
- Show,
-} from "solid-js";
-import { PResp } from "~/types";
-import bencode from "bencode";
-import crypto from "crypto-js";
-import { useTasks } from "../offlinedownload/task";
-import TaskItem from "../offlinedownload/TaskProgress";
-import { FullLoading } from "~/components/FullLoading";
+ Show
+} from "solid-js"
+import { PResp } from "~/types"
+import bencode from "bencode"
+import crypto from "crypto-js"
+import { useTasks } from "../offlinedownload/task"
+import TaskItem from "../offlinedownload/TaskProgress"
+import { FullLoading } from "~/components/FullLoading"
const deletePolicies = [
"upload_download_stream",
"delete_on_upload_succeed",
"delete_on_upload_failed",
"delete_never",
- "delete_always",
-] as const;
+ "delete_always"
+] as const
-type DeletePolicy = (typeof deletePolicies)[number];
+type DeletePolicy = (typeof deletePolicies)[number]
function utf8Decode(data: Uint8Array): string {
- return crypto.enc.Utf8.stringify(crypto.lib.WordArray.create(data));
+ return crypto.enc.Utf8.stringify(crypto.lib.WordArray.create(data))
}
function toMagnetUrl(torrentBuffer: Uint8Array) {
- const data = bencode.decode(torrentBuffer as any);
- const infoEncode = bencode.encode(data.info) as unknown as Uint8Array;
+ const data = bencode.decode(torrentBuffer as any)
+ const infoEncode = bencode.encode(data.info) as unknown as Uint8Array
const infoHash = crypto
.SHA1(crypto.lib.WordArray.create(infoEncode))
- .toString();
- let params = {} as any;
+ .toString()
+ let params = {} as any
if (Number.isInteger(data?.info?.length)) {
- params.xl = data.info.length;
+ params.xl = data.info.length
}
if (data.info.name) {
- params.dn = utf8Decode(data.info.name);
+ params.dn = utf8Decode(data.info.name)
}
if (data.announce) {
- params.tr = utf8Decode(data.announce);
+ params.tr = utf8Decode(data.announce)
}
return `magnet:?xt=urn:btih:${infoHash}&${new URLSearchParams(
- params,
- ).toString()}`;
+ params
+ ).toString()}`
}
export const OfflineDownload = () => {
- const t = useT();
- const [tools, setTools] = createSignal([] as string[]);
+ const t = useT()
+ const [tools, setTools] = createSignal([] as string[])
const [toolsLoading, reqTool] = useFetch((): PResp => {
- return r.get("/public/offline_download_tools");
- });
- const [tool, setTool] = createSignal("");
+ return r.get("/public/offline_download_tools")
+ })
+ const [tool, setTool] = createSignal("")
const [deletePolicy, setDeletePolicy] = createSignal(
- "upload_download_stream",
- );
+ "upload_download_stream"
+ )
onMount(async () => {
- const resp = await reqTool();
+ const resp = await reqTool()
handleResp(resp, (data) => {
- setTools(data);
- setTool(data[0]);
- });
- });
+ setTools(data)
+ setTool(data[0])
+ })
+ })
- const { isOpen, onOpen, onClose } = createDisclosure();
- const [loading, ok] = useFetch(offlineDownload);
- const { pathname } = useRouter();
+ const { isOpen, onOpen, onClose } = createDisclosure()
+ const [loading, ok] = useFetch(offlineDownload)
+ const { pathname } = useRouter()
// 监听工具栏事件
const handler = (name: string) => {
if (name === "offline_download") {
- onOpen();
- setShowTasks(true);
+ onOpen()
+ setShowTasks(true)
}
- };
- bus.on("tool", handler);
+ }
+ bus.on("tool", handler)
onCleanup(() => {
- bus.off("tool", handler);
- });
+ bus.off("tool", handler)
+ })
- const { tasks, loading: tasksLoading, fetchTasks } = useTasks();
- const [showTasks, setShowTasks] = createSignal(false);
+ const { tasks, loading: tasksLoading, fetchTasks } = useTasks()
+ const [showTasks, setShowTasks] = createSignal(false)
const handleSubmit = async (
urls: string,
- setValue: (value: string) => void,
+ setValue: (value: string) => void
) => {
- const resp = await ok(pathname(), urls.split("\n"), tool(), deletePolicy());
+ const resp = await ok(pathname(), urls.split("\n"), tool(), deletePolicy())
handleRespWithNotifySuccess(resp, () => {
- fetchTasks(false); // 不显示 loading
- setValue("");
- });
- };
+ fetchTasks(false) // 不显示 loading
+ setValue("")
+ })
+ }
// 定时刷新任务进度(仅当显示任务列表时)
- let timer: number | undefined;
+ let timer: number | undefined
createEffect(() => {
if (showTasks()) {
// 打开时立即获取一次最新数据,不显示 loading
- fetchTasks(false);
+ fetchTasks(false)
// 启动定时器,每隔3秒刷新(也不显示 loading)
- timer = setInterval(() => fetchTasks(false), 3000);
+ timer = setInterval(() => fetchTasks(false), 3000)
} else {
- clearInterval(timer);
- timer = undefined;
+ clearInterval(timer)
+ timer = undefined
}
- });
+ })
onCleanup(() => {
- clearInterval(timer);
- });
+ clearInterval(timer)
+ })
// 拖拽种子文件转换为磁力链接
const handleTorrentFileDrop = async (
e: DragEvent,
- setValue: (value: string) => void,
+ setValue: (value: string) => void
) => {
- e.preventDefault();
+ e.preventDefault()
if (e.dataTransfer?.files.length) {
- const values = [];
+ const values = []
for (const file of e.dataTransfer.files) {
if (file.name.toLowerCase().endsWith(".torrent")) {
try {
- const buffer = await file.arrayBuffer();
- values.push(toMagnetUrl(new Uint8Array(buffer)));
+ const buffer = await file.arrayBuffer()
+ values.push(toMagnetUrl(new Uint8Array(buffer)))
} catch (err) {
- console.error(
- "Failed to convert torrent file to magnet link:",
- err,
- );
+ console.error("Failed to convert torrent file to magnet link:", err)
}
}
}
if (values.length) {
- setValue(values.join("\n"));
+ setValue(values.join("\n"))
}
}
- };
+ }
return (
{
type="text"
opened={isOpen()}
onClose={() => {
- onClose();
- setShowTasks(false);
+ onClose()
+ setShowTasks(false)
}}
loading={toolsLoading() || loading()}
tips={t("home.toolbar.offline_download-tips")}
@@ -182,12 +179,12 @@ export const OfflineDownload = () => {
v !== "SimpleHttp" &&
deletePolicy() === "upload_download_stream"
) {
- setDeletePolicy("delete_on_upload_succeed");
+ setDeletePolicy("delete_on_upload_succeed")
}
- setTool(v);
+ setTool(v)
}}
options={tools().map((tool) => {
- return { value: tool, label: tool };
+ return { value: tool, label: tool }
})}
/>
@@ -202,11 +199,11 @@ export const OfflineDownload = () => {
.filter((policy) =>
policy === "upload_download_stream"
? tool() === "SimpleHttp"
- : true,
+ : true
)
.map((policy) => ({
value: policy,
- label: t(`home.toolbar.delete_policy.${policy}`),
+ label: t(`home.toolbar.delete_policy.${policy}`)
}))}
/>
@@ -241,5 +238,5 @@ export const OfflineDownload = () => {
}
onSubmitWithValue={handleSubmit}
/>
- );
-};
+ )
+}
From 94c1b8aa359172a5a524c35a22b05051cdb1f80d Mon Sep 17 00:00:00 2001
From: zwzzrl <66906196+zwzzrl@users.noreply.github.com>
Date: Tue, 10 Mar 2026 23:05:45 +0800
Subject: [PATCH 03/12] =?UTF-8?q?=E8=A7=A6=E5=8F=91Actions?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
tarytoActions.txt | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 tarytoActions.txt
diff --git a/tarytoActions.txt b/tarytoActions.txt
new file mode 100644
index 000000000..e69de29bb
From f35d7522fed8fb65423f83e07619cbe6a467cbbd Mon Sep 17 00:00:00 2001
From: zwzzrl <66906196+zwzzrl@users.noreply.github.com>
Date: Tue, 10 Mar 2026 23:09:08 +0800
Subject: [PATCH 04/12] =?UTF-8?q?Revert=20"=E8=A7=A6=E5=8F=91Actions"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This reverts commit 94c1b8aa359172a5a524c35a22b05051cdb1f80d.
---
tarytoActions.txt | 0
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 tarytoActions.txt
diff --git a/tarytoActions.txt b/tarytoActions.txt
deleted file mode 100644
index e69de29bb..000000000
From 0088ad9291e66b32dc89d4da908ae05e1da62d16 Mon Sep 17 00:00:00 2001
From: MadDogOwner
Date: Wed, 11 Mar 2026 10:00:00 +0800
Subject: [PATCH 05/12] pnpm format
Signed-off-by: MadDogOwner
---
src/components/ModalInput.tsx | 4 ++--
.../home/offlinedownload/TaskProgress.tsx | 14 ++++++-------
src/pages/home/offlinedownload/task.ts | 2 +-
src/pages/home/toolbar/OfflineDownload.tsx | 20 +++++++++----------
4 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/src/components/ModalInput.tsx b/src/components/ModalInput.tsx
index 6312d38ba..7365034a1 100644
--- a/src/components/ModalInput.tsx
+++ b/src/components/ModalInput.tsx
@@ -25,7 +25,7 @@ export type ModalInputProps = {
title: string
isRenamingFile?: boolean
onSubmit?: (text: string) => void
- onSubmitWithValue?: (text: string,setValue: (value: string) => void) => void
+ onSubmitWithValue?: (text: string, setValue: (value: string) => void) => void
type?: string
defaultValue?: string
loading?: boolean
@@ -93,7 +93,7 @@ export const ModalInput = (props: ModalInputProps) => {
}
}
props.onSubmit?.(value())
- props.onSubmitWithValue?.(value(),setValue)
+ props.onSubmitWithValue?.(value(), setValue)
}
const handleInput = (newValue: string) => {
diff --git a/src/pages/home/offlinedownload/TaskProgress.tsx b/src/pages/home/offlinedownload/TaskProgress.tsx
index 173d98318..db8b699db 100644
--- a/src/pages/home/offlinedownload/TaskProgress.tsx
+++ b/src/pages/home/offlinedownload/TaskProgress.tsx
@@ -5,7 +5,7 @@ import {
Text,
Badge,
Progress,
- ProgressIndicator
+ ProgressIndicator,
} from "@hope-ui/solid"
import { TaskInfo } from "./task"
import { getFileSize } from "~/utils"
@@ -17,7 +17,7 @@ import { me } from "~/store"
const getPath = (
device: string,
path: string,
- asLink: boolean = true
+ asLink: boolean = true,
): JSX.Element => {
const fullPath = (device === "/" ? "" : device) + path
const prefix = me().base_path === "/" ? "" : me().base_path
@@ -45,12 +45,12 @@ const parseTaskName = (name: string) => {
return {
type: "download" as const,
fileName: match[1],
- path: match[2]
+ path: match[2],
}
}
// transfer/upload 类型:transfer [设备](路径) to [目标设备](目标路径) 或 upload [文件名](URL) to [目标设备](目标路径)
match = name.match(
- /^(transfer|upload) \[(.*?)\]\((.*?)\) to \[(.*?)\]\((.*?)\)$/
+ /^(transfer|upload) \[(.*?)\]\((.*?)\) to \[(.*?)\]\((.*?)\)$/,
)
if (match) {
const type = match[1] as "transfer" | "upload"
@@ -68,7 +68,7 @@ const parseTaskName = (name: string) => {
srcDevice: bracketContent,
srcPath: urlOrPath,
dstDevice,
- dstPath
+ dstPath,
}
} else {
// upload 类型:文件名直接取自方括号
@@ -78,7 +78,7 @@ const parseTaskName = (name: string) => {
srcDevice: "",
srcPath: urlOrPath,
dstDevice,
- dstPath
+ dstPath,
}
}
}
@@ -91,7 +91,7 @@ export const StatusColor = {
2: "warning",
3: "danger",
4: "success",
- 5: "info"
+ 5: "info",
} as const
export const TaskItem = (props: TaskInfo) => {
diff --git a/src/pages/home/offlinedownload/task.ts b/src/pages/home/offlinedownload/task.ts
index 6fe0f7d26..294422e7b 100644
--- a/src/pages/home/offlinedownload/task.ts
+++ b/src/pages/home/offlinedownload/task.ts
@@ -27,7 +27,7 @@ export const fetchTasks = async (showLoading = true) => {
r.get("/task/offline_download/undone").catch(() => ({ data: [] })),
r
.get("/task/offline_download_transfer/undone")
- .catch(() => ({ data: [] }))
+ .catch(() => ({ data: [] })),
])
const taskMap = new Map()
diff --git a/src/pages/home/toolbar/OfflineDownload.tsx b/src/pages/home/toolbar/OfflineDownload.tsx
index 5fb10ae24..dc08b3083 100644
--- a/src/pages/home/toolbar/OfflineDownload.tsx
+++ b/src/pages/home/toolbar/OfflineDownload.tsx
@@ -5,7 +5,7 @@ import {
Heading,
Button,
VStack,
- Text
+ Text,
} from "@hope-ui/solid"
import { ModalInput, SelectWrapper } from "~/components"
import { useFetch, useRouter, useT } from "~/hooks"
@@ -14,7 +14,7 @@ import {
bus,
handleRespWithNotifySuccess,
r,
- handleResp
+ handleResp,
} from "~/utils"
import {
createSignal,
@@ -22,7 +22,7 @@ import {
onMount,
createEffect,
For,
- Show
+ Show,
} from "solid-js"
import { PResp } from "~/types"
import bencode from "bencode"
@@ -36,7 +36,7 @@ const deletePolicies = [
"delete_on_upload_succeed",
"delete_on_upload_failed",
"delete_never",
- "delete_always"
+ "delete_always",
] as const
type DeletePolicy = (typeof deletePolicies)[number]
@@ -65,7 +65,7 @@ function toMagnetUrl(torrentBuffer: Uint8Array) {
}
return `magnet:?xt=urn:btih:${infoHash}&${new URLSearchParams(
- params
+ params,
).toString()}`
}
@@ -77,7 +77,7 @@ export const OfflineDownload = () => {
})
const [tool, setTool] = createSignal("")
const [deletePolicy, setDeletePolicy] = createSignal(
- "upload_download_stream"
+ "upload_download_stream",
)
onMount(async () => {
const resp = await reqTool()
@@ -108,7 +108,7 @@ export const OfflineDownload = () => {
const handleSubmit = async (
urls: string,
- setValue: (value: string) => void
+ setValue: (value: string) => void,
) => {
const resp = await ok(pathname(), urls.split("\n"), tool(), deletePolicy())
handleRespWithNotifySuccess(resp, () => {
@@ -137,7 +137,7 @@ export const OfflineDownload = () => {
// 拖拽种子文件转换为磁力链接
const handleTorrentFileDrop = async (
e: DragEvent,
- setValue: (value: string) => void
+ setValue: (value: string) => void,
) => {
e.preventDefault()
if (e.dataTransfer?.files.length) {
@@ -199,11 +199,11 @@ export const OfflineDownload = () => {
.filter((policy) =>
policy === "upload_download_stream"
? tool() === "SimpleHttp"
- : true
+ : true,
)
.map((policy) => ({
value: policy,
- label: t(`home.toolbar.delete_policy.${policy}`)
+ label: t(`home.toolbar.delete_policy.${policy}`),
}))}
/>
From 4c2c68d4dda65901d4f1c6296890453e02776ccf Mon Sep 17 00:00:00 2001
From: MadDogOwner
Date: Wed, 11 Mar 2026 10:04:46 +0800
Subject: [PATCH 06/12] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=9A=E6=97=B6?=
=?UTF-8?q?=E5=99=A8=E8=AE=BE=E7=BD=AE=EF=BC=8C=E7=A1=AE=E4=BF=9D=E4=BD=BF?=
=?UTF-8?q?=E7=94=A8=20window.setInterval?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: MadDogOwner
---
src/pages/home/toolbar/OfflineDownload.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pages/home/toolbar/OfflineDownload.tsx b/src/pages/home/toolbar/OfflineDownload.tsx
index dc08b3083..f29084254 100644
--- a/src/pages/home/toolbar/OfflineDownload.tsx
+++ b/src/pages/home/toolbar/OfflineDownload.tsx
@@ -124,7 +124,7 @@ export const OfflineDownload = () => {
// 打开时立即获取一次最新数据,不显示 loading
fetchTasks(false)
// 启动定时器,每隔3秒刷新(也不显示 loading)
- timer = setInterval(() => fetchTasks(false), 3000)
+ timer = window.setInterval(() => fetchTasks(false), 3000)
} else {
clearInterval(timer)
timer = undefined
From 4e3b20579107ab77faaf323e7f474296476832bf Mon Sep 17 00:00:00 2001
From: zwzzrl <66906196+zwzzrl@users.noreply.github.com>
Date: Wed, 11 Mar 2026 15:36:36 +0800
Subject: [PATCH 07/12] =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=87=8D=E5=A4=8D?=
=?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=B9=B6=E4=BC=98=E5=8C=96=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/ModalInput.tsx | 9 ++--
.../home/offlinedownload/TaskProgress.tsx | 31 ++-----------
src/pages/home/offlinedownload/task.ts | 43 ++++++++-----------
src/pages/home/toolbar/OfflineDownload.tsx | 31 +++++++++----
4 files changed, 51 insertions(+), 63 deletions(-)
diff --git a/src/components/ModalInput.tsx b/src/components/ModalInput.tsx
index 7365034a1..4be2f9af1 100644
--- a/src/components/ModalInput.tsx
+++ b/src/components/ModalInput.tsx
@@ -92,10 +92,13 @@ export const ModalInput = (props: ModalInputProps) => {
return
}
}
- props.onSubmit?.(value())
- props.onSubmitWithValue?.(value(), setValue)
+ if (props.onSubmitWithValue) {
+ props.onSubmitWithValue(value(), setValue)
+ } else {
+ props.onSubmit?.(value())
+ }
}
-
+
const handleInput = (newValue: string) => {
setValue(newValue)
if (props.validateFilename) {
diff --git a/src/pages/home/offlinedownload/TaskProgress.tsx b/src/pages/home/offlinedownload/TaskProgress.tsx
index db8b699db..05c88c3f2 100644
--- a/src/pages/home/offlinedownload/TaskProgress.tsx
+++ b/src/pages/home/offlinedownload/TaskProgress.tsx
@@ -1,4 +1,3 @@
-// src/pages/home/toolbar/TaskProgress.tsx
import {
VStack,
HStack,
@@ -7,35 +6,11 @@ import {
Progress,
ProgressIndicator,
} from "@hope-ui/solid"
-import { TaskInfo } from "./task"
+import type { TaskInfo } from "~/types"
import { getFileSize } from "~/utils"
-import { Show, createSignal, JSX } from "solid-js"
+import { Show } from "solid-js"
import { useT } from "~/hooks"
-import { me } from "~/store"
-
-// 复制 helper.tsx 中的 getPath 函数(用于生成路径链接)
-const getPath = (
- device: string,
- path: string,
- asLink: boolean = true,
-): JSX.Element => {
- const fullPath = (device === "/" ? "" : device) + path
- const prefix = me().base_path === "/" ? "" : me().base_path
- const accessible = fullPath.startsWith(prefix)
- const [underline, setUnderline] = createSignal(false)
- return accessible && asLink ? (
- setUnderline(true)}
- onMouseOut={() => setUnderline(false)}
- href={fullPath.slice(prefix.length)}
- >
- {fullPath}
-
- ) : (
- {fullPath}
- )
-}
+import { getPath } from "../../manage/tasks/helper"
// 解析任务名称,返回文件名和路径信息
const parseTaskName = (name: string) => {
diff --git a/src/pages/home/offlinedownload/task.ts b/src/pages/home/offlinedownload/task.ts
index 294422e7b..637fc61e8 100644
--- a/src/pages/home/offlinedownload/task.ts
+++ b/src/pages/home/offlinedownload/task.ts
@@ -1,21 +1,7 @@
-// src/store/task.ts
import { createSignal } from "solid-js"
import { createStore } from "solid-js/store"
import { r } from "~/utils"
-
-export interface TaskInfo {
- id: string
- name: string
- creator: string
- creator_role: number
- state: number
- status: string
- progress: number
- start_time: string | null
- end_time: string | null
- total_bytes: number
- error: string
-}
+import type { TaskInfo } from "~/types"
const [tasks, setTasks] = createStore([])
const [loading, setLoading] = createSignal(false)
@@ -24,21 +10,30 @@ export const fetchTasks = async (showLoading = true) => {
if (showLoading) setLoading(true)
try {
const [respOld, respNew] = await Promise.all([
- r.get("/task/offline_download/undone").catch(() => ({ data: [] })),
- r
- .get("/task/offline_download_transfer/undone")
- .catch(() => ({ data: [] })),
+ r.get("/task/offline_download/undone"),
+ r.get("/task/offline_download_transfer/undone"),
])
-
+ const getTasksFromResp = (resp: any, label: string): any[] => {
+ if (!resp || resp.code !== 200) {
+ const message =
+ resp && typeof resp.message === "string"
+ ? resp.message
+ : "Unknown error"
+ throw new Error(`Failed to fetch ${label}: ${message}`)
+ }
+ const data = resp.data
+ return Array.isArray(data) ? data : []
+ }
const taskMap = new Map()
-
- const oldTasks = respOld.data || []
+ const oldTasks = getTasksFromResp(respOld, "offline download tasks")
oldTasks.forEach((item: any) => {
if (!item.state) item.state = 0
taskMap.set(item.id, item)
})
-
- const newTasks = respNew.data || []
+ const newTasks = getTasksFromResp(
+ respNew,
+ "offline download transfer tasks",
+ )
newTasks.forEach((item: any) => {
taskMap.set(item.id, item)
})
diff --git a/src/pages/home/toolbar/OfflineDownload.tsx b/src/pages/home/toolbar/OfflineDownload.tsx
index f29084254..850e285a3 100644
--- a/src/pages/home/toolbar/OfflineDownload.tsx
+++ b/src/pages/home/toolbar/OfflineDownload.tsx
@@ -3,7 +3,6 @@ import {
createDisclosure,
HStack,
Heading,
- Button,
VStack,
Text,
} from "@hope-ui/solid"
@@ -96,6 +95,7 @@ export const OfflineDownload = () => {
if (name === "offline_download") {
onOpen()
setShowTasks(true)
+ fetchTasks(true)
}
}
bus.on("tool", handler)
@@ -112,28 +112,43 @@ export const OfflineDownload = () => {
) => {
const resp = await ok(pathname(), urls.split("\n"), tool(), deletePolicy())
handleRespWithNotifySuccess(resp, () => {
- fetchTasks(false) // 不显示 loading
+ fetchTasks(true) // 显示 loading
setValue("")
})
}
// 定时刷新任务进度(仅当显示任务列表时)
let timer: number | undefined
+ let isFetching = false
createEffect(() => {
if (showTasks()) {
- // 打开时立即获取一次最新数据,不显示 loading
- fetchTasks(false)
- // 启动定时器,每隔3秒刷新(也不显示 loading)
- timer = window.setInterval(() => fetchTasks(false), 3000)
+ const poll = async () => {
+ if (!showTasks()) return
+ if (isFetching) {
+ timer = window.setTimeout(poll, 3000)
+ return
+ }
+ isFetching = true
+ try {
+ await fetchTasks(false)
+ } finally {
+ isFetching = false
+ }
+ if (showTasks()) {
+ timer = window.setTimeout(poll, 3000)
+ }
+ }
+ void poll()
} else {
- clearInterval(timer)
+ clearTimeout(timer)
timer = undefined
}
})
onCleanup(() => {
- clearInterval(timer)
+ clearTimeout(timer)
})
+
// 拖拽种子文件转换为磁力链接
const handleTorrentFileDrop = async (
e: DragEvent,
From e6429856203beeea6f6a85458909e1def503152d Mon Sep 17 00:00:00 2001
From: MadDogOwner
Date: Wed, 11 Mar 2026 16:36:52 +0800
Subject: [PATCH 08/12] =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E6=96=87=E4=BB=B6?=
=?UTF-8?q?=E4=BD=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: MadDogOwner
---
src/pages/home/toolbar/OfflineDownload.tsx | 4 ++--
src/pages/home/{offlinedownload => toolbar}/TaskProgress.tsx | 0
src/{pages/home/offlinedownload => store}/task.ts | 0
3 files changed, 2 insertions(+), 2 deletions(-)
rename src/pages/home/{offlinedownload => toolbar}/TaskProgress.tsx (100%)
rename src/{pages/home/offlinedownload => store}/task.ts (100%)
diff --git a/src/pages/home/toolbar/OfflineDownload.tsx b/src/pages/home/toolbar/OfflineDownload.tsx
index 850e285a3..0b6f10a0d 100644
--- a/src/pages/home/toolbar/OfflineDownload.tsx
+++ b/src/pages/home/toolbar/OfflineDownload.tsx
@@ -26,8 +26,8 @@ import {
import { PResp } from "~/types"
import bencode from "bencode"
import crypto from "crypto-js"
-import { useTasks } from "../offlinedownload/task"
-import TaskItem from "../offlinedownload/TaskProgress"
+import { useTasks } from "~/store/task"
+import TaskItem from "./TaskProgress"
import { FullLoading } from "~/components/FullLoading"
const deletePolicies = [
diff --git a/src/pages/home/offlinedownload/TaskProgress.tsx b/src/pages/home/toolbar/TaskProgress.tsx
similarity index 100%
rename from src/pages/home/offlinedownload/TaskProgress.tsx
rename to src/pages/home/toolbar/TaskProgress.tsx
diff --git a/src/pages/home/offlinedownload/task.ts b/src/store/task.ts
similarity index 100%
rename from src/pages/home/offlinedownload/task.ts
rename to src/store/task.ts
From e3e411ad4dc05a36bb6875ea8cddbbce6c3cbada Mon Sep 17 00:00:00 2001
From: MadDogOwner
Date: Wed, 11 Mar 2026 16:37:24 +0800
Subject: [PATCH 09/12] pnpm format
Signed-off-by: MadDogOwner
---
src/components/ModalInput.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/components/ModalInput.tsx b/src/components/ModalInput.tsx
index 4be2f9af1..7d89e6aad 100644
--- a/src/components/ModalInput.tsx
+++ b/src/components/ModalInput.tsx
@@ -92,13 +92,13 @@ export const ModalInput = (props: ModalInputProps) => {
return
}
}
- if (props.onSubmitWithValue) {
+ if (props.onSubmitWithValue) {
props.onSubmitWithValue(value(), setValue)
} else {
props.onSubmit?.(value())
- }
+ }
}
-
+
const handleInput = (newValue: string) => {
setValue(newValue)
if (props.validateFilename) {
From 1ab5476db4173119afa3ef144acd84e455b61209 Mon Sep 17 00:00:00 2001
From: MadDogOwner
Date: Wed, 11 Mar 2026 17:05:00 +0800
Subject: [PATCH 10/12] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AF=BC=E5=85=A5?=
=?UTF-8?q?=E8=B7=AF=E5=BE=84=EF=BC=8C=E4=BF=AE=E5=A4=8D=E6=96=87=E4=BB=B6?=
=?UTF-8?q?=E5=90=8D=E6=98=BE=E7=A4=BA=E5=92=8C=E7=8A=B6=E6=80=81=E9=A2=9C?=
=?UTF-8?q?=E8=89=B2=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: MadDogOwner
---
src/pages/home/toolbar/OfflineDownload.tsx | 33 +++++++++++-----------
src/pages/home/toolbar/TaskProgress.tsx | 9 ++++--
2 files changed, 22 insertions(+), 20 deletions(-)
diff --git a/src/pages/home/toolbar/OfflineDownload.tsx b/src/pages/home/toolbar/OfflineDownload.tsx
index 0b6f10a0d..0d6b02d4a 100644
--- a/src/pages/home/toolbar/OfflineDownload.tsx
+++ b/src/pages/home/toolbar/OfflineDownload.tsx
@@ -1,34 +1,33 @@
import {
Box,
createDisclosure,
- HStack,
Heading,
- VStack,
+ HStack,
Text,
+ VStack,
} from "@hope-ui/solid"
-import { ModalInput, SelectWrapper } from "~/components"
-import { useFetch, useRouter, useT } from "~/hooks"
-import {
- offlineDownload,
- bus,
- handleRespWithNotifySuccess,
- r,
- handleResp,
-} from "~/utils"
+import bencode from "bencode"
+import crypto from "crypto-js"
import {
+ createEffect,
createSignal,
+ For,
onCleanup,
onMount,
- createEffect,
- For,
Show,
} from "solid-js"
-import { PResp } from "~/types"
-import bencode from "bencode"
-import crypto from "crypto-js"
+import { FullLoading, ModalInput, SelectWrapper } from "~/components"
+import { useFetch, useRouter, useT } from "~/hooks"
import { useTasks } from "~/store/task"
+import { PResp } from "~/types"
+import {
+ bus,
+ handleResp,
+ handleRespWithNotifySuccess,
+ offlineDownload,
+ r,
+} from "~/utils"
import TaskItem from "./TaskProgress"
-import { FullLoading } from "~/components/FullLoading"
const deletePolicies = [
"upload_download_stream",
diff --git a/src/pages/home/toolbar/TaskProgress.tsx b/src/pages/home/toolbar/TaskProgress.tsx
index 05c88c3f2..8fc1d6e6b 100644
--- a/src/pages/home/toolbar/TaskProgress.tsx
+++ b/src/pages/home/toolbar/TaskProgress.tsx
@@ -10,7 +10,7 @@ import type { TaskInfo } from "~/types"
import { getFileSize } from "~/utils"
import { Show } from "solid-js"
import { useT } from "~/hooks"
-import { getPath } from "../../manage/tasks/helper"
+import { getPath } from "~/pages/manage/tasks/helper"
// 解析任务名称,返回文件名和路径信息
const parseTaskName = (name: string) => {
@@ -36,7 +36,8 @@ const parseTaskName = (name: string) => {
if (type === "transfer") {
// 从路径中提取文件名(最后一段,去除参数)
- const fileName = urlOrPath.split("/").pop()?.split("?")[0] || "未知文件"
+ const fileName =
+ urlOrPath.split("/").pop()?.split("?")[0] || "Unknown file"
return {
type,
fileName,
@@ -110,7 +111,9 @@ export const TaskItem = (props: TaskInfo) => {
)}
{t("tasks.state." + props.state)}
From 5189a48b00c84c2474119e574558ead9cfca3929 Mon Sep 17 00:00:00 2001
From: MadDogOwner
Date: Wed, 11 Mar 2026 17:05:08 +0800
Subject: [PATCH 11/12] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=BF=9B=E5=BA=A6?=
=?UTF-8?q?=E6=9D=A1=E5=80=BC=E7=9A=84=E8=AE=A1=E7=AE=97=EF=BC=8C=E7=9B=B4?=
=?UTF-8?q?=E6=8E=A5=E4=BD=BF=E7=94=A8=20props.progress?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: MadDogOwner
---
src/pages/home/toolbar/TaskProgress.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pages/home/toolbar/TaskProgress.tsx b/src/pages/home/toolbar/TaskProgress.tsx
index 8fc1d6e6b..7e910a591 100644
--- a/src/pages/home/toolbar/TaskProgress.tsx
+++ b/src/pages/home/toolbar/TaskProgress.tsx
@@ -123,7 +123,7 @@ export const TaskItem = (props: TaskInfo) => {
w="$full"
trackColor="$info3"
rounded="$full"
- value={props.progress * 100}
+ value={props.progress}
size="sm"
>
From 13d40d3128930007aaf878fccd30dda2e543614a Mon Sep 17 00:00:00 2001
From: zwzzrl <66906196+zwzzrl@users.noreply.github.com>
Date: Wed, 11 Mar 2026 21:54:39 +0800
Subject: [PATCH 12/12] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=87=BD=E6=95=B0?=
=?UTF-8?q?=E5=90=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/pages/home/toolbar/OfflineDownload.tsx | 12 ++++++------
src/pages/home/toolbar/TaskProgress.tsx | 4 ++--
src/store/task.ts | 4 ++--
3 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/pages/home/toolbar/OfflineDownload.tsx b/src/pages/home/toolbar/OfflineDownload.tsx
index 0d6b02d4a..55f1a1287 100644
--- a/src/pages/home/toolbar/OfflineDownload.tsx
+++ b/src/pages/home/toolbar/OfflineDownload.tsx
@@ -27,7 +27,7 @@ import {
offlineDownload,
r,
} from "~/utils"
-import TaskItem from "./TaskProgress"
+import OfflineDownloadTaskItem from "./TaskProgress"
const deletePolicies = [
"upload_download_stream",
@@ -94,7 +94,7 @@ export const OfflineDownload = () => {
if (name === "offline_download") {
onOpen()
setShowTasks(true)
- fetchTasks(true)
+ fetchOfflineDownloadTasks(true)
}
}
bus.on("tool", handler)
@@ -102,7 +102,7 @@ export const OfflineDownload = () => {
bus.off("tool", handler)
})
- const { tasks, loading: tasksLoading, fetchTasks } = useTasks()
+ const { tasks, loading: tasksLoading, fetchOfflineDownloadTasks } = useTasks()
const [showTasks, setShowTasks] = createSignal(false)
const handleSubmit = async (
@@ -111,7 +111,7 @@ export const OfflineDownload = () => {
) => {
const resp = await ok(pathname(), urls.split("\n"), tool(), deletePolicy())
handleRespWithNotifySuccess(resp, () => {
- fetchTasks(true) // 显示 loading
+ fetchOfflineDownloadTasks(true) // 显示 loading
setValue("")
})
}
@@ -129,7 +129,7 @@ export const OfflineDownload = () => {
}
isFetching = true
try {
- await fetchTasks(false)
+ await fetchOfflineDownloadTasks(false)
} finally {
isFetching = false
}
@@ -238,7 +238,7 @@ export const OfflineDownload = () => {
}>
- {(task) => }
+ {(task) => }
{t("tasks.attr.offline_download.no_tasks")}
diff --git a/src/pages/home/toolbar/TaskProgress.tsx b/src/pages/home/toolbar/TaskProgress.tsx
index 7e910a591..11878b2ad 100644
--- a/src/pages/home/toolbar/TaskProgress.tsx
+++ b/src/pages/home/toolbar/TaskProgress.tsx
@@ -70,7 +70,7 @@ export const StatusColor = {
5: "info",
} as const
-export const TaskItem = (props: TaskInfo) => {
+export const OfflineDownloadTaskItem = (props: TaskInfo) => {
const t = useT()
const parsed = parseTaskName(props.name)
@@ -137,4 +137,4 @@ export const TaskItem = (props: TaskInfo) => {
)
}
-export default TaskItem
+export default OfflineDownloadTaskItem
diff --git a/src/store/task.ts b/src/store/task.ts
index 637fc61e8..7c02b8d96 100644
--- a/src/store/task.ts
+++ b/src/store/task.ts
@@ -6,7 +6,7 @@ import type { TaskInfo } from "~/types"
const [tasks, setTasks] = createStore([])
const [loading, setLoading] = createSignal(false)
-export const fetchTasks = async (showLoading = true) => {
+export const fetchOfflineDownloadTasks = async (showLoading = true) => {
if (showLoading) setLoading(true)
try {
const [respOld, respNew] = await Promise.all([
@@ -56,4 +56,4 @@ export const fetchTasks = async (showLoading = true) => {
}
}
-export const useTasks = () => ({ tasks, loading, fetchTasks })
+export const useTasks = () => ({ tasks, loading, fetchOfflineDownloadTasks })