refactor(frontend): rewrite with Thaw UI components and inline modal

This commit is contained in:
spinline
2026-01-31 14:15:05 +03:00
parent e61dd7b19d
commit 892cb02b98
12 changed files with 496 additions and 1297 deletions

View File

@@ -1,4 +1,7 @@
pub mod modal;
pub mod context_menu;
pub mod ui;
pub mod toolbar;
pub mod sidebar;
pub mod status_bar;
pub mod torrent_table;

View File

@@ -6,7 +6,7 @@ pub fn Modal(
children: Children,
#[prop(into)] on_confirm: Callback<()>,
#[prop(into)] on_cancel: Callback<()>,
#[prop(into)] visible: Signal<bool>,
#[prop(into)] is_open: MaybeSignal<bool>,
#[prop(into, default = "Confirm".to_string())] confirm_text: String,
#[prop(into, default = "Cancel".to_string())] cancel_text: String,
#[prop(into, default = false)] is_danger: bool,
@@ -20,7 +20,7 @@ pub fn Modal(
let cancel_text = store_value(cancel_text);
view! {
<Show when=move || visible.get() fallback=|| ()>
<Show when=move || is_open.get() fallback=|| ()>
<div class="fixed inset-0 bg-background/80 backdrop-blur-sm flex items-end md:items-center justify-center z-[200] animate-in fade-in duration-200 sm:p-4">
<div class="bg-card p-6 rounded-t-2xl md:rounded-lg w-full max-w-sm shadow-xl border border-border ring-0 transform transition-all animate-in slide-in-from-bottom-10 md:slide-in-from-bottom-0 md:zoom-in-95">
<h3 class="text-lg font-semibold text-card-foreground mb-4">{title.get_value()}</h3>

View File

@@ -0,0 +1,52 @@
use leptos::*;
use thaw::*;
use shared::TorrentStatus;
#[component]
pub fn Sidebar(
#[prop(into)] active_filter: Signal<Option<TorrentStatus>>,
#[prop(into)] on_filter_change: Callback<Option<TorrentStatus>>,
) -> impl IntoView {
view! {
<div class="w-64 border-r border-border bg-card/30 flex flex-col">
<div class="p-4 font-bold text-lg">"Groups"</div>
<div class="flex-1 overflow-y-auto p-2 space-y-1">
<Button
variant=if active_filter.get().is_none() { ButtonVariant::Primary } else { ButtonVariant::Text }
class="w-full justify-start text-left"
on_click=move |_| on_filter_change.call(None)
>
"All"
</Button>
<Button
variant=if active_filter.get() == Some(TorrentStatus::Downloading) { ButtonVariant::Primary } else { ButtonVariant::Text }
class="w-full justify-start text-left"
on_click=move |_| on_filter_change.call(Some(TorrentStatus::Downloading))
>
"Downloading"
</Button>
<Button
variant=if active_filter.get() == Some(TorrentStatus::Seeding) { ButtonVariant::Primary } else { ButtonVariant::Text }
class="w-full justify-start text-left"
on_click=move |_| on_filter_change.call(Some(TorrentStatus::Seeding))
>
"Seeding"
</Button>
<Button
variant=if active_filter.get() == Some(TorrentStatus::Paused) { ButtonVariant::Primary } else { ButtonVariant::Text }
class="w-full justify-start text-left"
on_click=move |_| on_filter_change.call(Some(TorrentStatus::Paused))
>
"Paused"
</Button>
<Button
variant=if active_filter.get() == Some(TorrentStatus::Error) { ButtonVariant::Primary } else { ButtonVariant::Text }
class="w-full justify-start text-left"
on_click=move |_| on_filter_change.call(Some(TorrentStatus::Error))
>
"Errors"
</Button>
</div>
</div>
}
}

View File

@@ -0,0 +1,20 @@
use leptos::*;
use thaw::*;
#[component]
pub fn StatusBar() -> impl IntoView {
view! {
<div class="h-8 border-t border-border bg-card/30 flex items-center px-4 text-xs space-x-4">
<div class="flex items-center gap-1">
<span class="i-mdi-arrow-down text-green-500"></span>
"0 KB/s"
</div>
<div class="flex items-center gap-1">
<span class="i-mdi-arrow-up text-blue-500"></span>
"0 KB/s"
</div>
<div class="flex-1"></div>
<div>"Free Space: 700 GB"</div>
</div>
}
}

View File

@@ -0,0 +1,34 @@
use leptos::*;
use thaw::*;
#[component]
pub fn Toolbar(
#[prop(into)] on_add: Callback<()>,
#[prop(into)] on_start: Callback<()>,
#[prop(into)] on_pause: Callback<()>,
#[prop(into)] on_delete: Callback<()>,
#[prop(into)] on_settings: Callback<()>,
) -> impl IntoView {
view! {
<div class="flex items-center gap-2 p-2 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<Button variant=ButtonVariant::Text on_click=move |_| on_add.call(())>
<span class="i-mdi-plus mr-2"/> "Add"
</Button>
<div class="h-4 w-px bg-border mx-2"></div>
<Button variant=ButtonVariant::Text on_click=move |_| on_start.call(())>
<span class="i-mdi-play mr-2"/> "Start"
</Button>
<Button variant=ButtonVariant::Text on_click=move |_| on_pause.call(())>
<span class="i-mdi-pause mr-2"/> "Pause"
</Button>
<Button variant=ButtonVariant::Text color=ButtonColor::Error on_click=move |_| on_delete.call(())>
<span class="i-mdi-delete mr-2"/> "Delete"
</Button>
<div class="flex-1"></div>
<Input placeholder="Filter..." class="w-48" />
<Button variant=ButtonVariant::Text on_click=move |_| on_settings.call(())>
<span class="i-mdi-cog mr-2"/> "Settings"
</Button>
</div>
}
}

View File

@@ -0,0 +1,51 @@
use leptos::*;
use thaw::*;
use shared::Torrent;
#[component]
pub fn TorrentTable(
#[prop(into)] torrents: Signal<Vec<Torrent>>
) -> impl IntoView {
view! {
<div class="flex-1 overflow-auto bg-background">
<table class="w-full text-left text-xs">
<thead class="bg-muted/50 border-b border-border text-muted-foreground font-medium sticky top-0 bg-background z-10">
<tr>
<th class="px-2 py-1.5 font-medium">"Name"</th>
<th class="px-2 py-1.5 font-medium w-20 text-right">"Size"</th>
<th class="px-2 py-1.5 font-medium w-24">"Progress"</th>
<th class="px-2 py-1.5 font-medium w-20 text-center">"Status"</th>
<th class="px-2 py-1.5 font-medium w-20 text-right">"Down"</th>
<th class="px-2 py-1.5 font-medium w-20 text-right">"Up"</th>
<th class="px-2 py-1.5 font-medium w-20 text-right">"ETA"</th>
</tr>
</thead>
<tbody class="divide-y divide-border">
<For
each=move || torrents.get()
key=|t| t.hash.clone()
children=move |torrent| {
view! {
<tr class="hover:bg-muted/50 group transition-colors">
<td class="px-2 py-1.5 truncate max-w-[200px]">{torrent.name}</td>
<td class="px-2 py-1.5 text-right whitespace-nowrap text-muted-foreground">{torrent.size}</td>
<td class="px-2 py-1.5">
<Progress percentage=torrent.percent_complete as f32 />
</td>
<td class="px-2 py-1.5 text-center">
<span class="text-[10px] px-1.5 py-0.5 rounded-full border border-border bg-background">
{format!("{:?}", torrent.status)}
</span>
</td>
<td class="px-2 py-1.5 text-right whitespace-nowrap text-blue-500">{torrent.down_rate}</td>
<td class="px-2 py-1.5 text-right whitespace-nowrap text-green-500">{torrent.up_rate}</td>
<td class="px-2 py-1.5 text-right whitespace-nowrap text-muted-foreground">{torrent.eta}</td>
</tr>
}
}
/>
</tbody>
</table>
</div>
}
}

View File

@@ -1,62 +0,0 @@
use leptos::*;
use crate::utils::cn;
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum ButtonVariant {
#[default]
Default,
Destructive,
Outline,
Secondary,
Ghost,
Link,
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum ButtonSize {
#[default]
Default,
Sm,
Lg,
Icon,
}
#[component]
pub fn Button(
#[prop(into, optional)] variant: ButtonVariant,
#[prop(into, optional)] size: ButtonSize,
#[prop(into, optional)] class: MaybeSignal<String>,
#[prop(into, optional)] on_click: Option<Callback<web_sys::MouseEvent>>,
children: Children,
) -> impl IntoView {
let variant_classes = match variant {
ButtonVariant::Default => "bg-primary text-primary-foreground hover:bg-primary/90",
ButtonVariant::Destructive => "bg-destructive text-destructive-foreground hover:bg-destructive/90",
ButtonVariant::Outline => "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ButtonVariant::Secondary => "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ButtonVariant::Ghost => "hover:bg-accent hover:text-accent-foreground",
ButtonVariant::Link => "text-primary underline-offset-4 hover:underline",
};
let size_classes = match size {
ButtonSize::Default => "h-10 px-4 py-2",
ButtonSize::Sm => "h-9 rounded-md px-3",
ButtonSize::Lg => "h-11 rounded-md px-8",
ButtonSize::Icon => "h-10 w-10",
};
let base_classes = "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50";
view! {
<button
class=move || cn(format!("{} {} {} {}", base_classes, variant_classes, size_classes, class.get()))
on:click=move |e| {
if let Some(cb) = on_click {
cb.call(e);
}
}
>
{children()}
</button>
}
}

View File

@@ -1 +0,0 @@
pub mod button;