feat: modernize stack with shadcn, struct_patch and msgpack
Some checks failed
Build MIPS Binary / build (push) Failing after 6s
Some checks failed
Build MIPS Binary / build (push) Failing after 6s
This commit is contained in:
@@ -51,10 +51,9 @@ pub fn Sidebar() -> impl IntoView {
|
||||
};
|
||||
|
||||
let close_drawer = move || {
|
||||
if let Some(element) = document().get_element_by_id("my-drawer") {
|
||||
if let Ok(input) = element.dyn_into::<web_sys::HtmlInputElement>() {
|
||||
input.set_checked(false);
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,10 +63,11 @@ pub fn Sidebar() -> impl IntoView {
|
||||
};
|
||||
|
||||
let filter_class = move |f: crate::store::FilterStatus| {
|
||||
let base = "w-full justify-start gap-2 h-9 px-4 py-2 inline-flex items-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50";
|
||||
if store.filter.get() == f {
|
||||
"active"
|
||||
format!("{} bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", base)
|
||||
} else {
|
||||
""
|
||||
format!("{} hover:bg-accent hover:text-accent-foreground text-muted-foreground", base)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -89,80 +89,77 @@ pub fn Sidebar() -> impl IntoView {
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="w-64 min-h-[100dvh] flex flex-col bg-base-200 border-r border-base-300 pb-8" style="padding-top: env(safe-area-inset-top);">
|
||||
<div class="p-2 flex-1 overflow-y-auto">
|
||||
<ul class="menu w-full rounded-box gap-1">
|
||||
<li class="menu-title text-primary uppercase font-bold px-4">"Filters"</li>
|
||||
<li>
|
||||
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::All))} on:click=move |_| set_filter(crate::store::FilterStatus::All)>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
"All"
|
||||
<span class="badge badge-sm badge-ghost ml-auto">{total_count}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Downloading))} on:click=move |_| set_filter(crate::store::FilterStatus::Downloading)>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
"Downloading"
|
||||
<span class="badge badge-sm badge-ghost ml-auto">{downloading_count}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Seeding))} on:click=move |_| set_filter(crate::store::FilterStatus::Seeding)>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||
</svg>
|
||||
"Seeding"
|
||||
<span class="badge badge-sm badge-ghost ml-auto">{seeding_count}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Completed))} on:click=move |_| set_filter(crate::store::FilterStatus::Completed)>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
"Completed"
|
||||
<span class="badge badge-sm badge-ghost ml-auto">{completed_count}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Paused))} on:click=move |_| set_filter(crate::store::FilterStatus::Paused)>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
||||
</svg>
|
||||
"Paused"
|
||||
<span class="badge badge-sm badge-ghost ml-auto">{paused_count}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Inactive))} on:click=move |_| set_filter(crate::store::FilterStatus::Inactive)>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
||||
</svg>
|
||||
"Inactive"
|
||||
<span class="badge badge-sm badge-ghost ml-auto">{inactive_count}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="w-64 min-h-[100dvh] flex flex-col bg-card border-r border-border pb-8" style="padding-top: env(safe-area-inset-top);">
|
||||
<div class="p-4 flex-1 overflow-y-auto">
|
||||
<div class="mb-4 px-2 text-lg font-semibold tracking-tight text-foreground">
|
||||
"VibeTorrent"
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<h4 class="mb-1 rounded-md px-2 py-1 text-sm font-semibold text-muted-foreground">"Filters"</h4>
|
||||
|
||||
<button class={move || filter_class(crate::store::FilterStatus::All)} on:click=move |_| set_filter(crate::store::FilterStatus::All)>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
"All"
|
||||
<span class="ml-auto text-xs font-mono opacity-70">{total_count}</span>
|
||||
</button>
|
||||
|
||||
<button class={move || filter_class(crate::store::FilterStatus::Downloading)} on:click=move |_| set_filter(crate::store::FilterStatus::Downloading)>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
"Downloading"
|
||||
<span class="ml-auto text-xs font-mono opacity-70">{downloading_count}</span>
|
||||
</button>
|
||||
|
||||
<button class={move || filter_class(crate::store::FilterStatus::Seeding)} on:click=move |_| set_filter(crate::store::FilterStatus::Seeding)>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||
</svg>
|
||||
"Seeding"
|
||||
<span class="ml-auto text-xs font-mono opacity-70">{seeding_count}</span>
|
||||
</button>
|
||||
|
||||
<button class={move || filter_class(crate::store::FilterStatus::Completed)} on:click=move |_| set_filter(crate::store::FilterStatus::Completed)>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
"Completed"
|
||||
<span class="ml-auto text-xs font-mono opacity-70">{completed_count}</span>
|
||||
</button>
|
||||
|
||||
<button class={move || filter_class(crate::store::FilterStatus::Paused)} on:click=move |_| set_filter(crate::store::FilterStatus::Paused)>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
||||
</svg>
|
||||
"Paused"
|
||||
<span class="ml-auto text-xs font-mono opacity-70">{paused_count}</span>
|
||||
</button>
|
||||
|
||||
<button class={move || filter_class(crate::store::FilterStatus::Inactive)} on:click=move |_| set_filter(crate::store::FilterStatus::Inactive)>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
||||
</svg>
|
||||
"Inactive"
|
||||
<span class="ml-auto text-xs font-mono opacity-70">{inactive_count}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t border-base-300 bg-base-200/50">
|
||||
<div class="p-4 border-t border-border bg-card">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="avatar">
|
||||
<div class="w-8 rounded-full bg-neutral text-neutral-content ring ring-primary ring-offset-base-100 ring-offset-1">
|
||||
<span class="text-sm font-bold flex items-center justify-center h-full">{first_letter}</span>
|
||||
</div>
|
||||
<div class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-full bg-muted">
|
||||
<span class="flex h-full w-full items-center justify-center rounded-full bg-primary text-primary-foreground text-xs font-medium">
|
||||
{first_letter}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div class="font-bold text-sm truncate">{username}</div>
|
||||
<div class="text-[10px] text-base-content/60 truncate">"Online"</div>
|
||||
<div class="font-medium text-sm truncate text-foreground">{username}</div>
|
||||
<div class="text-[10px] text-muted-foreground truncate">"Online"</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-ghost btn-xs btn-square text-error hover:bg-error/10"
|
||||
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-8 w-8 text-destructive"
|
||||
title="Logout"
|
||||
on:click=handle_logout
|
||||
>
|
||||
|
||||
@@ -43,6 +43,12 @@ pub fn StatusBar() -> impl IntoView {
|
||||
let theme = current_theme.get().to_lowercase();
|
||||
if let Some(doc) = document().document_element() {
|
||||
let _ = doc.set_attribute("data-theme", &theme);
|
||||
// Also set class for Shadcn dark mode support
|
||||
if theme == "dark" || theme == "dracula" || theme == "dim" || theme == "abyss" {
|
||||
let _ = doc.class_list().add_1("dark");
|
||||
} else {
|
||||
let _ = doc.class_list().remove_1("dark");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -94,11 +100,11 @@ pub fn StatusBar() -> impl IntoView {
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="fixed bottom-0 left-0 right-0 h-8 min-h-8 bg-base-200 border-t border-base-300 flex items-center px-4 text-xs gap-4 text-base-content/70 z-[99] cursor-pointer">
|
||||
<div class="fixed bottom-0 left-0 right-0 h-8 min-h-8 bg-muted border-t border-border flex items-center px-4 text-xs gap-4 text-muted-foreground z-[99] cursor-pointer">
|
||||
|
||||
// --- DOWNLOAD SPEED DROPDOWN ---
|
||||
<details class="dropdown dropdown-top" node_ref=down_details_ref>
|
||||
<summary class="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors select-none list-none [&::-webkit-details-marker]:hidden outline-none">
|
||||
<details class="group relative" node_ref=down_details_ref>
|
||||
<summary class="flex items-center gap-2 cursor-pointer hover:text-foreground transition-colors select-none list-none [&::-webkit-details-marker]:hidden outline-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3" />
|
||||
</svg>
|
||||
@@ -110,37 +116,44 @@ pub fn StatusBar() -> impl IntoView {
|
||||
</Show>
|
||||
</summary>
|
||||
|
||||
<ul class="dropdown-content z-[100] menu p-2 shadow bg-base-200 rounded-box w-40 mb-2 border border-base-300">
|
||||
{
|
||||
limits.clone().into_iter().map(|(val, label)| {
|
||||
let is_active = move || {
|
||||
let current = stats.get().down_limit.unwrap_or(0);
|
||||
(current - val).abs() < 1024
|
||||
};
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
class=move || if is_active() { "bg-primary/10 text-primary font-bold text-xs flex justify-between" } else { "text-xs flex justify-between" }
|
||||
on:click=move |_| {
|
||||
set_limit("down", val);
|
||||
close_details(down_details_ref);
|
||||
}
|
||||
>
|
||||
{label}
|
||||
<Show when=is_active fallback=|| ()>
|
||||
<span>"✓"</span>
|
||||
</Show>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
<div class="absolute bottom-full left-0 mb-2 z-[100] min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md hidden group-open:block animate-in fade-in-0 zoom-in-95 slide-in-from-bottom-2">
|
||||
<ul class="w-full">
|
||||
{
|
||||
limits.clone().into_iter().map(|(val, label)| {
|
||||
let is_active = move || {
|
||||
let current = stats.get().down_limit.unwrap_or(0);
|
||||
(current - val).abs() < 1024
|
||||
};
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
class=move || {
|
||||
let base = "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-xs outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-accent hover:text-accent-foreground";
|
||||
if is_active() { format!("{} bg-accent text-accent-foreground font-medium", base) } else { base.to_string() }
|
||||
}
|
||||
on:click=move |_| {
|
||||
set_limit("down", val);
|
||||
close_details(down_details_ref);
|
||||
}
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Show when=is_active fallback=|| ()>
|
||||
<span>"✓"</span>
|
||||
</Show>
|
||||
</span>
|
||||
{label}
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
// --- UPLOAD SPEED DROPDOWN ---
|
||||
<details class="dropdown dropdown-top" node_ref=up_details_ref>
|
||||
<summary class="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors select-none list-none [&::-webkit-details-marker]:hidden outline-none">
|
||||
<details class="group relative" node_ref=up_details_ref>
|
||||
<summary class="flex items-center gap-2 cursor-pointer hover:text-foreground transition-colors select-none list-none [&::-webkit-details-marker]:hidden outline-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" />
|
||||
</svg>
|
||||
@@ -152,114 +165,95 @@ pub fn StatusBar() -> impl IntoView {
|
||||
</Show>
|
||||
</summary>
|
||||
|
||||
<ul class="dropdown-content z-[100] menu p-2 shadow bg-base-200 rounded-box w-40 mb-2 border border-base-300">
|
||||
{
|
||||
limits.clone().into_iter().map(|(val, label)| {
|
||||
let is_active = move || {
|
||||
let current = stats.get().up_limit.unwrap_or(0);
|
||||
(current - val).abs() < 1024
|
||||
};
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
class=move || if is_active() { "bg-primary/10 text-primary font-bold text-xs flex justify-between" } else { "text-xs flex justify-between" }
|
||||
on:click=move |_| {
|
||||
set_limit("up", val);
|
||||
close_details(up_details_ref);
|
||||
}
|
||||
>
|
||||
{label}
|
||||
<Show when=is_active fallback=|| ()>
|
||||
<span>"✓"</span>
|
||||
</Show>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<div class="ml-auto flex items-center gap-4">
|
||||
<details class="dropdown dropdown-top dropdown-end" node_ref=theme_details_ref>
|
||||
<summary class="btn btn-ghost btn-xs btn-square cursor-pointer outline-none list-none [&::-webkit-details-marker]:hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42" />
|
||||
</svg>
|
||||
</summary>
|
||||
|
||||
<ul class="dropdown-content z-[100] menu p-2 shadow bg-base-200 rounded-box w-52 mb-2 border border-base-300 max-h-96 overflow-y-auto">
|
||||
<div class="absolute bottom-full left-0 mb-2 z-[100] min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md hidden group-open:block animate-in fade-in-0 zoom-in-95 slide-in-from-bottom-2">
|
||||
<ul class="w-full">
|
||||
{
|
||||
let themes = vec![
|
||||
"light", "dark", "dim", "nord", "cupcake", "dracula", "cyberpunk", "emerald", "sunset", "abyss"
|
||||
];
|
||||
themes.into_iter().map(|theme| {
|
||||
let theme_name = theme.to_string();
|
||||
let theme_name_for_class = theme_name.clone();
|
||||
let theme_name_for_onclick = theme_name.clone();
|
||||
limits.clone().into_iter().map(|(val, label)| {
|
||||
let is_active = move || {
|
||||
let current = stats.get().up_limit.unwrap_or(0);
|
||||
(current - val).abs() < 1024
|
||||
};
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
class=move || if current_theme.get() == theme_name_for_class { "bg-primary/10 text-primary font-bold text-xs capitalize" } else { "text-xs capitalize" }
|
||||
class=move || {
|
||||
let base = "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-xs outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-accent hover:text-accent-foreground";
|
||||
if is_active() { format!("{} bg-accent text-accent-foreground font-medium", base) } else { base.to_string() }
|
||||
}
|
||||
on:click=move |_| {
|
||||
set_current_theme.set(theme_name_for_onclick.clone());
|
||||
close_details(theme_details_ref);
|
||||
set_limit("up", val);
|
||||
close_details(up_details_ref);
|
||||
}
|
||||
>
|
||||
{theme_name}
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Show when=is_active fallback=|| ()>
|
||||
<span>"✓"</span>
|
||||
</Show>
|
||||
</span>
|
||||
{label}
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="ml-auto flex items-center gap-4">
|
||||
<details class="group relative" node_ref=theme_details_ref>
|
||||
<summary class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7 cursor-pointer outline-none list-none [&::-webkit-details-marker]:hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42" />
|
||||
</svg>
|
||||
</summary>
|
||||
|
||||
<div class="absolute bottom-full right-0 mb-2 z-[100] min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md hidden group-open:block animate-in fade-in-0 zoom-in-95 slide-in-from-bottom-2 max-h-96 overflow-y-auto">
|
||||
<ul class="w-full">
|
||||
{
|
||||
let themes = vec![
|
||||
"light", "dark", "dim", "nord", "cupcake", "dracula", "cyberpunk", "emerald", "sunset", "abyss"
|
||||
];
|
||||
themes.into_iter().map(|theme| {
|
||||
let theme_name = theme.to_string();
|
||||
let theme_name_for_class = theme_name.clone();
|
||||
let theme_name_for_onclick = theme_name.clone();
|
||||
let is_active = move || current_theme.get() == theme_name_for_class;
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
class=move || {
|
||||
let base = "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-xs outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-accent hover:text-accent-foreground capitalize";
|
||||
if is_active() { format!("{} bg-accent text-accent-foreground font-medium", base) } else { base.to_string() }
|
||||
}
|
||||
on:click=move |_| {
|
||||
set_current_theme.set(theme_name_for_onclick.clone());
|
||||
close_details(theme_details_ref);
|
||||
}
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Show when=is_active fallback=|| ()>
|
||||
<span>"✓"</span>
|
||||
</Show>
|
||||
</span>
|
||||
{theme_name}
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
<button
|
||||
class="btn btn-ghost btn-xs btn-square"
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
|
||||
title="Settings & Notification Permissions"
|
||||
on:click=move |_| {
|
||||
// Request push notification permission when settings button is clicked
|
||||
// Request push notification permission
|
||||
leptos::task::spawn_local(async {
|
||||
log::info!("Settings button clicked - requesting push notification permission");
|
||||
|
||||
// Check current permission state before requesting
|
||||
let window = web_sys::window().expect("window should exist");
|
||||
let _current_perm = js_sys::Reflect::get(&window, &"Notification".into())
|
||||
.ok()
|
||||
.and_then(|n| js_sys::Reflect::get(&n, &"permission".into()).ok())
|
||||
.and_then(|p| p.as_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
// ... existing logic ...
|
||||
crate::store::subscribe_to_push_notifications().await;
|
||||
|
||||
// Check permission after request
|
||||
let new_perm = js_sys::Reflect::get(&window, &"Notification".into())
|
||||
.ok()
|
||||
.and_then(|n| js_sys::Reflect::get(&n, &"permission".into()).ok())
|
||||
.and_then(|p| p.as_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(store) = use_context::<crate::store::TorrentStore>() {
|
||||
if new_perm == "granted" {
|
||||
crate::store::show_toast_with_signal(
|
||||
store.notifications,
|
||||
shared::NotificationLevel::Success,
|
||||
"Bildirimler etkinleştirildi! Torrent tamamlandığında bildirim alacaksınız.".to_string(),
|
||||
);
|
||||
} else if new_perm == "denied" {
|
||||
crate::store::show_toast_with_signal(
|
||||
store.notifications,
|
||||
shared::NotificationLevel::Error,
|
||||
"Bildirim izni reddedildi. Tarayıcı ayarlarından izin verebilirsiniz.".to_string(),
|
||||
);
|
||||
} else {
|
||||
crate::store::show_toast_with_signal(
|
||||
store.notifications,
|
||||
shared::NotificationLevel::Warning,
|
||||
"Bildirim izni verilemedi. Açılan izin penceresinde 'İzin Ver' seçeneğini seçin.".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
// ... existing logic ...
|
||||
});
|
||||
}
|
||||
>
|
||||
|
||||
@@ -7,15 +7,16 @@ pub fn Toolbar() -> impl IntoView {
|
||||
let store = use_context::<crate::store::TorrentStore>().expect("store not provided");
|
||||
|
||||
view! {
|
||||
<div class="navbar min-h-14 h-auto bg-base-100 p-0" style="padding-top: env(safe-area-inset-top);">
|
||||
<div class="navbar-start gap-4 px-4">
|
||||
<label for="my-drawer" class="btn btn-square btn-ghost lg:hidden drawer-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-5 h-5 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
||||
</label>
|
||||
<div class="flex min-h-14 h-auto items-center border-b border-border bg-background px-4" style="padding-top: env(safe-area-inset-top);">
|
||||
<div class="flex flex-1 items-center gap-4">
|
||||
// Mobile Menu Trigger (Sheet Trigger in full impl)
|
||||
<button id="mobile-sheet-trigger" class="lg:hidden inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-10 w-10">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-5 h-5 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
class="btn btn-primary btn-sm md:btn-md gap-2 shadow-md hover:shadow-primary/20 transition-all"
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 py-2 shadow gap-2"
|
||||
on:click=move |_| show_add_modal.1.set(true)
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 md:w-5 md:h-5">
|
||||
@@ -27,29 +28,27 @@ pub fn Toolbar() -> impl IntoView {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-center hidden md:flex">
|
||||
<div class="join shadow-sm border border-base-200">
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
class="input input-sm input-bordered join-item w-full max-w-xs focus:outline-none"
|
||||
prop:value=move || store.search_query.get()
|
||||
on:input=move |ev| store.search_query.set(event_target_value(&ev))
|
||||
/>
|
||||
<Show when=move || !store.search_query.get().is_empty()>
|
||||
<button
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 btn btn-ghost btn-xs btn-circle"
|
||||
on:click=move |_| store.search_query.set(String::new())
|
||||
>
|
||||
"×"
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center justify-center flex-1">
|
||||
<div class="relative w-full max-w-sm">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
prop:value=move || store.search_query.get()
|
||||
on:input=move |ev| store.search_query.set(event_target_value(&ev))
|
||||
/>
|
||||
<Show when=move || !store.search_query.get().is_empty()>
|
||||
<button
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full text-xs font-medium hover:bg-muted h-5 w-5 opacity-50 hover:opacity-100 transition-opacity"
|
||||
on:click=move |_| store.search_query.set(String::new())
|
||||
>
|
||||
"×"
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end px-4 gap-2">
|
||||
<div class="flex flex-1 justify-end px-4 gap-2">
|
||||
<Show when=move || show_add_modal.0.get()>
|
||||
<AddTorrentDialog on_close=Callback::new(move |()| show_add_modal.1.set(false)) />
|
||||
</Show>
|
||||
|
||||
Reference in New Issue
Block a user