Compare commits
3 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d88084fb9a | ||
|
|
f8639f2967 | ||
|
|
7129c9a8eb |
1
frontend/src/components/demos/mod.rs
Normal file
1
frontend/src/components/demos/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod demo_shimmer;
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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::*;
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user