Compare commits

..

8 Commits

Author SHA1 Message Date
spinline
7169e44f4e fix: incorrect import of on_click_outside in statusbar.rs
All checks were successful
Build MIPS Binary / build (push) Successful in 4m28s
2026-02-08 21:00:32 +03:00
spinline
51fb85c2d8 fix: iOS Safari uyumluluğu için menü yapıları ve click bubbling sorunları giderildi
Some checks failed
Build MIPS Binary / build (push) Failing after 1m8s
2026-02-08 20:58:14 +03:00
spinline
46882caea0 fix: mobilde menülerin açılmama sorunu HTML details/summary yapısı ile kökten çözüldü
All checks were successful
Build MIPS Binary / build (push) Successful in 4m31s
2026-02-08 20:47:42 +03:00
spinline
e339ca1c49 fix: menülerin dışarıya tıklayınca kapanmama sorunu giderildi (stop_propagation kaldırıldı ve click event'ine geçildi)
All checks were successful
Build MIPS Binary / build (push) Successful in 4m34s
2026-02-08 20:35:44 +03:00
spinline
a08fd9698f refactor: açılır menüler leptos-use::on_click_outside ile modernize edildi, şeffaf backdrop katmanları kaldırıldı
All checks were successful
Build MIPS Binary / build (push) Successful in 5m52s
2026-02-08 20:16:05 +03:00
spinline
7d46dbd437 refactor: tema yönetimi leptos-use::use_local_storage ile reaktif hale getirildi
All checks were successful
Build MIPS Binary / build (push) Successful in 4m28s
2026-02-08 20:02:01 +03:00
spinline
5f107299e3 refactor: long press mantığı leptos-use::use_timeout_fn ile modernize edildi
All checks were successful
Build MIPS Binary / build (push) Successful in 4m29s
2026-02-08 19:53:10 +03:00
spinline
c34133ded1 refactor: manuel Closure ve timer yönetimi Leptos set_timeout ile değiştirildi
All checks were successful
Build MIPS Binary / build (push) Successful in 4m16s
2026-02-08 19:39:38 +03:00
5 changed files with 105 additions and 212 deletions

View File

@@ -22,35 +22,11 @@ wasm-bindgen-futures = "0.4"
uuid = { version = "1", features = ["v4", "js"] } uuid = { version = "1", features = ["v4", "js"] }
futures = "0.3" futures = "0.3"
chrono = { version = "0.4", features = ["serde", "wasm-bindgen"] } chrono = { version = "0.4", features = ["serde", "wasm-bindgen"] }
web-sys = { version = "0.3", features = [ web-sys = { version = "0.3", features = ["HtmlDivElement", "HtmlUListElement", "HtmlLiElement", "HtmlAnchorElement", "MouseEvent", "Event", "Window", "Document", "Element", "DomTokenList", "CssStyleDeclaration", "Storage", "TouchEvent", "TouchList", "Touch", "Navigator", "Notification", "NotificationOptions", "NotificationPermission", "ServiceWorkerContainer", "ServiceWorkerRegistration", "PushManager", "PushSubscription", "PushSubscriptionOptions", "PushSubscriptionOptionsInit", "HtmlDetailsElement"] }
"HtmlDivElement",
"HtmlUListElement",
"HtmlLiElement",
"HtmlAnchorElement",
"MouseEvent",
"Event",
"Window",
"Document",
"Element",
"DomTokenList",
"CssStyleDeclaration",
"Storage",
"TouchEvent",
"TouchList",
"Touch",
"Navigator",
"Notification",
"NotificationOptions",
"NotificationPermission",
"ServiceWorkerContainer",
"ServiceWorkerRegistration",
"PushManager",
"PushSubscription",
"PushSubscriptionOptions",
"PushSubscriptionOptionsInit"
] }
shared = { path = "../shared" } shared = { path = "../shared" }
tailwind_fuse = "0.3.2" tailwind_fuse = "0.3.2"
js-sys = "0.3.85" js-sys = "0.3.85"
base64 = "0.22.1" base64 = "0.22.1"
serde-wasm-bindgen = "0.6.5" serde-wasm-bindgen = "0.6.5"
leptos-use = "0.13"
codee = "0.2"

View File

@@ -81,7 +81,7 @@
</script> </script>
</head> </head>
<body> <body style="cursor: pointer;">
<div <div
id="app-loading" id="app-loading"
style=" style="

View File

@@ -1,4 +1,5 @@
use leptos::*; use leptos::*;
use leptos_use::on_click_outside;
#[component] #[component]
pub fn ContextMenu( pub fn ContextMenu(
@@ -8,6 +9,10 @@ pub fn ContextMenu(
on_close: Callback<()>, on_close: Callback<()>,
on_action: Callback<(String, String)>, // (Action, Hash) on_action: Callback<(String, String)>, // (Action, Hash)
) -> impl IntoView { ) -> impl IntoView {
let container_ref = create_node_ref::<html::Div>();
let _ = on_click_outside(container_ref, move |_| on_close.call(()));
let handle_action = move |action: &str| { let handle_action = move |action: &str| {
let hash = torrent_hash.clone(); let hash = torrent_hash.clone();
let action_str = action.to_string(); let action_str = action.to_string();
@@ -22,16 +27,8 @@ pub fn ContextMenu(
} }
view! { view! {
// Backdrop to catch clicks outside
<div
class="fixed inset-0 z-[99] cursor-default"
role="button"
tabindex="-1"
on:click=move |_| on_close.call(())
on:contextmenu=move |e| e.prevent_default()
></div>
<div <div
node_ref=container_ref
class="fixed z-[100] min-w-[200px] animate-in fade-in zoom-in-95 duration-100" class="fixed z-[100] min-w-[200px] animate-in fade-in zoom-in-95 duration-100"
style=format!("left: {}px; top: {}px", position.0, position.1) style=format!("left: {}px; top: {}px", position.0, position.1)
on:contextmenu=move |e| e.prevent_default() on:contextmenu=move |e| e.prevent_default()

View File

@@ -1,4 +1,7 @@
use leptos::*; use leptos::*;
use leptos_use::on_click_outside;
use leptos_use::storage::use_local_storage;
use codee::string::FromToStringCodec;
use shared::GlobalLimitRequest; use shared::GlobalLimitRequest;
fn format_bytes(bytes: i64) -> String { fn format_bytes(bytes: i64) -> String {
@@ -26,34 +29,19 @@ pub fn StatusBar() -> impl IntoView {
let store = use_context::<crate::store::TorrentStore>().expect("store not provided"); let store = use_context::<crate::store::TorrentStore>().expect("store not provided");
let stats = store.global_stats; let stats = store.global_stats;
let initial_theme = if let Some(win) = web_sys::window() { // Use leptos-use for reactive localStorage management
if let Some(doc) = win.document() { let (current_theme, set_current_theme, _) = use_local_storage::<String, FromToStringCodec>("vibetorrent_theme");
doc.document_element()
.and_then(|el| el.get_attribute("data-theme")) // Initialize with default if empty
.unwrap_or_else(|| "dark".to_string()) if current_theme.get_untracked().is_empty() {
} else { set_current_theme.set("dark".to_string());
"dark".to_string()
} }
} else {
"dark".to_string()
};
let (current_theme, set_current_theme) = create_signal(initial_theme);
// Automatically sync theme to document attribute
create_effect(move |_| { create_effect(move |_| {
if let Some(win) = web_sys::window() { let theme = current_theme.get().to_lowercase();
if let Some(storage) = win.local_storage().ok().flatten() { if let Some(doc) = document().document_element() {
if let Ok(Some(stored_theme)) = storage.get_item("vibetorrent_theme") { let _ = doc.set_attribute("data-theme", &theme);
let theme = stored_theme.to_lowercase();
set_current_theme.set(theme.clone());
if let Some(doc) = win.document() {
let _ = doc
.document_element()
.unwrap()
.set_attribute("data-theme", &theme);
}
}
}
} }
}); });
@@ -109,45 +97,28 @@ pub fn StatusBar() -> impl IntoView {
}); });
}; };
// Signal-based dropdown state: 0=none, 1=download, 2=upload, 3=theme // Refs for click outside detection
let (active_dropdown, set_active_dropdown) = create_signal(0u8); let down_details_ref = create_node_ref::<html::Details>();
let up_details_ref = create_node_ref::<html::Details>();
let theme_details_ref = create_node_ref::<html::Details>();
// Toggle a specific dropdown // Helper to close a details element
let toggle = move |id: u8| { let close_details = |node_ref: NodeRef<html::Details>| {
let current = active_dropdown.get_untracked(); if let Some(el) = node_ref.get_untracked() {
if current == id { el.set_open(false);
set_active_dropdown.set(0);
} else {
set_active_dropdown.set(id);
} }
}; };
// Close all dropdowns let _ = on_click_outside(down_details_ref, move |_| close_details(down_details_ref));
let close_all = move || { let _ = on_click_outside(up_details_ref, move |_| close_details(up_details_ref));
set_active_dropdown.set(0); let _ = on_click_outside(theme_details_ref, move |_| close_details(theme_details_ref));
};
view! { view! {
// Transparent overlay to close dropdowns when clicking outside
<Show when=move || active_dropdown.get() != 0>
<div
class="fixed inset-0 z-[98] cursor-default"
on:pointerdown=move |_| close_all()
></div>
</Show>
<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]"> <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]">
// --- DOWNLOAD SPEED DROPDOWN --- // --- DOWNLOAD SPEED DROPDOWN ---
<div class="relative"> <details class="dropdown dropdown-top" node_ref=down_details_ref>
<div <summary class="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors select-none list-none [&::-webkit-details-marker]:hidden outline-none">
class="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors select-none"
title="Global Download Speed - Click to Set Limit"
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" />
</svg> </svg>
@@ -157,12 +128,9 @@ 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>
</div> </summary>
<ul <ul class="dropdown-content z-[100] menu p-2 shadow bg-base-200 rounded-box w-40 mb-2 border border-base-300">
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" }
>
{ {
limits.clone().into_iter().map(|(val, label)| { limits.clone().into_iter().map(|(val, label)| {
let is_active = move || { let is_active = move || {
@@ -173,10 +141,9 @@ pub fn StatusBar() -> impl IntoView {
<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:pointerdown=move |e| { on:click=move |_| {
e.stop_propagation();
set_limit("down", val); set_limit("down", val);
close_all(); close_details(down_details_ref);
} }
> >
{label} {label}
@@ -189,18 +156,11 @@ pub fn StatusBar() -> impl IntoView {
}).collect::<Vec<_>>() }).collect::<Vec<_>>()
} }
</ul> </ul>
</div> </details>
// --- UPLOAD SPEED DROPDOWN --- // --- UPLOAD SPEED DROPDOWN ---
<div class="relative"> <details class="dropdown dropdown-top" node_ref=up_details_ref>
<div <summary class="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors select-none list-none [&::-webkit-details-marker]:hidden outline-none">
class="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors select-none"
title="Global Upload Speed - Click to Set Limit"
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" />
</svg> </svg>
@@ -210,12 +170,9 @@ 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>
</div> </summary>
<ul <ul class="dropdown-content z-[100] menu p-2 shadow bg-base-200 rounded-box w-40 mb-2 border border-base-300">
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" }
>
{ {
limits.clone().into_iter().map(|(val, label)| { limits.clone().into_iter().map(|(val, label)| {
let is_active = move || { let is_active = move || {
@@ -226,10 +183,9 @@ pub fn StatusBar() -> impl IntoView {
<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:pointerdown=move |e| { on:click=move |_| {
e.stop_propagation();
set_limit("up", val); set_limit("up", val);
close_all(); close_details(up_details_ref);
} }
> >
{label} {label}
@@ -242,27 +198,17 @@ pub fn StatusBar() -> impl IntoView {
}).collect::<Vec<_>>() }).collect::<Vec<_>>()
} }
</ul> </ul>
</div> </details>
<div class="ml-auto flex items-center gap-4"> <div class="ml-auto flex items-center gap-4">
<div class="relative"> <details class="dropdown dropdown-top dropdown-end" node_ref=theme_details_ref>
<div <summary class="btn btn-ghost btn-xs btn-square cursor-pointer outline-none list-none [&::-webkit-details-marker]:hidden">
class="btn btn-ghost btn-xs btn-square"
title="Change Theme"
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>
</div> </summary>
<ul <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">
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" }
>
{ {
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"
@@ -272,18 +218,9 @@ pub fn StatusBar() -> impl IntoView {
<li> <li>
<button <button
class=move || if current_theme.get() == theme { "bg-primary/10 text-primary font-bold text-xs capitalize" } else { "text-xs capitalize" } class=move || if current_theme.get() == theme { "bg-primary/10 text-primary font-bold text-xs capitalize" } else { "text-xs capitalize" }
on:pointerdown=move |e| { on:click=move |_| {
e.stop_propagation();
set_current_theme.set(theme.to_string()); set_current_theme.set(theme.to_string());
if let Some(win) = web_sys::window() { close_details(theme_details_ref);
if let Some(doc) = win.document() {
let _ = doc.document_element().unwrap().set_attribute("data-theme", theme);
}
if let Some(storage) = win.local_storage().ok().flatten() {
let _ = storage.set_item("vibetorrent_theme", theme);
}
}
close_all();
} }
> >
{theme} {theme}
@@ -293,7 +230,7 @@ pub fn StatusBar() -> impl IntoView {
}).collect::<Vec<_>>() }).collect::<Vec<_>>()
} }
</ul> </ul>
</div> </details>
<button <button
class="btn btn-ghost btn-xs btn-square" class="btn btn-ghost btn-xs btn-square"
title="Settings & Notification Permissions" title="Settings & Notification Permissions"

View File

@@ -1,6 +1,5 @@
use leptos::*; use leptos::*;
use wasm_bindgen::closure::Closure; use leptos_use::{on_click_outside, use_timeout_fn};
use wasm_bindgen::JsCast;
use crate::store::{get_action_messages, show_toast_with_signal}; use crate::store::{get_action_messages, show_toast_with_signal};
use shared::NotificationLevel; use shared::NotificationLevel;
@@ -165,8 +164,13 @@ pub fn TorrentTable() -> impl IntoView {
} }
}; };
// Signal-based sort dropdown for mobile // Refs for click outside detection
let (sort_open, set_sort_open) = create_signal(false); let sort_details_ref = create_node_ref::<html::Details>();
let _ = on_click_outside(sort_details_ref, move |_| {
if let Some(el) = sort_details_ref.get_untracked() {
el.set_open(false);
}
});
let sort_arrow = move |col: SortColumn| { let sort_arrow = move |col: SortColumn| {
if sort_col.get() == col { if sort_col.get() == col {
@@ -345,25 +349,16 @@ pub fn TorrentTable() -> impl IntoView {
<div class="px-3 py-2 border-b border-base-200 flex justify-between items-center bg-base-100/95 backdrop-blur z-10 shrink-0"> <div class="px-3 py-2 border-b border-base-200 flex justify-between items-center bg-base-100/95 backdrop-blur z-10 shrink-0">
<span class="text-xs font-bold opacity-50 uppercase tracking-wider">"Torrents"</span> <span class="text-xs font-bold opacity-50 uppercase tracking-wider">"Torrents"</span>
<div class="relative"> <details class="dropdown dropdown-end" node_ref=sort_details_ref>
<div <summary class="flex items-center gap-1 opacity-70 cursor-pointer list-none [&::-webkit-details-marker]:hidden">
role="button" <div class="btn btn-ghost btn-xs gap-1 font-normal pointer-events-none">
class="btn btn-ghost btn-xs gap-1 opacity-70 font-normal"
on:pointerdown=move |e| {
e.stop_propagation();
let cur = sort_open.get_untracked();
set_sort_open.set(!cur);
}
>
<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="M3 7.5L7.5 3m0 0L12 7.5M7.5 3v13.5m13.5 0L16.5 21m0 0L12 16.5m4.5 4.5V7.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="M3 7.5L7.5 3m0 0L12 7.5M7.5 3v13.5m13.5 0L16.5 21m0 0L12 16.5m4.5 4.5V7.5" />
</svg> </svg>
"Sort" "Sort"
</div> </div>
<ul </summary>
class="absolute top-full right-0 z-[100] menu p-2 shadow bg-base-100 rounded-box w-48 mt-1 border border-base-200 text-xs" <ul class="dropdown-content z-[100] menu p-2 shadow bg-base-100 rounded-box w-48 mt-1 border border-base-200 text-xs">
style=move || if sort_open.get() { "display: block" } else { "display: none" }
>
<li class="menu-title px-2 py-1 opacity-50 text-[10px] uppercase font-bold">"Sort By"</li> <li class="menu-title px-2 py-1 opacity-50 text-[10px] uppercase font-bold">"Sort By"</li>
{ {
let columns = vec![ let columns = vec![
@@ -386,10 +381,11 @@ pub fn TorrentTable() -> impl IntoView {
<button <button
type="button" type="button"
class=move || if is_active() { "bg-primary/10 text-primary font-bold flex justify-between" } else { "flex justify-between" } class=move || if is_active() { "bg-primary/10 text-primary font-bold flex justify-between" } else { "flex justify-between" }
on:pointerdown=move |e| { on:click=move |_| {
e.stop_propagation();
handle_sort(col); handle_sort(col);
set_sort_open.set(false); if let Some(el) = sort_details_ref.get_untracked() {
el.set_open(false);
}
} }
> >
{label} {label}
@@ -407,7 +403,7 @@ pub fn TorrentTable() -> impl IntoView {
}).collect::<Vec<_>>() }).collect::<Vec<_>>()
} }
</ul> </ul>
</div> </details>
</div> </div>
<div class="overflow-y-auto p-3 pb-20 flex-1 grid grid-cols-1 content-start gap-3"> {move || filtered_torrents().into_iter().map(|t| { <div class="overflow-y-auto p-3 pb-20 flex-1 grid grid-cols-1 content-start gap-3"> {move || filtered_torrents().into_iter().map(|t| {
@@ -423,58 +419,45 @@ pub fn TorrentTable() -> impl IntoView {
let _t_hash = t.hash.clone(); let _t_hash = t.hash.clone();
let t_hash_click = t.hash.clone(); let t_hash_click = t.hash.clone();
let (timer_id, set_timer_id) = create_signal(Option::<i32>::None);
let t_hash_long = t.hash.clone(); let t_hash_long = t.hash.clone();
let leptos_use::UseTimeoutFnReturn { start, stop, .. } = use_timeout_fn(
let clear_timer = move || { move |pos: (i32, i32)| {
if let Some(id) = timer_id.get_untracked() { set_menu_position.set(pos);
window().clear_timeout_with_handle(id); set_selected_hash.set(Some(t_hash_long.clone()));
set_timer_id.set(None);
}
};
let handle_touchstart = {
let t_hash = t_hash_long.clone();
move |e: web_sys::TouchEvent| {
clear_timer();
if let Some(touch) = e.touches().get(0) {
let x = touch.client_x();
let y = touch.client_y();
let hash = t_hash.clone();
let closure = Closure::wrap(Box::new(move || {
set_menu_position.set((x, y));
set_selected_hash.set(Some(hash.clone()));
set_menu_visible.set(true); set_menu_visible.set(true);
// Haptic feedback (iOS Safari doesn't support vibrate) // Haptic feedback
let navigator = window().navigator(); let navigator = window().navigator();
if js_sys::Reflect::has(&navigator, &wasm_bindgen::JsValue::from_str("vibrate")).unwrap_or(false) { if let Ok(vibrate) = js_sys::Reflect::get(&navigator, &"vibrate".into()) {
if vibrate.is_function() {
let _ = navigator.vibrate_with_duration(50); let _ = navigator.vibrate_with_duration(50);
} }
}) as Box<dyn Fn()>); }
},
600.0,
);
let id = window() let handle_touchstart = {
.set_timeout_with_callback_and_timeout_and_arguments_0( let start = start.clone();
closure.as_ref().unchecked_ref(), move |e: web_sys::TouchEvent| {
600 if let Some(touch) = e.touches().get(0) {
) start((touch.client_x(), touch.client_y()));
.unwrap_or(0);
closure.forget();
set_timer_id.set(Some(id));
} }
} }
}; };
let handle_touchmove = move |_| { let handle_touchmove = {
clear_timer(); let stop = stop.clone();
move |_| stop()
}; };
let handle_touchend = move |_| { let handle_touchend = {
clear_timer(); let stop = stop.clone();
move |_| stop()
}; };
let handle_touchcancel = move |_| stop();
view! { view! {
<div <div
class=move || { class=move || {
@@ -492,7 +475,7 @@ pub fn TorrentTable() -> impl IntoView {
on:touchstart=handle_touchstart on:touchstart=handle_touchstart
on:touchmove=handle_touchmove on:touchmove=handle_touchmove
on:touchend=handle_touchend on:touchend=handle_touchend
on:touchcancel=handle_touchend on:touchcancel=handle_touchcancel
> >
<div class="card-body gap-3"> <div class="card-body gap-3">
<div class="flex justify-between items-start gap-2"> <div class="flex justify-between items-start gap-2">