feat: implement official DataTable components and fix row spacing issues
Some checks failed
Build MIPS Binary / build (push) Failing after 1m33s

This commit is contained in:
spinline
2026-02-12 00:32:58 +03:00
parent 04cb7d51cb
commit 56e8cc03d1
4 changed files with 74 additions and 54 deletions

View File

@@ -5,7 +5,7 @@ use crate::api;
use shared::NotificationLevel; use shared::NotificationLevel;
use crate::components::context_menu::TorrentContextMenu; use crate::components::context_menu::TorrentContextMenu;
use crate::components::ui::card::{Card, CardHeader, CardTitle, CardContent as CardBody}; use crate::components::ui::card::{Card, CardHeader, CardTitle, CardContent as CardBody};
use crate::components::ui::table::*; use crate::components::ui::data_table::*;
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"];
@@ -136,38 +136,38 @@ pub fn TorrentTable() -> impl IntoView {
<div class="h-full bg-background relative flex flex-col overflow-hidden px-4 py-2"> <div class="h-full bg-background relative flex flex-col overflow-hidden px-4 py-2">
// --- DESKTOP VIEW --- // --- DESKTOP VIEW ---
<div class="hidden md:flex flex-col h-full overflow-hidden"> <div class="hidden md:flex flex-col h-full overflow-hidden">
<TableWrapper class="flex-1 flex flex-col min-h-0 bg-card/50"> <DataTableWrapper class="flex-1 min-h-0 bg-card/50">
<div class="flex-1 overflow-y-auto overflow-x-hidden"> <div class="h-full overflow-auto">
<Table class="w-full"> <DataTable>
<TableHeader class="sticky top-0 bg-muted/80 backdrop-blur-sm z-10"> <DataTableHeader class="sticky top-0 bg-muted/80 backdrop-blur-sm z-10">
<TableRow class="hover:bg-transparent"> <DataTableRow class="hover:bg-transparent">
<TableHead class="cursor-pointer group select-none whitespace-nowrap" on:click=move |_| handle_sort(SortColumn::Name)> <DataTableHead class="cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Name)>
<div class="flex items-center">"Name" {move || sort_arrow(SortColumn::Name)}</div> <div class="flex items-center">"Name" {move || sort_arrow(SortColumn::Name)}</div>
</TableHead> </DataTableHead>
<TableHead class="w-24 cursor-pointer group select-none whitespace-nowrap" on:click=move |_| handle_sort(SortColumn::Size)> <DataTableHead class="w-24 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Size)>
<div class="flex items-center">"Size" {move || sort_arrow(SortColumn::Size)}</div> <div class="flex items-center">"Size" {move || sort_arrow(SortColumn::Size)}</div>
</TableHead> </DataTableHead>
<TableHead class="w-48 cursor-pointer group select-none whitespace-nowrap" on:click=move |_| handle_sort(SortColumn::Progress)> <DataTableHead class="w-48 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Progress)>
<div class="flex items-center">"Progress" {move || sort_arrow(SortColumn::Progress)}</div> <div class="flex items-center">"Progress" {move || sort_arrow(SortColumn::Progress)}</div>
</TableHead> </DataTableHead>
<TableHead class="w-24 cursor-pointer group select-none whitespace-nowrap" on:click=move |_| handle_sort(SortColumn::Status)> <DataTableHead class="w-24 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Status)>
<div class="flex items-center">"Status" {move || sort_arrow(SortColumn::Status)}</div> <div class="flex items-center">"Status" {move || sort_arrow(SortColumn::Status)}</div>
</TableHead> </DataTableHead>
<TableHead class="w-24 cursor-pointer group select-none whitespace-nowrap" on:click=move |_| handle_sort(SortColumn::DownSpeed)> <DataTableHead class="w-24 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::DownSpeed)>
<div class="flex items-center">"DL Speed" {move || sort_arrow(SortColumn::DownSpeed)}</div> <div class="flex items-center">"DL Speed" {move || sort_arrow(SortColumn::DownSpeed)}</div>
</TableHead> </DataTableHead>
<TableHead class="w-24 cursor-pointer group select-none whitespace-nowrap" on:click=move |_| handle_sort(SortColumn::UpSpeed)> <DataTableHead class="w-24 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::UpSpeed)>
<div class="flex items-center">"Up Speed" {move || sort_arrow(SortColumn::UpSpeed)}</div> <div class="flex items-center">"Up Speed" {move || sort_arrow(SortColumn::UpSpeed)}</div>
</TableHead> </DataTableHead>
<TableHead class="w-24 cursor-pointer group select-none whitespace-nowrap" on:click=move |_| handle_sort(SortColumn::ETA)> <DataTableHead class="w-24 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::ETA)>
<div class="flex items-center">"ETA" {move || sort_arrow(SortColumn::ETA)}</div> <div class="flex items-center">"ETA" {move || sort_arrow(SortColumn::ETA)}</div>
</TableHead> </DataTableHead>
<TableHead class="w-32 cursor-pointer group select-none whitespace-nowrap" on:click=move |_| handle_sort(SortColumn::AddedDate)> <DataTableHead class="w-32 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::AddedDate)>
<div class="flex items-center">"Date" {move || sort_arrow(SortColumn::AddedDate)}</div> <div class="flex items-center">"Date" {move || sort_arrow(SortColumn::AddedDate)}</div>
</TableHead> </DataTableHead>
</TableRow> </DataTableRow>
</TableHeader> </DataTableHeader>
<TableBody> <DataTableBody>
<For each=move || filtered_hashes.get() key=|hash| hash.clone() children={ <For each=move || filtered_hashes.get() key=|hash| hash.clone() children={
let on_action = on_action.clone(); let on_action = on_action.clone();
move |hash| { move |hash| {
@@ -176,10 +176,10 @@ pub fn TorrentTable() -> impl IntoView {
} }
} }
} /> } />
</TableBody> </DataTableBody>
</Table> </DataTable>
</div> </div>
</TableWrapper> </DataTableWrapper>
</div> </div>
// --- MOBILE VIEW --- // --- MOBILE VIEW ---
@@ -232,41 +232,41 @@ fn TorrentRow(
view! { view! {
<TorrentContextMenu torrent_hash=h_for_menu on_action=on_action.clone()> <TorrentContextMenu torrent_hash=h_for_menu on_action=on_action.clone()>
<TableRow <DataTableRow
class="cursor-pointer h-12" class="cursor-pointer group"
attr:data-state=move || if is_selected.get() { "selected" } else { "" } attr:data-state=move || if is_selected.get() { "selected" } else { "" }
on:click=move |_| store.selected_torrent.set(Some(stored_hash.get_value())) on:click=move |_| store.selected_torrent.set(Some(stored_hash.get_value()))
> >
<TableCell class="font-medium truncate max-w-[200px] lg:max-w-md px-2 py-0 h-12 flex items-center border-0" attr:title=t_name_for_title> <DataTableCell class="font-medium truncate max-w-[200px] lg:max-w-md" attr:title=t_name_for_title>
{t_name_for_content} {t_name_for_content}
</TableCell> </DataTableCell>
<TableCell class="font-mono text-xs text-muted-foreground px-2"> <DataTableCell class="font-mono text-xs text-muted-foreground">
{format_bytes(t.size)} {format_bytes(t.size)}
</TableCell> </DataTableCell>
<TableCell class="px-2"> <DataTableCell>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="h-1.5 w-full bg-secondary rounded-full overflow-hidden"> <div class="h-1.5 w-full bg-secondary rounded-full overflow-hidden">
<div class="h-full bg-primary transition-all duration-500" style=format!("width: {}%", t.percent_complete)></div> <div class="h-full bg-primary transition-all duration-500" style=format!("width: {}%", t.percent_complete)></div>
</div> </div>
<span class="text-[10px] text-muted-foreground w-10 text-right">{format!("{:.1}%", t.percent_complete)}</span> <span class="text-[10px] text-muted-foreground w-10 text-right">{format!("{:.1}%", t.percent_complete)}</span>
</div> </div>
</TableCell> </DataTableCell>
<TableCell class={format!("px-2 text-xs font-semibold {}", status_color)}> <DataTableCell class={format!("text-xs font-semibold {}", status_color)}>
{format!("{:?}", t.status)} {format!("{:?}", t.status)}
</TableCell> </DataTableCell>
<TableCell class="text-right font-mono text-xs text-green-600 dark:text-green-500 px-2 whitespace-nowrap"> <DataTableCell class="text-right font-mono text-xs text-green-600 dark:text-green-500 whitespace-nowrap">
{format_speed(t.down_rate)} {format_speed(t.down_rate)}
</TableCell> </DataTableCell>
<TableCell class="text-right font-mono text-xs text-blue-600 dark:text-blue-500 px-2 whitespace-nowrap"> <DataTableCell class="text-right font-mono text-xs text-blue-600 dark:text-blue-500 whitespace-nowrap">
{format_speed(t.up_rate)} {format_speed(t.up_rate)}
</TableCell> </DataTableCell>
<TableCell class="text-right font-mono text-xs text-muted-foreground px-2 whitespace-nowrap"> <DataTableCell class="text-right font-mono text-xs text-muted-foreground whitespace-nowrap">
{format_duration(t.eta)} {format_duration(t.eta)}
</TableCell> </DataTableCell>
<TableCell class="text-right font-mono text-xs text-muted-foreground px-2 whitespace-nowrap"> <DataTableCell class="text-right font-mono text-xs text-muted-foreground whitespace-nowrap">
{format_date(t.added_date)} {format_date(t.added_date)}
</TableCell> </DataTableCell>
</TableRow> </DataTableRow>
</TorrentContextMenu> </TorrentContextMenu>
}.into_any() }.into_any()
} }
@@ -342,4 +342,4 @@ fn TorrentCard(
} }
</Show> </Show>
}.into_any() }.into_any()
} }

