feat(ui): add mobile sorting dropdown with native focus implementation

This commit is contained in:
spinline
2026-02-04 19:39:37 +03:00
parent 6076637012
commit 9f44688a4f

View File

@@ -284,7 +284,82 @@ pub fn TorrentTable() -> impl IntoView {
</table>
</div>
<div class="md:hidden grid grid-cols-1 content-start gap-3 p-3 pb-20 overflow-y-auto h-full">
<div class="md:hidden flex flex-col h-full bg-base-100">
// Toolbar
<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>
// Sort Dropdown
<div
class="dropdown dropdown-end"
on:touchstart=move |e| e.stop_propagation()
>
<div
tabindex="0"
role="button"
class="btn btn-ghost btn-xs gap-1 opacity-70 font-normal"
>
<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" />
</svg>
"Sort"
</div>
<ul tabindex="0" class="dropdown-content z-[100] menu p-2 shadow bg-base-100 rounded-box w-48 border border-base-200 text-xs">
<li class="menu-title px-2 py-1 opacity-50 text-[10px] uppercase font-bold">"Sort By"</li>
{
let columns = vec![
(SortColumn::Name, "Name"),
(SortColumn::Size, "Size"),
(SortColumn::Progress, "Progress"),
(SortColumn::Status, "Status"),
(SortColumn::DownSpeed, "Down Speed"),
(SortColumn::UpSpeed, "Up Speed"),
(SortColumn::ETA, "ETA"),
];
// Helper to close dropdown using the same logic as statusbar
let close_dropdown = move || {
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
if let Some(active) = doc.active_element() {
let _ = active.dyn_into::<web_sys::HtmlElement>().map(|el| el.blur());
}
}
};
columns.into_iter().map(|(col, label)| {
let is_active = move || sort_col.get() == col;
let current_dir = move || sort_dir.get();
let close = close_dropdown.clone();
view! {
<li>
<button
class=move || if is_active() { "bg-primary/10 text-primary font-bold flex justify-between" } else { "flex justify-between" }
on:click=move |_| {
handle_sort(col);
close();
}
>
{label}
<Show when=is_active fallback=|| ()>
<span class="opacity-70 text-[10px]">
{move || match current_dir() {
SortDirection::Ascending => "",
SortDirection::Descending => "",
}}
</span>
</Show>
</button>
</li>
}
}).collect::<Vec<_>>()
}
</ul>
</div>
</div>
// Scrollable Content
<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| {
let progress_class = if t.percent_complete >= 100.0 { "progress-success" } else { "progress-primary" };
let status_str = format!("{:?}", t.status);
@@ -299,8 +374,6 @@ pub fn TorrentTable() -> impl IntoView {
// 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();
// Long press logic
let (timer_id, set_timer_id) = create_signal(Option::<i32>::None);
let t_hash_long = t.hash.clone();
@@ -417,6 +490,7 @@ pub fn TorrentTable() -> impl IntoView {
}).collect::<Vec<_>>()}
</div>
<Show when=move || menu_visible.get() fallback=|| ()>
<crate::components::context_menu::ContextMenu
visible=true