From 97c5378a7184fa9c54f3353f74e7b3e3ca6495bb Mon Sep 17 00:00:00 2001 From: spinline Date: Sat, 31 Jan 2026 16:47:13 +0300 Subject: [PATCH] feat: Implement functional torrent tables and sidebar filters with real-time SSE updates --- frontend/public/tailwind.css | 117 ++++++++++++++++++++ frontend/src/app.rs | 2 + frontend/src/components/layout/sidebar.rs | 34 ++++-- frontend/src/components/torrent/table.rs | 125 +++++++++------------- frontend/src/lib.rs | 1 + frontend/src/store.rs | 79 ++++++++++++++ 6 files changed, 277 insertions(+), 81 deletions(-) create mode 100644 frontend/src/store.rs diff --git a/frontend/public/tailwind.css b/frontend/public/tailwind.css index d09f7d7..4f37113 100644 --- a/frontend/public/tailwind.css +++ b/frontend/public/tailwind.css @@ -837,6 +837,9 @@ .fixed { position: fixed; } + .static { + position: static; + } .inset-0 { inset: calc(var(--spacing) * 0); } @@ -846,6 +849,51 @@ .z-\[200\] { z-index: 200; } + .filter { + @layer daisyui.l1.l2.l3 { + display: flex; + flex-wrap: wrap; + input[type="radio"] { + width: auto; + } + input { + overflow: hidden; + opacity: 100%; + scale: 1; + transition: margin 0.1s, opacity 0.3s, padding 0.3s, border-width 0.1s; + &:not(:last-child) { + margin-inline-end: calc(0.25rem * 1); + } + &.filter-reset { + aspect-ratio: 1 / 1; + &::after { + --tw-content: "×"; + content: var(--tw-content); + } + } + } + &:not(:has(input:checked:not(.filter-reset))) { + .filter-reset, input[type="reset"] { + scale: 0; + border-width: 0; + margin-inline: calc(0.25rem * 0); + width: calc(0.25rem * 0); + padding-inline: calc(0.25rem * 0); + opacity: 0%; + } + } + &:has(input:checked:not(.filter-reset)) { + input:not(:checked, .filter-reset, input[type="reset"]) { + scale: 0; + border-width: 0; + margin-inline: calc(0.25rem * 0); + width: calc(0.25rem * 0); + padding-inline: calc(0.25rem * 0); + opacity: 0%; + } + } + } + } .input-sm { @layer daisyui.l1.l2 { --size: calc(var(--size-field, 0.25rem) * 8); @@ -1397,6 +1445,9 @@ } } } + .filter { + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } .backdrop-blur-sm { --tw-backdrop-blur: blur(var(--blur-sm)); -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); @@ -1918,6 +1969,59 @@ inherits: false; initial-value: 0 0 #0000; } +@property --tw-blur { + syntax: "*"; + inherits: false; +} +@property --tw-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} @property --tw-backdrop-blur { syntax: "*"; inherits: false; @@ -1983,6 +2087,19 @@ --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-offset-shadow: 0 0 #0000; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; --tw-backdrop-blur: initial; --tw-backdrop-brightness: initial; --tw-backdrop-contrast: initial; diff --git a/frontend/src/app.rs b/frontend/src/app.rs index b3b1cc1..0d21e77 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -6,6 +6,8 @@ use crate::components::torrent::table::TorrentTable; #[component] pub fn App() -> impl IntoView { + crate::store::provide_torrent_store(); + view! {
// Toolbar at the top diff --git a/frontend/src/components/layout/sidebar.rs b/frontend/src/components/layout/sidebar.rs index eb95f58..d35c630 100644 --- a/frontend/src/components/layout/sidebar.rs +++ b/frontend/src/components/layout/sidebar.rs @@ -2,52 +2,70 @@ use leptos::*; #[component] pub fn Sidebar() -> impl IntoView { + let store = use_context::().expect("store not provided"); + + let total_count = move || store.torrents.get().len(); + let downloading_count = move || store.torrents.get().iter().filter(|t| t.status == shared::TorrentStatus::Downloading).count(); + let seeding_count = move || store.torrents.get().iter().filter(|t| t.status == shared::TorrentStatus::Seeding).count(); + let completed_count = move || store.torrents.get().iter().filter(|t| t.status == shared::TorrentStatus::Seeding || t.status == shared::TorrentStatus::Paused).count(); + let inactive_count = move || store.torrents.get().iter().filter(|t| t.status == shared::TorrentStatus::Paused || t.status == shared::TorrentStatus::Error).count(); + + let set_filter = move |f: crate::store::FilterStatus| { + store.filter.set(f); + }; + + let filter_class = move |f: crate::store::FilterStatus| { + if store.filter.get() == f { "active" } else { "" } + }; + view! {