View File

@@ -0,0 +1,6 @@
// * Reuse @table.rs
pub use crate::components::ui::table::{
Table as DataTable, TableBody as DataTableBody, TableCaption as DataTableCaption, TableCell as DataTableCell,
TableFooter as DataTableFooter, TableHead as DataTableHead, TableHeader as DataTableHeader,
TableRow as DataTableRow, TableWrapper as DataTableWrapper,
};

View File

@@ -5,4 +5,6 @@ pub mod toast;
pub mod context_menu; pub mod context_menu;
pub mod theme_toggle; pub mod theme_toggle;
pub mod svg_icon; pub mod svg_icon;
pub mod table; pub mod table;
pub mod data_table;
pub mod sonner;

View File

@@ -3,19 +3,25 @@ use tw_merge::tw_merge;
#[component] #[component]
pub fn TableWrapper(children: Children, #[prop(optional, into)] class: String) -> impl IntoView { pub fn TableWrapper(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let class = tw_merge!("overflow-hidden rounded-md border", class); let class = tw_merge!("overflow-hidden rounded-md border w-full", class);
view! { <div class=class>{children()}</div> } view! { <div class=class>{children()}</div> }
} }
#[component] #[component]
pub fn Table(children: Children, #[prop(optional, into)] class: String) -> impl IntoView { pub fn Table(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let class = tw_merge!("w-full text-sm caption-bottom", class); let class = tw_merge!("w-full text-sm border-collapse", class);
view! { <table class=class>{children()}</table> } view! { <table class=class>{children()}</table> }
} }
#[component]
pub fn TableCaption(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let class = tw_merge!("mt-4 text-sm text-muted-foreground", class);
view! { <caption class=class>{children()}</caption> }
}
#[component] #[component]
pub fn TableHeader(children: Children, #[prop(optional, into)] class: String) -> impl IntoView { pub fn TableHeader(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let class = tw_merge!("[&_tr]:border-b", class); let class = tw_merge!("[&_tr]:border-b bg-muted/50", class);
view! { <thead class=class>{children()}</thead> } view! { <thead class=class>{children()}</thead> }
} }
@@ -27,7 +33,7 @@ pub fn TableRow(children: Children, #[prop(optional, into)] class: String) -> im
#[component] #[component]
pub fn TableHead(children: Children, #[prop(optional, into)] class: String) -> impl IntoView { pub fn TableHead(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let class = tw_merge!("h-10 px-2 text-left align-middle font-medium text-muted-foreground", class); let class = tw_merge!("h-10 px-4 text-left align-middle font-medium text-muted-foreground whitespace-nowrap", class);
view! { <th class=class>{children()}</th> } view! { <th class=class>{children()}</th> }
} }
@@ -39,6 +45,12 @@ pub fn TableBody(children: Children, #[prop(optional, into)] class: String) -> i
#[component] #[component]
pub fn TableCell(children: Children, #[prop(optional, into)] class: String) -> impl IntoView { pub fn TableCell(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let class = tw_merge!("p-2 align-middle", class); let class = tw_merge!("p-2 px-4 align-middle", class);
view! { <td class=class>{children()}</td> } view! { <td class=class>{children()}</td> }
} }
#[component]
pub fn TableFooter(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let class = tw_merge!("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", class);
view! { <tfoot class=class>{children()}</tfoot> }
}