feat: implement mobile PWA card design, refine toolbar and status bar
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@ pub fn App() -> impl IntoView {
|
||||
// Toolbar at the top
|
||||
<Toolbar />
|
||||
|
||||
<main class="flex-1 flex flex-col min-w-0 bg-base-100 overflow-hidden p-4 md:p-6 space-y-6">
|
||||
<main class="flex-1 flex flex-col min-w-0 bg-base-100 overflow-hidden space-y-6">
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" view=move || view! { <TorrentTable /> } />
|
||||
|
||||
@@ -68,57 +68,67 @@ pub fn ContextMenu(
|
||||
view! {
|
||||
<div
|
||||
node_ref=target
|
||||
class="fixed z-[100] bg-[#111116]/95 backdrop-blur-xl border border-white/10 rounded-xl shadow-2xl py-2 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)
|
||||
on:contextmenu=move |e| e.prevent_default()
|
||||
>
|
||||
<div class="px-3 py-1 text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">"Actions"</div>
|
||||
<ul class="menu bg-base-200 text-base-content rounded-box shadow-xl border border-white/5 p-2 gap-1">
|
||||
<li class="menu-title px-4 py-1.5 text-xs opacity-60 uppercase tracking-wider font-bold">"Actions"</li>
|
||||
|
||||
<button
|
||||
class="w-full text-left px-4 py-2.5 hover:bg-white/10 flex items-center gap-3 transition-colors text-white"
|
||||
on:click={
|
||||
let handle_action = handle_action.clone();
|
||||
move |_| handle_action("start")
|
||||
}
|
||||
>
|
||||
<svg class="w-4 h-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
"Resume"
|
||||
</button>
|
||||
<li>
|
||||
<button
|
||||
class="gap-3 active:bg-primary active:text-primary-content"
|
||||
on:click={
|
||||
let handle_action = handle_action.clone();
|
||||
move |_| handle_action("start")
|
||||
}
|
||||
>
|
||||
<svg class="w-4 h-4 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
"Resume"
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<button
|
||||
class="w-full text-left px-4 py-2.5 hover:bg-white/10 flex items-center gap-3 transition-colors text-white"
|
||||
on:click={
|
||||
let handle_action = handle_action.clone();
|
||||
move |_| handle_action("stop")
|
||||
}
|
||||
>
|
||||
<svg class="w-4 h-4 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
"Pause"
|
||||
</button>
|
||||
<li>
|
||||
<button
|
||||
class="gap-3 active:bg-primary active:text-primary-content"
|
||||
on:click={
|
||||
let handle_action = handle_action.clone();
|
||||
move |_| handle_action("stop")
|
||||
}
|
||||
>
|
||||
<svg class="w-4 h-4 text-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
"Pause"
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<div class="h-px bg-white/10 my-1"></div>
|
||||
<div class="divider my-0 h-px p-0 opacity-10"></div>
|
||||
|
||||
<button
|
||||
class="w-full text-left px-4 py-2.5 hover:bg-red-500/20 text-red-500 hover:text-red-400 flex items-center gap-3 transition-colors"
|
||||
on:click={
|
||||
let handle_action = handle_action.clone();
|
||||
move |_| handle_action("delete")
|
||||
}
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||
"Delete"
|
||||
</button>
|
||||
<li>
|
||||
<button
|
||||
class="gap-3 text-error hover:bg-error/10 active:bg-error active:text-error-content"
|
||||
on:click={
|
||||
let handle_action = handle_action.clone();
|
||||
move |_| handle_action("delete")
|
||||
}
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||
"Delete"
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<button
|
||||
class="w-full text-left px-4 py-2.5 hover:bg-red-900/20 text-red-600 hover:text-red-400 flex items-center gap-3 transition-colors text-xs"
|
||||
on:click={
|
||||
let handle_action = handle_action.clone();
|
||||
move |_| handle_action("delete_with_data")
|
||||
}
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||
<span>"Delete with Data"</span>
|
||||
</button>
|
||||
<li>
|
||||
<button
|
||||
class="gap-3 text-error hover:bg-error/10 active:bg-error active:text-error-content text-xs"
|
||||
on:click={
|
||||
let handle_action = handle_action.clone();
|
||||
move |_| handle_action("delete_with_data")
|
||||
}
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||
<span>"Delete with Data"</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}.into_view()
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ pub fn Sidebar() -> impl IntoView {
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="w-64 h-full flex flex-col">
|
||||
<div class="p-4">
|
||||
<h2 class="text-xl font-bold px-4 mb-2 text-primary">"Filters"</h2>
|
||||
<div class="w-64 h-full flex flex-col bg-base-200 border-r border-base-300">
|
||||
<div class="p-2">
|
||||
<ul class="menu w-full rounded-box gap-1">
|
||||
<li class="menu-title text-primary uppercase font-bold px-4">"Filters"</li>
|
||||
<li>
|
||||
<a 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-5 h-5">
|
||||
@@ -71,9 +71,9 @@ pub fn Sidebar() -> impl IntoView {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto p-4 border-t border-base-300">
|
||||
<h3 class="text-xs font-bold text-base-content/50 uppercase mb-2 px-4">"Trackers"</h3>
|
||||
<div class="mt-auto p-2 border-t border-base-300">
|
||||
<ul class="menu w-full rounded-box gap-1 text-sm">
|
||||
<li class="menu-title text-base-content/50 uppercase font-bold px-4">"Trackers"</li>
|
||||
<li><a>"All Trackers"</a></li>
|
||||
<li><a>"Error"</a></li>
|
||||
</ul>
|
||||
|
||||
@@ -23,6 +23,34 @@ pub fn StatusBar() -> impl IntoView {
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="dropdown dropdown-top dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-xs btn-square" title="Change Theme">
|
||||
<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.098 19.902a3.75 3.75 0 0 0 5.304 0l6.401-6.402M6.75 21A3.75 3.75 0 0 1 3 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072c0 .657.66 1.175 1.312 1.133 3.421-.22 6.187 2.546 6.187 5.965 0 1.595-.572 3.064-1.524 4.195" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52 mb-2 border border-base-300">
|
||||
{
|
||||
let themes = vec!["light", "dark", "cupcake", "dracula", "cyberpunk", "emerald", "luxury", "nord"];
|
||||
themes.into_iter().map(|theme| {
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
class="text-xs capitalize"
|
||||
on:click=move |_| {
|
||||
let doc = web_sys::window().unwrap().document().unwrap();
|
||||
let _ = doc.document_element().unwrap().set_attribute("data-theme", theme);
|
||||
}
|
||||
>
|
||||
{theme}
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-ghost btn-xs btn-square" title="Settings">
|
||||
<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.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.212 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
||||
|
||||
@@ -5,53 +5,31 @@ pub fn Toolbar() -> impl IntoView {
|
||||
let (show_add_modal, set_show_add_modal) = create_signal(false);
|
||||
|
||||
view! {
|
||||
<div class="h-14 min-h-14 flex items-center px-4 border-b border-base-300 bg-base-100 gap-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="join">
|
||||
<button class="join-item btn btn-sm btn-outline gap-2" title="Open Torrent">
|
||||
<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="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
"Open"
|
||||
</button>
|
||||
<button
|
||||
class="join-item btn btn-sm btn-outline gap-2"
|
||||
title="Magnet Link"
|
||||
on:click=move |_| set_show_add_modal.set(true)
|
||||
>
|
||||
<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="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
</svg>
|
||||
"URL"
|
||||
</button>
|
||||
<div class="navbar min-h-14 h-14 bg-base-100 p-0">
|
||||
<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 gap-2">
|
||||
<button
|
||||
class="btn btn-sm btn-primary gap-2 font-normal"
|
||||
title="Add Magnet Link"
|
||||
on:click=move |_| set_show_add_modal.set(true)
|
||||
>
|
||||
<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="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
"Add Torrent"
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="join">
|
||||
<button class="join-item btn btn-sm btn-ghost" title="Start">
|
||||
<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 text-success">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="join-item btn btn-sm btn-ghost" title="Pause">
|
||||
<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 text-warning">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="join">
|
||||
<button class="join-item btn btn-sm btn-ghost text-error" title="Remove">
|
||||
<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="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<div class="navbar-end gap-2 px-4">
|
||||
<input type="text" placeholder="Filter..." class="input input-sm input-bordered w-full max-w-xs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when=move || show_add_modal.get()>
|
||||
<crate::components::torrent::add_torrent::AddTorrentModal on_close=move |_| set_show_add_modal.set(false) />
|
||||
|
||||
@@ -106,14 +106,14 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
}
|
||||
};
|
||||
|
||||
let (selected_hash, set_selected_hash) = create_signal(Option::<String>::None);
|
||||
let (menu_visible, set_menu_visible) = create_signal(false);
|
||||
let (menu_position, set_menu_position) = create_signal((0, 0));
|
||||
let (active_hash, set_active_hash) = create_signal(String::new());
|
||||
|
||||
let handle_context_menu = move |e: web_sys::MouseEvent, hash: String| {
|
||||
e.prevent_default();
|
||||
set_menu_position.set((e.client_x(), e.client_y()));
|
||||
set_active_hash.set(hash);
|
||||
set_selected_hash.set(Some(hash)); // Select on right click too
|
||||
set_menu_visible.set(true);
|
||||
};
|
||||
|
||||
@@ -152,88 +152,170 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<div class="overflow-x-auto h-full bg-base-100 relative"> // Added relative for positioning context if needed, though menu is fixed
|
||||
<table class="table table-xs table-pin-rows w-full max-w-full whitespace-nowrap">
|
||||
<thead>
|
||||
<tr class="bg-base-200 text-base-content/70">
|
||||
<th class="w-8">
|
||||
<label>
|
||||
<input type="checkbox" class="checkbox checkbox-xs rounded-none" />
|
||||
</label>
|
||||
</th>
|
||||
<th class="cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::Name)>
|
||||
<div class="flex items-center">"Name" {move || sort_arrow(SortColumn::Name)}</div>
|
||||
</th>
|
||||
<th class="w-24 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::Size)>
|
||||
<div class="flex items-center">"Size" {move || sort_arrow(SortColumn::Size)}</div>
|
||||
</th>
|
||||
<th class="w-48 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::Progress)>
|
||||
<div class="flex items-center">"Progress" {move || sort_arrow(SortColumn::Progress)}</div>
|
||||
</th>
|
||||
<th class="w-24 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::Status)>
|
||||
<div class="flex items-center">"Status" {move || sort_arrow(SortColumn::Status)}</div>
|
||||
</th>
|
||||
<th class="w-24 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::DownSpeed)>
|
||||
<div class="flex items-center">"Down Speed" {move || sort_arrow(SortColumn::DownSpeed)}</div>
|
||||
</th>
|
||||
<th class="w-24 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::UpSpeed)>
|
||||
<div class="flex items-center">"Up Speed" {move || sort_arrow(SortColumn::UpSpeed)}</div>
|
||||
</th>
|
||||
<th class="w-24 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::ETA)>
|
||||
<div class="flex items-center">"ETA" {move || sort_arrow(SortColumn::ETA)}</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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);
|
||||
let status_class = match t.status {
|
||||
shared::TorrentStatus::Seeding => "text-success",
|
||||
shared::TorrentStatus::Downloading => "text-primary",
|
||||
shared::TorrentStatus::Paused => "text-warning",
|
||||
shared::TorrentStatus::Error => "text-error",
|
||||
_ => "text-base-content/50"
|
||||
};
|
||||
let t_hash = t.hash.clone(); // Clone for closure using it in handler
|
||||
<div class="hidden md:block h-full overflow-x-auto">
|
||||
<table class="table table-sm table-pin-rows w-full max-w-full whitespace-nowrap">
|
||||
<thead>
|
||||
<tr class="text-xs uppercase text-base-content/60 border-b border-base-200">
|
||||
<th class="cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::Name)>
|
||||
<div class="flex items-center">"Name" {move || sort_arrow(SortColumn::Name)}</div>
|
||||
</th>
|
||||
<th class="w-24 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::Size)>
|
||||
<div class="flex items-center">"Size" {move || sort_arrow(SortColumn::Size)}</div>
|
||||
</th>
|
||||
<th class="w-48 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::Progress)>
|
||||
<div class="flex items-center">"Progress" {move || sort_arrow(SortColumn::Progress)}</div>
|
||||
</th>
|
||||
<th class="w-24 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::Status)>
|
||||
<div class="flex items-center">"Status" {move || sort_arrow(SortColumn::Status)}</div>
|
||||
</th>
|
||||
<th class="w-24 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::DownSpeed)>
|
||||
<div class="flex items-center">"Down Speed" {move || sort_arrow(SortColumn::DownSpeed)}</div>
|
||||
</th>
|
||||
<th class="w-24 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::UpSpeed)>
|
||||
<div class="flex items-center">"Up Speed" {move || sort_arrow(SortColumn::UpSpeed)}</div>
|
||||
</th>
|
||||
<th class="w-24 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::ETA)>
|
||||
<div class="flex items-center">"ETA" {move || sort_arrow(SortColumn::ETA)}</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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);
|
||||
let status_class = match t.status {
|
||||
shared::TorrentStatus::Seeding => "text-success",
|
||||
shared::TorrentStatus::Downloading => "text-primary",
|
||||
shared::TorrentStatus::Paused => "text-warning",
|
||||
shared::TorrentStatus::Error => "text-error",
|
||||
_ => "text-base-content/50"
|
||||
};
|
||||
let t_hash = t.hash.clone();
|
||||
let t_hash_click = t.hash.clone();
|
||||
|
||||
view! {
|
||||
<tr
|
||||
class="hover group border-b border-base-200 cursor-context-menu"
|
||||
on:contextmenu={
|
||||
let t_hash = t_hash.clone();
|
||||
move |e: web_sys::MouseEvent| handle_context_menu(e, t_hash.clone())
|
||||
let is_selected_fn = move || {
|
||||
selected_hash.get() == Some(t_hash.clone())
|
||||
};
|
||||
|
||||
view! {
|
||||
<tr
|
||||
class=move || {
|
||||
let base = "hover border-b border-base-200 transition-colors select-none";
|
||||
if is_selected_fn() {
|
||||
format!("{} bg-primary/10", base)
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
}
|
||||
on:contextmenu={
|
||||
let t_hash = t_hash_click.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()))
|
||||
}
|
||||
>
|
||||
<td class="font-medium truncate max-w-xs" title={t.name.clone()}>
|
||||
{t.name}
|
||||
</td>
|
||||
<td class="opacity-80 font-mono text-[11px]">{format_bytes(t.size)}</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<progress class={format!("progress w-24 {}", progress_class)} value={t.percent_complete} max="100"></progress>
|
||||
<span class="text-[10px] opacity-70">{format!("{:.1}%", t.percent_complete)}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class={format!("text-[11px] font-medium {}", status_class)}>{status_str}</td>
|
||||
<td class="text-right font-mono text-[11px] opacity-80 text-success">{format_speed(t.down_rate)}</td>
|
||||
<td class="text-right font-mono text-[11px] opacity-80 text-primary">{format_speed(t.up_rate)}</td>
|
||||
<td class="text-right font-mono text-[11px] opacity-80">{if t.eta > 0 { format!("{}s", t.eta) } else { "∞".to_string() }}</td>
|
||||
</tr>
|
||||
}
|
||||
}).collect::<Vec<_>>()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden grid grid-cols-1 gap-3 p-3 pb-20 overflow-y-auto h-full">
|
||||
{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);
|
||||
let status_badge_class = match t.status {
|
||||
shared::TorrentStatus::Seeding => "badge-success badge-soft",
|
||||
shared::TorrentStatus::Downloading => "badge-primary badge-soft",
|
||||
shared::TorrentStatus::Paused => "badge-warning badge-soft",
|
||||
shared::TorrentStatus::Error => "badge-error badge-soft",
|
||||
_ => "badge-ghost"
|
||||
};
|
||||
let t_hash = t.hash.clone();
|
||||
let t_hash_click = t.hash.clone();
|
||||
let t_hash_ctx = t.hash.clone();
|
||||
|
||||
let is_selected_fn = move || {
|
||||
selected_hash.get() == Some(t_hash.clone())
|
||||
};
|
||||
|
||||
view! {
|
||||
<div
|
||||
class=move || {
|
||||
let base = "card card-compact bg-base-100 shadow-sm border border-base-200 transition-all active:scale-[0.99]";
|
||||
if is_selected_fn() {
|
||||
format!("{} ring-2 ring-primary", base)
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
>
|
||||
<th>
|
||||
<label>
|
||||
<input type="checkbox" class="checkbox checkbox-xs rounded-none" />
|
||||
</label>
|
||||
</th>
|
||||
<td class="font-medium truncate max-w-xs" title={t.name.clone()}>
|
||||
{t.name}
|
||||
</td>
|
||||
<td class="opacity-80 font-mono text-[11px]">{format_bytes(t.size)}</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<progress class={format!("progress w-24 {}", progress_class)} value={t.percent_complete} max="100"></progress>
|
||||
<span class="text-[10px] opacity-70">{format!("{:.1}%", t.percent_complete)}</span>
|
||||
}
|
||||
on:contextmenu={
|
||||
let t_hash = t_hash_ctx.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()))
|
||||
}
|
||||
>
|
||||
<div class="card-body gap-3">
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<h3 class="font-medium text-sm line-clamp-2 leading-tight">{t.name}</h3>
|
||||
<div class={format!("badge badge-xs text-[10px] whitespace-nowrap {}", status_badge_class)}>
|
||||
{status_str}
|
||||
</div>
|
||||
</td>
|
||||
<td class={format!("text-[11px] font-medium {}", status_class)}>{status_str}</td>
|
||||
<td class="text-right font-mono text-[11px] opacity-80 text-success">{format_speed(t.down_rate)}</td>
|
||||
<td class="text-right font-mono text-[11px] opacity-80 text-primary">{format_speed(t.up_rate)}</td>
|
||||
<td class="text-right font-mono text-[11px] opacity-80">{if t.eta > 0 { format!("{}s", t.eta) } else { "∞".to_string() }}</td>
|
||||
</tr>
|
||||
}
|
||||
}).collect::<Vec<_>>()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex justify-between text-[10px] opacity-70">
|
||||
<span>{format_bytes(t.size)}</span>
|
||||
<span>{format!("{:.1}%", t.percent_complete)}</span>
|
||||
</div>
|
||||
<progress class={format!("progress w-full h-1.5 {}", progress_class)} value={t.percent_complete} max="100"></progress>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2 text-[10px] font-mono opacity-80 pt-1 border-t border-base-200/50">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-[9px] opacity-60 uppercase">"Down"</span>
|
||||
<span class="text-success">{format_speed(t.down_rate)}</span>
|
||||
</div>
|
||||
<div class="flex flex-col text-center border-l border-r border-base-200/50">
|
||||
<span class="text-[9px] opacity-60 uppercase">"Up"</span>
|
||||
<span class="text-primary">{format_speed(t.up_rate)}</span>
|
||||
</div>
|
||||
<div class="flex flex-col text-right">
|
||||
<span class="text-[9px] opacity-60 uppercase">"ETA"</span>
|
||||
<span>{if t.eta > 0 { format!("{}s", t.eta) } else { "∞".to_string() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}).collect::<Vec<_>>()}
|
||||
</div>
|
||||
|
||||
<Show when=move || menu_visible.get() fallback=|| ()>
|
||||
<crate::components::context_menu::ContextMenu
|
||||
visible=true
|
||||
position=menu_position.get()
|
||||
torrent_hash=active_hash.get()
|
||||
torrent_hash=selected_hash.get().unwrap_or_default() // Use selected_hash as source of truth
|
||||
on_close=Callback::from(move |_| set_menu_visible.set(false))
|
||||
on_action=Callback::from(on_action)
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user