From 30f3745ac3c3ce49b241483aa1b599cc4d4f6c69 Mon Sep 17 00:00:00 2001 From: bf1942 Date: Tue, 10 Feb 2026 17:19:39 +0800 Subject: [PATCH] feat: add batch controls for video sources and bump version to 2.10.4 --- Cargo.lock | 6 +- Cargo.toml | 2 +- docs/introduction.md | 2 +- web/package-lock.json | 4 +- web/package.json | 2 +- web/src/routes/video-sources/+page.svelte | 176 +++++++++++++++++++++- 6 files changed, 183 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbd55c2..efeff82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -353,7 +353,7 @@ dependencies = [ [[package]] name = "bili_sync" -version = "2.10.3" +version = "2.10.4" dependencies = [ "anyhow", "arc-swap", @@ -412,7 +412,7 @@ dependencies = [ [[package]] name = "bili_sync_entity" -version = "2.10.3" +version = "2.10.4" dependencies = [ "derivative", "regex", @@ -423,7 +423,7 @@ dependencies = [ [[package]] name = "bili_sync_migration" -version = "2.10.3" +version = "2.10.4" dependencies = [ "sea-orm-migration", ] diff --git a/Cargo.toml b/Cargo.toml index 556fad8..16cc800 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = ["crates/bili_sync"] resolver = "2" [workspace.package] -version = "2.10.3" +version = "2.10.4" authors = ["amtoaer "] license = "MIT" description = "由 Rust & Tokio 驱动的哔哩哔哩同步工具" diff --git a/docs/introduction.md b/docs/introduction.md index 4fdedba..9b67ecd 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -1,7 +1,7 @@ # bili-sync 是什么? > [!TIP] -> 当前最新程序版本为 v2.10.3,文档将始终与最新程序版本保持一致。 +> 当前最新程序版本为 v2.10.4,文档将始终与最新程序版本保持一致。 bili-sync 是一款专为 NAS 用户编写的哔哩哔哩同步工具。 diff --git a/web/package-lock.json b/web/package-lock.json index 3d65e7b..b744f1a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "bili-sync-web", - "version": "2.10.3", + "version": "2.10.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bili-sync-web", - "version": "2.10.3", + "version": "2.10.4", "dependencies": { "@types/qrcode": "^1.5.6", "qrcode": "^1.5.4" diff --git a/web/package.json b/web/package.json index 1135e71..86fe22b 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "bili-sync-web", - "version": "2.10.3", + "version": "2.10.4", "devDependencies": { "@eslint/compat": "^1.4.1", "@eslint/js": "^9.39.2", diff --git a/web/src/routes/video-sources/+page.svelte b/web/src/routes/video-sources/+page.svelte index 890d85d..1fc389b 100644 --- a/web/src/routes/video-sources/+page.svelte +++ b/web/src/routes/video-sources/+page.svelte @@ -32,6 +32,7 @@ let videoSourcesData: VideoSourcesDetailsResponse | null = null; let loading = false; let activeTab = 'favorites'; + let batchSaving = false; // 添加对话框状态 let showAddDialog = false; @@ -70,6 +71,12 @@ let favoriteForm = { fid: '' }; let collectionForm = { sid: '', mid: '', collection_type: '2' }; // 默认为合集 let submissionForm = { upper_id: '' }; + let selectedIds: Record = { + favorites: [], + collections: [], + submissions: [], + watch_later: [] + }; const TAB_CONFIG = { favorites: { label: '收藏夹', icon: HeartIcon }, @@ -84,6 +91,7 @@ try { const response = await api.getVideoSourcesDetails(); videoSourcesData = response.data; + selectedIds = { favorites: [], collections: [], submissions: [], watch_later: [] }; } catch (error) { toast.error('加载视频源失败', { description: (error as ApiError).message @@ -194,6 +202,10 @@ ] as VideoSourceDetail[]; sources.splice(removeIdx, 1); videoSourcesData = { ...videoSourcesData }; + selectedIds = { + ...selectedIds, + [removeType]: (selectedIds[removeType] ?? []).filter((id) => id !== removeSource.id) + }; } showRemoveDialog = false; toast.success('删除视频源成功'); @@ -214,6 +226,105 @@ return videoSourcesData[tabValue as keyof VideoSourcesDetailsResponse] as VideoSourceDetail[]; } + function isSelected(tab: string, id: number): boolean { + return selectedIds[tab]?.includes(id) ?? false; + } + + function toggleSelect(tab: string, id: number, checked: boolean) { + const current = selectedIds[tab] ?? []; + selectedIds = { + ...selectedIds, + [tab]: checked ? [...new Set([...current, id])] : current.filter((item) => item !== id) + }; + } + + function toggleSelectAll(tab: string, sourceIds: number[], checked: boolean) { + selectedIds = { + ...selectedIds, + [tab]: checked ? [...sourceIds] : [] + }; + } + + async function batchUpdateSource( + tab: string, + changes: { enabled?: boolean; useDynamicApi?: boolean } + ) { + if (!videoSourcesData) return; + const ids = selectedIds[tab] ?? []; + if (ids.length === 0) { + toast.error('请先勾选要操作的视频源'); + return; + } + const sources = getSourcesForTab(tab).filter((source) => ids.includes(source.id)); + if (sources.length === 0) { + toast.error('未找到可更新的视频源'); + return; + } + + batchSaving = true; + try { + const results = await Promise.allSettled( + sources.map((source) => + api.updateVideoSource(tab, source.id, { + path: source.path, + enabled: changes.enabled ?? source.enabled, + rule: source.rule, + useDynamicApi: + changes.useDynamicApi === undefined + ? source.useDynamicApi + : changes.useDynamicApi + }) + ) + ); + let successCount = 0; + let failedCount = 0; + const ruleDisplayMap = new Map(); + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + successCount += 1; + ruleDisplayMap.set(sources[index].id, result.value.data.ruleDisplay); + } else { + failedCount += 1; + } + }); + + if (successCount > 0) { + const list = videoSourcesData[tab as keyof VideoSourcesDetailsResponse] as VideoSourceDetail[]; + videoSourcesData = { + ...videoSourcesData, + [tab]: list.map((item) => { + if (!ids.includes(item.id)) return item; + return { + ...item, + enabled: changes.enabled ?? item.enabled, + useDynamicApi: + changes.useDynamicApi === undefined ? item.useDynamicApi : changes.useDynamicApi, + ruleDisplay: ruleDisplayMap.get(item.id) ?? item.ruleDisplay + }; + }) + }; + selectedIds = { + ...selectedIds, + [tab]: [] + }; + } + + if (failedCount > 0) { + toast.warning('批量更新部分失败', { + description: `成功 ${successCount} 条,失败 ${failedCount} 条` + }); + } else { + toast.success(`批量更新成功,共 ${successCount} 条`); + } + } catch (error) { + toast.error('批量更新失败', { + description: (error as ApiError).message + }); + } finally { + batchSaving = false; + } + } + // 打开添加对话框 function openAddDialog(type: 'favorites' | 'collections' | 'submissions') { addDialogType = type; @@ -299,9 +410,46 @@ {#each Object.entries(TAB_CONFIG) as [key, config] (key)} {@const sources = getSourcesForTab(key)} + {@const sourceIds = sources.map((source) => source.id)} + {@const selectedCount = (selectedIds[key] ?? []).length}
-
+
+ + + {#if key === 'submissions'} + + + {/if} +
{#if key === 'favorites' || key === 'collections' || key === 'submissions'}