From 28971c3ff3c5c072dba4aac536f3e4bb31dfc847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=80=E1=B4=8D=E1=B4=9B=E1=B4=8F=E1=B4=80=E1=B4=87?= =?UTF-8?q?=CA=80?= Date: Tue, 17 Jun 2025 18:55:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E6=BA=90=E7=AE=A1=E7=90=86=E9=A1=B5=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=B7=AF=E5=BE=84=E4=B8=8E=E5=90=AF=E7=94=A8?= =?UTF-8?q?=E7=8A=B6=E6=80=81=20(#369)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/bili_sync/src/api/handler.rs | 152 +++++++++- crates/bili_sync/src/api/request.rs | 7 + crates/bili_sync/src/api/response.rs | 20 +- web/bun.lock | 6 +- web/components.json | 4 +- web/package.json | 4 +- web/src/lib/api.ts | 36 ++- web/src/lib/components/app-sidebar.svelte | 19 ++ web/src/lib/components/ui/switch/index.ts | 7 + .../lib/components/ui/switch/switch.svelte | 29 ++ web/src/lib/components/ui/table/index.ts | 28 ++ .../lib/components/ui/table/table-body.svelte | 20 ++ .../components/ui/table/table-caption.svelte | 20 ++ .../lib/components/ui/table/table-cell.svelte | 20 ++ .../components/ui/table/table-footer.svelte | 20 ++ .../lib/components/ui/table/table-head.svelte | 23 ++ .../components/ui/table/table-header.svelte | 20 ++ .../lib/components/ui/table/table-row.svelte | 23 ++ web/src/lib/components/ui/table/table.svelte | 22 ++ web/src/lib/types.ts | 22 ++ web/src/routes/video-sources/+page.svelte | 283 ++++++++++++++++++ 21 files changed, 769 insertions(+), 16 deletions(-) create mode 100644 web/src/lib/components/ui/switch/index.ts create mode 100644 web/src/lib/components/ui/switch/switch.svelte create mode 100644 web/src/lib/components/ui/table/index.ts create mode 100644 web/src/lib/components/ui/table/table-body.svelte create mode 100644 web/src/lib/components/ui/table/table-caption.svelte create mode 100644 web/src/lib/components/ui/table/table-cell.svelte create mode 100644 web/src/lib/components/ui/table/table-footer.svelte create mode 100644 web/src/lib/components/ui/table/table-head.svelte create mode 100644 web/src/lib/components/ui/table/table-header.svelte create mode 100644 web/src/lib/components/ui/table/table-row.svelte create mode 100644 web/src/lib/components/ui/table/table.svelte create mode 100644 web/src/routes/video-sources/+page.svelte diff --git a/crates/bili_sync/src/api/handler.rs b/crates/bili_sync/src/api/handler.rs index 9fe118b..b49a60f 100644 --- a/crates/bili_sync/src/api/handler.rs +++ b/crates/bili_sync/src/api/handler.rs @@ -6,7 +6,7 @@ use axum::Router; use axum::body::Body; use axum::extract::{Extension, Path, Query}; use axum::response::Response; -use axum::routing::{get, post}; +use axum::routing::{get, post, put}; use bili_sync_entity::*; use bili_sync_migration::{Expr, OnConflict}; use reqwest::{Method, StatusCode, header}; @@ -18,17 +18,19 @@ use sea_orm::{ use utoipa::OpenApi; use super::request::ImageProxyParams; +use crate::adapter::_ActiveModel; use crate::api::auth::OpenAPIAuth; use crate::api::error::InnerApiError; use crate::api::helper::{update_page_download_status, update_video_download_status}; use crate::api::request::{ - FollowedCollectionsRequest, FollowedUppersRequest, UpdateVideoStatusRequest, UpsertCollectionRequest, - UpsertFavoriteRequest, UpsertSubmissionRequest, VideosRequest, + FollowedCollectionsRequest, FollowedUppersRequest, UpdateVideoSourceRequest, UpdateVideoStatusRequest, + UpsertCollectionRequest, UpsertFavoriteRequest, UpsertSubmissionRequest, VideosRequest, }; use crate::api::response::{ CollectionWithSubscriptionStatus, CollectionsResponse, FavoriteWithSubscriptionStatus, FavoritesResponse, PageInfo, ResetAllVideosResponse, ResetVideoResponse, UpdateVideoStatusResponse, UpperWithSubscriptionStatus, UppersResponse, - VideoInfo, VideoResponse, VideoSource, VideoSourcesResponse, VideosResponse, + VideoInfo, VideoResponse, VideoSource, VideoSourceDetail, VideoSourcesDetailsResponse, VideoSourcesResponse, + VideosResponse, }; use crate::api::wrapper::{ApiError, ApiResponse, ValidatedJson}; use crate::bilibili::{BiliClient, Collection, CollectionItem, FavoriteList, Me, Submission}; @@ -37,7 +39,7 @@ use crate::utils::status::{PageStatus, VideoStatus}; #[derive(OpenApi)] #[openapi( paths( - get_video_sources, get_videos, get_video, reset_video, reset_all_videos, update_video_status, + get_video_sources, get_video_sources_details, update_video_source, get_videos, get_video, reset_video, reset_all_videos, update_video_status, get_created_favorites, get_followed_collections, get_followed_uppers, upsert_favorite, upsert_collection, upsert_submission ), @@ -51,6 +53,8 @@ pub struct ApiDoc; pub fn api_router() -> Router { Router::new() .route("/api/video-sources", get(get_video_sources)) + .route("/api/video-sources/details", get(get_video_sources_details)) + .route("/api/video-sources/{type}/{id}", put(update_video_source)) .route("/api/video-sources/collections", post(upsert_collection)) .route("/api/video-sources/favorites", post(upsert_favorite)) .route("/api/video-sources/submissions", post(upsert_submission)) @@ -76,7 +80,7 @@ pub fn api_router() -> Router { pub async fn get_video_sources( Extension(db): Extension>, ) -> Result, ApiError> { - let (collection, favorite, submission, watch_later) = tokio::try_join!( + let (collection, favorite, submission, mut watch_later) = tokio::try_join!( collection::Entity::find() .select_only() .columns([collection::Column::Id, collection::Column::Name]) @@ -100,6 +104,13 @@ pub async fn get_video_sources( .into_model::() .all(db.as_ref()) )?; + // watch_later 是一个特殊的视频来源,如果不存在则添加一个默认项 + if watch_later.is_empty() { + watch_later.push(VideoSource { + id: 1, + name: "稍后再看".to_string(), + }); + } Ok(ApiResponse::ok(VideoSourcesResponse { collection, favorite, @@ -643,6 +654,135 @@ pub async fn upsert_submission( Ok(ApiResponse::ok(true)) } +/// 获取所有视频源的详细信息,包括 path 和 enabled 状态 +#[utoipa::path( + get, + path = "/api/video-sources/details", + responses( + (status = 200, body = ApiResponse), + ) +)] +pub async fn get_video_sources_details( + Extension(db): Extension>, +) -> Result, ApiError> { + let (collections, favorites, submissions, mut watch_later) = tokio::try_join!( + collection::Entity::find() + .select_only() + .columns([ + collection::Column::Id, + collection::Column::Name, + collection::Column::Path, + collection::Column::Enabled + ]) + .into_model::() + .all(db.as_ref()), + favorite::Entity::find() + .select_only() + .columns([ + favorite::Column::Id, + favorite::Column::Name, + favorite::Column::Path, + favorite::Column::Enabled + ]) + .into_model::() + .all(db.as_ref()), + submission::Entity::find() + .select_only() + .column(submission::Column::Id) + .column_as(submission::Column::UpperName, "name") + .columns([submission::Column::Path, submission::Column::Enabled]) + .into_model::() + .all(db.as_ref()), + watch_later::Entity::find() + .select_only() + .column(watch_later::Column::Id) + .column_as(Expr::value("稍后再看"), "name") + .columns([watch_later::Column::Path, watch_later::Column::Enabled]) + .into_model::() + .all(db.as_ref()) + )?; + if watch_later.is_empty() { + watch_later.push(VideoSourceDetail { + id: 1, + name: "稍后再看".to_string(), + path: String::new(), + enabled: false, + }) + } + Ok(ApiResponse::ok(VideoSourcesDetailsResponse { + collections, + favorites, + submissions, + watch_later, + })) +} + +/// 更新视频源的 path 和 enabled 状态 +#[utoipa::path( + put, + path = "/api/video-sources/{type}/{id}", + request_body = UpdateVideoSourceRequest, + responses( + (status = 200, body = ApiResponse), + ) +)] +pub async fn update_video_source( + Path((source_type, id)): Path<(String, i32)>, + Extension(db): Extension>, + ValidatedJson(request): ValidatedJson, +) -> Result, ApiError> { + let active_model = match source_type.as_str() { + "collections" => collection::Entity::find_by_id(id).one(db.as_ref()).await?.map(|model| { + let mut active_model: collection::ActiveModel = model.into(); + active_model.path = Set(request.path); + active_model.enabled = Set(request.enabled); + _ActiveModel::Collection(active_model) + }), + "favorites" => favorite::Entity::find_by_id(id).one(db.as_ref()).await?.map(|model| { + let mut active_model: favorite::ActiveModel = model.into(); + active_model.path = Set(request.path); + active_model.enabled = Set(request.enabled); + _ActiveModel::Favorite(active_model) + }), + "submissions" => submission::Entity::find_by_id(id).one(db.as_ref()).await?.map(|model| { + let mut active_model: submission::ActiveModel = model.into(); + active_model.path = Set(request.path); + active_model.enabled = Set(request.enabled); + _ActiveModel::Submission(active_model) + }), + "watch_later" => match watch_later::Entity::find_by_id(id).one(db.as_ref()).await? { + // 稍后再看需要做特殊处理,get 时如果稍后再看不存在返回的是 id 为 1 的假记录 + // 因此此处可能是更新也可能是插入,做个额外的处理 + Some(model) => { + // 如果有记录,使用 id 对应的记录更新 + let mut active_model: watch_later::ActiveModel = model.into(); + active_model.path = Set(request.path); + active_model.enabled = Set(request.enabled); + Some(_ActiveModel::WatchLater(active_model)) + } + None => { + if id != 1 { + None + } else { + // 如果没有记录且 id 为 1,插入一个新的稍后再看记录 + Some(_ActiveModel::WatchLater(watch_later::ActiveModel { + id: Set(1), + path: Set(request.path), + enabled: Set(request.enabled), + ..Default::default() + })) + } + } + }, + _ => return Err(InnerApiError::BadRequest("Invalid video source type".to_string()).into()), + }; + let Some(active_model) = active_model else { + return Err(InnerApiError::NotFound(id).into()); + }; + active_model.save(db.as_ref()).await?; + Ok(ApiResponse::ok(true)) +} + /// B 站的图片会检查 referer,需要做个转发伪造一下,否则直接返回 403 pub async fn image_proxy( Extension(bili_client): Extension>, diff --git a/crates/bili_sync/src/api/request.rs b/crates/bili_sync/src/api/request.rs index f316a78..58dd7cd 100644 --- a/crates/bili_sync/src/api/request.rs +++ b/crates/bili_sync/src/api/request.rs @@ -80,3 +80,10 @@ pub struct UpsertSubmissionRequest { pub struct ImageProxyParams { pub url: String, } + +#[derive(Deserialize, ToSchema, Validate)] +pub struct UpdateVideoSourceRequest { + #[validate(custom(function = "crate::utils::validation::validate_path"))] + pub path: String, + pub enabled: bool, +} diff --git a/crates/bili_sync/src/api/response.rs b/crates/bili_sync/src/api/response.rs index 0ba0c5a..1052d22 100644 --- a/crates/bili_sync/src/api/response.rs +++ b/crates/bili_sync/src/api/response.rs @@ -48,8 +48,8 @@ pub struct UpdateVideoStatusResponse { #[derive(FromQueryResult, Serialize, ToSchema)] pub struct VideoSource { - id: i32, - name: String, + pub id: i32, + pub name: String, } #[derive(Serialize, ToSchema, DerivePartialModel, FromQueryResult)] @@ -135,3 +135,19 @@ pub struct UppersResponse { pub uppers: Vec, pub total: i64, } + +#[derive(Serialize, ToSchema)] +pub struct VideoSourcesDetailsResponse { + pub collections: Vec, + pub favorites: Vec, + pub submissions: Vec, + pub watch_later: Vec, +} + +#[derive(Serialize, ToSchema, FromQueryResult)] +pub struct VideoSourceDetail { + pub id: i32, + pub name: String, + pub path: String, + pub enabled: bool, +} diff --git a/web/bun.lock b/web/bun.lock index 546ddbc..bbb73c3 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -6,7 +6,7 @@ "devDependencies": { "@eslint/compat": "^1.2.5", "@eslint/js": "^9.18.0", - "@internationalized/date": "^3.5.6", + "@internationalized/date": "^3.8.1", "@lucide/svelte": "^0.482.0", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/kit": "^2.16.0", @@ -14,7 +14,7 @@ "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.0.0", - "bits-ui": "^2.1.0", + "bits-ui": "^2.7.0", "clsx": "^2.1.1", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", @@ -276,7 +276,7 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "bits-ui": ["bits-ui@2.3.0", "", { "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/dom": "^1.7.0", "css.escape": "^1.5.1", "esm-env": "^1.1.2", "runed": "^0.28.0", "svelte-toolbelt": "^0.9.1", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-PgDcs49PdHGRC7T1tTNqWhMrTHgOVE0q/q03K7n64V5MxeB2aMrbAqoW9Wg+43P4u2vMzUn8ifhLBPSfZDNceg=="], + "bits-ui": ["bits-ui@2.8.0", "", { "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/dom": "^1.7.0", "css.escape": "^1.5.1", "esm-env": "^1.1.2", "runed": "^0.28.0", "svelte-toolbelt": "^0.9.1", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-WiTZcCbYLm4Cx6/67NqXVSD0BkfNmdX8Abs84HpIaplX/wRRbg8tkMtJYlLw7mepgGvwGR3enLi6tFkcHU3JXA=="], "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], diff --git a/web/components.json b/web/components.json index 897ab35..c5d91b4 100644 --- a/web/components.json +++ b/web/components.json @@ -1,5 +1,5 @@ { - "$schema": "https://next.shadcn-svelte.com/schema.json", + "$schema": "https://shadcn-svelte.com/schema.json", "tailwind": { "css": "src/app.css", "baseColor": "slate" @@ -12,5 +12,5 @@ "lib": "$lib" }, "typescript": true, - "registry": "https://next.shadcn-svelte.com/registry" + "registry": "https://shadcn-svelte.com/registry" } diff --git a/web/package.json b/web/package.json index 4ca3c0d..2f7e658 100644 --- a/web/package.json +++ b/web/package.json @@ -16,7 +16,7 @@ "devDependencies": { "@eslint/compat": "^1.2.5", "@eslint/js": "^9.18.0", - "@internationalized/date": "^3.5.6", + "@internationalized/date": "^3.8.1", "@lucide/svelte": "^0.482.0", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/kit": "^2.16.0", @@ -24,7 +24,7 @@ "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.0.0", - "bits-ui": "^2.1.0", + "bits-ui": "^2.7.0", "clsx": "^2.1.1", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 12aeeec..afdb90b 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -14,7 +14,9 @@ import type { UppersResponse, UpsertFavoriteRequest, UpsertCollectionRequest, - UpsertSubmissionRequest + UpsertSubmissionRequest, + VideoSourcesDetailsResponse, + UpdateVideoSourceRequest } from './types'; // API 基础配置 @@ -235,6 +237,27 @@ class ApiClient { async upsertSubmission(request: UpsertSubmissionRequest): Promise> { return this.post('/video-sources/submissions', request); } + + /** + * 获取所有视频源的详细信息 + */ + async getVideoSourcesDetails(): Promise> { + return this.get('/video-sources/details'); + } + + /** + * 更新视频源 + * @param type 视频源类型 + * @param id 视频源 ID + * @param request 更新请求 + */ + async updateVideoSource( + type: string, + id: number, + request: UpdateVideoSourceRequest + ): Promise> { + return this.put(`/video-sources/${type}/${id}`, request); + } } // 创建默认的 API 客户端实例 @@ -305,6 +328,17 @@ export const api = { */ upsertSubmission: (request: UpsertSubmissionRequest) => apiClient.upsertSubmission(request), + /** + * 获取所有视频源的详细信息 + */ + getVideoSourcesDetails: () => apiClient.getVideoSourcesDetails(), + + /** + * 更新视频源 + */ + updateVideoSource: (type: string, id: number, request: UpdateVideoSourceRequest) => + apiClient.updateVideoSource(type, id, request), + /** * 设置认证 token */ diff --git a/web/src/lib/components/app-sidebar.svelte b/web/src/lib/components/app-sidebar.svelte index b7eb187..b6da1bc 100644 --- a/web/src/lib/components/app-sidebar.svelte +++ b/web/src/lib/components/app-sidebar.svelte @@ -4,6 +4,7 @@ import UserIcon from '@lucide/svelte/icons/user'; import HeartIcon from '@lucide/svelte/icons/heart'; import FolderIcon from '@lucide/svelte/icons/folder'; + import DatabaseIcon from '@lucide/svelte/icons/database'; import * as Sidebar from '$lib/components/ui/sidebar/index.js'; import { useSidebar } from '$lib/components/ui/sidebar/context.svelte.js'; import { @@ -183,6 +184,24 @@
+ + + { + if (sidebar.isMobile) { + sidebar.setOpenMobile(false); + } + }} + > +
+ + 视频源管理 +
+
+
+
+ import { Switch as SwitchPrimitive } from 'bits-ui'; + import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'; + + let { + ref = $bindable(null), + class: className, + checked = $bindable(false), + ...restProps + }: WithoutChildrenOrChild = $props(); + + + + + diff --git a/web/src/lib/components/ui/table/index.ts b/web/src/lib/components/ui/table/index.ts new file mode 100644 index 0000000..99239ae --- /dev/null +++ b/web/src/lib/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Root from './table.svelte'; +import Body from './table-body.svelte'; +import Caption from './table-caption.svelte'; +import Cell from './table-cell.svelte'; +import Footer from './table-footer.svelte'; +import Head from './table-head.svelte'; +import Header from './table-header.svelte'; +import Row from './table-row.svelte'; + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow +}; diff --git a/web/src/lib/components/ui/table/table-body.svelte b/web/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 0000000..cf720f4 --- /dev/null +++ b/web/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/table/table-caption.svelte b/web/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 0000000..61ac415 --- /dev/null +++ b/web/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/table/table-cell.svelte b/web/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 0000000..1fb325b --- /dev/null +++ b/web/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/table/table-footer.svelte b/web/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 0000000..53a9eb4 --- /dev/null +++ b/web/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,20 @@ + + +tr]:last:border-b-0', className)} + {...restProps} +> + {@render children?.()} + diff --git a/web/src/lib/components/ui/table/table-head.svelte b/web/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 0000000..1ef7f1d --- /dev/null +++ b/web/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/table/table-header.svelte b/web/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 0000000..615b5d8 --- /dev/null +++ b/web/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/table/table-row.svelte b/web/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 0000000..d0d5fed --- /dev/null +++ b/web/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/table/table.svelte b/web/src/lib/components/ui/table/table.svelte new file mode 100644 index 0000000..38b0176 --- /dev/null +++ b/web/src/lib/components/ui/table/table.svelte @@ -0,0 +1,22 @@ + + +
+ + {@render children?.()} +
+
diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 9c49915..65534eb 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -160,3 +160,25 @@ export interface UpsertSubmissionRequest { upper_id: number; path: string; } + +// 视频源详细信息类型 +export interface VideoSourceDetail { + id: number; + name: string; + path: string; + enabled: boolean; +} + +// 视频源详细信息响应类型 +export interface VideoSourcesDetailsResponse { + collections: VideoSourceDetail[]; + favorites: VideoSourceDetail[]; + submissions: VideoSourceDetail[]; + watch_later: VideoSourceDetail[]; +} + +// 更新视频源请求类型 +export interface UpdateVideoSourceRequest { + path: string; + enabled: boolean; +} diff --git a/web/src/routes/video-sources/+page.svelte b/web/src/routes/video-sources/+page.svelte new file mode 100644 index 0000000..05b86f9 --- /dev/null +++ b/web/src/routes/video-sources/+page.svelte @@ -0,0 +1,283 @@ + + + + 视频源管理 - Bili Sync + + +
+ {#if loading} +
+
加载中...
+
+ {:else if videoSourcesData} + + + {#each Object.entries(TAB_CONFIG) as [key, config] (key)} + {@const sources = getSourcesForTab(key)} + +
+ +
+ + {sources.length} +
+ {/each} +
+ {#each Object.entries(TAB_CONFIG) as [key, config] (key)} + {@const sources = getSourcesForTab(key)} + + {#if sources.length > 0} +
+ + + + 名称 + 下载路径 + 状态 + 操作 + + + + {#each sources as source, index (index)} + + {source.name} + + {#if source.editing} + + {:else} + + {source.path || '未设置'} + + {/if} + + + {#if source.editing} +
+ +
+ {:else} +
+ + + {source.enabled ? '已启用' : '已禁用'} + +
+ {/if} +
+ + {#if source.editing} +
+ + +
+ {:else} + + {/if} +
+
+ {/each} +
+
+
+ {:else} +
+
+ +
+
暂无{config.label}
+

+ 请先添加{config.label}订阅 +

+
+ {/if} +
+ {/each} +
+ {:else} +
+
加载失败
+

请刷新页面重试

+ +
+ {/if} +