feat: implement official DataTable components and fix row spacing issues
Some checks failed
Build MIPS Binary / build (push) Failing after 1m33s
Some checks failed
Build MIPS Binary / build (push) Failing after 1m33s
This commit is contained in:
@@ -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()
|
||||||
}
|
}
|
||||||
6
frontend/src/components/ui/data_table.rs
Normal file
6
frontend/src/components/ui/data_table.rs
Normal 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,
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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> }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user