Compare commits

...

7 Commits

Author SHA1 Message Date
spinline
6106d1cd22 fix(ui): prevent selection checkboxes from opening torrent details sheet
All checks were successful
Build MIPS Binary / build (push) Successful in 1m56s
2026-02-21 00:50:26 +03:00
spinline
50b83ebacf chore(ui): remove debugging logs for sse connection
All checks were successful
Build MIPS Binary / build (push) Successful in 1m56s
2026-02-21 00:44:50 +03:00
spinline
566308d889 feat(backend): require rTorrent to be running for backend to start
All checks were successful
Build MIPS Binary / build (push) Successful in 1m54s
2026-02-21 00:42:42 +03:00
spinline
e878d1fe33 chore(ui): add debug logs for SSE connection lifecycle
All checks were successful
Build MIPS Binary / build (push) Successful in 1m56s
2026-02-21 00:29:27 +03:00
spinline
d88084fb9a fix(ui): fix service worker crashes for chrome extensions and bump cache version
All checks were successful
Build MIPS Binary / build (push) Successful in 1m57s
2026-02-21 00:25:28 +03:00
spinline
f8639f2967 chore(ui): add debug logs for SSE deserialization errors
All checks were successful
Build MIPS Binary / build (push) Successful in 1m56s
2026-02-21 00:23:23 +03:00
spinline
7129c9a8eb fix(ui): prevent panic on unwrap when selected torrent is None
All checks were successful
Build MIPS Binary / build (push) Successful in 1m57s
2026-02-21 00:19:14 +03:00
8 changed files with 79 additions and 26 deletions

View File

@@ -243,12 +243,14 @@ async fn main() {
let socket_path = std::path::Path::new(&args.socket); let socket_path = std::path::Path::new(&args.socket);
if !socket_path.exists() { if !socket_path.exists() {
tracing::error!("CRITICAL: rTorrent socket not found at {:?}.", socket_path); tracing::error!("CRITICAL: rTorrent socket not found at {:?}.", socket_path);
tracing::warn!( tracing::error!(
"HINT: Make sure rTorrent is running and the SCGI socket is enabled in .rtorrent.rc" "HINT: Make sure rTorrent is running and the SCGI socket is enabled in .rtorrent.rc"
); );
tracing::warn!( tracing::error!(
"HINT: You can configure the socket path via --socket ARG or RTORRENT_SOCKET ENV." "HINT: You can configure the socket path via --socket ARG or RTORRENT_SOCKET ENV."
); );
tracing::error!("FATAL: VibeTorrent cannot start without a running rTorrent instance. Exiting.");
std::process::exit(1);
} else { } else {
tracing::info!("Socket file exists. Testing connection..."); tracing::info!("Socket file exists. Testing connection...");
let client = xmlrpc::RtorrentClient::new(&args.socket); let client = xmlrpc::RtorrentClient::new(&args.socket);
@@ -259,7 +261,11 @@ async fn main() {
let version = xmlrpc::parse_string_response(&xml).unwrap_or(xml); let version = xmlrpc::parse_string_response(&xml).unwrap_or(xml);
tracing::info!("Connected to rTorrent successfully. Version: {}", version); tracing::info!("Connected to rTorrent successfully. Version: {}", version);
} }
Err(e) => tracing::error!("Socket exists but failed to connect to rTorrent: {}", e), Err(e) => {
tracing::error!("CRITICAL: Socket exists but failed to connect to rTorrent: {}", e);
tracing::error!("FATAL: Ensure rTorrent is fully started and the socket has correct permissions. Exiting.");
std::process::exit(1);
}
} }
} }

View File

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

View File

