diff --git a/Cargo.lock b/Cargo.lock index b57c6f8..e315065 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -685,6 +685,7 @@ dependencies = [ "futures", "gloo-net 0.5.0", "leptos", + "leptos_router", "log", "serde", "serde_json", @@ -1340,6 +1341,32 @@ dependencies = [ "web-sys", ] +[[package]] +name = "leptos_router" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d71dea7d42c0d29c40842750232d3425ed1cf10e313a1f898076d20871dad32" +dependencies = [ + "cfg-if", + "gloo-net 0.6.0", + "itertools", + "js-sys", + "lazy_static", + "leptos", + "linear-map", + "once_cell", + "percent-encoding", + "send_wrapper", + "serde", + "serde_json", + "serde_qs 0.13.0", + "thiserror", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "leptos_server" version = "0.6.15" @@ -1362,6 +1389,16 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" +dependencies = [ + "serde", + "serde_test", +] + [[package]] name = "litemap" version = "0.8.1" @@ -2008,6 +2045,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -2017,6 +2065,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_test" +version = "1.0.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2047,7 +2104,7 @@ dependencies = [ "send_wrapper", "serde", "serde_json", - "serde_qs", + "serde_qs 0.12.0", "server_fn_macro_default", "thiserror", "url", diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 532f177..8463cf8 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -8,6 +8,8 @@ crate-type = ["cdylib", "rlib"] [dependencies] leptos = { version = "0.6", features = ["csr"] } +leptos_router = { version = "0.6", features = ["csr"] } + console_error_panic_hook = "0.1" console_log = "1" log = "0.4" diff --git a/frontend/index.html b/frontend/index.html index 937ef03..9d78b43 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -51,6 +51,19 @@ display: none !important; } + \ No newline at end of file diff --git a/frontend/manifest.json b/frontend/manifest.json index 5fa0936..95064c0 100644 --- a/frontend/manifest.json +++ b/frontend/manifest.json @@ -5,16 +5,35 @@ "display": "standalone", "background_color": "#111827", "theme_color": "#000000", + "orientation": "any", "icons": [ { "src": "icon-512.png", "sizes": "512x512", - "type": "image/png" + "type": "image/png", + "purpose": "any maskable" }, { "src": "icon-192.png", "sizes": "192x192", - "type": "image/png" + "type": "image/png", + "purpose": "any maskable" + } + ], + "screenshots": [ + { + "src": "icon-512.png", + "sizes": "512x512", + "type": "image/png", + "form_factor": "wide", + "label": "Desktop Home Screen" + }, + { + "src": "icon-192.png", + "sizes": "192x192", + "type": "image/png", + "form_factor": "narrow", + "label": "Mobile Home Screen" } ] } \ No newline at end of file diff --git a/frontend/public/tailwind.css b/frontend/public/tailwind.css index 0fe9ed0..364acde 100644 --- a/frontend/public/tailwind.css +++ b/frontend/public/tailwind.css @@ -192,6 +192,96 @@ } } @layer utilities { + .drawer-side { + :where(&) { + @layer daisyui.l1.l2.l3 { + overflow-x: hidden; + overflow-y: hidden; + } + } + @layer daisyui.l1.l2.l3 { + pointer-events: none; + visibility: hidden; + position: fixed; + inset-inline-start: calc(0.25rem * 0); + top: calc(0.25rem * 0); + z-index: 10; + grid-column-start: 1; + grid-row-start: 1; + display: grid; + width: 100%; + grid-template-columns: repeat(1, minmax(0, 1fr)); + grid-template-rows: repeat(1, minmax(0, 1fr)); + align-items: flex-start; + justify-items: start; + overscroll-behavior: contain; + background-color: transparent; + opacity: 0%; + transition: opacity 0.2s ease-out 0.1s allow-discrete, visibility 0.3s ease-out 0.1s allow-discrete; + height: 100vh; + height: 100dvh; + > .drawer-overlay { + position: sticky; + top: calc(0.25rem * 0); + cursor: pointer; + place-self: stretch; + background-color: oklch(0% 0 0 / 40%); + } + > * { + grid-column-start: 1; + grid-row-start: 1; + } + > *:not(.drawer-overlay) { + will-change: transform; + transition: translate 0.3s ease-out, width 0.2s ease-out; + translate: -100%; + [dir="rtl"] & { + translate: 100%; + } + } + } + } + .drawer-toggle { + @layer daisyui.l1.l2.l3 { + position: fixed; + height: calc(0.25rem * 0); + width: calc(0.25rem * 0); + appearance: none; + opacity: 0%; + } + @layer daisyui.l1.l2 { + :where(&:checked ~ .drawer-side) { + scrollbar-color: currentColor oklch(0 0 0 / calc(var(--page-has-backdrop, 0) * 0.4)); + @supports (color: color-mix(in lab, red, red)) { + scrollbar-color: color-mix(in oklch, currentColor 35%, #0000) oklch(0 0 0 / calc(var(--page-has-backdrop, 0) * 0.4)); + } + } + :where(:root:has(&:checked)) { + --page-has-backdrop: 1; + --page-overflow: hidden; + --page-scroll-bg: var(--page-scroll-bg-on); + --page-scroll-gutter: stable; + --page-scroll-transition: var(--page-scroll-transition-on); + animation: set-page-has-scroll forwards; + animation-timeline: scroll(); + } + } + @layer daisyui.l1.l2 { + :where(&:checked ~ .drawer-side) { + pointer-events: auto; + visibility: visible; + overflow-y: auto; + opacity: 100%; + & > *:not(.drawer-overlay) { + translate: 0%; + } + } + &:focus-visible ~ .drawer-content label.drawer-button { + outline: 2px solid; + outline-offset: 2px; + } + } + } .menu { @layer daisyui.l1.l2.l3 { display: flex; @@ -781,6 +871,14 @@ } } } + .drawer { + @layer daisyui.l1.l2.l3 { + position: relative; + display: grid; + width: 100%; + grid-auto-columns: max-content auto; + } + } .progress { @layer daisyui.l1.l2.l3 { position: relative; @@ -831,24 +929,34 @@ } } } - .absolute { - position: absolute; - } .fixed { position: fixed; } + .relative { + position: relative; + } .static { position: static; } .inset-0 { inset: calc(var(--spacing) * 0); } + .z-40 { + z-index: 40; + } .z-\[100\] { z-index: 100; } .z-\[200\] { z-index: 200; } + .drawer-content { + @layer daisyui.l1.l2.l3 { + grid-column-start: 2; + grid-row-start: 1; + min-width: calc(0.25rem * 0); + } + } .filter { @layer daisyui.l1.l2.l3 { display: flex; @@ -909,6 +1017,45 @@ .my-1 { margin-block: calc(var(--spacing) * 1); } + .label { + @layer daisyui.l1.l2.l3 { + display: inline-flex; + align-items: center; + gap: calc(0.25rem * 1.5); + white-space: nowrap; + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 60%, transparent); + } + &:has(input) { + cursor: pointer; + } + &:is(.input > *, .select > *) { + display: flex; + height: calc(100% - 0.5rem); + align-items: center; + padding-inline: calc(0.25rem * 3); + white-space: nowrap; + font-size: inherit; + &:first-child { + margin-inline-start: calc(0.25rem * -3); + margin-inline-end: calc(0.25rem * 3); + border-inline-end: var(--border) solid currentColor; + @supports (color: color-mix(in lab, red, red)) { + border-inline-end: var(--border) solid color-mix(in oklab, currentColor 10%, #0000); + } + } + &:last-child { + margin-inline-start: calc(0.25rem * 3); + margin-inline-end: calc(0.25rem * -3); + border-inline-start: var(--border) solid currentColor; + @supports (color: color-mix(in lab, red, red)) { + border-inline-start: var(--border) solid color-mix(in oklab, currentColor 10%, #0000); + } + } + } + } + } .join-item { &:where(*:not(:first-child, :disabled, [disabled], .btn-disabled)) { margin-inline-start: calc(var(--border, 1px) * -1); @@ -1051,6 +1198,9 @@ .flex { display: flex; } + .inline-block { + display: inline-block; + } .inline-flex { display: inline-flex; } @@ -1094,6 +1244,9 @@ .min-h-14 { min-height: calc(var(--spacing) * 14); } + .min-h-full { + min-height: 100%; + } .w-4 { width: calc(var(--spacing) * 4); } @@ -1103,9 +1256,6 @@ .w-8 { width: calc(var(--spacing) * 8); } - .w-20 { - width: calc(var(--spacing) * 20); - } .w-24 { width: calc(var(--spacing) * 24); } @@ -1118,9 +1268,6 @@ .w-full { width: 100%; } - .w-screen { - width: 100vw; - } .max-w-full { max-width: 100%; } @@ -1142,6 +1289,9 @@ .transform { transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); } + .cursor-context-menu { + cursor: context-menu; + } .cursor-pointer { cursor: pointer; } @@ -1172,6 +1322,13 @@ .gap-4 { gap: calc(var(--spacing) * 4); } + .space-y-6 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); + } + } .truncate { overflow: hidden; text-overflow: ellipsis; @@ -1256,12 +1413,18 @@ background-color: color-mix(in oklab, var(--color-white) 10%, transparent); } } + .stroke-current { + stroke: currentcolor; + } .checkbox-xs { @layer daisyui.l1.l2 { padding: 0.125rem; --size: calc(var(--size-selector, 0.25rem) * 4); } } + .p-0 { + padding: calc(var(--spacing) * 0); + } .p-4 { padding: calc(var(--spacing) * 4); } @@ -1348,6 +1511,9 @@ --tw-tracking: var(--tracking-wider); letter-spacing: var(--tracking-wider); } + .whitespace-nowrap { + white-space: nowrap; + } .progress-primary { @layer daisyui.l1.l2 { color: var(--color-primary); @@ -1482,6 +1648,10 @@ --tw-duration: 200ms; transition-duration: 200ms; } + .duration-300 { + --tw-duration: 300ms; + transition-duration: 300ms; + } .btn-outline { @layer daisyui.l1 { &:not( .btn-active, :hover, :active:focus, :focus-visible, input:checked:not(.filter .btn), :disabled, [disabled], .btn-disabled ) { @@ -1638,6 +1808,67 @@ border-radius: var(--radius-lg); } } + .md\:p-6 { + @media (width >= 48rem) { + padding: calc(var(--spacing) * 6); + } + } + .lg\:drawer-open { + @media (width >= 64rem) { + @layer daisyui.l1.l2 { + & > .drawer-toggle:checked { + & ~ .drawer-side { + scrollbar-color: revert-layer; + } + :root:has(&) { + --page-overflow: revert-layer; + --page-scroll-gutter: revert-layer; + --page-scroll-bg: revert-layer; + --page-scroll-transition: revert-layer; + --page-has-backdrop: revert-layer; + animation: revert-layer; + animation-timeline: revert-layer; + } + } + } + @layer daisyui.l1.l2 { + & > .drawer-side { + overflow-y: auto; + } + > .drawer-toggle { + display: none; + & ~ .drawer-side { + pointer-events: auto; + visibility: visible; + position: sticky; + display: block; + width: auto; + overscroll-behavior: auto; + opacity: 100%; + & > .drawer-overlay { + cursor: default; + background-color: transparent; + } + & > *:not(.drawer-overlay) { + translate: 0%; + [dir="rtl"] & { + translate: 0%; + } + } + } + &:checked ~ .drawer-side { + pointer-events: auto; + visibility: visible; + } + } + } + } + } + .lg\:hidden { + @media (width >= 64rem) { + display: none; + } + } } @layer base { html, body { @@ -1911,6 +2142,11 @@ syntax: "*"; inherits: false; } +@property --tw-space-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} @property --tw-border-style { syntax: "*"; inherits: false; @@ -2090,6 +2326,7 @@ --tw-rotate-z: initial; --tw-skew-x: initial; --tw-skew-y: initial; + --tw-space-y-reverse: 0; --tw-border-style: solid; --tw-font-weight: initial; --tw-tracking: initial; diff --git a/frontend/src/app.rs b/frontend/src/app.rs index 0d21e77..2378b68 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -1,4 +1,5 @@ use leptos::*; +use leptos_router::*; use crate::components::layout::sidebar::Sidebar; use crate::components::layout::toolbar::Toolbar; use crate::components::layout::statusbar::StatusBar; @@ -9,22 +10,32 @@ pub fn App() -> impl IntoView { crate::store::provide_torrent_store(); view! { -
- // Toolbar at the top - +
+ + +
+ // Toolbar at the top + -
- // Sidebar on the left - - - // Main Content Area -
- +
+ + + } /> + "Settings Page (Coming Soon)"
} /> + + + + // Status Bar at the bottom +
- // Status Bar at the bottom - +
+ + +
} } diff --git a/frontend/src/components/context_menu.rs b/frontend/src/components/context_menu.rs index 3b86d11..2fb58e7 100644 --- a/frontend/src/components/context_menu.rs +++ b/frontend/src/components/context_menu.rs @@ -1,4 +1,40 @@ use leptos::*; +use leptos::html::Div; +use wasm_bindgen::JsCast; + +pub fn use_click_outside( + target: NodeRef
, + callback: impl Fn() + Clone + 'static, +) { + create_effect(move |_| { + if let Some(_) = target.get() { + let handle_click = { + let callback = callback.clone(); + let target = target.clone(); + move |ev: web_sys::MouseEvent| { + if let Some(el) = target.get() { + let ev_target = ev.target().unwrap().unchecked_into::(); + let el_node = el.unchecked_ref::(); + if !el_node.contains(Some(&ev_target)) { + callback(); + } + } + } + }; + + let window = web_sys::window().unwrap(); + let closure = wasm_bindgen::closure::Closure::::new(handle_click); + let _ = window.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref()); + + // Cleanup + on_cleanup(move || { + let window = web_sys::window().unwrap(); + let _ = window.remove_event_listener_with_callback("click", closure.as_ref().unchecked_ref()); + }); + } + }); +} + #[component] pub fn ContextMenu( @@ -17,69 +53,72 @@ pub fn ContextMenu( on_close.call(()); // Close menu AFTER }; + let target = create_node_ref::
(); + + use_click_outside(target, move || { + if visible { + on_close.call(()); + } + }); + if !visible { return view! {}.into_view(); } view! {
-
"Actions"
+ + - - - -
- - - - -
+ + "Resume" + + + + +
+ + + +
}.into_view() } diff --git a/frontend/src/components/layout/sidebar.rs b/frontend/src/components/layout/sidebar.rs index d35c630..4827581 100644 --- a/frontend/src/components/layout/sidebar.rs +++ b/frontend/src/components/layout/sidebar.rs @@ -19,7 +19,7 @@ pub fn Sidebar() -> impl IntoView { }; view! { - +
} } diff --git a/frontend/src/components/layout/toolbar.rs b/frontend/src/components/layout/toolbar.rs index ed015a4..138067d 100644 --- a/frontend/src/components/layout/toolbar.rs +++ b/frontend/src/components/layout/toolbar.rs @@ -4,6 +4,9 @@ use leptos::*; pub fn Toolbar() -> impl IntoView { view! {
+