From 7bb4e7bc4460315ace5a9f17b3e7c2b3f294b90a 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: Sun, 6 Jul 2025 22:49:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=89=8D=E7=AB=AF=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=A0=B9=E6=8D=AE=20ID=20=E6=89=8B=E5=8A=A8=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AE=A2=E9=98=85=20(#374)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/bili_sync/src/api/handler.rs | 62 ++-- crates/bili_sync/src/api/request.rs | 6 +- web/bun.lock | 18 +- web/package.json | 4 +- web/src/lib/api.ts | 18 +- .../lib/components/subscription-dialog.svelte | 18 +- .../components/ui/dialog/dialog-close.svelte | 7 + .../ui/dialog/dialog-content.svelte | 43 +++ .../ui/dialog/dialog-description.svelte | 17 ++ .../components/ui/dialog/dialog-footer.svelte | 20 ++ .../components/ui/dialog/dialog-header.svelte | 20 ++ .../ui/dialog/dialog-overlay.svelte | 20 ++ .../components/ui/dialog/dialog-title.svelte | 17 ++ .../ui/dialog/dialog-trigger.svelte | 7 + web/src/lib/components/ui/dialog/index.ts | 37 +++ web/src/lib/components/ui/select/index.ts | 37 +++ .../ui/select/select-content.svelte | 40 +++ .../ui/select/select-group-heading.svelte | 21 ++ .../components/ui/select/select-group.svelte | 7 + .../components/ui/select/select-item.svelte | 38 +++ .../components/ui/select/select-label.svelte | 20 ++ .../select/select-scroll-down-button.svelte | 20 ++ .../ui/select/select-scroll-up-button.svelte | 20 ++ .../ui/select/select-separator.svelte | 18 ++ .../ui/select/select-trigger.svelte | 29 ++ web/src/lib/types.ts | 6 +- web/src/routes/settings/+page.svelte | 4 +- web/src/routes/video-sources/+page.svelte | 268 ++++++++++++++++-- 28 files changed, 736 insertions(+), 106 deletions(-) create mode 100644 web/src/lib/components/ui/dialog/dialog-close.svelte create mode 100644 web/src/lib/components/ui/dialog/dialog-content.svelte create mode 100644 web/src/lib/components/ui/dialog/dialog-description.svelte create mode 100644 web/src/lib/components/ui/dialog/dialog-footer.svelte create mode 100644 web/src/lib/components/ui/dialog/dialog-header.svelte create mode 100644 web/src/lib/components/ui/dialog/dialog-overlay.svelte create mode 100644 web/src/lib/components/ui/dialog/dialog-title.svelte create mode 100644 web/src/lib/components/ui/dialog/dialog-trigger.svelte create mode 100644 web/src/lib/components/ui/dialog/index.ts create mode 100644 web/src/lib/components/ui/select/index.ts create mode 100644 web/src/lib/components/ui/select/select-content.svelte create mode 100644 web/src/lib/components/ui/select/select-group-heading.svelte create mode 100644 web/src/lib/components/ui/select/select-group.svelte create mode 100644 web/src/lib/components/ui/select/select-item.svelte create mode 100644 web/src/lib/components/ui/select/select-label.svelte create mode 100644 web/src/lib/components/ui/select/select-scroll-down-button.svelte create mode 100644 web/src/lib/components/ui/select/select-scroll-up-button.svelte create mode 100644 web/src/lib/components/ui/select/select-separator.svelte create mode 100644 web/src/lib/components/ui/select/select-trigger.svelte diff --git a/crates/bili_sync/src/api/handler.rs b/crates/bili_sync/src/api/handler.rs index 0fc2df6..2a83289 100644 --- a/crates/bili_sync/src/api/handler.rs +++ b/crates/bili_sync/src/api/handler.rs @@ -8,7 +8,7 @@ use axum::extract::{Extension, Path, Query}; use axum::response::Response; use axum::routing::{get, post, put}; use bili_sync_entity::*; -use bili_sync_migration::{Expr, OnConflict}; +use bili_sync_migration::Expr; use reqwest::{Method, StatusCode, header}; use sea_orm::ActiveValue::Set; use sea_orm::{ @@ -23,8 +23,8 @@ 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, UpdateVideoSourceRequest, UpdateVideoStatusRequest, - UpsertCollectionRequest, UpsertFavoriteRequest, UpsertSubmissionRequest, VideosRequest, + FollowedCollectionsRequest, FollowedUppersRequest, InsertCollectionRequest, InsertFavoriteRequest, + InsertSubmissionRequest, UpdateVideoSourceRequest, UpdateVideoStatusRequest, VideosRequest, }; use crate::api::response::{ CollectionWithSubscriptionStatus, CollectionsResponse, FavoriteWithSubscriptionStatus, FavoritesResponse, PageInfo, @@ -43,7 +43,7 @@ use crate::utils::status::{PageStatus, VideoStatus}; paths( 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 + insert_favorite, insert_collection, insert_submission ), modifiers(&OpenAPIAuth), security( @@ -57,9 +57,9 @@ pub fn api_router() -> Router { .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)) + .route("/api/video-sources/collections", post(insert_collection)) + .route("/api/video-sources/favorites", post(insert_favorite)) + .route("/api/video-sources/submissions", post(insert_submission)) .route("/api/videos", get(get_videos)) .route("/api/videos/{id}", get(get_video)) .route("/api/videos/{id}/reset", post(reset_video)) @@ -314,19 +314,20 @@ pub async fn reset_all_videos( } }) .collect::>(); - let resetted = !(resetted_videos_info.is_empty() && resetted_pages_info.is_empty()); - if resetted { + let has_video_updates = !resetted_videos_info.is_empty(); + let has_page_updates = !resetted_pages_info.is_empty(); + if has_video_updates || has_page_updates { let txn = db.begin().await?; - if !resetted_videos_info.is_empty() { + if has_video_updates { update_video_download_status(&txn, &resetted_videos_info, Some(500)).await?; } - if !resetted_pages_info.is_empty() { + if has_page_updates { update_page_download_status(&txn, &resetted_pages_info, Some(500)).await?; } txn.commit().await?; } Ok(ApiResponse::ok(ResetAllVideosResponse { - resetted, + resetted: has_video_updates || has_page_updates, resetted_videos_count: resetted_videos_info.len(), resetted_pages_count: resetted_pages_info.len(), })) @@ -549,15 +550,15 @@ pub async fn get_followed_uppers( #[utoipa::path( post, path = "/api/video-sources/favorites", - request_body = UpsertFavoriteRequest, + request_body = InsertFavoriteRequest, responses( (status = 200, body = ApiResponse), ) )] -pub async fn upsert_favorite( +pub async fn insert_favorite( Extension(db): Extension>, Extension(bili_client): Extension>, - ValidatedJson(request): ValidatedJson, + ValidatedJson(request): ValidatedJson, ) -> Result, ApiError> { let favorite = FavoriteList::new(bili_client.as_ref(), request.fid.to_string()); let favorite_info = favorite.get_info().await?; @@ -567,11 +568,6 @@ pub async fn upsert_favorite( path: Set(request.path), ..Default::default() }) - .on_conflict( - OnConflict::column(favorite::Column::FId) - .update_columns([favorite::Column::Name, favorite::Column::Path]) - .to_owned(), - ) .exec(db.as_ref()) .await?; @@ -581,15 +577,15 @@ pub async fn upsert_favorite( #[utoipa::path( post, path = "/api/video-sources/collections", - request_body = UpsertCollectionRequest, + request_body = InsertCollectionRequest, responses( (status = 200, body = ApiResponse), ) )] -pub async fn upsert_collection( +pub async fn insert_collection( Extension(db): Extension>, Extension(bili_client): Extension>, - ValidatedJson(request): ValidatedJson, + ValidatedJson(request): ValidatedJson, ) -> Result, ApiError> { let collection = Collection::new( bili_client.as_ref(), @@ -609,15 +605,6 @@ pub async fn upsert_collection( path: Set(request.path), ..Default::default() }) - .on_conflict( - OnConflict::columns([ - collection::Column::SId, - collection::Column::MId, - collection::Column::Type, - ]) - .update_columns([collection::Column::Name, collection::Column::Path]) - .to_owned(), - ) .exec(db.as_ref()) .await?; @@ -628,15 +615,15 @@ pub async fn upsert_collection( #[utoipa::path( post, path = "/api/video-sources/submissions", - request_body = UpsertSubmissionRequest, + request_body = InsertSubmissionRequest, responses( (status = 200, body = ApiResponse), ) )] -pub async fn upsert_submission( +pub async fn insert_submission( Extension(db): Extension>, Extension(bili_client): Extension>, - ValidatedJson(request): ValidatedJson, + ValidatedJson(request): ValidatedJson, ) -> Result, ApiError> { let submission = Submission::new(bili_client.as_ref(), request.upper_id.to_string()); let upper = submission.get_info().await?; @@ -647,11 +634,6 @@ pub async fn upsert_submission( path: Set(request.path), ..Default::default() }) - .on_conflict( - OnConflict::column(submission::Column::UpperId) - .update_columns([submission::Column::UpperName, submission::Column::Path]) - .to_owned(), - ) .exec(db.as_ref()) .await?; diff --git a/crates/bili_sync/src/api/request.rs b/crates/bili_sync/src/api/request.rs index 58dd7cd..5f2c8a1 100644 --- a/crates/bili_sync/src/api/request.rs +++ b/crates/bili_sync/src/api/request.rs @@ -52,14 +52,14 @@ pub struct FollowedUppersRequest { } #[derive(Deserialize, ToSchema, Validate)] -pub struct UpsertFavoriteRequest { +pub struct InsertFavoriteRequest { pub fid: i64, #[validate(custom(function = "crate::utils::validation::validate_path"))] pub path: String, } #[derive(Deserialize, ToSchema, Validate)] -pub struct UpsertCollectionRequest { +pub struct InsertCollectionRequest { pub sid: i64, pub mid: i64, #[schema(value_type = i8)] @@ -70,7 +70,7 @@ pub struct UpsertCollectionRequest { } #[derive(Deserialize, ToSchema, Validate)] -pub struct UpsertSubmissionRequest { +pub struct InsertSubmissionRequest { pub upper_id: i64, #[validate(custom(function = "crate::utils::validation::validate_path"))] pub path: String, diff --git a/web/bun.lock b/web/bun.lock index bbb73c3..97e9700 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -7,14 +7,14 @@ "@eslint/compat": "^1.2.5", "@eslint/js": "^9.18.0", "@internationalized/date": "^3.8.1", - "@lucide/svelte": "^0.482.0", + "@lucide/svelte": "^0.515.0", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.0.0", - "bits-ui": "^2.7.0", + "bits-ui": "^2.8.6", "clsx": "^2.1.1", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", @@ -110,9 +110,9 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.1", "", { "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" } }, "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w=="], - "@floating-ui/core": ["@floating-ui/core@1.7.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA=="], + "@floating-ui/core": ["@floating-ui/core@1.7.1", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw=="], - "@floating-ui/dom": ["@floating-ui/dom@1.7.0", "", { "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg=="], + "@floating-ui/dom": ["@floating-ui/dom@1.7.1", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/utils": "^0.2.9" } }, "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], @@ -138,7 +138,7 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], - "@lucide/svelte": ["@lucide/svelte@0.482.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-n2ycHU9cNcleRDwwpEHBJ6pYzVhHIaL3a+9dQa8kns9hB2g05bY+v2p2KP8v0pZwtNhYTHk/F2o2uZ1bVtQGhw=="], + "@lucide/svelte": ["@lucide/svelte@0.515.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-CEAyqcZmNBfYzVgaRmK2RFJP5tnbXxekRyDk0XX/eZQRfsJmkDvmQwXNX8C869BgNeryzmrRyjHhUL6g9ZOHNA=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -276,7 +276,7 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "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=="], + "bits-ui": ["bits-ui@2.8.10", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.29.1", "svelte-toolbelt": "^0.9.3", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-MOobkqapDZNrpcNmeL2g664xFmH4tZBOKBTxFmsQYMZQuybSZHQnPXy+AjM5XZEXRmCFx5+XRmo6+fC3vHh1hQ=="], "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], @@ -302,8 +302,6 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], - "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], @@ -526,7 +524,7 @@ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="], + "runed": ["runed@0.29.1", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-RGQEB8ZiWv4OvzBJhbMj2hMgRM8QrEptzTrDr7TDfkHaRePKjiUka4vJ9QHGY+8s87KymNvFoZAxFdQ4jtZNcA=="], "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], @@ -556,7 +554,7 @@ "svelte-sonner": ["svelte-sonner@1.0.4", "", { "dependencies": { "runed": "^0.26.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-ctm9jeV0Rf3im2J6RU1emccrJFjRSdNSPsLlxaF62TLZw9bB1D40U/U7+wqEgohJY/X7FBdghdj0BFQF/IqKPQ=="], - "svelte-toolbelt": ["svelte-toolbelt@0.9.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.28.0", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-wBX6MtYw/kpht80j5zLpxJyR9soLizXPIAIWEVd9llAi17SR44ZdG291bldjB7r/K5duC0opDFcuhk2cA1hb8g=="], + "svelte-toolbelt": ["svelte-toolbelt@0.9.3", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.29.0", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw=="], "tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="], diff --git a/web/package.json b/web/package.json index 2f7e658..b4b0cf1 100644 --- a/web/package.json +++ b/web/package.json @@ -17,14 +17,14 @@ "@eslint/compat": "^1.2.5", "@eslint/js": "^9.18.0", "@internationalized/date": "^3.8.1", - "@lucide/svelte": "^0.482.0", + "@lucide/svelte": "^0.515.0", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.0.0", - "bits-ui": "^2.7.0", + "bits-ui": "^2.8.6", "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 eb49263..bc350bb 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -12,9 +12,9 @@ import type { FavoritesResponse, CollectionsResponse, UppersResponse, - UpsertFavoriteRequest, - UpsertCollectionRequest, - UpsertSubmissionRequest, + InsertFavoriteRequest, + InsertCollectionRequest, + InsertSubmissionRequest, VideoSourcesDetailsResponse, UpdateVideoSourceRequest, Config @@ -185,15 +185,15 @@ class ApiClient { return this.get('/me/uppers', params as Record); } - async upsertFavorite(request: UpsertFavoriteRequest): Promise> { + async insertFavorite(request: InsertFavoriteRequest): Promise> { return this.post('/video-sources/favorites', request); } - async upsertCollection(request: UpsertCollectionRequest): Promise> { + async insertCollection(request: InsertCollectionRequest): Promise> { return this.post('/video-sources/collections', request); } - async upsertSubmission(request: UpsertSubmissionRequest): Promise> { + async insertSubmission(request: InsertSubmissionRequest): Promise> { return this.post('/video-sources/submissions', request); } @@ -235,9 +235,9 @@ const api = { apiClient.getFollowedCollections(pageNum, pageSize), getFollowedUppers: (pageNum?: number, pageSize?: number) => apiClient.getFollowedUppers(pageNum, pageSize), - upsertFavorite: (request: UpsertFavoriteRequest) => apiClient.upsertFavorite(request), - upsertCollection: (request: UpsertCollectionRequest) => apiClient.upsertCollection(request), - upsertSubmission: (request: UpsertSubmissionRequest) => apiClient.upsertSubmission(request), + insertFavorite: (request: InsertFavoriteRequest) => apiClient.insertFavorite(request), + insertCollection: (request: InsertCollectionRequest) => apiClient.insertCollection(request), + insertSubmission: (request: InsertSubmissionRequest) => apiClient.insertSubmission(request), getVideoSourcesDetails: () => apiClient.getVideoSourcesDetails(), updateVideoSource: (type: string, id: number, request: UpdateVideoSourceRequest) => apiClient.updateVideoSource(type, id, request), diff --git a/web/src/lib/components/subscription-dialog.svelte b/web/src/lib/components/subscription-dialog.svelte index 2f5a32c..eef5434 100644 --- a/web/src/lib/components/subscription-dialog.svelte +++ b/web/src/lib/components/subscription-dialog.svelte @@ -16,9 +16,9 @@ FavoriteWithSubscriptionStatus, CollectionWithSubscriptionStatus, UpperWithSubscriptionStatus, - UpsertFavoriteRequest, - UpsertCollectionRequest, - UpsertSubmissionRequest, + InsertFavoriteRequest, + InsertCollectionRequest, + InsertSubmissionRequest, ApiError } from '$lib/types'; @@ -94,30 +94,30 @@ switch (type) { case 'favorite': { const favorite = item as FavoriteWithSubscriptionStatus; - const request: UpsertFavoriteRequest = { + const request: InsertFavoriteRequest = { fid: favorite.fid, path: customPath.trim() }; - response = await api.upsertFavorite(request); + response = await api.insertFavorite(request); break; } case 'collection': { const collection = item as CollectionWithSubscriptionStatus; - const request: UpsertCollectionRequest = { + const request: InsertCollectionRequest = { sid: collection.sid, mid: collection.mid, path: customPath.trim() }; - response = await api.upsertCollection(request); + response = await api.insertCollection(request); break; } case 'upper': { const upper = item as UpperWithSubscriptionStatus; - const request: UpsertSubmissionRequest = { + const request: InsertSubmissionRequest = { upper_id: upper.mid, path: customPath.trim() }; - response = await api.upsertSubmission(request); + response = await api.insertSubmission(request); break; } } diff --git a/web/src/lib/components/ui/dialog/dialog-close.svelte b/web/src/lib/components/ui/dialog/dialog-close.svelte new file mode 100644 index 0000000..e8a96a7 --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/dialog/dialog-content.svelte b/web/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 0000000..c0e54b8 --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,43 @@ + + + + + + {@render children?.()} + {#if showCloseButton} + + + Close + + {/if} + + diff --git a/web/src/lib/components/ui/dialog/dialog-description.svelte b/web/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 0000000..c658420 --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/dialog/dialog-footer.svelte b/web/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 0000000..c457d56 --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/dialog/dialog-header.svelte b/web/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 0000000..5fe4145 --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/dialog/dialog-overlay.svelte b/web/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 0000000..938ab1e --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/web/src/lib/components/ui/dialog/dialog-title.svelte b/web/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 0000000..7073699 --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/dialog/dialog-trigger.svelte b/web/src/lib/components/ui/dialog/dialog-trigger.svelte new file mode 100644 index 0000000..ac04d9f --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/dialog/index.ts b/web/src/lib/components/ui/dialog/index.ts new file mode 100644 index 0000000..d9e5fb8 --- /dev/null +++ b/web/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,37 @@ +import { Dialog as DialogPrimitive } from 'bits-ui'; + +import Title from './dialog-title.svelte'; +import Footer from './dialog-footer.svelte'; +import Header from './dialog-header.svelte'; +import Overlay from './dialog-overlay.svelte'; +import Content from './dialog-content.svelte'; +import Description from './dialog-description.svelte'; +import Trigger from './dialog-trigger.svelte'; +import Close from './dialog-close.svelte'; + +const Root = DialogPrimitive.Root; +const Portal = DialogPrimitive.Portal; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose +}; diff --git a/web/src/lib/components/ui/select/index.ts b/web/src/lib/components/ui/select/index.ts new file mode 100644 index 0000000..bfa73d9 --- /dev/null +++ b/web/src/lib/components/ui/select/index.ts @@ -0,0 +1,37 @@ +import { Select as SelectPrimitive } from 'bits-ui'; + +import Group from './select-group.svelte'; +import Label from './select-label.svelte'; +import Item from './select-item.svelte'; +import Content from './select-content.svelte'; +import Trigger from './select-trigger.svelte'; +import Separator from './select-separator.svelte'; +import ScrollDownButton from './select-scroll-down-button.svelte'; +import ScrollUpButton from './select-scroll-up-button.svelte'; +import GroupHeading from './select-group-heading.svelte'; + +const Root = SelectPrimitive.Root; + +export { + Root, + Group, + Label, + Item, + Content, + Trigger, + Separator, + ScrollDownButton, + ScrollUpButton, + GroupHeading, + // + Root as Select, + Group as SelectGroup, + Label as SelectLabel, + Item as SelectItem, + Content as SelectContent, + Trigger as SelectTrigger, + Separator as SelectSeparator, + ScrollDownButton as SelectScrollDownButton, + ScrollUpButton as SelectScrollUpButton, + GroupHeading as SelectGroupHeading +}; diff --git a/web/src/lib/components/ui/select/select-content.svelte b/web/src/lib/components/ui/select/select-content.svelte new file mode 100644 index 0000000..ca9d1b5 --- /dev/null +++ b/web/src/lib/components/ui/select/select-content.svelte @@ -0,0 +1,40 @@ + + + + + + + {@render children?.()} + + + + diff --git a/web/src/lib/components/ui/select/select-group-heading.svelte b/web/src/lib/components/ui/select/select-group-heading.svelte new file mode 100644 index 0000000..3ff7003 --- /dev/null +++ b/web/src/lib/components/ui/select/select-group-heading.svelte @@ -0,0 +1,21 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/select/select-group.svelte b/web/src/lib/components/ui/select/select-group.svelte new file mode 100644 index 0000000..2520795 --- /dev/null +++ b/web/src/lib/components/ui/select/select-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/select/select-item.svelte b/web/src/lib/components/ui/select/select-item.svelte new file mode 100644 index 0000000..06e48be --- /dev/null +++ b/web/src/lib/components/ui/select/select-item.svelte @@ -0,0 +1,38 @@ + + + + {#snippet children({ selected, highlighted })} + + {#if selected} + + {/if} + + {#if childrenProp} + {@render childrenProp({ selected, highlighted })} + {:else} + {label || value} + {/if} + {/snippet} + diff --git a/web/src/lib/components/ui/select/select-label.svelte b/web/src/lib/components/ui/select/select-label.svelte new file mode 100644 index 0000000..6c76ced --- /dev/null +++ b/web/src/lib/components/ui/select/select-label.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/select/select-scroll-down-button.svelte b/web/src/lib/components/ui/select/select-scroll-down-button.svelte new file mode 100644 index 0000000..3502148 --- /dev/null +++ b/web/src/lib/components/ui/select/select-scroll-down-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/web/src/lib/components/ui/select/select-scroll-up-button.svelte b/web/src/lib/components/ui/select/select-scroll-up-button.svelte new file mode 100644 index 0000000..3d35e04 --- /dev/null +++ b/web/src/lib/components/ui/select/select-scroll-up-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/web/src/lib/components/ui/select/select-separator.svelte b/web/src/lib/components/ui/select/select-separator.svelte new file mode 100644 index 0000000..3624d4c --- /dev/null +++ b/web/src/lib/components/ui/select/select-separator.svelte @@ -0,0 +1,18 @@ + + + diff --git a/web/src/lib/components/ui/select/select-trigger.svelte b/web/src/lib/components/ui/select/select-trigger.svelte new file mode 100644 index 0000000..f14abe0 --- /dev/null +++ b/web/src/lib/components/ui/select/select-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 2459a7e..d8b0479 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -145,19 +145,19 @@ export interface UppersResponse { total: number; } -export interface UpsertFavoriteRequest { +export interface InsertFavoriteRequest { fid: number; path: string; } -export interface UpsertCollectionRequest { +export interface InsertCollectionRequest { sid: number; mid: number; collection_type?: number; path: string; } -export interface UpsertSubmissionRequest { +export interface InsertSubmissionRequest { upper_id: number; path: string; } diff --git a/web/src/routes/settings/+page.svelte b/web/src/routes/settings/+page.svelte index 08f41ca..0dd66b2 100644 --- a/web/src/routes/settings/+page.svelte +++ b/web/src/routes/settings/+page.svelte @@ -623,8 +623,8 @@ class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" bind:value={formData.nfo_time_type} > - - + + diff --git a/web/src/routes/video-sources/+page.svelte b/web/src/routes/video-sources/+page.svelte index 05b86f9..b13d6f3 100644 --- a/web/src/routes/video-sources/+page.svelte +++ b/web/src/routes/video-sources/+page.svelte @@ -2,8 +2,11 @@ import { onMount } from 'svelte'; import { Button } from '$lib/components/ui/button/index.js'; import { Switch } from '$lib/components/ui/switch/index.js'; + import { Input } from '$lib/components/ui/input/index.js'; + import { Label } from '$lib/components/ui/label/index.js'; import * as Table from '$lib/components/ui/table/index.js'; import * as Tabs from '$lib/components/ui/tabs/index.js'; + import * as Dialog from '$lib/components/ui/dialog/index.js'; import EditIcon from '@lucide/svelte/icons/edit'; import SaveIcon from '@lucide/svelte/icons/save'; import XIcon from '@lucide/svelte/icons/x'; @@ -11,6 +14,7 @@ import HeartIcon from '@lucide/svelte/icons/heart'; import UserIcon from '@lucide/svelte/icons/user'; import ClockIcon from '@lucide/svelte/icons/clock'; + import PlusIcon from '@lucide/svelte/icons/plus'; import { toast } from 'svelte-sonner'; import { setBreadcrumb } from '$lib/stores/breadcrumb'; import { goto } from '$app/navigation'; @@ -22,6 +26,16 @@ let loading = false; let activeTab = 'favorites'; + // 添加对话框状态 + let showAddDialog = false; + let addDialogType: 'favorites' | 'collections' | 'submissions' = 'favorites'; + let adding = false; + + // 表单数据 + let favoriteForm = { fid: '', path: '' }; + let collectionForm = { sid: '', mid: '', collection_type: '2', path: '' }; // 默认为合集 + let submissionForm = { upper_id: '', path: '' }; + type ExtendedVideoSource = VideoSourceDetail & { type: string; originalIndex: number; @@ -31,10 +45,10 @@ }; const TAB_CONFIG = { - favorites: { label: '收藏夹', icon: HeartIcon, color: 'bg-red-500' }, - collections: { label: '合集 / 列表', icon: FolderIcon, color: 'bg-blue-500' }, - submissions: { label: '用户投稿', icon: UserIcon, color: 'bg-green-500' }, - watch_later: { label: '稍后再看', icon: ClockIcon, color: 'bg-yellow-500' } + favorites: { label: '收藏夹', icon: HeartIcon }, + collections: { label: '合集 / 列表', icon: FolderIcon }, + submissions: { label: '用户投稿', icon: UserIcon }, + watch_later: { label: '稍后再看', icon: ClockIcon } } as const; // 数据加载 @@ -123,6 +137,67 @@ }); } + // 打开添加对话框 + function openAddDialog(type: 'favorites' | 'collections' | 'submissions') { + addDialogType = type; + // 重置表单 + favoriteForm = { fid: '', path: '' }; + collectionForm = { sid: '', mid: '', collection_type: '2', path: '' }; + submissionForm = { upper_id: '', path: '' }; + showAddDialog = true; + } + + // 处理添加 + async function handleAdd() { + adding = true; + try { + switch (addDialogType) { + case 'favorites': + if (!favoriteForm.fid || !favoriteForm.path.trim()) { + toast.error('请填写完整的收藏夹信息'); + return; + } + await api.insertFavorite({ + fid: parseInt(favoriteForm.fid), + path: favoriteForm.path + }); + break; + case 'collections': + if (!collectionForm.sid || !collectionForm.mid || !collectionForm.path.trim()) { + toast.error('请填写完整的合集信息'); + return; + } + await api.insertCollection({ + sid: parseInt(collectionForm.sid), + mid: parseInt(collectionForm.mid), + collection_type: parseInt(collectionForm.collection_type), + path: collectionForm.path + }); + break; + case 'submissions': + if (!submissionForm.upper_id || !submissionForm.path.trim()) { + toast.error('请填写完整的用户投稿信息'); + return; + } + await api.insertSubmission({ + upper_id: parseInt(submissionForm.upper_id), + path: submissionForm.path + }); + break; + } + + toast.success('添加成功'); + showAddDialog = false; + loadVideoSources(); // 重新加载数据 + } catch (error) { + toast.error('添加失败', { + description: (error as ApiError).message + }); + } finally { + adding = false; + } + } + // 初始化 onMount(() => { setBreadcrumb([ @@ -142,36 +217,33 @@ 视频源管理 - Bili Sync -
+
{#if loading}
加载中...
{:else if videoSourcesData} - + {#each Object.entries(TAB_CONFIG) as [key, config] (key)} {@const sources = getSourcesForTab(key)} - -
- -
- - {sources.length} + + {config.label}({sources.length}) {/each}
{#each Object.entries(TAB_CONFIG) as [key, config] (key)} {@const sources = getSourcesForTab(key)} +
+

{config.label}管理

+ {#if key === 'favorites' || key === 'collections' || key === 'submissions'} + + {/if} +
{#if sources.length > 0}
@@ -259,15 +331,25 @@
{:else}
-
- -
-
暂无{config.label}
-

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

暂无{config.label}
+

+ {#if key === 'favorites'} + 还没有添加任何收藏夹订阅 + {:else if key === 'collections'} + 还没有添加任何合集或列表订阅 + {:else if key === 'submissions'} + 还没有添加任何用户投稿订阅 + {:else} + 还没有添加稍后再看订阅 + {/if}

+ {#if key === 'favorites' || key === 'collections' || key === 'submissions'} + + {/if}
{/if}
@@ -280,4 +362,134 @@
{/if} + + + + + + {#if addDialogType === 'favorites'} + 添加收藏夹 + {:else if addDialogType === 'collections'} + 添加合集 + {:else} + 添加用户投稿 + {/if} + +
+ {#if addDialogType === 'favorites'} +
+
+ + +
+
+ {:else if addDialogType === 'collections'} +
+
+ + +
+
+
+ + +
+
+ + +
+
+

可从合集/列表页面URL中获取相应ID

+
+ {:else} +
+
+ + +
+
+ {/if} +
+ + {#if addDialogType === 'favorites'} + + {:else if addDialogType === 'collections'} + + {:else} + + {/if} +
+
+
+ + +
+
+