chore(frontend): remove unused imports and variables

This commit is contained in:
spinline
2026-02-03 22:30:43 +03:00
parent f7d6ddb7a9
commit 8f16265107
2 changed files with 121 additions and 89 deletions

View File

@@ -1,5 +1,4 @@
use leptos::*; use leptos::*;
use wasm_bindgen::JsCast;
#[component] #[component]
pub fn StatusBar() -> impl IntoView { pub fn StatusBar() -> impl IntoView {
@@ -34,10 +33,10 @@ pub fn StatusBar() -> impl IntoView {
base.to_string() base.to_string()
} }
}> }>
<div <div
tabindex="0" tabindex="0"
role="button" role="button"
class="btn btn-ghost btn-xs btn-square" class="btn btn-ghost btn-xs btn-square"
title="Change Theme" title="Change Theme"
on:click=move |_| set_theme_open.update(|v| *v = !*v) on:click=move |_| set_theme_open.update(|v| *v = !*v)
> >
@@ -49,8 +48,8 @@ pub fn StatusBar() -> impl IntoView {
<Show when=move || theme_open.get() fallback=|| ()> <Show when=move || theme_open.get() fallback=|| ()>
// Backdrop to close on outside click // Backdrop to close on outside click
// iOS Safari requires cursor:pointer inline style for click events on div elements // iOS Safari requires cursor:pointer inline style for click events on div elements
<div <div
class="fixed inset-0 z-[99] bg-black/0" class="fixed inset-0 z-[99] bg-black/0"
style="cursor: pointer; -webkit-tap-highlight-color: transparent;" style="cursor: pointer; -webkit-tap-highlight-color: transparent;"
role="button" role="button"
tabindex="-1" tabindex="-1"
@@ -65,19 +64,19 @@ pub fn StatusBar() -> impl IntoView {
<ul tabindex="0" 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 block"> <ul tabindex="0" 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 block">
{ {
let themes = vec![ let themes = vec![
"light", "dark", "cupcake", "dracula", "cyberpunk", "light", "dark", "cupcake", "dracula", "cyberpunk",
"emerald", "luxury", "nord", "sunset", "winter", "emerald", "luxury", "nord", "sunset", "winter",
"night", "synthwave", "retro", "forest" "night", "synthwave", "retro", "forest"
]; ];
themes.into_iter().map(|theme| { themes.into_iter().map(|theme| {
view! { view! {
<li> <li>
<button <button
class="text-xs capitalize" class="text-xs capitalize"
on:click=move |_| { on:click=move |_| {
let doc = web_sys::window().unwrap().document().unwrap(); let doc = web_sys::window().unwrap().document().unwrap();
let _ = doc.document_element().unwrap().set_attribute("data-theme", theme); let _ = doc.document_element().unwrap().set_attribute("data-theme", theme);
if let Some(meta) = doc.query_selector("meta[name='theme-color']").unwrap() { if let Some(meta) = doc.query_selector("meta[name='theme-color']").unwrap() {
let window = web_sys::window().unwrap(); let window = web_sys::window().unwrap();
if let Ok(Some(style)) = window.get_computed_style(&doc.body().unwrap()) { if let Ok(Some(style)) = window.get_computed_style(&doc.body().unwrap()) {
@@ -98,7 +97,7 @@ pub fn StatusBar() -> impl IntoView {
} }
</ul> </ul>
</div> </div>
<button class="btn btn-ghost btn-xs btn-square" title="Settings"> <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"> <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" /> <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" />

View File

@@ -2,16 +2,17 @@ use leptos::*;
use wasm_bindgen::closure::Closure; use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast; 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"];
if bytes < 1024 { if bytes < 1024 {
return format!("{} B", bytes); return format!("{} B", bytes);
} }
let i = (bytes as f64).log2().div_euclid(10.0) as usize; let i = (bytes as f64).log2().div_euclid(10.0) as usize;
format!("{:.1} {}", (bytes as f64) / 1024_f64.powi(i as i32), UNITS[i]) format!(
"{:.1} {}",
(bytes as f64) / 1024_f64.powi(i as i32),
UNITS[i]
)
} }
fn format_speed(bytes_per_sec: i64) -> String { fn format_speed(bytes_per_sec: i64) -> String {
@@ -41,32 +42,47 @@ enum SortDirection {
#[component] #[component]
pub fn TorrentTable() -> impl IntoView { pub fn TorrentTable() -> 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 sort_col = create_rw_signal(SortColumn::Name); let sort_col = create_rw_signal(SortColumn::Name);
let sort_dir = create_rw_signal(SortDirection::Ascending); let sort_dir = create_rw_signal(SortDirection::Ascending);
let filtered_torrents = move || { let filtered_torrents = move || {
let mut torrents = store.torrents.get().into_iter().filter(|t| { let mut torrents = store
let filter = store.filter.get(); .torrents
let search = store.search_query.get().to_lowercase(); .get()
.into_iter()
let matches_filter = match filter { .filter(|t| {
crate::store::FilterStatus::All => true, let filter = store.filter.get();
crate::store::FilterStatus::Downloading => t.status == shared::TorrentStatus::Downloading, let search = store.search_query.get().to_lowercase();
crate::store::FilterStatus::Seeding => t.status == shared::TorrentStatus::Seeding,
crate::store::FilterStatus::Completed => t.status == shared::TorrentStatus::Seeding || t.status == shared::TorrentStatus::Paused, // Approximate
crate::store::FilterStatus::Inactive => t.status == shared::TorrentStatus::Paused || t.status == shared::TorrentStatus::Error,
_ => true
};
let matches_search = if search.is_empty() { let matches_filter = match filter {
true crate::store::FilterStatus::All => true,
} else { crate::store::FilterStatus::Downloading => {
t.name.to_lowercase().contains(&search) t.status == shared::TorrentStatus::Downloading
}; }
crate::store::FilterStatus::Seeding => {
t.status == shared::TorrentStatus::Seeding
}
crate::store::FilterStatus::Completed => {
t.status == shared::TorrentStatus::Seeding
|| t.status == shared::TorrentStatus::Paused
} // Approximate
crate::store::FilterStatus::Inactive => {
t.status == shared::TorrentStatus::Paused
|| t.status == shared::TorrentStatus::Error
}
_ => true,
};
matches_filter && matches_search let matches_search = if search.is_empty() {
}).collect::<Vec<_>>(); true
} else {
t.name.to_lowercase().contains(&search)
};
matches_filter && matches_search
})
.collect::<Vec<_>>();
torrents.sort_by(|a, b| { torrents.sort_by(|a, b| {
let col = sort_col.get(); let col = sort_col.get();
@@ -74,32 +90,41 @@ pub fn TorrentTable() -> impl IntoView {
let cmp = match col { let cmp = match col {
SortColumn::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()), SortColumn::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
SortColumn::Size => a.size.cmp(&b.size), SortColumn::Size => a.size.cmp(&b.size),
SortColumn::Progress => a.percent_complete.partial_cmp(&b.percent_complete).unwrap_or(std::cmp::Ordering::Equal), SortColumn::Progress => a
.percent_complete
.partial_cmp(&b.percent_complete)
.unwrap_or(std::cmp::Ordering::Equal),
SortColumn::Status => format!("{:?}", a.status).cmp(&format!("{:?}", b.status)), SortColumn::Status => format!("{:?}", a.status).cmp(&format!("{:?}", b.status)),
SortColumn::DownSpeed => a.down_rate.cmp(&b.down_rate), SortColumn::DownSpeed => a.down_rate.cmp(&b.down_rate),
SortColumn::UpSpeed => a.up_rate.cmp(&b.up_rate), SortColumn::UpSpeed => a.up_rate.cmp(&b.up_rate),
SortColumn::ETA => { SortColumn::ETA => {
// ETA 0 means infinity usually, so we need to handle it. // ETA 0 means infinity usually, so we need to handle it.
// But for simple sorting, maybe just treat is numeric? // But for simple sorting, maybe just treat is numeric?
// Let's treat 0 as MAX for ascending, MIN for descending? Or just as is? // Let's treat 0 as MAX for ascending, MIN for descending? Or just as is?
// Usually negative or 0 means unknown/inf. // Usually negative or 0 means unknown/inf.
// Let's handle 0 as very large number for sorting purposes if we want it at the end of ascending // Let's handle 0 as very large number for sorting purposes if we want it at the end of ascending
let a_eta = if a.eta <= 0 { i64::MAX } else { a.eta }; let a_eta = if a.eta <= 0 { i64::MAX } else { a.eta };
let b_eta = if b.eta <= 0 { i64::MAX } else { b.eta }; let b_eta = if b.eta <= 0 { i64::MAX } else { b.eta };
a_eta.cmp(&b_eta) a_eta.cmp(&b_eta)
} }
}; };
if dir == SortDirection::Descending { cmp.reverse() } else { cmp } if dir == SortDirection::Descending {
cmp.reverse()
} else {
cmp
}
}); });
torrents torrents
}; };
let handle_sort = move |col: SortColumn| { let handle_sort = move |col: SortColumn| {
if sort_col.get() == col { if sort_col.get() == col {
sort_dir.update(|d| *d = match d { sort_dir.update(|d| {
SortDirection::Ascending => SortDirection::Descending, *d = match d {
SortDirection::Descending => SortDirection::Ascending, SortDirection::Ascending => SortDirection::Descending,
SortDirection::Descending => SortDirection::Ascending,
}
}); });
} else { } else {
sort_col.set(col); sort_col.set(col);
@@ -110,11 +135,16 @@ pub fn TorrentTable() -> impl IntoView {
let sort_arrow = move |col: SortColumn| { let sort_arrow = move |col: SortColumn| {
if sort_col.get() == col { if sort_col.get() == col {
match sort_dir.get() { match sort_dir.get() {
SortDirection::Ascending => view!{ <span class="ml-1 text-xs">""</span> }.into_view(), SortDirection::Ascending => {
SortDirection::Descending => view!{ <span class="ml-1 text-xs">""</span> }.into_view(), view! { <span class="ml-1 text-xs">""</span> }.into_view()
}
SortDirection::Descending => {
view! { <span class="ml-1 text-xs">""</span> }.into_view()
}
} }
} else { } else {
view!{ <span class="ml-1 text-xs opacity-0 group-hover:opacity-50">""</span> }.into_view() view! { <span class="ml-1 text-xs opacity-0 group-hover:opacity-50">""</span> }
.into_view()
} }
}; };
@@ -134,29 +164,34 @@ pub fn TorrentTable() -> impl IntoView {
set_menu_visible.set(false); // Close menu immediately set_menu_visible.set(false); // Close menu immediately
spawn_local(async move { spawn_local(async move {
let action_req = if action == "delete_with_data" { "delete_with_data" } else { &action }; let action_req = if action == "delete_with_data" {
"delete_with_data"
} else {
&action
};
let req_body = shared::TorrentActionRequest { let req_body = shared::TorrentActionRequest {
hash: hash.clone(), hash: hash.clone(),
action: action_req.to_string(), action: action_req.to_string(),
}; };
let client = gloo_net::http::Request::post("/api/torrents/action") let client = gloo_net::http::Request::post("/api/torrents/action").json(&req_body);
.json(&req_body);
match client { match client {
Ok(req) => { Ok(req) => match req.send().await {
match req.send().await { Ok(resp) => {
Ok(resp) => { if !resp.ok() {
if !resp.ok() { logging::error!(
logging::error!("Failed to execute action: {} {}", resp.status(), resp.status_text()); "Failed to execute action: {} {}",
} else { resp.status(),
logging::log!("Action {} executed successfully", action); resp.status_text()
} );
} else {
logging::log!("Action {} executed successfully", action);
} }
Err(e) => logging::error!("Network error executing action: {}", e),
} }
} Err(e) => logging::error!("Network error executing action: {}", e),
},
Err(e) => logging::error!("Failed to serialize request: {}", e), Err(e) => logging::error!("Failed to serialize request: {}", e),
} }
}); });
@@ -202,17 +237,17 @@ pub fn TorrentTable() -> impl IntoView {
shared::TorrentStatus::Error => "text-error", shared::TorrentStatus::Error => "text-error",
_ => "text-base-content/50" _ => "text-base-content/50"
}; };
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 is_selected_fn = move || { let is_selected_fn = move || {
selected_hash.get() == Some(t_hash.clone()) selected_hash.get() == Some(t_hash.clone())
}; };
view! { view! {
<tr <tr
class=move || { class=move || {
let base = "hover border-b border-base-200 select-none"; let base = "hover border-b border-base-200 select-none";
if is_selected_fn() { if is_selected_fn() {
format!("{} bg-primary/10", base) format!("{} bg-primary/10", base)
} else { } else {
@@ -262,16 +297,14 @@ pub fn TorrentTable() -> impl IntoView {
}; };
let t_hash = t.hash.clone(); let t_hash = t.hash.clone();
// We don't need t_hash_click separately if we use t_hash, but existing pattern uses clones // 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 t_hash_click = t.hash.clone();
let is_selected_fn = move || {
selected_hash.get() == Some(t_hash.clone())
};
// Long press logic // Long press logic
let (timer_id, set_timer_id) = create_signal(Option::<i32>::None); 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 clear_timer = move || { let clear_timer = move || {
if let Some(id) = timer_id.get_untracked() { if let Some(id) = timer_id.get_untracked() {
window().clear_timeout_with_handle(id); window().clear_timeout_with_handle(id);
@@ -282,38 +315,38 @@ pub fn TorrentTable() -> impl IntoView {
let handle_touchstart = { let handle_touchstart = {
let t_hash = t_hash_long.clone(); let t_hash = t_hash_long.clone();
move |e: web_sys::TouchEvent| { move |e: web_sys::TouchEvent| {
// Don't prevent default immediately, or we can't scroll. // 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. // 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. // -webkit-touch-callout: none (in CSS) handles the iOS menu suppression usually.
clear_timer(); clear_timer();
if let Some(touch) = e.touches().get(0) { if let Some(touch) = e.touches().get(0) {
let x = touch.client_x(); let x = touch.client_x();
let y = touch.client_y(); let y = touch.client_y();
let hash = t_hash.clone(); let hash = t_hash.clone();
let closure = Closure::wrap(Box::new(move || { let closure = Closure::wrap(Box::new(move || {
set_menu_position.set((x, y)); set_menu_position.set((x, y));
set_selected_hash.set(Some(hash.clone())); set_selected_hash.set(Some(hash.clone()));
set_menu_visible.set(true); set_menu_visible.set(true);
// Haptic feedback if available // Haptic feedback if available
let navigator = window().navigator(); let navigator = window().navigator();
let _ = navigator.vibrate_with_duration(50); let _ = navigator.vibrate_with_duration(50);
}) as Box<dyn Fn()>); }) as Box<dyn Fn()>);
let id = window() let id = window()
.set_timeout_with_callback_and_timeout_and_arguments_0( .set_timeout_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(), closure.as_ref().unchecked_ref(),
600 // 600ms long press 600 // 600ms long press
) )
.unwrap_or(0); .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. 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. // 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. // Actually, `closure.forget()` is standard for one-off callbacks that the JS side consumes.
set_timer_id.set(Some(id)); set_timer_id.set(Some(id));
} }
} }
@@ -329,7 +362,7 @@ pub fn TorrentTable() -> impl IntoView {
}; };
view! { view! {
<div <div
class=move || { class=move || {
"card card-compact bg-base-100 shadow-sm border border-base-200 transition-transform active:scale-[0.99] select-none" "card card-compact bg-base-100 shadow-sm border border-base-200 transition-transform active:scale-[0.99] select-none"
} }
@@ -355,7 +388,7 @@ pub fn TorrentTable() -> impl IntoView {
{status_str} {status_str}
</div> </div>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="flex justify-between text-[10px] opacity-70"> <div class="flex justify-between text-[10px] opacity-70">
<span>{format_bytes(t.size)}</span> <span>{format_bytes(t.size)}</span>
@@ -383,7 +416,7 @@ pub fn TorrentTable() -> impl IntoView {
} }
}).collect::<Vec<_>>()} }).collect::<Vec<_>>()}
</div> </div>
<Show when=move || menu_visible.get() fallback=|| ()> <Show when=move || menu_visible.get() fallback=|| ()>
<crate::components::context_menu::ContextMenu <crate::components::context_menu::ContextMenu
visible=true visible=true