From ce29831d407318f260e164dd6f488830f1c22c77 Mon Sep 17 00:00:00 2001 From: spinline Date: Sun, 1 Feb 2026 15:38:08 +0300 Subject: [PATCH] feat: Implement standard long-press context menu for mobile cards --- frontend/src/components/torrent/table.rs | 94 ++++++++++++++++++------ 1 file changed, 72 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/torrent/table.rs b/frontend/src/components/torrent/table.rs index 7cb8e1d..f2f87e4 100644 --- a/frontend/src/components/torrent/table.rs +++ b/frontend/src/components/torrent/table.rs @@ -259,13 +259,74 @@ pub fn TorrentTable() -> impl IntoView { _ => "badge-ghost" }; let t_hash = t.hash.clone(); - let t_hash_click = t.hash.clone(); - let t_hash_ctx = t.hash.clone(); + // We don't need t_hash_click separately if we use t_hash, but existing pattern uses clones + let t_hash_click = t.hash.clone(); let is_selected_fn = move || { selected_hash.get() == Some(t_hash.clone()) }; + // Long press logic + let (timer_id, set_timer_id) = create_signal(Option::::None); + let t_hash_long = t.hash.clone(); + + let clear_timer = move || { + if let Some(id) = timer_id.get_untracked() { + window().clear_timeout_with_handle(id); + set_timer_id.set(None); + } + }; + + let handle_touchstart = { + let t_hash = t_hash_long.clone(); + move |e: web_sys::TouchEvent| { + // Don't prevent default immediately, or we can't scroll. + // But for long press, we might need to if we want to stop iOS menu. + // -webkit-touch-callout: none (in CSS) handles the iOS menu suppression usually. + + 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); + + // Haptic feedback if available + if let Ok(navigator) = window().navigator() { + let _ = navigator.vibrate_with_duration(50); + } + }) as Box); + + let id = window() + .set_timeout_with_callback_and_timeout_and_arguments_0( + closure.as_ref().unchecked_ref(), + 600 // 600ms long press + ) + .unwrap_or(0); + + closure.forget(); // Leak memory? effectively yes, but for a simplified timeout it's "okay" in this context or we need to store the closure key. + // In a real app we might want to store the closure to drop it, but `set_timeout` takes a function pointer effectively. + // Actually, `closure.forget()` is standard for one-off callbacks that the JS side consumes. + + set_timer_id.set(Some(id)); + } + } + }; + + let handle_touchmove = move |_| { + // If moving, it's likely a scroll, so cancel the long press + clear_timer(); + }; + + let handle_touchend = move |_| { + clear_timer(); + }; + view! {
impl IntoView { } style="user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;" on:contextmenu={ - let t_hash = t_hash_ctx.clone(); + // Fallback for desktop/mouse right click still works + let t_hash = t.hash.clone(); move |e: web_sys::MouseEvent| handle_context_menu(e, t_hash.clone()) } on:click={ let t_hash = t_hash_click.clone(); move |_| set_selected_hash.set(Some(t_hash.clone())) } + on:touchstart=handle_touchstart + on:touchmove=handle_touchmove + on:touchend=handle_touchend + on:touchcancel=handle_touchend >
-

{t.name}

-
-
- {status_str} -
- +

{t.name}

+
+ {status_str}