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);
if !socket_path.exists() {
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"
);
tracing::warn!(
tracing::error!(
"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 {
tracing::info!("Socket file exists. Testing connection...");
let client = xmlrpc::RtorrentClient::new(&args.socket);
@@ -259,7 +261,11 @@ async fn main() {
let version = xmlrpc::parse_string_response(&xml).unwrap_or(xml);
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 {
format!("{}_{}", prefix, js_sys::Math::random().to_string().replace(".", ""))
const PREFIX: &str = "rust_ui"; // Must NOT contain "/" or "-"
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 toast; (Removed)
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">
<Show when=move || selected_torrent.get().is_some() fallback=move || view! { <Skeleton class="h-6 w-48" /> }>
<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>
</Show>
<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">
{move || format!("{:?}", selected_torrent.get().unwrap().status)}
<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>
{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 || selected_torrent.get().map(|t| format!("{:.1}%", t.percent_complete)).unwrap_or_default()}</span>
</p>
</Show>
</div>

View File

@@ -597,10 +597,12 @@ fn TorrentRow(
on:click=move |_| store.selected_torrent.set(Some(stored_hash.get_value()))
>
<DataTableCell class="w-12 px-4">
<Checkbox
checked=is_selected
on_checked_change=on_select
/>
<div on:click=move |e| e.stop_propagation()>
<Checkbox
checked=is_selected
on_checked_change=on_select
/>
</div>
</DataTableCell>
{move || visible_columns.get().contains("Name").then({
@@ -730,17 +732,23 @@ fn TorrentCard(
}
)
on:click=move |_| {
let current = is_selected.get();
on_select.run(!current);
store.selected_torrent.set(Some(stored_hash.get_value()));
}
>
<div class="p-4 space-y-3">
<div class="flex justify-between items-start gap-3">
<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 class="flex items-start gap-3 flex-1 min-w-0">
<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>
<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)}
</Badge>
</div>

View File

@@ -3,13 +3,17 @@ use leptos_ui::clx;
mod components {
use super::*;
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! {CardContent, div, "px-6"}
clx! {CardDescription, p, "text-muted-foreground text-sm"}
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 = [
"/",
"/index.html",
@@ -51,6 +51,11 @@ self.addEventListener("activate", (event) => {
self.addEventListener("fetch", (event) => {
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
if (url.pathname.startsWith("/api/")) {
event.respondWith(
@@ -75,10 +80,12 @@ self.addEventListener("fetch", (event) => {
fetch(event.request)
.then((response) => {
// Cache the latest version of the HTML
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
if (response && response.status === 200) {
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
}
return response;
})
.catch(() => {