@@ -1,5 +1,31 @@
use leptos::prelude::*; use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::atomic::{AtomicUsize, Ordering};
pub fn use_random_id_for(prefix: &str) -> String { const PREFIX: &str = "rust_ui"; // Must NOT contain "/" or "-"
format!("{}_{}", prefix, js_sys::Math::random().to_string().replace(".", ""))
pub fn use_random_id() -> String {
format!("_{PREFIX}_{}", generate_hash())
} }
pub fn use_random_id_for(element: &str) -> String {
format!("{}_{PREFIX}_{}", element, generate_hash())
}
pub fn use_random_transition_name() -> String {
let random_id = use_random_id();
format!("view-transition-name: {random_id}")
}
/* ========================================================== */
/* ✨ FUNCTIONS ✨ */
/* ========================================================== */
static COUNTER: AtomicUsize = AtomicUsize::new(1);
fn generate_hash() -> u64 {
let mut hasher = DefaultHasher::new();
let counter = COUNTER.fetch_add(1, Ordering::SeqCst);
counter.hash(&mut hasher);
hasher.finish()
}

View File

@@ -5,3 +5,4 @@ pub mod torrent;
pub mod auth; pub mod auth;
// pub mod toast; (Removed) // pub mod toast; (Removed)
pub mod ui; pub mod ui;
pub mod demos;

View File

