Compare commits

..

3 Commits

Author SHA1 Message Date
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
7 changed files with 83 additions and 34 deletions

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

@@ -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

@@ -105,29 +105,39 @@ pub fn provide_torrent_store() {
} }
if let Some(data_str) = msg.data().as_string() { if let Some(data_str) = msg.data().as_string() {
if let Ok(bytes) = BASE64.decode(&data_str) { match BASE64.decode(&data_str) {
if let Ok(event) = rmp_serde::from_slice::<AppEvent>(&bytes) { Ok(bytes) => {
match event { match rmp_serde::from_slice::<AppEvent>(&bytes) {
AppEvent::FullList(list, _) => { Ok(event) => {
torrents_for_sse.update(|map| { match event {
let new_hashes: std::collections::HashSet<String> = list.iter().map(|t| t.hash.clone()).collect(); AppEvent::FullList(list, _) => {
map.retain(|hash, _| new_hashes.contains(hash)); torrents_for_sse.update(|map| {
for new_torrent in list { map.insert(new_torrent.hash.clone(), new_torrent); } let new_hashes: std::collections::HashSet<String> = list.iter().map(|t| t.hash.clone()).collect();
}); map.retain(|hash, _| new_hashes.contains(hash));
} for new_torrent in list { map.insert(new_torrent.hash.clone(), new_torrent); }
AppEvent::Update(patch) => { });
if let Some(hash) = patch.hash.clone() { }
torrents_for_sse.update(|map| { if let Some(t) = map.get_mut(&hash) { t.apply(patch); } }); AppEvent::Update(patch) => {
} if let Some(hash) = patch.hash.clone() {
} torrents_for_sse.update(|map| { if let Some(t) = map.get_mut(&hash) { t.apply(patch); } });
AppEvent::Stats(stats) => { global_stats_for_sse.set(stats); } }
AppEvent::Notification(n) => { }
show_toast(n.level.clone(), n.message.clone()); AppEvent::Stats(stats) => { global_stats_for_sse.set(stats); }
if n.message.contains("tamamlandı") || n.level == shared::NotificationLevel::Error { AppEvent::Notification(n) => {
show_browser_notification("VibeTorrent", &n.message); show_toast(n.level.clone(), n.message.clone());
if n.message.contains("tamamlandı") || n.level == shared::NotificationLevel::Error {
show_browser_notification("VibeTorrent", &n.message);
}
}
} }
},
Err(e) => {
log::error!("[SSE] Failed to deserialize AppEvent: {:?}", e);
} }
} }
},
Err(e) => {
log::error!("[SSE] Failed to decode base64: {:?}", e);
} }
} }
} }

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(() => {