diff --git a/Cargo.lock b/Cargo.lock index 31a8fc7..4afd32b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1260,7 +1260,15 @@ dependencies = [ "gloo-timers", "js-sys", "leptos", - "leptos-shadcn-ui", + "leptos-shadcn-avatar", + "leptos-shadcn-badge", + "leptos-shadcn-button", + "leptos-shadcn-card", + "leptos-shadcn-context-menu", + "leptos-shadcn-input", + "leptos-shadcn-progress", + "leptos-shadcn-separator", + "leptos-shadcn-sheet", "leptos-use", "leptos_router", "log", @@ -2149,6 +2157,35 @@ dependencies = [ "send_wrapper", ] +[[package]] +name = "leptos-shadcn-avatar" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb3c5b1f5ba02f7282b55fde1513cdfecef3b25bf5fa44e1eb29fcaf8b927c5" +dependencies = [ + "leptos", + "leptos-shadcn-signal-management", + "leptos-style", + "tailwind_fuse", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos-shadcn-badge" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24578fb0bc21eb21be4e686e6719c7e183acb8fd071a4f81fb27fe452751c88a" +dependencies = [ + "leptos", + "leptos-node-ref", + "leptos-shadcn-signal-management", + "leptos-struct-component", + "leptos-style", + "tailwind_fuse", + "web-sys", +] + [[package]] name = "leptos-shadcn-button" version = "0.8.1" @@ -2164,6 +2201,37 @@ dependencies = [ "web-sys", ] +[[package]] +name = "leptos-shadcn-card" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b5cda16742d1e20284e5f6805eab88b6e54c1378d1548a8e15a5eedda1ea3eb" +dependencies = [ + "leptos", + "leptos-node-ref", + "leptos-shadcn-signal-management", + "leptos-struct-component", + "leptos-style", + "tailwind_fuse", + "web-sys", +] + +[[package]] +name = "leptos-shadcn-context-menu" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f440e9a7517dfe6ba758080ddba1dfe42e4697008f60adfc112c5da02dca8d" +dependencies = [ + "leptos", + "leptos-node-ref", + "leptos-shadcn-signal-management", + "leptos-struct-component", + "leptos-style", + "tailwind_fuse", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "leptos-shadcn-input" version = "0.8.1" @@ -2180,6 +2248,51 @@ dependencies = [ "web-sys", ] +[[package]] +name = "leptos-shadcn-progress" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34ca41b8ebfd7f29126e4f8656987834f3613717016f11f3983da85a90669f6" +dependencies = [ + "leptos", + "leptos-node-ref", + "leptos-shadcn-signal-management", + "leptos-struct-component", + "leptos-style", + "tailwind_fuse", + "web-sys", +] + +[[package]] +name = "leptos-shadcn-separator" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5dfda49f059fd4d1549d663e6743e37a5c6c84d1ac2d6daec32caa3156bc268" +dependencies = [ + "leptos", + "leptos-node-ref", + "leptos-shadcn-signal-management", + "leptos-struct-component", + "leptos-style", + "tailwind_fuse", + "web-sys", +] + +[[package]] +name = "leptos-shadcn-sheet" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba85819a0c94a7705ed92989442c64cc75d9ed3a4540e711e87c56b206431611" +dependencies = [ + "leptos", + "leptos-node-ref", + "leptos-shadcn-signal-management", + "leptos-struct-component", + "leptos-style", + "tailwind_fuse", + "web-sys", +] + [[package]] name = "leptos-shadcn-signal-management" version = "0.1.0" @@ -2194,23 +2307,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "leptos-shadcn-ui" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43430605d3d049a4cf68fb7dff4e6f940426ec48131f4662963f62f11baa3e18" -dependencies = [ - "gloo-timers", - "leptos", - "leptos-node-ref", - "leptos-shadcn-button", - "leptos-shadcn-input", - "leptos-struct-component", - "leptos-style", - "leptos_router", - "tailwind_fuse", -] - [[package]] name = "leptos-struct-component" version = "0.2.0" diff --git a/backend/src/main.rs b/backend/src/main.rs index 85d6015..82979cd 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -346,10 +346,7 @@ async fn main() { match diff::diff_torrents(&previous_torrents, &new_torrents) { diff::DiffResult::FullUpdate => { - let _ = event_bus_tx.send(AppEvent::FullList { - torrents: new_torrents.clone(), - timestamp: now, - }); + let _ = event_bus_tx.send(AppEvent::FullList(new_torrents.clone(), now)); } diff::DiffResult::Partial(updates) => { for update in updates { diff --git a/backend/src/sse.rs b/backend/src/sse.rs index 1f14263..09b2f0a 100644 --- a/backend/src/sse.rs +++ b/backend/src/sse.rs @@ -210,10 +210,7 @@ pub async fn sse_handler( .unwrap() .as_secs(); - let event_data = AppEvent::FullList { - torrents: initial_torrents, - timestamp, - }; + let event_data = AppEvent::FullList(initial_torrents, timestamp); match rmp_serde::to_vec(&event_data) { Ok(bytes) => Event::default().data(BASE64.encode(bytes)), @@ -250,7 +247,7 @@ pub async fn sse_handler( .keep_alive(axum::response::sse::KeepAlive::default()); ( - [("content-type", "application/x-msgpack")], + [("content-type", "text/event-stream")], sse ) } \ No newline at end of file diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 04491aa..1b6d51a 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -33,4 +33,14 @@ codee = "0.3" thiserror = "2.0" rmp-serde = "1.3" struct-patch = "0.5" -leptos-shadcn-ui = { version = "0.9.0", default-features = false, features = ["button", "input"] } + +# ShadCN UI Components (Individual) +leptos-shadcn-button = "0.8" +leptos-shadcn-input = "0.8" +leptos-shadcn-card = "0.8" +leptos-shadcn-badge = "0.8" +leptos-shadcn-context-menu = "0.8" +leptos-shadcn-separator = "0.8" +leptos-shadcn-progress = "0.8" +leptos-shadcn-avatar = "0.8" +leptos-shadcn-sheet = "0.8" \ No newline at end of file diff --git a/frontend/input.css b/frontend/input.css index bc485a4..b261934 100644 --- a/frontend/input.css +++ b/frontend/input.css @@ -126,6 +126,14 @@ body { @apply bg-background text-foreground; } + + /* Ensure Shadcn Utilities are always available */ + .bg-popover { background-color: hsl(var(--popover)); } + .text-popover-foreground { color: hsl(var(--popover-foreground)); } + .border-border { border-color: hsl(var(--border)); } + .shadow-md { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); } + .z-50 { z-index: 50; } + .z-100 { z-index: 100; } } /* Fix for iOS click/blur events */ diff --git a/frontend/public/tailwind.css b/frontend/public/tailwind.css index 75f985b..b06a21b 100644 --- a/frontend/public/tailwind.css +++ b/frontend/public/tailwind.css @@ -50,18 +50,17 @@ --text-lg--line-height: calc(1.75 / 1.125); --text-2xl: 1.5rem; --text-2xl--line-height: calc(2 / 1.5); - --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; --tracking-tight: -0.025em; - --tracking-wider: 0.05em; --leading-tight: 1.25; --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); --radius-xl: 0.75rem; + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); --animate-spin: spin 1s linear infinite; + --blur-sm: 8px; --default-transition-duration: 150ms; --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); --default-font-family: var(--font-sans); @@ -239,9 +238,6 @@ .pointer-events-auto { pointer-events: auto; } - .pointer-events-none { - pointer-events: none; - } .absolute { position: absolute; } @@ -254,12 +250,15 @@ .static { position: static; } + .inset-0 { + inset: calc(var(--spacing) * 0); + } + .inset-y-0 { + inset-block: calc(var(--spacing) * 0); + } .top-1\/2 { top: calc(1/2 * 100%); } - .top-full { - top: 100%; - } .right-0 { right: calc(var(--spacing) * 0); } @@ -278,8 +277,11 @@ .left-2 { left: calc(var(--spacing) * 2); } - .z-10 { - z-index: 10; + .z-40 { + z-index: 40; + } + .z-50 { + z-index: 50; } .z-\[99\] { z-index: 99; @@ -305,12 +307,6 @@ max-width: 96rem; } } - .my-0\.5 { - margin-block: calc(var(--spacing) * 0.5); - } - .mt-1 { - margin-top: calc(var(--spacing) * 1); - } .mt-2 { margin-top: calc(var(--spacing) * 2); } @@ -413,9 +409,6 @@ .min-h-14 { min-height: calc(var(--spacing) * 14); } - .min-h-\[100dvh\] { - min-height: 100dvh; - } .min-h-screen { min-height: 100vh; } @@ -452,6 +445,9 @@ .w-48 { width: calc(var(--spacing) * 48); } + .w-56 { + width: calc(var(--spacing) * 56); + } .w-64 { width: calc(var(--spacing) * 64); } @@ -470,18 +466,20 @@ .min-w-\[8rem\] { min-width: 8rem; } - .min-w-\[10rem\] { - min-width: 10rem; - } - .min-w-\[200px\] { - min-width: 200px; - } .flex-1 { flex: 1; } .shrink-0 { flex-shrink: 0; } + .-translate-x-full { + --tw-translate-x: -100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .translate-x-0 { + --tw-translate-x: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } .-translate-y-1\/2 { --tw-translate-y: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -528,9 +526,6 @@ .justify-start { justify-content: flex-start; } - .gap-0\.5 { - gap: calc(var(--spacing) * 0.5); - } .gap-1 { gap: calc(var(--spacing) * 1); } @@ -592,9 +587,6 @@ .rounded-full { border-radius: calc(infinity * 1px); } - .rounded-lg { - border-radius: var(--radius-lg); - } .rounded-md { border-radius: var(--radius-md); } @@ -682,10 +674,10 @@ .bg-background { background-color: var(--color-background); } - .bg-background\/95 { - background-color: color-mix(in srgb, hsl(var(--background)) 95%, transparent); + .bg-background\/80 { + background-color: color-mix(in srgb, hsl(var(--background)) 80%, transparent); @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-background) 95%, transparent); + background-color: color-mix(in oklab, var(--color-background) 80%, transparent); } } .bg-blue-100 { @@ -802,15 +794,15 @@ .pr-2 { padding-right: calc(var(--spacing) * 2); } + .pb-0 { + padding-bottom: calc(var(--spacing) * 0); + } .pb-2 { padding-bottom: calc(var(--spacing) * 2); } .pb-3 { padding-bottom: calc(var(--spacing) * 3); } - .pb-8 { - padding-bottom: calc(var(--spacing) * 8); - } .pl-8 { padding-left: calc(var(--spacing) * 8); } @@ -858,10 +850,6 @@ --tw-font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium); } - .font-normal { - --tw-font-weight: var(--font-weight-normal); - font-weight: var(--font-weight-normal); - } .font-semibold { --tw-font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold); @@ -870,10 +858,6 @@ --tw-tracking: var(--tracking-tight); letter-spacing: var(--tracking-tight); } - .tracking-wider { - --tw-tracking: var(--tracking-wider); - letter-spacing: var(--tracking-wider); - } .whitespace-nowrap { white-space: nowrap; } @@ -977,25 +961,14 @@ --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } - .shadow-xl { - --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .ring-2 { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .ring-primary { - --tw-ring-color: var(--color-primary); - } .ring-offset-background { --tw-ring-offset-color: var(--color-background); } .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 { - --tw-backdrop-blur: blur(8px); + .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,); 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,); } @@ -1014,14 +987,23 @@ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } - .duration-100 { - --tw-duration: 100ms; - transition-duration: 100ms; + .transition-transform { + transition-property: transform, translate, scale, rotate; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .duration-300 { + --tw-duration: 300ms; + transition-duration: 300ms; } .duration-500 { --tw-duration: 500ms; transition-duration: 500ms; } + .ease-in-out { + --tw-ease: var(--ease-in-out); + transition-timing-function: var(--ease-in-out); + } .outline-none { --tw-outline-style: none; outline-style: none; @@ -1030,9 +1012,6 @@ -webkit-user-select: none; user-select: none; } - .ring-inset { - --tw-ring-inset: inset; - } .group-open\:block { &:is(:where(.group):is([open], :popover-open, :open) *) { display: block; @@ -1083,6 +1062,13 @@ color: var(--color-muted-foreground); } } + .hover\:border-primary { + &:hover { + @media (hover: hover) { + border-color: var(--color-primary); + } + } + } .hover\:bg-accent { &:hover { @media (hover: hover) { @@ -1107,13 +1093,6 @@ } } } - .hover\:bg-primary { - &:hover { - @media (hover: hover) { - background-color: var(--color-primary); - } - } - } .hover\:bg-primary\/90 { &:hover { @media (hover: hover) { @@ -1160,11 +1139,21 @@ background-color: var(--color-accent); } } + .focus\:bg-destructive { + &:focus { + background-color: var(--color-destructive); + } + } .focus\:text-accent-foreground { &:focus { color: var(--color-accent-foreground); } } + .focus\:text-destructive-foreground { + &:focus { + color: var(--color-destructive-foreground); + } + } .focus\:ring-2 { &:focus { --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); @@ -1297,11 +1286,22 @@ max-width: 420px; } } + .lg\:relative { + @media (width >= 64rem) { + position: relative; + } + } .lg\:hidden { @media (width >= 64rem) { display: none; } } + .lg\:translate-x-0 { + @media (width >= 64rem) { + --tw-translate-x: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } .dark\:border-blue-800 { @media (prefers-color-scheme: dark) { border-color: var(--color-blue-800); @@ -1479,6 +1479,24 @@ background-color: var(--color-background); color: var(--color-foreground); } + .bg-popover { + background-color: hsl(var(--popover)); + } + .text-popover-foreground { + color: hsl(var(--popover-foreground)); + } + .border-border { + border-color: hsl(var(--border)); + } + .shadow-md { + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + } + .z-50 { + z-index: 50; + } + .z-100 { + z-index: 100; + } } @media (hover: none) { body { @@ -1712,6 +1730,10 @@ syntax: "*"; inherits: false; } +@property --tw-ease { + syntax: "*"; + inherits: false; +} @keyframes spin { to { transform: rotate(360deg); @@ -1771,6 +1793,7 @@ --tw-backdrop-saturate: initial; --tw-backdrop-sepia: initial; --tw-duration: initial; + --tw-ease: initial; } } } diff --git a/frontend/src/app.rs b/frontend/src/app.rs index 6e975b0..d54ed6f 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -3,7 +3,6 @@ use crate::components::toast::ToastContainer; use crate::components::torrent::table::TorrentTable; use crate::components::auth::login::Login; use crate::components::auth::setup::Setup; -use crate::api; use leptos::prelude::*; use leptos::task::spawn_local; use leptos_router::components::{Router, Routes, Route}; @@ -122,14 +121,14 @@ pub fn App() -> impl IntoView {

"Yükleniyor..."

- }> + }.into_any()> - } + }.into_any() }/> impl IntoView { let user = username.0.get(); let pass = password.0.get(); - log::info!("Attempting login for user: {}", user); - spawn_local(async move { match shared::server_fns::auth::login(user, pass).await { Ok(_) => { - log::info!("Login successful, redirecting..."); let window = web_sys::window().expect("window should exist"); let _ = window.location().set_href("/"); } - Err(e) => { - log::error!("Login failed: {:?}", e); + Err(_) => { error.1.set(Some("Geçersiz kullanıcı adı veya şifre".to_string())); loading.1.set(false); } @@ -35,7 +31,7 @@ pub fn Login() -> impl IntoView { }; view! { -
+
@@ -51,13 +47,11 @@ pub fn Login() -> impl IntoView {
- + impl IntoView { />
- + impl IntoView { />
- -
- {move || error.0.get().unwrap_or_default()} + +
+ {move || error.0.get().unwrap_or_default()}
diff --git a/frontend/src/components/context_menu.rs b/frontend/src/components/context_menu.rs index 19ea8eb..26217b6 100644 --- a/frontend/src/components/context_menu.rs +++ b/frontend/src/components/context_menu.rs @@ -1,97 +1,84 @@ use leptos::prelude::*; -use leptos::html; -use leptos_use::on_click_outside; - -fn handle_action( - hash: String, - action: &str, - on_action: Callback<(String, String)>, - on_close: Callback<()>, -) { - log::info!("ContextMenu: Action '{}' for hash '{}'", action, hash); - on_action.run((action.to_string(), hash)); - on_close.run(()); -} +use leptos::portal::Portal; +use leptos_shadcn_context_menu::{ + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, + ContextMenuSeparator, +}; #[component] -pub fn ContextMenu( - position: (i32, i32), +pub fn TorrentContextMenu( + children: Children, torrent_hash: String, - on_close: Callback<()>, on_action: Callback<(String, String)>, ) -> impl IntoView { - let container_ref = NodeRef::::new(); - - let _ = on_click_outside(container_ref, move |_| on_close.run(())); - - let (x, y) = position; - - let hash1 = torrent_hash.clone(); - let hash2 = torrent_hash.clone(); - let hash3 = torrent_hash.clone(); - let hash4 = torrent_hash.clone(); - let hash5 = torrent_hash; + let hash = StoredValue::new(torrent_hash); + let on_action = StoredValue::new(on_action); view! { -
- -
+ "Remove Data" + + + + } -} +} \ No newline at end of file diff --git a/frontend/src/components/layout/protected.rs b/frontend/src/components/layout/protected.rs index fde974f..fd1e7f0 100644 --- a/frontend/src/components/layout/protected.rs +++ b/frontend/src/components/layout/protected.rs @@ -5,28 +5,48 @@ use crate::components::layout::statusbar::StatusBar; #[component] pub fn Protected(children: Children) -> impl IntoView { + // Mobil menü durumu için bir sinyal oluşturuyoruz (RwSignal for easier passing) + let is_mobile_menu_open = RwSignal::new(false); + + // Sinyali context olarak sağlıyoruz ki Toolbar ve Sidebar buna erişebilsin + provide_context(is_mobile_menu_open); + view! { -
- +
-
+ // --- SIDEBAR (Desktop: Sabit, Mobil: Overlay) --- + + + // Mobil arka plan karartma (Overlay) + +
+
+ + // --- MAIN CONTENT AREA --- +
// --- TOOLBAR (TOP) --- // --- MAIN CONTENT --- -
+
{children()}
// --- STATUS BAR (BOTTOM) ---
- - // --- SIDEBAR (DRAWER) --- -
- - -
} } \ No newline at end of file diff --git a/frontend/src/components/layout/sidebar.rs b/frontend/src/components/layout/sidebar.rs index d6a0c3d..8f610c6 100644 --- a/frontend/src/components/layout/sidebar.rs +++ b/frontend/src/components/layout/sidebar.rs @@ -1,11 +1,10 @@ use leptos::prelude::*; -use leptos::wasm_bindgen::JsCast; use leptos::task::spawn_local; -use crate::api; #[component] pub fn Sidebar() -> impl IntoView { let store = use_context::().expect("store not provided"); + let is_mobile_menu_open = use_context::>().expect("mobile menu state not provided"); let total_count = move || store.torrents.with(|map| map.len()); let downloading_count = move || { @@ -50,16 +49,9 @@ pub fn Sidebar() -> impl IntoView { }) }; - let close_drawer = move || { - // With Shadcn Sheet, this logic might change, but for now we keep DOM manipulation minimal or handled by parent - if let Some(element) = document().get_element_by_id("mobile-sheet-trigger") { - // Logic to close sheet if open (simulated click or state change) - } - }; - let set_filter = move |f: crate::store::FilterStatus| { store.filter.set(f); - close_drawer(); + is_mobile_menu_open.set(false); }; let filter_class = move |f: crate::store::FilterStatus| { @@ -73,7 +65,7 @@ pub fn Sidebar() -> impl IntoView { let handle_logout = move |_| { spawn_local(async move { - if api::auth::logout().await.is_ok() { + if shared::server_fns::auth::logout().await.is_ok() { let window = web_sys::window().expect("window should exist"); let _ = window.location().set_href("/login"); } @@ -89,7 +81,7 @@ pub fn Sidebar() -> impl IntoView { }; view! { -
+
"VibeTorrent" @@ -171,4 +163,4 @@ pub fn Sidebar() -> impl IntoView {
} -} \ No newline at end of file +} diff --git a/frontend/src/components/layout/toolbar.rs b/frontend/src/components/layout/toolbar.rs index b9f0b76..50a643a 100644 --- a/frontend/src/components/layout/toolbar.rs +++ b/frontend/src/components/layout/toolbar.rs @@ -5,12 +5,16 @@ use crate::components::torrent::add_torrent::AddTorrentDialog; pub fn Toolbar() -> impl IntoView { let show_add_modal = signal(false); let store = use_context::().expect("store not provided"); + let is_mobile_menu_open = use_context::>().expect("mobile menu state not provided"); view! {
- // Mobile Menu Trigger (Sheet Trigger in full impl) - diff --git a/frontend/src/components/torrent/table.rs b/frontend/src/components/torrent/table.rs index ab22caf..ea33924 100644 --- a/frontend/src/components/torrent/table.rs +++ b/frontend/src/components/torrent/table.rs @@ -1,10 +1,10 @@ use leptos::prelude::*; -use leptos::html; use leptos::task::spawn_local; -use leptos_use::{use_timeout_fn, UseTimeoutFnReturn}; use crate::store::{get_action_messages, show_toast_with_signal}; use crate::api; use shared::NotificationLevel; +use crate::components::context_menu::TorrentContextMenu; +use leptos_shadcn_card::{Card, CardHeader, CardTitle, CardContent}; fn format_bytes(bytes: i64) -> String { const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"]; @@ -50,7 +50,7 @@ pub fn TorrentTable() -> impl IntoView { let sort_col = signal(SortColumn::AddedDate); let sort_dir = signal(SortDirection::Descending); - let filtered_hashes = move || { + let filtered_hashes = Memo::new(move |_| { let torrents_map = store.torrents.get(); let filter = store.filter.get(); let search = store.search_query.get(); @@ -90,7 +90,7 @@ pub fn TorrentTable() -> impl IntoView { if dir == SortDirection::Descending { cmp.reverse() } else { cmp } }); torrents.into_iter().map(|t| t.hash.clone()).collect::>() - }; + }); let handle_sort = move |col: SortColumn| { if sort_col.0.get() == col { @@ -103,8 +103,6 @@ pub fn TorrentTable() -> impl IntoView { } }; - let sort_details_ref = NodeRef::::new(); - let sort_arrow = move |col: SortColumn| { if sort_col.0.get() == col { match sort_dir.0.get() { @@ -114,18 +112,7 @@ pub fn TorrentTable() -> impl IntoView { } else { view! { "▲" }.into_any() } }; - let selected_hash = signal(Option::::None); - let menu_visible = signal(false); - let menu_position = signal((0, 0)); - - let handle_context_menu = move |e: web_sys::MouseEvent, hash: String| { - e.prevent_default(); - menu_position.1.set((e.client_x(), e.client_y())); - selected_hash.1.set(Some(hash)); - menu_visible.1.set(true); - }; - - let on_action = move |(action, hash): (String, String)| { + let on_action = Callback::new(move |(action, hash): (String, String)| { let (success_msg_str, error_msg_str): (&'static str, &'static str) = get_action_messages(&action); let success_msg = success_msg_str.to_string(); let error_msg = error_msg_str.to_string(); @@ -143,10 +130,10 @@ pub fn TorrentTable() -> impl IntoView { Err(e) => show_toast_with_signal(notifications, NotificationLevel::Error, format!("{}: {:?}", error_msg, e)), } }); - }; + }); view! { -
+
// --- DESKTOP VIEW ---
- // Regular List (Standard For loop) + // Regular List
- + + + } } } /> @@ -197,61 +182,22 @@ pub fn TorrentTable() -> impl IntoView { // --- MOBILE VIEW ---
-
- "Torrents" - -
-
- - + + +
} } } />
- - - -
} } @@ -259,9 +205,6 @@ pub fn TorrentTable() -> impl IntoView { #[component] fn TorrentRow( hash: String, - selected_hash: ReadSignal>, - set_selected_hash: WriteSignal>, - on_context_menu: impl Fn(web_sys::MouseEvent, String) + 'static + Clone + Send + Sync, ) -> impl IntoView { let store = use_context::().expect("store not provided"); let h = hash.clone(); @@ -270,34 +213,13 @@ fn TorrentRow( view! { { - let on_context_menu = on_context_menu.clone(); - let hash = hash.clone(); move || { let t = torrent.get().unwrap(); - let t_hash = hash.clone(); let t_name = t.name.clone(); let status_color = match t.status { shared::TorrentStatus::Seeding => "text-green-500", shared::TorrentStatus::Downloading => "text-blue-500", shared::TorrentStatus::Paused => "text-yellow-500", shared::TorrentStatus::Error => "text-red-500", _ => "text-muted-foreground" }; - let selected_hash_clone = selected_hash.clone(); - let t_hash_row = t_hash.clone(); - view! { -
+
{t_name.clone()}
{format_bytes(t.size)}
@@ -324,11 +246,6 @@ fn TorrentRow( #[component] fn TorrentCard( hash: String, - selected_hash: ReadSignal>, - set_selected_hash: WriteSignal>, - set_menu_position: WriteSignal<(i32, i32)>, - set_menu_visible: WriteSignal, - on_context_menu: impl Fn(web_sys::MouseEvent, String) + 'static + Clone + Send + Sync, ) -> impl IntoView { let store = use_context::().expect("store not provided"); let h = hash.clone(); @@ -337,57 +254,20 @@ fn TorrentCard( view! { { - let hash = hash.clone(); - let on_context_menu = on_context_menu.clone(); move || { let t = torrent.get().unwrap(); - let t_hash = hash.clone(); let t_name = t.name.clone(); let status_badge_class = match t.status { shared::TorrentStatus::Seeding => "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 border-green-200 dark:border-green-800", shared::TorrentStatus::Downloading => "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 border-blue-200 dark:border-blue-800", shared::TorrentStatus::Paused => "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800", shared::TorrentStatus::Error => "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 border-red-200 dark:border-red-800", _ => "bg-muted text-muted-foreground" }; - let t_hash_long = t_hash.clone(); - let set_menu_position = set_menu_position.clone(); - let set_selected_hash = set_selected_hash.clone(); - let set_menu_visible = set_menu_visible.clone(); - let UseTimeoutFnReturn { start, .. } = use_timeout_fn( - move |pos: (i32, i32)| { - set_menu_position.set(pos); - set_selected_hash.set(Some(t_hash_long.clone())); - set_menu_visible.set(true); - let _ = window().navigator().vibrate_with_duration(50); - }, - 600.0, - ); - - let selected_hash_clone = selected_hash.clone(); - let t_hash_card = t_hash.clone(); - view! { -
-
+ +
-

{t_name.clone()}

+ {t_name.clone()}
{format!("{:?}", t.status)}
+
+
{format_bytes(t.size)} @@ -403,11 +283,11 @@ fn TorrentCard(
"ETA"{format_duration(t.eta)}
"DATE"{format_date(t.added_date)}
-
-
+ + } } } } -} \ No newline at end of file +} diff --git a/frontend/src/store.rs b/frontend/src/store.rs index 5b08eb6..44796fc 100644 --- a/frontend/src/store.rs +++ b/frontend/src/store.rs @@ -124,7 +124,7 @@ pub fn provide_torrent_store() { match rmp_serde::from_slice::(&bytes) { Ok(event) => { match event { - AppEvent::FullList { torrents: list, .. } => { + AppEvent::FullList(list, _) => { log::info!("SSE: Received FullList with {} torrents", list.len()); torrents_for_sse.update(|map| { let new_hashes: std::collections::HashSet = list.iter().map(|t| t.hash.clone()).collect(); @@ -136,13 +136,14 @@ pub fn provide_torrent_store() { log::debug!("SSE: torrents map now has {} entries", torrents_for_sse.with(|m| m.len())); } AppEvent::Update(patch) => { - torrents_for_sse.update(|map| { - if let Some(hash) = patch.hash.as_ref() { - if let Some(t) = map.get_mut(hash) { + let hash_opt = patch.hash.clone(); + if let Some(hash) = hash_opt { + torrents_for_sse.update(|map| { + if let Some(t) = map.get_mut(&hash) { t.apply(patch); } - } - }); + }); + } } AppEvent::Stats(stats) => { global_stats_for_sse.set(stats); } AppEvent::Notification(n) => { diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 3c6e6b2..0b8dad3 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -53,20 +53,10 @@ pub enum TorrentStatus { } #[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] -#[serde(tag = "t", content = "d")] pub enum AppEvent { - #[serde(rename = "f")] - FullList { - #[serde(rename = "t")] - torrents: Vec, - #[serde(rename = "ts")] - timestamp: u64, - }, - #[serde(rename = "u")] + FullList(Vec, u64), Update(TorrentUpdate), - #[serde(rename = "s")] Stats(GlobalStats), - #[serde(rename = "n")] Notification(SystemNotification), }