@@ -35,13 +35,13 @@ pub fn TorrentDetailsSheet() -> impl IntoView {
<div class="flex flex-col gap-1 min-w-0"> <div class="flex flex-col gap-1 min-w-0">
<Show when=move || selected_torrent.get().is_some() fallback=move || view! { <Skeleton class="h-6 w-48" /> }> <Show when=move || selected_torrent.get().is_some() fallback=move || view! { <Skeleton class="h-6 w-48" /> }>
<h2 class="font-bold text-lg truncate"> <h2 class="font-bold text-lg truncate">
{move || selected_torrent.get().unwrap().name} {move || selected_torrent.get().map(|t| t.name).unwrap_or_default()}
</h2> </h2>
</Show> </Show>
<Show when=move || selected_torrent.get().is_some() fallback=move || view! { <Skeleton class="h-4 w-24" /> }> <Show when=move || selected_torrent.get().is_some() fallback=move || view! { <Skeleton class="h-4 w-24" /> }>
<p class="text-xs text-muted-foreground uppercase tracking-widest font-semibold flex items-center gap-2"> <p class="text-xs text-muted-foreground uppercase tracking-widest font-semibold flex items-center gap-2">
{move || format!("{:?}", selected_torrent.get().unwrap().status)} {move || selected_torrent.get().map(|t| format!("{:?}", t.status)).unwrap_or_default()}
<span class="bg-primary/20 text-primary px-1.5 py-0.5 rounded text-[10px] lowercase">{move || format!("{:.1}%", selected_torrent.get().unwrap().percent_complete)}</span> <span class="bg-primary/20 text-primary px-1.5 py-0.5 rounded text-[10px] lowercase">{move || selected_torrent.get().map(|t| format!("{:.1}%", t.percent_complete)).unwrap_or_default()}</span>
</p> </p>
</Show> </Show>
</div> </div>

View File

@@ -597,10 +597,12 @@ fn TorrentRow(
on:click=move |_| store.selected_torrent.set(Some(stored_hash.get_value())) on:click=move |_| store.selected_torrent.set(Some(stored_hash.get_value()))
> >
<DataTableCell class="w-12 px-4"> <DataTableCell class="w-12 px-4">
<Checkbox <div on:click=move |e| e.stop_propagation()>
checked=is_selected <Checkbox
on_checked_change=on_select checked=is_selected
/> on_checked_change=on_select
/>
</div>
</DataTableCell> </DataTableCell>
{move || visible_columns.get().contains("Name").then({ {move || visible_columns.get().contains("Name").then({
@@ -730,17 +732,23 @@ fn TorrentCard(
} }
) )
on:click=move |_| { on:click=move |_| {
let current = is_selected.get();
on_select.run(!current);
store.selected_torrent.set(Some(stored_hash.get_value())); store.selected_torrent.set(Some(stored_hash.get_value()));
} }
> >
<div class="p-4 space-y-3"> <div class="p-4 space-y-3">
<div class="flex justify-between items-start gap-3"> <div class="flex justify-between items-start gap-3">
<div class="flex-1 min-w-0"> <div class="flex items-start gap-3 flex-1 min-w-0">
<h3 class="text-sm font-bold leading-tight line-clamp-2 break-all">{t_name.clone()}</h3> <div on:click=move |e| e.stop_propagation() class="mt-0.5">
<Checkbox
checked=is_selected
on_checked_change=on_select
/>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-sm font-bold leading-tight line-clamp-2 break-all">{t_name.clone()}</h3>
</div>
</div> </div>
<Badge variant=status_variant class="uppercase tracking-wider text-[10px]"> <Badge variant=status_variant class="uppercase tracking-wider text-[10px] shrink-0">
{format!("{:?}", t.status)} {format!("{:?}", t.status)}
</Badge> </Badge>
</div> </div>

View File

@@ -3,13 +3,17 @@ use leptos_ui::clx;
mod components { mod components {
use super::*; use super::*;
clx! {Card, div, "bg-card text-card-foreground flex flex-col gap-4 rounded-xl border py-6 shadow-sm"} clx! {Card, div, "bg-card text-card-foreground flex flex-col gap-4 rounded-xl border py-6 shadow-sm"}
clx! {CardHeader, div, "@container/card-header flex flex-col items-start gap-1.5 px-6 [.border-b]:pb-6"} // TODO. Change data-slot=card-action by data-name="CardAction".
clx! {CardHeader, div, "@container/card-header flex flex-col items-start gap-1.5 px-6 [.border-b]:pb-6 sm:grid sm:auto-rows-min sm:grid-rows-[auto_auto] has-data-[slot=card-action]:sm:grid-cols-[1fr_auto]"}
clx! {CardTitle, h2, "leading-none font-semibold"} clx! {CardTitle, h2, "leading-none font-semibold"}
clx! {CardContent, div, "px-6"} clx! {CardContent, div, "px-6"}
clx! {CardDescription, p, "text-muted-foreground text-sm"} clx! {CardDescription, p, "text-muted-foreground text-sm"}
clx! {CardFooter, footer, "flex items-center px-6 [.border-t]:pt-6", "gap-2"} clx! {CardFooter, footer, "flex items-center px-6 [.border-t]:pt-6", "gap-2"}
clx! {CardAction, div, "self-start sm:col-start-2 sm:row-span-2 sm:row-start-1 sm:justify-self-end"}
clx! {CardList, ul, "flex flex-col gap-4"}
clx! {CardItem, li, "flex items-center [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0"}
} }
pub use components::*; pub use components::*;

View File

@@ -1,4 +1,4 @@
const CACHE_NAME = "vibetorrent-v2"; const CACHE_NAME = "vibetorrent-v3";
const ASSETS_TO_CACHE = [ const ASSETS_TO_CACHE = [
"/", "/",
"/index.html", "/index.html",
@@ -51,6 +51,11 @@ self.addEventListener("activate", (event) => {
self.addEventListener("fetch", (event) => { self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url); const url = new URL(event.request.url);
// Skip unsupported schemes (like chrome-extension://)
if (!url.protocol.startsWith("http")) {
return;
}
// Network-first strategy for API calls // Network-first strategy for API calls
if (url.pathname.startsWith("/api/")) { if (url.pathname.startsWith("/api/")) {
event.respondWith( event.respondWith(
@@ -75,10 +80,12 @@ self.addEventListener("fetch", (event) => {
fetch(event.request) fetch(event.request)
.then((response) => { .then((response) => {
// Cache the latest version of the HTML // Cache the latest version of the HTML
const responseToCache = response.clone(); if (response && response.status === 200) {
caches.open(CACHE_NAME).then((cache) => { const responseToCache = response.clone();
cache.put(event.request, responseToCache); caches.open(CACHE_NAME).then((cache) => {
}); cache.put(event.request, responseToCache);
});
}
return response; return response;
}) })
.catch(() => { .catch(() => {