feat: resolve complex closure errors and finalize advanced DataTable features
Some checks failed
Build MIPS Binary / build (push) Failing after 1m30s
Some checks failed
Build MIPS Binary / build (push) Failing after 1m30s
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos::task::spawn_local;
|
||||
use std::collections::HashSet;
|
||||
use icons::{ArrowUpDown, Inbox};
|
||||
use icons::{ArrowUpDown, Inbox, Settings2, Play, Square, Trash2, Ellipsis};
|
||||
use crate::store::{get_action_messages, show_toast};
|
||||
use crate::api;
|
||||
use shared::NotificationLevel;
|
||||
@@ -9,8 +9,23 @@ use crate::components::context_menu::TorrentContextMenu;
|
||||
use crate::components::ui::card::{Card, CardHeader, CardTitle, CardContent as CardBody};
|
||||
use crate::components::ui::data_table::*;
|
||||
use crate::components::ui::checkbox::Checkbox;
|
||||
use crate::components::ui::button::{Button, ButtonVariant};
|
||||
use crate::components::ui::button::{Button, ButtonVariant, ButtonSize};
|
||||
use crate::components::ui::empty::*;
|
||||
use crate::components::ui::input::Input;
|
||||
use crate::components::ui::multi_select::*;
|
||||
use crate::components::ui::dropdown_menu::*;
|
||||
use crate::components::ui::alert_dialog::*;
|
||||
|
||||
const ALL_COLUMNS: [(&str, &str); 8] = [
|
||||
("Name", "Name"),
|
||||
("Size", "Size"),
|
||||
("Progress", "Progress"),
|
||||
("Status", "Status"),
|
||||
("DownSpeed", "DL Speed"),
|
||||
("UpSpeed", "UP Speed"),
|
||||
("ETA", "ETA"),
|
||||
("AddedDate", "Date"),
|
||||
];
|
||||
|
||||
fn format_bytes(bytes: i64) -> String {
|
||||
const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||
@@ -56,8 +71,13 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
let sort_col = signal(SortColumn::AddedDate);
|
||||
let sort_dir = signal(SortDirection::Descending);
|
||||
|
||||
// Multi-selection state
|
||||
let selected_hashes = RwSignal::new(HashSet::<String>::new());
|
||||
|
||||
let visible_columns = RwSignal::new(HashSet::from([
|
||||
"Name".to_string(), "Size".to_string(), "Progress".to_string(),
|
||||
"Status".to_string(), "DownSpeed".to_string(), "UpSpeed".to_string(),
|
||||
"ETA".to_string(), "AddedDate".to_string()
|
||||
]));
|
||||
|
||||
let sorted_hashes_data = Memo::new(move |_| {
|
||||
let torrents_map = store.torrents.get();
|
||||
@@ -116,11 +136,8 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
selected_hashes.update(|selected| {
|
||||
let hashes = filtered_hashes.get_untracked();
|
||||
for h in hashes {
|
||||
if checked {
|
||||
selected.insert(h);
|
||||
} else {
|
||||
selected.remove(&h);
|
||||
}
|
||||
if checked { selected.insert(h); }
|
||||
else { selected.remove(&h); }
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -138,14 +155,33 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
|
||||
let sort_icon = move |col: SortColumn| {
|
||||
let is_active = sort_col.0.get() == col;
|
||||
let class = if is_active {
|
||||
"size-3 opacity-100 text-primary"
|
||||
} else {
|
||||
"size-3 opacity-30 group-hover:opacity-100 transition-opacity"
|
||||
};
|
||||
view! {
|
||||
<ArrowUpDown class=class.to_string() />
|
||||
}
|
||||
let class = if is_active { "size-3 text-primary" } else { "size-3 opacity-30 group-hover:opacity-100 transition-opacity" };
|
||||
view! { <ArrowUpDown class=class.to_string() /> }
|
||||
};
|
||||
|
||||
let bulk_action = move |action: &'static str| {
|
||||
let hashes: Vec<String> = selected_hashes.get().into_iter().collect();
|
||||
if hashes.is_empty() { return; }
|
||||
|
||||
spawn_local(async move {
|
||||
let mut success = true;
|
||||
for hash in hashes {
|
||||
let res = match action {
|
||||
"start" => api::torrent::start(&hash).await,
|
||||
"stop" => api::torrent::stop(&hash).await,
|
||||
"delete" => api::torrent::delete(&hash).await,
|
||||
"delete_with_data" => api::torrent::delete_with_data(&hash).await,
|
||||
_ => Ok(()),
|
||||
};
|
||||
if res.is_err() { success = false; }
|
||||
}
|
||||
if success {
|
||||
show_toast(NotificationLevel::Success, format!("Toplu işlem başarıyla tamamlandı: {}", action));
|
||||
selected_hashes.update(|s| s.clear());
|
||||
} else {
|
||||
show_toast(NotificationLevel::Error, "Bazı işlemler başarısız oldu.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let on_action = Callback::new(move |(action, hash): (String, String)| {
|
||||
@@ -168,15 +204,97 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
});
|
||||
|
||||
view! {
|
||||
<div class="h-full bg-background relative flex flex-col overflow-hidden px-4 py-2">
|
||||
// --- DESKTOP VIEW ---
|
||||
<div class="hidden md:flex flex-col h-full overflow-hidden">
|
||||
<DataTableWrapper class="flex-1 min-h-0 bg-card/50">
|
||||
<div class="h-full bg-background relative flex flex-col overflow-hidden px-4 py-4 gap-4">
|
||||
// --- TOPBAR ---
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2 flex-1 max-w-md">
|
||||
<Input
|
||||
class="h-9"
|
||||
placeholder="Torrent ara..."
|
||||
bind_value=store.search_query
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when=move || selected_count.get() > 0>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant=ButtonVariant::Secondary size=ButtonSize::Sm class="gap-2">
|
||||
<Ellipsis class="size-4" />
|
||||
{move || format!("Toplu İşlem ({})", selected_count.get())}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-48">
|
||||
<DropdownMenuLabel>"Seçili Torrentler"</DropdownMenuLabel>
|
||||
<DropdownMenuGroup class="mt-2">
|
||||
<DropdownMenuItem on:click=move |_| bulk_action("start")>
|
||||
<Play class="mr-2 size-4" /> "Başlat"
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem on:click=move |_| bulk_action("stop")>
|
||||
<Square class="mr-2 size-4" /> "Durdur"
|
||||
</DropdownMenuItem>
|
||||
|
||||
<div class="my-1 h-px bg-border" />
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger class="w-full">
|
||||
<DropdownMenuItem class="text-destructive focus:bg-destructive/10">
|
||||
<Trash2 class="mr-2 size-4" /> "Toplu Sil"
|
||||
</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>"Toplu Silme Onayı"</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{move || format!("Seçili {} torrent silinecek. Bu işlem geri alınamaz.", selected_count.get())}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogClose>"İptal"</AlertDialogClose>
|
||||
<Button variant=ButtonVariant::Destructive on:click=move |_| bulk_action("delete")>
|
||||
"Sil"
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Show>
|
||||
|
||||
<MultiSelect values=visible_columns>
|
||||
<MultiSelectTrigger class="w-[140px] h-9">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<Settings2 class="size-4" />
|
||||
"Sütunlar"
|
||||
</div>
|
||||
</MultiSelectTrigger>
|
||||
<MultiSelectContent>
|
||||
<MultiSelectGroup>
|
||||
{ALL_COLUMNS.into_iter().map(|(id, label)| {
|
||||
let id_val = id.to_string();
|
||||
view! {
|
||||
<MultiSelectItem>
|
||||
<MultiSelectOption value=id_val.clone() attr:disabled=move || id_val == "Name">
|
||||
{label}
|
||||
</MultiSelectOption>
|
||||
</MultiSelectItem>
|
||||
}.into_any()
|
||||
}).collect_view()}
|
||||
</MultiSelectGroup>
|
||||
</MultiSelectContent>
|
||||
</MultiSelect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// --- MAIN TABLE ---
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<DataTableWrapper class="h-full bg-card/50">
|
||||
<div class="h-full overflow-auto">
|
||||
<DataTable>
|
||||
<DataTableHeader class="sticky top-0 bg-muted/80 backdrop-blur-sm z-10">
|
||||
<DataTableRow class="hover:bg-transparent">
|
||||
<DataTableHead class="w-12">
|
||||
<DataTableHead class="w-12 px-4">
|
||||
<Checkbox
|
||||
checked=Signal::derive(move || {
|
||||
let hashes = filtered_hashes.get();
|
||||
@@ -186,38 +304,53 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
/>
|
||||
</DataTableHead>
|
||||
|
||||
// Column Headers with Sorting
|
||||
<DataTableHead class="cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Name)>
|
||||
<div class="flex items-center gap-2">"Name" {move || sort_icon(SortColumn::Name)}</div>
|
||||
</DataTableHead>
|
||||
{move || visible_columns.get().contains("Name").then(|| view! {
|
||||
<DataTableHead class="cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Name)>
|
||||
<div class="flex items-center gap-2">"Name" {move || sort_icon(SortColumn::Name)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Size)>
|
||||
<div class="flex items-center gap-2">"Size" {move || sort_icon(SortColumn::Size)}</div>
|
||||
</DataTableHead>
|
||||
{move || visible_columns.get().contains("Size").then(|| view! {
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Size)>
|
||||
<div class="flex items-center gap-2">"Size" {move || sort_icon(SortColumn::Size)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
|
||||
<DataTableHead class="w-48 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Progress)>
|
||||
<div class="flex items-center gap-2">"Progress" {move || sort_icon(SortColumn::Progress)}</div>
|
||||
</DataTableHead>
|
||||
{move || visible_columns.get().contains("Progress").then(|| view! {
|
||||
<DataTableHead class="w-48 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Progress)>
|
||||
<div class="flex items-center gap-2">"Progress" {move || sort_icon(SortColumn::Progress)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Status)>
|
||||
<div class="flex items-center gap-2">"Status" {move || sort_icon(SortColumn::Status)}</div>
|
||||
</DataTableHead>
|
||||
{move || visible_columns.get().contains("Status").then(|| view! {
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Status)>
|
||||
<div class="flex items-center gap-2">"Status" {move || sort_icon(SortColumn::Status)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none text-right" on:click=move |_| handle_sort(SortColumn::DownSpeed)>
|
||||
<div class="flex items-center justify-end gap-2">"DL Speed" {move || sort_icon(SortColumn::DownSpeed)}</div>
|
||||
</DataTableHead>
|
||||
{move || visible_columns.get().contains("DownSpeed").then(|| view! {
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none text-right" on:click=move |_| handle_sort(SortColumn::DownSpeed)>
|
||||
<div class="flex items-center justify-end gap-2">"DL Speed" {move || sort_icon(SortColumn::DownSpeed)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none text-right" on:click=move |_| handle_sort(SortColumn::UpSpeed)>
|
||||
<div class="flex items-center justify-end gap-2">"UP Speed" {move || sort_icon(SortColumn::UpSpeed)}</div>
|
||||
</DataTableHead>
|
||||
{move || visible_columns.get().contains("UpSpeed").then(|| view! {
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none text-right" on:click=move |_| handle_sort(SortColumn::UpSpeed)>
|
||||
<div class="flex items-center justify-end gap-2">"UP Speed" {move || sort_icon(SortColumn::UpSpeed)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none text-right" on:click=move |_| handle_sort(SortColumn::ETA)>
|
||||
<div class="flex items-center justify-end gap-2">"ETA" {move || sort_icon(SortColumn::ETA)}</div>
|
||||
</DataTableHead>
|
||||
{move || visible_columns.get().contains("ETA").then(|| view! {
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none text-right" on:click=move |_| handle_sort(SortColumn::ETA)>
|
||||
<div class="flex items-center justify-end gap-2">"ETA" {move || sort_icon(SortColumn::ETA)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
|
||||
<DataTableHead class="w-32 cursor-pointer group select-none text-right" on:click=move |_| handle_sort(SortColumn::AddedDate)>
|
||||
<div class="flex items-center justify-end gap-2">"Date" {move || sort_icon(SortColumn::AddedDate)}</div>
|
||||
</DataTableHead>
|
||||
{move || visible_columns.get().contains("AddedDate").then(|| view! {
|
||||
<DataTableHead class="w-32 cursor-pointer group select-none text-right" on:click=move |_| handle_sort(SortColumn::AddedDate)>
|
||||
<div class="flex items-center justify-end gap-2">"Date" {move || sort_icon(SortColumn::AddedDate)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
</DataTableRow>
|
||||
</DataTableHeader>
|
||||
<DataTableBody>
|
||||
@@ -225,39 +358,25 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
when=move || !filtered_hashes.get().is_empty()
|
||||
fallback=move || view! {
|
||||
<DataTableRow class="hover:bg-transparent">
|
||||
<DataTableCell attr:colspan="9" class="h-[400px]">
|
||||
<DataTableCell attr:colspan="10" class="h-[400px]">
|
||||
<Empty class="h-full">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant=EmptyMediaVariant::Icon>
|
||||
<Inbox class="size-10" />
|
||||
<Inbox class="size-10 text-muted-foreground" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>"Torrent Bulunamadı"</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
{move || {
|
||||
let query = store.search_query.get();
|
||||
if query.is_empty() {
|
||||
"Henüz eklenmiş bir torrent bulunmuyor.".to_string()
|
||||
} else {
|
||||
format!("'{}' araması için sonuç bulunamadı.", query)
|
||||
}
|
||||
if query.is_empty() { "Henüz torrent bulunmuyor.".to_string() }
|
||||
else { "Arama kriterlerinize uygun sonuç bulunamadı.".to_string() }
|
||||
}}
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button
|
||||
variant=ButtonVariant::Outline
|
||||
on:click=move |_| {
|
||||
store.search_query.set(String::new());
|
||||
store.filter.set(crate::store::FilterStatus::All);
|
||||
}
|
||||
>
|
||||
"Tümünü Göster"
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</DataTableCell>
|
||||
</DataTableRow>
|
||||
}
|
||||
}.into_any()
|
||||
>
|
||||
<For each=move || filtered_hashes.get() key=|hash| hash.clone() children={
|
||||
let on_action = on_action.clone();
|
||||
@@ -272,6 +391,7 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
hash=hash.clone()
|
||||
on_action=on_action.clone()
|
||||
is_selected=is_selected
|
||||
visible_columns=visible_columns
|
||||
on_select=Callback::new(move |checked| {
|
||||
selected_hashes.update(|selected| {
|
||||
if checked { selected.insert(h_for_change.clone()); }
|
||||
@@ -287,44 +407,16 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
</DataTable>
|
||||
</div>
|
||||
</DataTableWrapper>
|
||||
|
||||
// Selection Info Footer
|
||||
<div class="flex items-center justify-between py-2 text-xs text-muted-foreground">
|
||||
<div>
|
||||
{move || format!("{} / {} torrent seçili", selected_count.get(), filtered_hashes.get().len())}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// --- MOBILE VIEW ---
|
||||
<div class="md:hidden flex flex-col h-full bg-muted/10 relative overflow-hidden">
|
||||
<div class="flex-1 overflow-y-auto p-3 min-h-0">
|
||||
<Show
|
||||
when=move || !filtered_hashes.get().is_empty()
|
||||
fallback=move || view! {
|
||||
<Empty class="h-64 mt-10">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant=EmptyMediaVariant::Icon>
|
||||
<Inbox class="size-10" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>"Boş Görünüyor"</EmptyTitle>
|
||||
<EmptyDescription>"Burada gösterilecek bir şey yok."</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
}
|
||||
>
|
||||
<For each=move || filtered_hashes.get() key=|hash| hash.clone() children={
|
||||
let on_action = on_action.clone();
|
||||
move |hash| {
|
||||
view! {
|
||||
<div class="pb-3">
|
||||
<TorrentCard hash=hash.clone() on_action=on_action.clone() />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
} />
|
||||
<div class="hidden md:flex items-center justify-between px-2 py-1 text-[11px] text-muted-foreground bg-muted/20 border rounded-md">
|
||||
<div class="flex gap-4">
|
||||
<span>{move || format!("Toplam: {} torrent", filtered_hashes.get().len())}</span>
|
||||
<Show when=move || selected_count.get() > 0>
|
||||
<span class="text-primary font-medium">{move || format!("{} torrent seçili", selected_count.get())}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<div>"VibeTorrent v3"</div>
|
||||
</div>
|
||||
</div>
|
||||
}.into_any()
|
||||
@@ -335,6 +427,7 @@ fn TorrentRow(
|
||||
hash: String,
|
||||
on_action: Callback<(String, String)>,
|
||||
is_selected: Signal<bool>,
|
||||
visible_columns: RwSignal<HashSet<String>>,
|
||||
on_select: Callback<bool>,
|
||||
) -> impl IntoView {
|
||||
let store = use_context::<crate::store::TorrentStore>().expect("store not provided");
|
||||
@@ -357,8 +450,6 @@ fn TorrentRow(
|
||||
selected.as_deref() == Some(stored_hash.get_value().as_str())
|
||||
});
|
||||
|
||||
let t_name_for_title = t_name.clone();
|
||||
let t_name_for_content = t_name.clone();
|
||||
let h_for_menu = stored_hash.get_value();
|
||||
|
||||
view! {
|
||||
@@ -368,41 +459,90 @@ fn TorrentRow(
|
||||
attr:data-state=move || if is_selected.get() || is_active_selection.get() { "selected" } else { "" }
|
||||
on:click=move |_| store.selected_torrent.set(Some(stored_hash.get_value()))
|
||||
>
|
||||
<DataTableCell class="w-12">
|
||||
<DataTableCell class="w-12 px-4">
|
||||
<Checkbox
|
||||
checked=is_selected
|
||||
on_checked_change=on_select
|
||||
/>
|
||||
</DataTableCell>
|
||||
<DataTableCell class="font-medium truncate max-w-[200px] lg:max-w-md" attr:title=t_name_for_title>
|
||||
{t_name_for_content}
|
||||
</DataTableCell>
|
||||
<DataTableCell class="font-mono text-xs text-muted-foreground">
|
||||
{format_bytes(t.size)}
|
||||
</DataTableCell>
|
||||
<DataTableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-1.5 w-full bg-secondary rounded-full overflow-hidden min-w-[80px]">
|
||||
<div class="h-full bg-primary transition-all duration-500" style=format!("width: {}%", t.percent_complete)></div>
|
||||
</div>
|
||||
<span class="text-[10px] text-muted-foreground w-10 text-right">{format!("{:.1}%", t.percent_complete)}</span>
|
||||
</div>
|
||||
</DataTableCell>
|
||||
<DataTableCell class={format!("text-xs font-semibold {}", status_color)}>
|
||||
{format!("{:?}", t.status)}
|
||||
</DataTableCell>
|
||||
<DataTableCell class="text-right font-mono text-xs text-green-600 dark:text-green-500 whitespace-nowrap">
|
||||
{format_speed(t.down_rate)}
|
||||
</DataTableCell>
|
||||
<DataTableCell class="text-right font-mono text-xs text-blue-600 dark:text-blue-500 whitespace-nowrap">
|
||||
{format_speed(t.up_rate)}
|
||||
</DataTableCell>
|
||||
<DataTableCell class="text-right font-mono text-xs text-muted-foreground whitespace-nowrap">
|
||||
{format_duration(t.eta)}
|
||||
</DataTableCell>
|
||||
<DataTableCell class="text-right font-mono text-xs text-muted-foreground whitespace-nowrap">
|
||||
{format_date(t.added_date)}
|
||||
</DataTableCell>
|
||||
|
||||
{let t_name = t_name.clone();
|
||||
move || visible_columns.get().contains("Name").then({
|
||||
let t_name = t_name.clone();
|
||||
move || view! {
|
||||
<DataTableCell class="font-medium truncate max-w-[200px] lg:max-w-md" attr:title=t_name.clone()>
|
||||
{t_name.clone()}
|
||||
</DataTableCell>
|
||||
}
|
||||
}).into_any()}
|
||||
|
||||
{move || visible_columns.get().contains("Size").then(|| {
|
||||
let size_str = format_bytes(t.size);
|
||||
view! {
|
||||
<DataTableCell class="font-mono text-xs text-muted-foreground whitespace-nowrap">
|
||||
{size_str}
|
||||
</DataTableCell>
|
||||
}
|
||||
}).into_any()}
|
||||
|
||||
{move || visible_columns.get().contains("Progress").then(|| {
|
||||
let percent = t.percent_complete;
|
||||
view! {
|
||||
<DataTableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-1.5 w-full bg-secondary rounded-full overflow-hidden min-w-[80px]">
|
||||
<div class="h-full bg-primary transition-all duration-500" style=format!("width: {}%", percent)></div>
|
||||
</div>
|
||||
<span class="text-[10px] text-muted-foreground w-10 text-right">{format!("{:.1}%", percent)}</span>
|
||||
</div>
|
||||
</DataTableCell>
|
||||
}
|
||||
}).into_any()}
|
||||
|
||||
{move || visible_columns.get().contains("Status").then(|| {
|
||||
let status_str = format!("{:?}", t.status);
|
||||
view! {
|
||||
<DataTableCell class={format!("text-xs font-semibold whitespace-nowrap {}", status_color)}>
|
||||
{status_str}
|
||||
</DataTableCell>
|
||||
}
|
||||
}).into_any()}
|
||||
|
||||
{move || visible_columns.get().contains("DownSpeed").then(|| {
|
||||
let speed_str = format_speed(t.down_rate);
|
||||
view! {
|
||||
<DataTableCell class="text-right font-mono text-xs text-green-600 dark:text-green-500 whitespace-nowrap">
|
||||
{speed_str}
|
||||
</DataTableCell>
|
||||
}
|
||||
}).into_any()}
|
||||
|
||||
{move || visible_columns.get().contains("UpSpeed").then(|| {
|
||||
let speed_str = format_speed(t.up_rate);
|
||||
view! {
|
||||
<DataTableCell class="text-right font-mono text-xs text-blue-600 dark:text-blue-500 whitespace-nowrap">
|
||||
{speed_str}
|
||||
</DataTableCell>
|
||||
}
|
||||
}).into_any()}
|
||||
|
||||
{move || visible_columns.get().contains("ETA").then(|| {
|
||||
let eta_str = format_duration(t.eta);
|
||||
view! {
|
||||
<DataTableCell class="text-right font-mono text-xs text-muted-foreground whitespace-nowrap">
|
||||
{eta_str}
|
||||
</DataTableCell>
|
||||
}
|
||||
}).into_any()}
|
||||
|
||||
{move || visible_columns.get().contains("AddedDate").then(|| {
|
||||
let date_str = format_date(t.added_date);
|
||||
view! {
|
||||
<DataTableCell class="text-right font-mono text-xs text-muted-foreground whitespace-nowrap">
|
||||
{date_str}
|
||||
</DataTableCell>
|
||||
}
|
||||
}).into_any()}
|
||||
</DataTableRow>
|
||||
</TorrentContextMenu>
|
||||
}.into_any()
|
||||
|
||||
Reference in New Issue
Block a user