frontend(statusbar): use signal-based dropdowns to fix iOS Safari toggle
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
use leptos::*;
|
use leptos::*;
|
||||||
use shared::GlobalLimitRequest;
|
use shared::GlobalLimitRequest;
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
|
|
||||||
fn format_bytes(bytes: i64) -> String {
|
fn format_bytes(bytes: i64) -> String {
|
||||||
const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
|
const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||||
@@ -110,64 +109,41 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to close all details dropdowns
|
// Signal-based dropdown state: 0=none, 1=download, 2=upload, 3=theme
|
||||||
let close_dropdown = move || {
|
let (active_dropdown, set_active_dropdown) = create_signal(0u8);
|
||||||
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
|
|
||||||
// Close all <details> elements in the status bar
|
// Toggle a specific dropdown
|
||||||
let details_list = doc.query_selector_all("details.dropdown[open]");
|
let toggle = move |id: u8| {
|
||||||
if let Ok(nodes) = details_list {
|
if active_dropdown.get_untracked() == id {
|
||||||
for i in 0..nodes.length() {
|
set_active_dropdown.set(0);
|
||||||
if let Some(node) = nodes.item(i) {
|
} else {
|
||||||
if let Ok(el) = node.dyn_into::<web_sys::Element>() {
|
set_active_dropdown.set(id);
|
||||||
el.remove_attribute("open").ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Also blur active element as fallback
|
|
||||||
if let Some(active) = doc.active_element() {
|
|
||||||
let _ = active
|
|
||||||
.dyn_into::<web_sys::HtmlElement>()
|
|
||||||
.map(|el| el.blur());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Toggle dropdown: close all, the <details> native toggle handles opening
|
// Close all dropdowns
|
||||||
let toggle_dropdown = move |_id: u8| {
|
let close_all = move || {
|
||||||
// No-op: <details> handles its own open/close natively
|
set_active_dropdown.set(0);
|
||||||
// We just need this for the close_dropdown helper
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Global listener to close dropdowns on touchstart outside
|
// Close dropdowns when tapping outside
|
||||||
let force_blur = move |_| {
|
let _ = window_event_listener(ev::pointerdown, move |_| {
|
||||||
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
|
// This fires first; dropdown buttons call stop_propagation to prevent closing
|
||||||
let details_list = doc.query_selector_all("details.dropdown[open]");
|
set_active_dropdown.set(0);
|
||||||
if let Ok(nodes) = details_list {
|
});
|
||||||
for i in 0..nodes.length() {
|
|
||||||
if let Some(node) = nodes.item(i) {
|
|
||||||
if let Ok(el) = node.dyn_into::<web_sys::Element>() {
|
|
||||||
el.remove_attribute("open").ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let _ = window_event_listener(ev::touchstart, force_blur);
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="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">
|
<div class="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">
|
||||||
|
|
||||||
// --- DOWNLOAD SPEED DROPDOWN ---
|
// --- DOWNLOAD SPEED DROPDOWN ---
|
||||||
<details
|
<div class="relative">
|
||||||
class="dropdown dropdown-top dropdown-start"
|
<div
|
||||||
on:touchstart=move |e| e.stop_propagation()
|
class="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors select-none"
|
||||||
>
|
|
||||||
<summary
|
|
||||||
class="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors select-none list-none [&::-webkit-details-marker]:hidden"
|
|
||||||
title="Global Download Speed - Click to Set Limit"
|
title="Global Download Speed - Click to Set Limit"
|
||||||
on:click=move |_| toggle_dropdown(1)
|
on:pointerdown=move |e| {
|
||||||
|
e.stop_propagation();
|
||||||
|
toggle(1);
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3" />
|
||||||
@@ -178,23 +154,26 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
{move || format!("(Limit: {})", format_speed(stats.get().down_limit.unwrap_or(0)))}
|
{move || format!("(Limit: {})", format_speed(stats.get().down_limit.unwrap_or(0)))}
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</summary>
|
</div>
|
||||||
|
|
||||||
<ul class="dropdown-content z-[100] menu p-2 shadow bg-base-200 rounded-box w-40 mb-2 border border-base-300">
|
<ul
|
||||||
|
class="absolute bottom-full left-0 z-[100] menu p-2 shadow bg-base-200 rounded-box w-40 mb-2 border border-base-300"
|
||||||
|
style=move || if active_dropdown.get() == 1 { "display: block" } else { "display: none" }
|
||||||
|
on:pointerdown=move |e| e.stop_propagation()
|
||||||
|
>
|
||||||
{
|
{
|
||||||
limits.clone().into_iter().map(|(val, label)| {
|
limits.clone().into_iter().map(|(val, label)| {
|
||||||
let is_active = move || {
|
let is_active = move || {
|
||||||
let current = stats.get().down_limit.unwrap_or(0);
|
let current = stats.get().down_limit.unwrap_or(0);
|
||||||
(current - val).abs() < 1024
|
(current - val).abs() < 1024
|
||||||
};
|
};
|
||||||
let close = close_dropdown.clone();
|
|
||||||
view! {
|
view! {
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
class=move || if is_active() { "bg-primary/10 text-primary font-bold text-xs flex justify-between" } else { "text-xs flex justify-between" }
|
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 |_| {
|
on:click=move |_| {
|
||||||
set_limit("down", val);
|
set_limit("down", val);
|
||||||
close();
|
close_all();
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@@ -207,17 +186,17 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
}).collect::<Vec<_>>()
|
}).collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</div>
|
||||||
|
|
||||||
// --- UPLOAD SPEED DROPDOWN ---
|
// --- UPLOAD SPEED DROPDOWN ---
|
||||||
<details
|
<div class="relative">
|
||||||
class="dropdown dropdown-top dropdown-start"
|
<div
|
||||||
on:touchstart=move |e| e.stop_propagation()
|
class="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors select-none"
|
||||||
>
|
|
||||||
<summary
|
|
||||||
class="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors select-none list-none [&::-webkit-details-marker]:hidden"
|
|
||||||
title="Global Upload Speed - Click to Set Limit"
|
title="Global Upload Speed - Click to Set Limit"
|
||||||
on:click=move |_| toggle_dropdown(2)
|
on:pointerdown=move |e| {
|
||||||
|
e.stop_propagation();
|
||||||
|
toggle(2);
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" />
|
||||||
@@ -228,23 +207,26 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
{move || format!("(Limit: {})", format_speed(stats.get().up_limit.unwrap_or(0)))}
|
{move || format!("(Limit: {})", format_speed(stats.get().up_limit.unwrap_or(0)))}
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</summary>
|
</div>
|
||||||
|
|
||||||
<ul class="dropdown-content z-[100] menu p-2 shadow bg-base-200 rounded-box w-40 mb-2 border border-base-300">
|
<ul
|
||||||
|
class="absolute bottom-full left-0 z-[100] menu p-2 shadow bg-base-200 rounded-box w-40 mb-2 border border-base-300"
|
||||||
|
style=move || if active_dropdown.get() == 2 { "display: block" } else { "display: none" }
|
||||||
|
on:pointerdown=move |e| e.stop_propagation()
|
||||||
|
>
|
||||||
{
|
{
|
||||||
limits.clone().into_iter().map(|(val, label)| {
|
limits.clone().into_iter().map(|(val, label)| {
|
||||||
let is_active = move || {
|
let is_active = move || {
|
||||||
let current = stats.get().up_limit.unwrap_or(0);
|
let current = stats.get().up_limit.unwrap_or(0);
|
||||||
(current - val).abs() < 1024
|
(current - val).abs() < 1024
|
||||||
};
|
};
|
||||||
let close = close_dropdown.clone();
|
|
||||||
view! {
|
view! {
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
class=move || if is_active() { "bg-primary/10 text-primary font-bold text-xs flex justify-between" } else { "text-xs flex justify-between" }
|
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 |_| {
|
on:click=move |_| {
|
||||||
set_limit("up", val);
|
set_limit("up", val);
|
||||||
close();
|
close_all();
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@@ -257,31 +239,33 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
}).collect::<Vec<_>>()
|
}).collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</div>
|
||||||
|
|
||||||
<div class="ml-auto flex items-center gap-4">
|
<div class="ml-auto flex items-center gap-4">
|
||||||
<details
|
<div class="relative">
|
||||||
class="dropdown dropdown-top dropdown-end"
|
<div
|
||||||
on:touchstart=move |e| e.stop_propagation()
|
class="btn btn-ghost btn-xs btn-square"
|
||||||
>
|
|
||||||
<summary
|
|
||||||
class="btn btn-ghost btn-xs btn-square list-none [&::-webkit-details-marker]:hidden"
|
|
||||||
title="Change Theme"
|
title="Change Theme"
|
||||||
on:click=move |_| toggle_dropdown(3)
|
on:pointerdown=move |e| {
|
||||||
|
e.stop_propagation();
|
||||||
|
toggle(3);
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
</summary>
|
</div>
|
||||||
|
|
||||||
<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">
|
<ul
|
||||||
|
class="absolute bottom-full right-0 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"
|
||||||
|
style=move || if active_dropdown.get() == 3 { "display: block" } else { "display: none" }
|
||||||
|
on:pointerdown=move |e| e.stop_propagation()
|
||||||
|
>
|
||||||
{
|
{
|
||||||
let themes = vec![
|
let themes = vec![
|
||||||
"light", "dark", "dim", "nord", "cupcake", "dracula", "cyberpunk", "emerald", "sunset", "abyss"
|
"light", "dark", "dim", "nord", "cupcake", "dracula", "cyberpunk", "emerald", "sunset", "abyss"
|
||||||
];
|
];
|
||||||
let close = close_dropdown.clone();
|
|
||||||
themes.into_iter().map(|theme| {
|
themes.into_iter().map(|theme| {
|
||||||
let close = close.clone();
|
|
||||||
view! {
|
view! {
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
@@ -296,7 +280,7 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
let _ = storage.set_item("vibetorrent_theme", theme);
|
let _ = storage.set_item("vibetorrent_theme", theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close();
|
close_all();
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{theme}
|
{theme}
|
||||||
@@ -306,7 +290,7 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
}).collect::<Vec<_>>()
|
}).collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost btn-xs btn-square"
|
class="btn btn-ghost btn-xs btn-square"
|
||||||
|
|||||||
Reference in New Issue
Block a user