From 1affe4d594a7c960f26ebd4c31067d5521380d34 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, 8 Jul 2025 12:48:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF=E6=8C=81=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=9F=A5=E7=9C=8B=E6=97=A5=E5=BF=97=20(#378)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 14 + Cargo.toml | 4 +- crates/bili_sync/src/api/mod.rs | 2 +- crates/bili_sync/src/api/routes/logs/mod.rs | 32 + crates/bili_sync/src/api/routes/logs/mpsc.rs | 53 ++ crates/bili_sync/src/api/routes/mod.rs | 4 + crates/bili_sync/src/bilibili/mod.rs | 2 +- crates/bili_sync/src/main.rs | 19 +- crates/bili_sync/src/task/http_server.rs | 11 +- crates/bili_sync/src/utils/mod.rs | 24 +- web/bun.lock | 65 +- web/package.json | 7 +- web/src/lib/api.ts | 24 +- web/src/lib/components/app-sidebar.svelte | 344 ++++------ web/src/lib/components/bread-crumb.svelte | 19 +- web/src/lib/components/dropdown-filter.svelte | 122 ++++ web/src/lib/components/filter-badge.svelte | 24 - web/src/lib/components/search-bar.svelte | 33 +- web/src/lib/components/status-editor.svelte | 82 ++- .../lib/components/subscription-card.svelte | 141 ++-- .../ui/collapsible/collapsible.svelte | 2 +- .../lib/components/ui/collapsible/index.ts | 8 +- .../ui/command/command-dialog.svelte | 40 ++ .../ui/command/command-empty.svelte | 17 + .../ui/command/command-group.svelte | 30 + .../ui/command/command-input.svelte | 26 + .../components/ui/command/command-item.svelte | 20 + .../ui/command/command-link-item.svelte | 20 + .../components/ui/command/command-list.svelte | 17 + .../ui/command/command-separator.svelte | 17 + .../ui/command/command-shortcut.svelte | 20 + .../lib/components/ui/command/command.svelte | 22 + web/src/lib/components/ui/command/index.ts | 40 ++ .../dropdown-menu-checkbox-item.svelte | 41 ++ .../dropdown-menu-content.svelte | 27 + .../dropdown-menu-group-heading.svelte | 22 + .../dropdown-menu/dropdown-menu-group.svelte | 7 + .../dropdown-menu/dropdown-menu-item.svelte | 27 + .../dropdown-menu/dropdown-menu-label.svelte | 24 + .../dropdown-menu-radio-group.svelte | 16 + .../dropdown-menu-radio-item.svelte | 31 + .../dropdown-menu-separator.svelte | 17 + .../dropdown-menu-shortcut.svelte | 20 + .../dropdown-menu-sub-content.svelte | 20 + .../dropdown-menu-sub-trigger.svelte | 29 + .../dropdown-menu-trigger.svelte | 7 + .../lib/components/ui/dropdown-menu/index.ts | 49 ++ web/src/lib/components/ui/input/input.svelte | 2 +- .../components/ui/separator/separator.svelte | 2 +- .../components/ui/sheet/sheet-overlay.svelte | 2 - .../components/ui/sidebar/sidebar-rail.svelte | 2 +- .../ui/sidebar/sidebar-separator.svelte | 2 +- .../lib/components/ui/sidebar/sidebar.svelte | 2 +- .../ui/tooltip/tooltip-content.svelte | 4 +- web/src/lib/components/video-card.svelte | 58 +- web/src/lib/hooks/is-mobile.svelte.ts | 6 +- web/src/lib/stores.ts | 0 web/src/lib/stores/breadcrumb.ts | 2 - web/src/lib/stores/filter.ts | 2 +- web/src/lib/stores/video-source.ts | 13 - web/src/routes/+layout.svelte | 84 +-- web/src/routes/+page.svelte | 628 +++++++++++------- web/src/routes/dashboard/+page.svelte | 425 ------------ web/src/routes/logs/+page.svelte | 96 +++ web/src/routes/me/collections/+page.svelte | 13 +- web/src/routes/me/favorites/+page.svelte | 16 +- web/src/routes/me/uppers/+page.svelte | 17 +- web/src/routes/settings/+page.svelte | 14 +- web/src/routes/video-sources/+page.svelte | 14 +- web/src/routes/video/[id]/+page.svelte | 10 +- web/src/routes/videos/+page.svelte | 294 ++++++++ 71 files changed, 2049 insertions(+), 1301 deletions(-) create mode 100644 crates/bili_sync/src/api/routes/logs/mod.rs create mode 100644 crates/bili_sync/src/api/routes/logs/mpsc.rs create mode 100644 web/src/lib/components/dropdown-filter.svelte delete mode 100644 web/src/lib/components/filter-badge.svelte create mode 100644 web/src/lib/components/ui/command/command-dialog.svelte create mode 100644 web/src/lib/components/ui/command/command-empty.svelte create mode 100644 web/src/lib/components/ui/command/command-group.svelte create mode 100644 web/src/lib/components/ui/command/command-input.svelte create mode 100644 web/src/lib/components/ui/command/command-item.svelte create mode 100644 web/src/lib/components/ui/command/command-link-item.svelte create mode 100644 web/src/lib/components/ui/command/command-list.svelte create mode 100644 web/src/lib/components/ui/command/command-separator.svelte create mode 100644 web/src/lib/components/ui/command/command-shortcut.svelte create mode 100644 web/src/lib/components/ui/command/command.svelte create mode 100644 web/src/lib/components/ui/command/index.ts create mode 100644 web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte create mode 100644 web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte create mode 100644 web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte create mode 100644 web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte create mode 100644 web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte create mode 100644 web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte create mode 100644 web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte create mode 100644 web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte create mode 100644 web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte create mode 100644 web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte create mode 100644 web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte create mode 100644 web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte create mode 100644 web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte create mode 100644 web/src/lib/components/ui/dropdown-menu/index.ts delete mode 100644 web/src/lib/stores.ts delete mode 100644 web/src/lib/stores/video-source.ts delete mode 100644 web/src/routes/dashboard/+page.svelte create mode 100644 web/src/routes/logs/+page.svelte create mode 100644 web/src/routes/videos/+page.svelte diff --git a/Cargo.lock b/Cargo.lock index 0b4bc38..5f53458 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3811,6 +3811,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -3952,6 +3953,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.19" @@ -3963,12 +3974,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8867f0f..52ca798 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,12 +67,12 @@ strum = { version = "0.27.1", features = ["derive"] } sysinfo = "0.35.2" thiserror = "2.0.12" tokio = { version = "1.45.0", features = ["full"] } -tokio-stream = "0.1.17" +tokio-stream = { version = "0.1.17", features = ["sync"] } tokio-util = { version = "0.7.15", features = ["io", "rt"] } toml = "0.8.22" tower = "0.5.2" tracing = "0.1.41" -tracing-subscriber = { version = "0.3.19", features = ["chrono"] } +tracing-subscriber = { version = "0.3.19", features = ["chrono", "json"] } url = "2.5.4" validator = { version = "0.20.0", features = ["derive"] } diff --git a/crates/bili_sync/src/api/mod.rs b/crates/bili_sync/src/api/mod.rs index f847b83..3415731 100644 --- a/crates/bili_sync/src/api/mod.rs +++ b/crates/bili_sync/src/api/mod.rs @@ -5,4 +5,4 @@ mod response; mod routes; mod wrapper; -pub use routes::router; +pub use routes::{MpscWriter, router}; diff --git a/crates/bili_sync/src/api/routes/logs/mod.rs b/crates/bili_sync/src/api/routes/logs/mod.rs new file mode 100644 index 0000000..fd5e708 --- /dev/null +++ b/crates/bili_sync/src/api/routes/logs/mod.rs @@ -0,0 +1,32 @@ +mod mpsc; +use std::convert::Infallible; +use std::time::Duration; + +use axum::response::sse::{Event, KeepAlive, Sse}; +use axum::routing::get; +use axum::{Extension, Router}; +use futures::{Stream, StreamExt}; +pub use mpsc::MpscWriter; +use tokio_stream::wrappers::BroadcastStream; + +pub(super) fn router() -> Router { + Router::new().route("/logs", get(logs)) +} + +async fn logs(Extension(log_writer): Extension) -> Sse>> { + let history = log_writer.log_history.lock(); + let rx = log_writer.sender.subscribe(); + let history_logs: Vec = history.iter().cloned().collect(); + drop(history); + + let history_stream = { futures::stream::iter(history_logs.into_iter().map(|msg| Ok(Event::default().data(msg)))) }; + + let stream = BroadcastStream::new(rx).filter_map(async |msg| match msg { + Ok(log_message) => Some(Ok(Event::default().data(log_message))), + Err(e) => { + error!("Broadcast stream error: {:?}", e); + None + } + }); + Sse::new(history_stream.chain(stream)).keep_alive(KeepAlive::new().interval(Duration::from_secs(10))) +} diff --git a/crates/bili_sync/src/api/routes/logs/mpsc.rs b/crates/bili_sync/src/api/routes/logs/mpsc.rs new file mode 100644 index 0000000..36da5a1 --- /dev/null +++ b/crates/bili_sync/src/api/routes/logs/mpsc.rs @@ -0,0 +1,53 @@ +use std::collections::VecDeque; +use std::sync::Arc; + +use parking_lot::Mutex; +use tokio::sync::broadcast; +use tracing_subscriber::fmt::MakeWriter; + +const MAX_HISTORY_LOGS: usize = 20; + +pub struct MpscWriter { + pub sender: broadcast::Sender, + pub log_history: Arc>>, +} + +impl MpscWriter { + pub fn new(sender: broadcast::Sender, log_history: Arc>>) -> Self { + MpscWriter { sender, log_history } + } +} + +impl<'a> MakeWriter<'a> for MpscWriter { + type Writer = Self; + + fn make_writer(&'a self) -> Self::Writer { + self.clone() + } +} + +impl std::io::Write for MpscWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let log_message = String::from_utf8_lossy(buf).to_string(); + let _ = self.sender.send(log_message.clone()); + let mut history = self.log_history.lock(); + history.push_back(log_message); + if history.len() > MAX_HISTORY_LOGS { + history.pop_front(); + } + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl Clone for MpscWriter { + fn clone(&self) -> Self { + MpscWriter { + sender: self.sender.clone(), + log_history: self.log_history.clone(), + } + } +} diff --git a/crates/bili_sync/src/api/routes/mod.rs b/crates/bili_sync/src/api/routes/mod.rs index 65ca971..05f6877 100644 --- a/crates/bili_sync/src/api/routes/mod.rs +++ b/crates/bili_sync/src/api/routes/mod.rs @@ -18,10 +18,13 @@ use crate::config::VersionedConfig; mod config; mod dashboard; +mod logs; mod me; mod video_sources; mod videos; +pub use logs::MpscWriter; + pub fn router() -> Router { Router::new().route("/image-proxy", get(image_proxy)).nest( "/api", @@ -30,6 +33,7 @@ pub fn router() -> Router { .merge(video_sources::router()) .merge(videos::router()) .merge(dashboard::router()) + .merge(logs::router()) .layer(middleware::from_fn(auth)), ) } diff --git a/crates/bili_sync/src/bilibili/mod.rs b/crates/bili_sync/src/bilibili/mod.rs index de0c4d8..1609cbf 100644 --- a/crates/bili_sync/src/bilibili/mod.rs +++ b/crates/bili_sync/src/bilibili/mod.rs @@ -147,7 +147,7 @@ mod tests { #[ignore = "only for manual test"] #[tokio::test] async fn test_video_info_type() { - init_logger("None,bili_sync=debug"); + init_logger("None,bili_sync=debug", None); let bili_client = BiliClient::new(); // 请求 UP 主视频必须要获取 mixin key,使用 key 计算请求参数的签名,否则直接提示权限不足返回空 let Ok(Some(mixin_key)) = bili_client.wbi_img().await.map(|wbi_img| wbi_img.into()) else { diff --git a/crates/bili_sync/src/main.rs b/crates/bili_sync/src/main.rs index 8bccb1f..960cbfe 100644 --- a/crates/bili_sync/src/main.rs +++ b/crates/bili_sync/src/main.rs @@ -12,16 +12,19 @@ mod task; mod utils; mod workflow; +use std::collections::VecDeque; use std::fmt::Debug; use std::future::Future; use std::sync::Arc; use bilibili::BiliClient; +use parking_lot::Mutex; use sea_orm::DatabaseConnection; use task::{http_server, video_downloader}; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; +use crate::api::MpscWriter; use crate::config::{ARGS, VersionedConfig}; use crate::database::setup_database; use crate::utils::init_logger; @@ -29,7 +32,7 @@ use crate::utils::signal::terminate; #[tokio::main] async fn main() { - let connection = init().await; + let (connection, log_writer) = init().await; let bili_client = Arc::new(BiliClient::new()); let token = CancellationToken::new(); @@ -37,7 +40,7 @@ async fn main() { spawn_task( "HTTP 服务", - http_server(connection.clone(), bili_client.clone()), + http_server(connection.clone(), bili_client.clone(), log_writer), &tracker, token.clone(), ); @@ -73,9 +76,13 @@ fn spawn_task( }); } -/// 初始化日志系统、打印欢迎信息,初始化数据库连接和全局配置,最终返回数据库连接 -async fn init() -> Arc { - init_logger(&ARGS.log_level); +/// 初始化日志系统、打印欢迎信息,初始化数据库连接和全局配置 +async fn init() -> (Arc, MpscWriter) { + let (tx, _rx) = tokio::sync::broadcast::channel(30); + let log_history = Arc::new(Mutex::new(VecDeque::with_capacity(20))); + let log_writer = MpscWriter::new(tx, log_history.clone()); + + init_logger(&ARGS.log_level, Some(log_writer.clone())); info!("欢迎使用 Bili-Sync,当前程序版本:{}", config::version()); info!("项目地址:https://github.com/amtoaer/bili-sync"); let connection = Arc::new(setup_database().await.expect("数据库初始化失败")); @@ -83,7 +90,7 @@ async fn init() -> Arc { VersionedConfig::init(&connection).await.expect("配置初始化失败"); info!("配置初始化完成"); - connection + (connection, log_writer) } async fn handle_shutdown(tracker: TaskTracker, token: CancellationToken) { diff --git a/crates/bili_sync/src/task/http_server.rs b/crates/bili_sync/src/task/http_server.rs index a1ed144..efe231a 100644 --- a/crates/bili_sync/src/task/http_server.rs +++ b/crates/bili_sync/src/task/http_server.rs @@ -10,7 +10,7 @@ use reqwest::StatusCode; use rust_embed_for_web::{EmbedableFile, RustEmbed}; use sea_orm::DatabaseConnection; -use crate::api::router; +use crate::api::{MpscWriter, router}; use crate::bilibili::BiliClient; use crate::config::VersionedConfig; @@ -20,11 +20,16 @@ use crate::config::VersionedConfig; #[folder = "../../web/build"] struct Asset; -pub async fn http_server(database_connection: Arc, bili_client: Arc) -> Result<()> { +pub async fn http_server( + database_connection: Arc, + bili_client: Arc, + log_writer: MpscWriter, +) -> Result<()> { let app = router() .fallback_service(get(frontend_files)) .layer(Extension(database_connection)) - .layer(Extension(bili_client)); + .layer(Extension(bili_client)) + .layer(Extension(log_writer)); let config = VersionedConfig::get().load_full(); let listener = tokio::net::TcpListener::bind(&config.bind_address) .await diff --git a/crates/bili_sync/src/utils/mod.rs b/crates/bili_sync/src/utils/mod.rs index c4dd23e..58b8dbc 100644 --- a/crates/bili_sync/src/utils/mod.rs +++ b/crates/bili_sync/src/utils/mod.rs @@ -6,17 +6,35 @@ pub mod nfo; pub mod signal; pub mod status; pub mod validation; +use tracing_subscriber::fmt; +use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -pub fn init_logger(log_level: &str) { - tracing_subscriber::fmt::Subscriber::builder() +use crate::api::MpscWriter; + +pub fn init_logger(log_level: &str, log_writer: Option) { + let log = tracing_subscriber::fmt::Subscriber::builder() .compact() .with_env_filter(tracing_subscriber::EnvFilter::builder().parse_lossy(log_level)) .with_target(false) .with_timer(tracing_subscriber::fmt::time::ChronoLocal::new( "%b %d %H:%M:%S".to_owned(), )) - .finish() + .finish(); + if let Some(writer) = log_writer { + log.with( + fmt::layer() + .with_ansi(false) + .with_timer(tracing_subscriber::fmt::time::ChronoLocal::new( + "%b %d %H:%M:%S".to_owned(), + )) + .json() + .flatten_event(true) + .with_writer(writer), + ) .try_init() .expect("初始化日志失败"); + } else { + log.try_init().expect("初始化日志失败"); + } } diff --git a/web/bun.lock b/web/bun.lock index aa3438c..364fbbf 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -3,13 +3,16 @@ "workspaces": { "": { "name": "my-app", + "dependencies": { + "@tanstack/svelte-query": "^5.81.5", + }, "devDependencies": { "@eslint/compat": "^1.2.5", "@eslint/js": "^9.18.0", "@internationalized/date": "^3.8.1", "@lucide/svelte": "^0.525.0", "@sveltejs/adapter-static": "^3.0.8", - "@sveltejs/kit": "^2.16.0", + "@sveltejs/kit": "2.22.2", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", @@ -47,55 +50,57 @@ "@dagrejs/graphlib": ["@dagrejs/graphlib@2.2.4", "", {}, "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.6", "", { "os": "aix", "cpu": "ppc64" }, "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.6", "", { "os": "android", "cpu": "arm" }, "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.6", "", { "os": "android", "cpu": "arm64" }, "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.6", "", { "os": "android", "cpu": "x64" }, "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.6", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.6", "", { "os": "linux", "cpu": "arm" }, "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.6", "", { "os": "linux", "cpu": "ia32" }, "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.6", "", { "os": "linux", "cpu": "x64" }, "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.6", "", { "os": "none", "cpu": "arm64" }, "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.6", "", { "os": "none", "cpu": "x64" }, "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.6", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.6", "", { "os": "openbsd", "cpu": "x64" }, "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.6", "", { "os": "none", "cpu": "arm64" }, "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.6", "", { "os": "sunos", "cpu": "x64" }, "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], @@ -207,7 +212,7 @@ "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.8", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg=="], - "@sveltejs/kit": ["@sveltejs/kit@2.21.1", "", { "dependencies": { "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.1.0", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0" }, "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-vLbtVwtDcK8LhJKnFkFYwM0uCdFmzioQnif0bjEYH1I24Arz22JPr/hLUiXGVYAwhu8INKx5qrdvr4tHgPwX6w=="], + "@sveltejs/kit": ["@sveltejs/kit@2.22.2", "", { "dependencies": { "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.1.0", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0", "vitefu": "^1.0.6" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-2MvEpSYabUrsJAoq5qCOBGAlkICjfjunrnLcx3YAk2XV7TvAIhomlKsAgR4H/4uns5rAfYmj7Wet5KRtc8dPIg=="], "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.0.3", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.0", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.15", "vitefu": "^1.0.4" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw=="], @@ -249,6 +254,10 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.8", "", { "dependencies": { "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "tailwindcss": "4.1.8" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A=="], + "@tanstack/query-core": ["@tanstack/query-core@5.81.5", "", {}, "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q=="], + + "@tanstack/svelte-query": ["@tanstack/svelte-query@5.81.5", "", { "dependencies": { "@tanstack/query-core": "5.81.5" }, "peerDependencies": { "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0" } }, "sha512-P2TaL+dGHWwQ83CyX8I9icb/1lYUSFwqQvGHI8jzFbacOEtVtQQXB0N12fotvn/BDn7Eh+tyCNiiMN56tFuFJw=="], + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], @@ -391,7 +400,7 @@ "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], - "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], + "esbuild": ["esbuild@0.25.6", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.6", "@esbuild/android-arm": "0.25.6", "@esbuild/android-arm64": "0.25.6", "@esbuild/android-x64": "0.25.6", "@esbuild/darwin-arm64": "0.25.6", "@esbuild/darwin-x64": "0.25.6", "@esbuild/freebsd-arm64": "0.25.6", "@esbuild/freebsd-x64": "0.25.6", "@esbuild/linux-arm": "0.25.6", "@esbuild/linux-arm64": "0.25.6", "@esbuild/linux-ia32": "0.25.6", "@esbuild/linux-loong64": "0.25.6", "@esbuild/linux-mips64el": "0.25.6", "@esbuild/linux-ppc64": "0.25.6", "@esbuild/linux-riscv64": "0.25.6", "@esbuild/linux-s390x": "0.25.6", "@esbuild/linux-x64": "0.25.6", "@esbuild/netbsd-arm64": "0.25.6", "@esbuild/netbsd-x64": "0.25.6", "@esbuild/openbsd-arm64": "0.25.6", "@esbuild/openbsd-x64": "0.25.6", "@esbuild/openharmony-arm64": "0.25.6", "@esbuild/sunos-x64": "0.25.6", "@esbuild/win32-arm64": "0.25.6", "@esbuild/win32-ia32": "0.25.6", "@esbuild/win32-x64": "0.25.6" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], diff --git a/web/package.json b/web/package.json index e362b69..d1436b1 100644 --- a/web/package.json +++ b/web/package.json @@ -7,7 +7,7 @@ "@internationalized/date": "^3.8.1", "@lucide/svelte": "^0.525.0", "@sveltejs/adapter-static": "^3.0.8", - "@sveltejs/kit": "^2.16.0", + "@sveltejs/kit": "2.22.2", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", @@ -47,5 +47,8 @@ "format": "prettier --write .", "lint": "prettier --check . && eslint ." }, - "type": "module" + "type": "module", + "dependencies": { + "@tanstack/svelte-query": "^5.81.5" + } } diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index d5e34ec..b000f32 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -223,14 +223,22 @@ class ApiClient { return this.get('/dashboard'); } - // 获取系统信息流(SSE) - async getSysInfoStream(): Promise { + createLogStream( + onMessage: (data: string) => void, + onError?: (error: Event) => void + ): EventSource { const token = localStorage.getItem('authToken'); - const url = `/api/dashboard/sysinfo${token ? `?token=${encodeURIComponent(token)}` : ''}`; - return new EventSource(url); + const url = `/api/logs${token ? `?token=${encodeURIComponent(token)}` : ''}`; + const eventSource = new EventSource(url); + eventSource.onmessage = (event) => { + onMessage(event.data); + }; + if (onError) { + eventSource.onerror = onError; + } + return eventSource; } - // 创建系统信息流的便捷方法 createSysInfoStream( onMessage: (data: SysInfoResponse) => void, onError?: (error: Event) => void @@ -238,7 +246,6 @@ class ApiClient { const token = localStorage.getItem('authToken'); const url = `/api/dashboard/sysinfo${token ? `?token=${encodeURIComponent(token)}` : ''}`; const eventSource = new EventSource(url); - eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data) as SysInfoResponse; @@ -247,11 +254,9 @@ class ApiClient { console.error('Failed to parse SSE data:', error); } }; - if (onError) { eventSource.onerror = onError; } - return eventSource; } } @@ -282,11 +287,12 @@ const api = { getConfig: () => apiClient.getConfig(), updateConfig: (config: Config) => apiClient.updateConfig(config), getDashboard: () => apiClient.getDashboard(), - getSysInfoStream: () => apiClient.getSysInfoStream(), createSysInfoStream: ( onMessage: (data: SysInfoResponse) => void, onError?: (error: Event) => void ) => apiClient.createSysInfoStream(onMessage, onError), + createLogStream: (onMessage: (data: string) => void, onError?: (error: Event) => void) => + apiClient.createLogStream(onMessage, onError), setAuthToken: (token: string) => apiClient.setAuthToken(token), clearAuthToken: () => apiClient.clearAuthToken() }; diff --git a/web/src/lib/components/app-sidebar.svelte b/web/src/lib/components/app-sidebar.svelte index b6da1bc..f32e8ee 100644 --- a/web/src/lib/components/app-sidebar.svelte +++ b/web/src/lib/components/app-sidebar.svelte @@ -1,226 +1,144 @@ - - - -
- Bili Sync -
-
- Bili Sync - 视频管理系统 -
-
+ + + + + + {#snippet child({ props })} + +
+ +
+
+ {data.header.title} + {data.header.subtitle} +
+
+ {/snippet} +
+
+
- -
- - - 视频筛选 - - - - {#each items as item (item.type)} - - - - {#snippet child({ props })} - -
- - {item.title} -
- -
- {/snippet} -
- -
- {#if $videoSourceStore} - {#if $videoSourceStore[item.type as keyof VideoSourcesResponse]?.length > 0} - {#each $videoSourceStore[item.type as keyof VideoSourcesResponse] as source (source.id)} - - - - {/each} - {:else} -
无数据
- {/if} - {:else} -
加载中...
- {/if} -
-
-
-
- {/each} -
-
-
- - - - 快捷订阅 - - - + + + {#each data.navMain as group (group.category)} + {group.category} + + {#each group.items as item (item.title)} - - { - if (sidebar.isMobile) { - sidebar.setOpenMobile(false); - } - }} - > - - 创建的收藏夹 - + + {#snippet child({ props })} + + + {item.title} + + {/snippet} - - - { - if (sidebar.isMobile) { - sidebar.setOpenMobile(false); - } - }} - > - - 关注的合集 - - - - - - { - if (sidebar.isMobile) { - sidebar.setOpenMobile(false); - } - }} - > - - 关注的 UP 主 - - - - - - -
- - - + {/each} + + {/each} +
+ + + + {#each data.footer as item (item.title)} + + + {#snippet child({ props })} + + + {item.title} + + {/snippet} + + + {/each} + +
diff --git a/web/src/lib/components/bread-crumb.svelte b/web/src/lib/components/bread-crumb.svelte index a84951b..f79a143 100644 --- a/web/src/lib/components/bread-crumb.svelte +++ b/web/src/lib/components/bread-crumb.svelte @@ -4,30 +4,21 @@ export let items: Array<{ href?: string; label: string; - isActive?: boolean; - onClick?: () => void; }> = [{ href: '/', label: '主页' }]; {#each items as item, index (item.label)} - - {#if item.isActive || (!item.href && !item.onClick)} - {item.label} - {:else if item.onClick} - - {:else} + {#if index < items.length - 1} - + diff --git a/web/src/lib/components/dropdown-filter.svelte b/web/src/lib/components/dropdown-filter.svelte new file mode 100644 index 0000000..80a7dd9 --- /dev/null +++ b/web/src/lib/components/dropdown-filter.svelte @@ -0,0 +1,122 @@ + + +
+ {#if filters} + + {#if selectedLabel && selectedLabel.type && selectedLabel.id} + 「{filters[selectedLabel.type]?.name || ''} : {filters[selectedLabel.type]!.values[ + selectedLabel.id + ] || ''}」 + {:else} + 未应用 + {/if} + + {/if} + + + + {#snippet child({ props })} + + {/snippet} + + + + {#if filters} + {#each Object.entries(filters) as [key, filter] (key)} + + + + + {filter.name} + + + + + + + 未找到"{filter.name.toLowerCase()}" + + {#each Object.entries(filter.values) as [id, name] (id)} + { + closeAndFocusTrigger(); + onSelect?.(key, id); + }} + > + {name} + + {/each} + + + + + + {/each} + {/if} + + { + closeAndFocusTrigger(); + onRemove?.(); + }} + > + + 移除筛选 + + + + +
diff --git a/web/src/lib/components/filter-badge.svelte b/web/src/lib/components/filter-badge.svelte deleted file mode 100644 index baab56c..0000000 --- a/web/src/lib/components/filter-badge.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -{#if filterTitle && filterName} -
- 当前筛选: - - {filterTitle}: {filterName} - - -
-{/if} diff --git a/web/src/lib/components/search-bar.svelte b/web/src/lib/components/search-bar.svelte index b16ed2f..0b52ba4 100644 --- a/web/src/lib/components/search-bar.svelte +++ b/web/src/lib/components/search-bar.svelte @@ -1,31 +1,28 @@ -
-
- - +
+
+ + { + if (e.key === 'Enter') { + handleSearch(); + } + }} + />
-
diff --git a/web/src/lib/components/status-editor.svelte b/web/src/lib/components/status-editor.svelte index f6c1d36..f20e846 100644 --- a/web/src/lib/components/status-editor.svelte +++ b/web/src/lib/components/status-editor.svelte @@ -39,21 +39,16 @@ pageStatuses = { ...pageStatuses }; } - // 编辑状态 let videoStatuses: number[] = []; let pageStatuses: Record = {}; - // 原始状态备份 let originalVideoStatuses: number[] = []; let originalPageStatuses: Record = {}; - // 响应式更新状态 - 当 video 或 pages props 变化时重新初始化 $: { - // 初始化视频状态 videoStatuses = [...video.download_status]; originalVideoStatuses = [...video.download_status]; - // 初始化分页状态 if (pages.length > 0) { pageStatuses = pages.reduce( (acc, page) => { @@ -77,20 +72,28 @@ function handleVideoStatusChange(taskIndex: number, newValue: number) { videoStatuses[taskIndex] = newValue; - videoStatuses = [...videoStatuses]; } function handlePageStatusChange(pageId: number, taskIndex: number, newValue: number) { if (!pageStatuses[pageId]) { - pageStatuses[pageId] = []; + return; } pageStatuses[pageId][taskIndex] = newValue; - pageStatuses = { ...pageStatuses }; } function resetAllStatuses() { videoStatuses = [...originalVideoStatuses]; - pageStatuses = { ...originalPageStatuses }; + if (pages.length > 0) { + pageStatuses = pages.reduce( + (acc, page) => { + acc[page.id] = [...page.download_status]; + return acc; + }, + {} as Record + ); + } else { + pageStatuses = {}; + } } function hasVideoChanges(): boolean { @@ -112,44 +115,37 @@ function buildRequest(): UpdateVideoStatusRequest { const request: UpdateVideoStatusRequest = {}; - // 构建视频状态更新 - if (hasVideoChanges()) { - request.video_updates = []; - videoStatuses.forEach((status, index) => { - if (status !== originalVideoStatuses[index]) { - request.video_updates!.push({ + request.video_updates = []; + videoStatuses.forEach((status, index) => { + if (status !== originalVideoStatuses[index]) { + request.video_updates!.push({ + status_index: index, + status_value: status + }); + } + }); + request.page_updates = []; + pages.forEach((page) => { + const currentStatuses = pageStatuses[page.id] || []; + const originalStatuses = originalPageStatuses[page.id] || []; + const updates: StatusUpdate[] = []; + + currentStatuses.forEach((status, index) => { + if (status !== originalStatuses[index]) { + updates.push({ status_index: index, status_value: status }); } }); - } - // 构建分页状态更新 - if (hasPageChanges()) { - request.page_updates = []; - pages.forEach((page) => { - const currentStatuses = pageStatuses[page.id] || []; - const originalStatuses = originalPageStatuses[page.id] || []; - const updates: StatusUpdate[] = []; - - currentStatuses.forEach((status, index) => { - if (status !== originalStatuses[index]) { - updates.push({ - status_index: index, - status_value: status - }); - } + if (updates.length > 0) { + request.page_updates!.push({ + page_id: page.id, + updates }); - - if (updates.length > 0) { - request.page_updates!.push({ - page_id: page.id, - updates - }); - } - }); - } + } + }); return request; } @@ -159,8 +155,11 @@ toast.info('没有状态变更需要提交'); return; } - const request = buildRequest(); + if (!request.video_updates?.length && !request.page_updates?.length) { + toast.info('没有状态变更需要提交'); + return; + } onsubmit(request); } @@ -179,7 +178,6 @@
-

视频状态

diff --git a/web/src/lib/components/subscription-card.svelte b/web/src/lib/components/subscription-card.svelte index adc3805..924c68b 100644 --- a/web/src/lib/components/subscription-card.svelte +++ b/web/src/lib/components/subscription-card.svelte @@ -67,11 +67,11 @@ function getSubtitle(): string { switch (type) { case 'favorite': - return `UP主ID: ${(item as FavoriteWithSubscriptionStatus).mid}`; + return `uid: ${(item as FavoriteWithSubscriptionStatus).mid}`; case 'collection': - return `UP主ID: ${(item as CollectionWithSubscriptionStatus).mid}`; + return `uid: ${(item as CollectionWithSubscriptionStatus).mid}`; case 'upper': - return ''; // UP主不需要副标题 + return ''; default: return ''; } @@ -158,96 +158,105 @@ const disabledReason = getDisabledReason(); - - -
-
- -
- {#if avatarUrl && type === 'upper'} - {title} - {:else} - - {/if} -
+ + +
+ +
+ {#if avatarUrl && type === 'upper'} + {title} + {:else} + + {/if} +
- -
+ +
+
{title} - {#if subtitle} -
- - {subtitle} -
- {/if} - {#if description} -

- {description} -

+ + + {#if disabled} + 不可用 + {:else} + + {subscribed ? '已订阅' : typeLabel} + {/if}
-
- -
- {#if disabled} - 不可用 -
- {disabledReason} + + {#if subtitle && !disabled} +
+ + {subtitle} +
+ {/if} + + {#if description && !disabled} +

+ {description} +

+ {/if} + + + {#if count !== null && !disabled} +
+ {count} + {countLabel}
- {:else} - - {subscribed ? '已订阅' : typeLabel} - - {#if count !== null} -
- {count} - {countLabel} -
- {/if} {/if}
- + +
{#if disabled} - {:else if subscribed} - {:else} {/if}
@@ -256,3 +265,5 @@ + + diff --git a/web/src/lib/components/ui/collapsible/collapsible.svelte b/web/src/lib/components/ui/collapsible/collapsible.svelte index 9588b32..7a8c5da 100644 --- a/web/src/lib/components/ui/collapsible/collapsible.svelte +++ b/web/src/lib/components/ui/collapsible/collapsible.svelte @@ -8,4 +8,4 @@ }: CollapsiblePrimitive.RootProps = $props(); - + diff --git a/web/src/lib/components/ui/collapsible/index.ts b/web/src/lib/components/ui/collapsible/index.ts index 7455459..8181f64 100644 --- a/web/src/lib/components/ui/collapsible/index.ts +++ b/web/src/lib/components/ui/collapsible/index.ts @@ -1,8 +1,6 @@ -import { Collapsible as CollapsiblePrimitive } from 'bits-ui'; - -const Root = CollapsiblePrimitive.Root; -const Trigger = CollapsiblePrimitive.Trigger; -const Content = CollapsiblePrimitive.Content; +import Root from './collapsible.svelte'; +import Trigger from './collapsible-trigger.svelte'; +import Content from './collapsible-content.svelte'; export { Root, diff --git a/web/src/lib/components/ui/command/command-dialog.svelte b/web/src/lib/components/ui/command/command-dialog.svelte new file mode 100644 index 0000000..c4d3587 --- /dev/null +++ b/web/src/lib/components/ui/command/command-dialog.svelte @@ -0,0 +1,40 @@ + + + + + {title} + {description} + + + + + diff --git a/web/src/lib/components/ui/command/command-empty.svelte b/web/src/lib/components/ui/command/command-empty.svelte new file mode 100644 index 0000000..c2ce165 --- /dev/null +++ b/web/src/lib/components/ui/command/command-empty.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/command/command-group.svelte b/web/src/lib/components/ui/command/command-group.svelte new file mode 100644 index 0000000..4097222 --- /dev/null +++ b/web/src/lib/components/ui/command/command-group.svelte @@ -0,0 +1,30 @@ + + + + {#if heading} + + {heading} + + {/if} + + diff --git a/web/src/lib/components/ui/command/command-input.svelte b/web/src/lib/components/ui/command/command-input.svelte new file mode 100644 index 0000000..3acb041 --- /dev/null +++ b/web/src/lib/components/ui/command/command-input.svelte @@ -0,0 +1,26 @@ + + +
+ + +
diff --git a/web/src/lib/components/ui/command/command-item.svelte b/web/src/lib/components/ui/command/command-item.svelte new file mode 100644 index 0000000..063adb7 --- /dev/null +++ b/web/src/lib/components/ui/command/command-item.svelte @@ -0,0 +1,20 @@ + + + diff --git a/web/src/lib/components/ui/command/command-link-item.svelte b/web/src/lib/components/ui/command/command-link-item.svelte new file mode 100644 index 0000000..4edde50 --- /dev/null +++ b/web/src/lib/components/ui/command/command-link-item.svelte @@ -0,0 +1,20 @@ + + + diff --git a/web/src/lib/components/ui/command/command-list.svelte b/web/src/lib/components/ui/command/command-list.svelte new file mode 100644 index 0000000..8ff87f0 --- /dev/null +++ b/web/src/lib/components/ui/command/command-list.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/command/command-separator.svelte b/web/src/lib/components/ui/command/command-separator.svelte new file mode 100644 index 0000000..d75e794 --- /dev/null +++ b/web/src/lib/components/ui/command/command-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/command/command-shortcut.svelte b/web/src/lib/components/ui/command/command-shortcut.svelte new file mode 100644 index 0000000..0c47933 --- /dev/null +++ b/web/src/lib/components/ui/command/command-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/command/command.svelte b/web/src/lib/components/ui/command/command.svelte new file mode 100644 index 0000000..adff6e0 --- /dev/null +++ b/web/src/lib/components/ui/command/command.svelte @@ -0,0 +1,22 @@ + + + diff --git a/web/src/lib/components/ui/command/index.ts b/web/src/lib/components/ui/command/index.ts new file mode 100644 index 0000000..dee2364 --- /dev/null +++ b/web/src/lib/components/ui/command/index.ts @@ -0,0 +1,40 @@ +import { Command as CommandPrimitive } from 'bits-ui'; + +import Root from './command.svelte'; +import Dialog from './command-dialog.svelte'; +import Empty from './command-empty.svelte'; +import Group from './command-group.svelte'; +import Item from './command-item.svelte'; +import Input from './command-input.svelte'; +import List from './command-list.svelte'; +import Separator from './command-separator.svelte'; +import Shortcut from './command-shortcut.svelte'; +import LinkItem from './command-link-item.svelte'; + +const Loading = CommandPrimitive.Loading; + +export { + Root, + Dialog, + Empty, + Group, + Item, + LinkItem, + Input, + List, + Separator, + Shortcut, + Loading, + // + Root as Command, + Dialog as CommandDialog, + Empty as CommandEmpty, + Group as CommandGroup, + Item as CommandItem, + LinkItem as CommandLinkItem, + Input as CommandInput, + List as CommandList, + Separator as CommandSeparator, + Shortcut as CommandShortcut, + Loading as CommandLoading +}; diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..e94c637 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,41 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..25223ef --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000..920848e --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 0000000..261ab7e --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..38bc45b --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..14e40f7 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..3e98749 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..3c6330a --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,31 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..38dabc1 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..dbe6e20 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..d3d71d3 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..e37ac1a --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 0000000..032b645 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/index.ts b/web/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..aeb398e --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,49 @@ +import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; +import CheckboxItem from './dropdown-menu-checkbox-item.svelte'; +import Content from './dropdown-menu-content.svelte'; +import Group from './dropdown-menu-group.svelte'; +import Item from './dropdown-menu-item.svelte'; +import Label from './dropdown-menu-label.svelte'; +import RadioGroup from './dropdown-menu-radio-group.svelte'; +import RadioItem from './dropdown-menu-radio-item.svelte'; +import Separator from './dropdown-menu-separator.svelte'; +import Shortcut from './dropdown-menu-shortcut.svelte'; +import Trigger from './dropdown-menu-trigger.svelte'; +import SubContent from './dropdown-menu-sub-content.svelte'; +import SubTrigger from './dropdown-menu-sub-trigger.svelte'; +import GroupHeading from './dropdown-menu-group-heading.svelte'; +const Sub = DropdownMenuPrimitive.Sub; +const Root = DropdownMenuPrimitive.Root; + +export { + CheckboxItem, + Content, + Root as DropdownMenu, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger +}; diff --git a/web/src/lib/components/ui/input/input.svelte b/web/src/lib/components/ui/input/input.svelte index 77a507a..7598a86 100644 --- a/web/src/lib/components/ui/input/input.svelte +++ b/web/src/lib/components/ui/input/input.svelte @@ -24,7 +24,7 @@ bind:this={ref} data-slot="input" class={cn( - 'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', + 'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', className diff --git a/web/src/lib/components/ui/separator/separator.svelte b/web/src/lib/components/ui/separator/separator.svelte index 5466f0e..6ceb6a5 100644 --- a/web/src/lib/components/ui/separator/separator.svelte +++ b/web/src/lib/components/ui/separator/separator.svelte @@ -11,7 +11,7 @@
diff --git a/web/src/lib/components/ui/tooltip/tooltip-content.svelte b/web/src/lib/components/ui/tooltip/tooltip-content.svelte index e6f2670..60d68f5 100644 --- a/web/src/lib/components/ui/tooltip/tooltip-content.svelte +++ b/web/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -34,9 +34,9 @@ class={cn( 'bg-primary z-50 size-2.5 rotate-45 rounded-[2px]', 'data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%_+_2px)]', - 'data-[side=bottom]:translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%_+_1px)]', + 'data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%_+_1px)]', 'data-[side=right]:translate-x-[calc(50%_+_2px)] data-[side=right]:translate-y-1/2', - 'data-[side=left]:translate-y-[calc(50%_-_3px)]', + 'data-[side=left]:-translate-y-[calc(50%_-_3px)]', arrowClasses )} {...props} diff --git a/web/src/lib/components/video-card.svelte b/web/src/lib/components/video-card.svelte index f5b6a3c..3ac2db6 100644 --- a/web/src/lib/components/video-card.svelte +++ b/web/src/lib/components/video-card.svelte @@ -17,8 +17,6 @@ export let customSubtitle: string = ''; // 自定义副标题 export let taskNames: string[] = []; // 自定义任务名称 export let showProgress: boolean = true; // 是否显示进度信息 - export let progressHeight: string = 'h-2'; // 进度条高度 - export let gap: string = 'gap-1'; // 进度条间距 export let onReset: (() => Promise) | null = null; // 自定义重置函数 export let resetDialogOpen = false; // 导出对话框状态,让父组件可以控制 export let resetting = false; @@ -35,11 +33,11 @@ function getSegmentColor(status: number): string { if (status === 7) { - return 'bg-green-500'; // 绿色 - 成功 + return 'bg-emerald-500'; // 恢复更高对比度的绿色 } else if (status === 0) { - return 'bg-yellow-500'; // 黄色 - 未开始 + return 'bg-slate-400'; // 恢复更清晰的灰色 - 未开始 } else { - return 'bg-red-500'; // 红色 - 失败 + return 'bg-rose-500'; // 恢复更清晰的红色 - 失败 } } @@ -52,9 +50,9 @@ const failed = downloadStatus.filter((status) => status !== 7 && status !== 0).length; if (completed === total) { - return { text: '全部完成', color: 'default' }; + return { text: '完成', color: 'outline' }; // 更简洁的文案 } else if (failed > 0) { - return { text: '部分失败', color: 'destructive' }; + return { text: '失败', color: 'destructive' }; } else { return { text: '进行中', color: 'secondary' }; } @@ -88,33 +86,30 @@ // 根据模式确定显示的标题和副标题 $: displayTitle = customTitle || video.name; $: displaySubtitle = customSubtitle || video.upper_name; - $: showUserIcon = mode === 'default'; $: cardClasses = mode === 'default' - ? 'group flex h-full min-w-0 flex-col transition-shadow hover:shadow-md' - : 'transition-shadow hover:shadow-md'; + ? 'group flex h-full min-w-0 flex-col transition-all hover:shadow-lg hover:shadow-primary/5 border-border/50' + : 'transition-all hover:shadow-lg border-border/50'; - -
+ +
{displayTitle} - + {overallStatus.text}
{#if displaySubtitle} -
- {#if showUserIcon} - - {/if} +
+ {displaySubtitle} @@ -122,34 +117,30 @@ {/if}
{#if showProgress}
-
+ +
下载进度 {completed}/{total}
- -
+
{#each video.download_status as status, index (index)}
-

{getTaskName(index)}: {getStatusText(status)}

+

{getTaskName(index)}: {getStatusText(status)}

{/each} @@ -157,13 +148,12 @@
{/if} - {#if showActions && mode === 'default'} -
+
+ {:else} +
+ + + 存储空间 + + + + {#if sysInfo} +
+
+
{formatBytes(sysInfo.available_disk)} 可用
+
+ 共 {formatBytes(sysInfo.total_disk)} +
+
+ +
+ 已使用 {diskUsagePercent.toFixed(1)}% 的存储空间 +
+
+ {:else} +
加载中...
+ {/if} +
+
+ + + 当前监听 + + + + {#if dashboardData} +
+
+
+ + 收藏夹 +
+ {dashboardData.enabled_favorites} +
+
+
+ + 合集 +
+ {dashboardData.enabled_collections} +
+
+
+ + 投稿 +
+ {dashboardData.enabled_submissions} +
+
+
+ + 稍后再看 +
+ + {dashboardData.enable_watch_later ? '启用' : '禁用'} + +
+
+ {:else} +
加载中...
+ {/if} +
+
-
-{/if} - -{#if loading} -
-
加载中...
-
-{:else if videosData?.videos.length} -
- {#each videosData.videos as video (video.id)} -
- { - await handleResetVideo(video.id); - }} - /> -
- {/each} -
- - - -{:else} -
-
-

暂无视频数据

-

尝试搜索或检查视频来源配置

+
+ + + 最近入库 + + + + {#if dashboardData && dashboardData.videos_by_day.length > 0} +
+
+ 近七日共新增视频 + {dashboardData.videos_by_day.reduce((sum, v) => sum + v.cnt, 0)} 个 +
+
+ + '' } + }} + > + {#snippet tooltip()} + + {/snippet} + + + {:else} +
+ 暂无视频统计数据 +
+ {/if}
+
-
-{/if} - - - - - 重置所有视频 - - 此操作将重置所有视频和分页的失败状态为未下载状态,使它们在下次下载任务中重新尝试。 -
- 此操作不可撤销,确定要继续吗? -
-
- - 取消 - - {#if resettingAll} - - 重置中... - {:else} - 确认重置 - {/if} - - -
-
+ +
+ + + + 内存使用情况 + + + + {#if sysInfo} +
+
+ 当前内存使用 + {formatBytes(sysInfo.used_memory)} / {formatBytes(sysInfo.total_memory)} +
+
+ {/if} + {#if memoryHistory.length > 0} + + '' + } + }} + > + {#snippet tooltip()} + { + return new Intl.DateTimeFormat('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: true + }).format(v); + }} + valueFormatter={(v: number) => formatBytes(v)} + indicator="line" + /> + {/snippet} + + + {:else} +
+ 等待数据... +
+ {/if} +
+
+ + + + CPU 使用情况 + + + + {#if sysInfo} +
+
+ 当前 CPU 使用率 + {formatCpu(sysInfo.used_cpu)} +
+
+ {/if} + {#if cpuHistory.length > 0} + + '' + } + }} + > + {#snippet tooltip()} + { + return new Intl.DateTimeFormat('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: true + }).format(v); + }} + valueFormatter={(v: number) => formatCpu(v)} + indicator="line" + /> + {/snippet} + + + {:else} +
+ 等待数据... +
+ {/if} +
+
+
+ {/if} +
diff --git a/web/src/routes/dashboard/+page.svelte b/web/src/routes/dashboard/+page.svelte deleted file mode 100644 index 0a9a73c..0000000 --- a/web/src/routes/dashboard/+page.svelte +++ /dev/null @@ -1,425 +0,0 @@ - - - - 仪表盘 - Bili Sync - - -
- {#if loading} -
-
加载中...
-
- {:else} -
- - - - 存储空间 - - - - {#if sysInfo} -
-
-
{formatBytes(sysInfo.available_disk)} 可用
-
- 共 {formatBytes(sysInfo.total_disk)} -
-
- -
- 已使用 {diskUsagePercent.toFixed(1)}% 的存储空间 -
-
- {:else} -
加载中...
- {/if} -
-
- - - 当前监听 - - - - {#if dashboardData} -
-
-
- - 收藏夹 -
- {dashboardData.enabled_favorites} -
-
-
- - 合集 -
- {dashboardData.enabled_collections} -
-
-
- - 投稿 -
- {dashboardData.enabled_submissions} -
-
-
- - 稍后再看 -
- - {dashboardData.enable_watch_later ? '启用' : '禁用'} - -
-
- {:else} -
加载中...
- {/if} -
-
-
- -
- - - 最近入库 - - - - {#if dashboardData && dashboardData.videos_by_day.length > 0} -
-
- 近七日共新增视频 - {dashboardData.videos_by_day.reduce((sum, v) => sum + v.cnt, 0)} 个 -
-
- - '' } - }} - > - {#snippet tooltip()} - - {/snippet} - - - {:else} -
- 暂无视频统计数据 -
- {/if}
-
-
- - -
- - - - 内存使用情况 - - - - {#if sysInfo} -
-
- 当前内存使用 - {formatBytes(sysInfo.used_memory)} / {formatBytes(sysInfo.total_memory)} -
-
- {/if} - {#if memoryHistory.length > 0} - - '' - } - }} - > - {#snippet tooltip()} - { - return new Intl.DateTimeFormat('en-US', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: true - }).format(v); - }} - valueFormatter={(v: number) => formatBytes(v)} - indicator="line" - /> - {/snippet} - - - {:else} -
- 等待数据... -
- {/if} -
-
- - - - - CPU 使用情况 - - - - {#if sysInfo} -
-
- 当前 CPU 使用率 - {formatCpu(sysInfo.used_cpu)} -
-
- {/if} - {#if cpuHistory.length > 0} - - '' - } - }} - > - {#snippet tooltip()} - { - return new Intl.DateTimeFormat('en-US', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: true - }).format(v); - }} - valueFormatter={(v: number) => formatCpu(v)} - indicator="line" - /> - {/snippet} - - - {:else} -
- 等待数据... -
- {/if} -
-
-
- {/if} -
diff --git a/web/src/routes/logs/+page.svelte b/web/src/routes/logs/+page.svelte new file mode 100644 index 0000000..03cfe22 --- /dev/null +++ b/web/src/routes/logs/+page.svelte @@ -0,0 +1,96 @@ + + + + 日志 - Bili Sync + + +
+ {#each logs as log, index (index)} +
+ + {log.timestamp} + + + {log.level} + + + {log.message} + +
+ {/each} + {#if logs.length === 0} +
暂无日志记录
+ {/if} +
diff --git a/web/src/routes/me/collections/+page.svelte b/web/src/routes/me/collections/+page.svelte index c5737a3..06f98b3 100644 --- a/web/src/routes/me/collections/+page.svelte +++ b/web/src/routes/me/collections/+page.svelte @@ -1,11 +1,9 @@ @@ -236,7 +226,7 @@ {@const sources = getSourcesForTab(key)}
-

{config.label}管理

+
{#if key === 'favorites' || key === 'collections' || key === 'submissions'} +
+
+{/if} + +{#if loading} +
+
加载中...
+
+{:else if videosData?.videos.length} +
+ {#each videosData.videos as video (video.id)} + { + await handleResetVideo(video.id); + }} + /> + {/each} +
+ + + +{:else} +
+
+

暂无视频数据

+

尝试搜索或检查视频来源配置

+
+
+{/if} + + + + + + 重置所有视频 + + 此操作将重置所有视频和分页的失败状态为未下载状态,使它们在下次下载任务中重新尝试。 +
+ 此操作不可撤销,确定要继续吗? +
+
+ + 取消 + + {#if resettingAll} + + 重置中... + {:else} + 确认重置 + {/if} + + +
+