feat: complete modernization with shadcn, stateless auth, and performance optimizations
All checks were successful
Build MIPS Binary / build (push) Successful in 5m20s

This commit is contained in:
spinline
2026-02-10 22:16:36 +03:00
parent 8815727620
commit fddc81365b
18 changed files with 1150 additions and 2615 deletions

View File

@@ -22,9 +22,8 @@ pub fn App() -> impl IntoView {
spawn_local(async move {
log::info!("App initialization started...");
let setup_res = api::setup::get_status().await;
match setup_res {
// Check if setup is needed via Server Function
match shared::server_fns::auth::get_setup_status().await {
Ok(status) => {
if !status.completed {
log::info!("Setup not completed");
@@ -36,21 +35,16 @@ pub fn App() -> impl IntoView {
Err(e) => log::error!("Failed to get setup status: {:?}", e),
}
let auth_res = api::auth::check_auth().await;
match auth_res {
Ok(true) => {
log::info!("Authenticated!");
if let Ok(user_info) = api::auth::get_user().await {
if let Some(s) = store {
s.user.set(Some(user_info.username));
}
// Check authentication via GetUser Server Function
match shared::server_fns::auth::get_user().await {
Ok(Some(user_info)) => {
log::info!("Authenticated as {}", user_info.username);
if let Some(s) = store {
s.user.set(Some(user_info.username));
}
is_authenticated.1.set(true);
}
Ok(false) => {
Ok(None) => {
log::info!("Not authenticated");
}
Err(e) => {
@@ -107,22 +101,26 @@ pub fn App() -> impl IntoView {
} />
<Route path=leptos_router::path!("/") view=move || {
let navigate = use_navigate();
Effect::new(move |_| {
if !is_loading.0.get() && needs_setup.0.get() {
log::info!("Setup not completed, redirecting to setup");
let navigate = use_navigate();
navigate("/setup", Default::default());
} else if !is_loading.0.get() && !is_authenticated.0.get() {
log::info!("Not authenticated, redirecting to login");
let navigate = use_navigate();
navigate("/login", Default::default());
if !is_loading.0.get() {
if needs_setup.0.get() {
log::info!("Setup not completed, redirecting to setup");
navigate("/setup", Default::default());
} else if !is_authenticated.0.get() {
log::info!("Not authenticated, redirecting to login");
navigate("/login", Default::default());
}
}
});
view! {
<Show when=move || !is_loading.0.get() fallback=|| view! {
<div class="flex items-center justify-center h-screen bg-base-100">
<span class="loading loading-spinner loading-lg"></span>
<div class="flex items-center justify-center h-screen bg-background">
<div class="flex flex-col items-center gap-4">
<div class="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
<p class="text-sm text-muted-foreground">"Yükleniyor..."</p>
</div>
</div>
}>
<Show when=move || is_authenticated.0.get() fallback=|| ()>

View File

@@ -1,6 +1,5 @@
use leptos::prelude::*;
use leptos::task::spawn_local;
use crate::api;
#[component]
pub fn Setup() -> impl IntoView {
@@ -32,7 +31,7 @@ pub fn Setup() -> impl IntoView {
let user = username.0.get();
spawn_local(async move {
match api::setup::setup(&user, &pass).await {
match shared::server_fns::auth::setup(user, pass).await {
Ok(_) => {
log::info!("Setup completed successfully, redirecting...");
let window = web_sys::window().expect("window should exist");
@@ -40,7 +39,7 @@ pub fn Setup() -> impl IntoView {
}
Err(e) => {
log::error!("Setup failed: {:?}", e);
error.1.set(Some(format!("Hata: {:?}", e)));
error.1.set(Some("Kurulum sırasında bir hata oluştu".to_string()));
loading.1.set(false);
}
}
@@ -48,56 +47,56 @@ pub fn Setup() -> impl IntoView {
};
view! {
<div class="flex items-center justify-center min-h-screen bg-base-200">
<div class="card w-full max-w-md shadow-xl bg-base-100">
<div class="card-body">
<div class="flex flex-col items-center mb-6 text-center">
<div class="w-16 h-16 bg-primary rounded-2xl flex items-center justify-center text-primary-content shadow-lg mb-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-10 h-10">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-3.497a2.548 2.548 0 113.586 3.586l-6.837 5.63m-5.108 3.497l2.496-3.03c.317-.384.74-.626 1.208-.766M15.75 9.25a2.548 2.548 0 11-5.096 0 2.548 2.548 0 015.096 0z" />
</svg>
</div>
<h2 class="card-title text-2xl font-bold">"VibeTorrent Kurulumu"</h2>
<p class="text-base-content/60 text-sm">"Yönetici hesabınızı oluşturun"</p>
<div class="flex items-center justify-center min-h-screen bg-muted/40 px-4">
<div class="w-full max-w-md rounded-xl border border-border bg-card text-card-foreground shadow-lg overflow-hidden">
<div class="flex flex-col space-y-1.5 p-6 pb-2 items-center text-center">
<div class="w-12 h-12 bg-primary rounded-xl flex items-center justify-center text-primary-foreground shadow-sm mb-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-3.497a2.548 2.548 0 113.586 3.586l-6.837 5.63m-5.108 3.497l2.496-3.03c.317-.384.74-.626 1.208-.766M15.75 9.25a2.548 2.548 0 11-5.096 0 2.548 2.548 0 015.096 0z" />
</svg>
</div>
<h3 class="font-semibold tracking-tight text-2xl">"VibeTorrent Kurulumu"</h3>
<p class="text-sm text-muted-foreground">"Yönetici hesabınızı oluşturun"</p>
</div>
<div class="p-6 pt-4">
<form on:submit=handle_setup class="space-y-4">
<div class="form-control">
<label class="label">
<span class="label-text">"Yönetici Kullanıcı Adı"</span>
<div class="space-y-2">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
"Yönetici Kullanıcı Adı"
</label>
<input
type="text"
placeholder="admin"
class="input input-bordered w-full"
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
prop:value=move || username.0.get()
on:input=move |ev| username.1.set(event_target_value(&ev))
disabled=move || loading.0.get()
required
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">"Şifre"</span>
<div class="space-y-2">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
"Şifre"
</label>
<input
type="password"
placeholder="******"
class="input input-bordered w-full"
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
prop:value=move || password.0.get()
on:input=move |ev| password.1.set(event_target_value(&ev))
disabled=move || loading.0.get()
required
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">"Şifre Onay"</span>
<div class="space-y-2">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
"Şifre Onay"
</label>
<input
type="password"
placeholder="******"
class="input input-bordered w-full"
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
prop:value=move || confirm_password.0.get()
on:input=move |ev| confirm_password.1.set(event_target_value(&ev))
disabled=move || loading.0.get()
@@ -106,19 +105,20 @@ pub fn Setup() -> impl IntoView {
</div>
<Show when=move || error.0.get().is_some() fallback=|| ()>
<div class="alert alert-error text-xs py-2 shadow-sm">
<div class="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive dark:border-destructive dark:bg-destructive/50 dark:text-destructive-foreground">
<span>{move || error.0.get().unwrap_or_default()}</span>
</div>
</Show>
<div class="form-control mt-6">
<div class="pt-2">
<button
class="btn btn-primary w-full"
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2 w-full"
type="submit"
disabled=move || loading.0.get()
>
<Show when=move || loading.0.get() fallback=|| "Kurulumu Tamamla">
<span class="loading loading-spinner"></span>
<span class="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
"Kuruluyor..."
</Show>
</button>
</div>
@@ -127,4 +127,4 @@ pub fn Setup() -> impl IntoView {
</div>
</div>
}
}
}

View File

@@ -226,19 +226,18 @@ pub fn StatusBar() -> impl IntoView {
let base = "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-xs outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-accent hover:text-accent-foreground capitalize";
if is_active() { format!("{} bg-accent text-accent-foreground font-medium", base) } else { base.to_string() }
}
on:click=move |_| {
set_current_theme.set(theme_name_for_onclick.clone());
close_details(theme_details_ref);
}
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<Show when=is_active fallback=|| ()>
<span>""</span>
</Show>
</span>
{theme_name}
</button>
</li>
on:click=move |_| {
set_current_theme.set(theme_name_for_onclick.clone());
close_details(theme_details_ref);
}
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<Show when=is_active.clone() fallback=|| ()>
<span>""</span>
</Show>
</span>
{theme_name}
</button> </li>
}
}).collect::<Vec<_>>()
}

View File

@@ -1,8 +1,7 @@
use leptos::prelude::*;
use leptos::html;
use leptos::task::spawn_local;
use leptos_use::{use_virtual_list, UseVirtualListOptions, UseVirtualListReturn};
use leptos_use::use_timeout_fn;
use leptos_use::{use_timeout_fn, UseTimeoutFnReturn};
use crate::store::{get_action_messages, show_toast_with_signal};
use crate::api;
use shared::NotificationLevel;
@@ -57,7 +56,7 @@ pub fn TorrentTable() -> impl IntoView {
let search = store.search_query.get();
let search_lower = search.to_lowercase();
let mut torrents: Vec<&shared::Torrent> = torrents_map.values().filter(|t| {
let mut torrents: Vec<shared::Torrent> = torrents_map.values().filter(|t| {
let matches_filter = match filter {
crate::store::FilterStatus::All => true,
crate::store::FilterStatus::Downloading => t.status == shared::TorrentStatus::Downloading,
@@ -69,7 +68,7 @@ pub fn TorrentTable() -> impl IntoView {
};
let matches_search = if search_lower.is_empty() { true } else { t.name.to_lowercase().contains(&search_lower) };
matches_filter && matches_search
}).collect();
}).cloned().collect();
torrents.sort_by(|a, b| {
let col = sort_col.0.get();
@@ -146,27 +145,6 @@ pub fn TorrentTable() -> impl IntoView {
});
};
// --- Virtual List Setup ---
let UseVirtualListReturn {
list: desktop_list,
container_el: desktop_container_el,
wrapper_style: desktop_wrapper_style,
..
} = use_virtual_list(
filtered_hashes,
UseVirtualListOptions::default().item_height(49.0), // Compact row height + border
);
let UseVirtualListReturn {
list: mobile_list,
container_el: mobile_container_el,
wrapper_style: mobile_wrapper_style,
..
} = use_virtual_list(
filtered_hashes,
UseVirtualListOptions::default().item_height(140.0), // Card height + gap
);
view! {
<div class="h-full bg-background relative flex flex-col">
// --- DESKTOP VIEW ---
@@ -199,32 +177,26 @@ pub fn TorrentTable() -> impl IntoView {
</div>
</div>
// Virtual List Container
<div class="flex-1 overflow-y-auto" node_ref=desktop_container_el>
<div style=desktop_wrapper_style class="relative">
// We use Flex/Div rows instead of Table for virtualization simplicity
<For each=desktop_list key=|hash| hash.data.clone() children={
let handle_context_menu = handle_context_menu.clone();
move |item| {
let top_offset = format!("{}px", item.index * 49); // Manual offset based on index
view! {
<div style=format!("position: absolute; top: {}; left: 0; right: 0; height: 49px;", top_offset)>
<TorrentRow
hash=item.data.clone()
selected_hash=selected_hash.0
set_selected_hash=selected_hash.1
on_context_menu=handle_context_menu.clone()
/>
</div>
}
// Regular List (Standard For loop)
<div class="flex-1 overflow-y-auto min-h-0">
<For each=filtered_hashes key=|hash| hash.clone() children={
let handle_context_menu = handle_context_menu.clone();
move |hash| {
view! {
<TorrentRow
hash=hash.clone()
selected_hash=selected_hash.0
set_selected_hash=selected_hash.1
on_context_menu=handle_context_menu.clone()
/>
}
} />
</div>
}
} />
</div>
</div>
// --- MOBILE VIEW ---
<div class="md:hidden flex flex-col h-full bg-muted/10 relative">
<div class="md:hidden flex flex-col h-full bg-muted/10 relative overflow-hidden">
<div class="px-3 py-2 border-b border-border flex justify-between items-center bg-background/95 backdrop-blur z-10 shrink-0">
<span class="text-xs font-bold opacity-50 uppercase tracking-wider text-muted-foreground">"Torrents"</span>
<details class="dropdown dropdown-end" node_ref=sort_details_ref>
@@ -254,29 +226,26 @@ pub fn TorrentTable() -> impl IntoView {
</details>
</div>
<div class="flex-1 overflow-y-auto p-3" node_ref=mobile_container_el>
<div style=mobile_wrapper_style class="relative">
<For each=mobile_list key=|hash| hash.data.clone() children={
let handle_context_menu = handle_context_menu.clone();
let menu_pos_setter = menu_position.1.clone();
let menu_vis_setter = menu_visible.1.clone();
move |item| {
let top_offset = format!("{}px", item.index * 140);
view! {
<div style=format!("position: absolute; top: {}; left: 0; right: 0; height: 140px; padding-bottom: 0.75rem;", top_offset)>
<TorrentCard
hash=item.data.clone()
selected_hash=selected_hash.0
set_selected_hash=selected_hash.1
set_menu_position=menu_pos_setter
set_menu_visible=menu_vis_setter
on_context_menu=handle_context_menu.clone()
/>
</div>
}
<div class="flex-1 overflow-y-auto p-3 min-h-0">
<For each=filtered_hashes key=|hash| hash.clone() children={
let handle_context_menu = handle_context_menu.clone();
let menu_pos_setter = menu_position.1.clone();
let menu_vis_setter = menu_visible.1.clone();
move |hash| {
view! {
<div class="pb-3">
<TorrentCard
hash=hash.clone()
selected_hash=selected_hash.0
set_selected_hash=selected_hash.1
set_menu_position=menu_pos_setter
set_menu_visible=menu_vis_setter
on_context_menu=handle_context_menu.clone()
/>
</div>
}
} />
</div>
}
} />
</div>
</div>
@@ -380,7 +349,7 @@ fn TorrentCard(
let set_menu_position = set_menu_position.clone();
let set_selected_hash = set_selected_hash.clone();
let set_menu_visible = set_menu_visible.clone();
let leptos_use::UseTimeoutFnReturn { start, .. } = use_timeout_fn(
let UseTimeoutFnReturn { start, .. } = use_timeout_fn(
move |pos: (i32, i32)| {
set_menu_position.set(pos);
set_selected_hash.set(Some(t_hash_long.clone()));

View File

@@ -4,7 +4,7 @@ use leptos::prelude::*;
use leptos::task::spawn_local;
use shared::{AppEvent, GlobalStats, NotificationLevel, SystemNotification, Torrent};
use std::collections::HashMap;
use struct_patch::traits::Patchable;
use struct_patch::traits::Patch;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
#[derive(Clone, Debug, PartialEq)]
@@ -137,8 +137,10 @@ pub fn provide_torrent_store() {
}
AppEvent::Update(patch) => {
torrents_for_sse.update(|map| {
if let Some(t) = map.get_mut(&patch.hash) {
t.apply(patch);
if let Some(hash) = patch.hash.as_ref() {
if let Some(t) = map.get_mut(hash) {
t.apply(patch);
}
}
});
}