Compare commits

..

10 Commits

Author SHA1 Message Date
spinline
ba7f1ffd91 fix(ui): adjust context menu position for CSS transformed containers
All checks were successful
Build MIPS Binary / build (push) Successful in 1m59s
2026-02-21 01:22:31 +03:00
spinline
daa24dd7ec fix(ui): refactor FileContextMenu to auticlose and match homepage
All checks were successful
Build MIPS Binary / build (push) Successful in 1m59s
2026-02-21 01:13:57 +03:00
spinline
45271b5060 fix: add missing files.rs and resolve Show type inference
All checks were successful
Build MIPS Binary / build (push) Successful in 2m0s
2026-02-21 01:05:58 +03:00
spinline
4d02bc655d fix: resolve E0282 type inference on CI and clear warnings
Some checks failed
Build MIPS Binary / build (push) Failing after 35s
2026-02-21 01:04:00 +03:00
spinline
3c2ba477f5 feat(ui): add files tab and priority context menu
Some checks failed
Build MIPS Binary / build (push) Failing after 36s
2026-02-21 00:58:35 +03:00
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
9 changed files with 403 additions and 153 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

@@ -106,10 +106,19 @@ pub fn TorrentDetailsSheet() -> impl IntoView {
</TabsContent> </TabsContent>
<TabsContent value="files" class="h-full"> <TabsContent value="files" class="h-full">
<div class="flex flex-col items-center justify-center h-48 opacity-60"> {move || match selected_torrent.get() {
<icons::File class="size-12 mb-3 text-muted-foreground" /> Some(t) => leptos::either::Either::Left(view! {
<p class="text-sm font-medium">"Dosya listesi yakında eklenecek"</p> <div class="h-full overflow-y-auto pr-2 pb-8">
</div> <crate::components::torrent::files::TorrentFilesTab hash=t.hash />
</div>
}),
None => leptos::either::Either::Right(view! {
<div class="flex flex-col items-center justify-center h-48 opacity-60">
<icons::File class="size-12 mb-3 text-muted-foreground" />
<p class="text-sm font-medium">"Dosya yükleniyor..."</p>
</div>
}),
}}
</TabsContent> </TabsContent>
<TabsContent value="trackers" class="h-full"> <TabsContent value="trackers" class="h-full">

View File

@@ -0,0 +1,224 @@
use leptos::prelude::*;
use crate::components::ui::table::*;
use crate::components::ui::badge::*;
use crate::components::ui::shimmer::*;
use crate::components::ui::context_menu::*;
use shared::TorrentFile;
#[component]
pub fn TorrentFilesTab(hash: String) -> impl IntoView {
let hash_clone = hash.clone();
// Fetch files resource
let files_resource = Resource::new(
move || hash_clone.clone(),
|h| async move { shared::server_fns::torrent::get_files(h).await.unwrap_or_default() }
);
// Refresh action
let refresh_files = Action::new(|h: &String| {
let h = h.clone();
async move { shared::server_fns::torrent::get_files(h).await.unwrap_or_default() }
});
let stored_hash = StoredValue::new(hash);
view! {
<Suspense fallback=move || view! { <FilesFallback /> }>
{move || {
let files = match refresh_files.value().get() {
Some(f) => f,
None => files_resource.get().unwrap_or_default(),
};
if files.is_empty() {
return view! {
<div class="flex flex-col items-center justify-center h-48 opacity-60">
<icons::File class="size-12 mb-3 text-muted-foreground" />
<p class="text-sm font-medium">"Bu torrent için dosya bulunamadı."</p>
</div>
}.into_any();
}
let files_len = files.len();
view! {
<div class="space-y-4">
<TableWrapper class="bg-card/50">
<Table>
<TableHeader class="sticky top-0 bg-muted/80 backdrop-blur-sm z-10">
<TableRow class="hover:bg-transparent">
<TableHead class="w-12 text-center text-xs">"#"</TableHead>
<TableHead class="text-xs">"Dosya Adı"</TableHead>
<TableHead class="w-24 text-right text-xs">"Boyut"</TableHead>
<TableHead class="w-24 text-right text-xs">"Tamamlanan"</TableHead>
<TableHead class="w-24 text-center text-xs">"Öncelik"</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<For
each=move || files.clone()
key=|f| f.index
children={move |f| {
let p_hash = stored_hash.get_value();
let r_files = refresh_files.clone();
view! {
<FileRow
file=f
hash=p_hash
refresh_action=r_files
/>
}
}}
/>
</TableBody>
</Table>
</TableWrapper>
<div class="flex items-center justify-between text-xs text-muted-foreground px-1">
<span>{format!("Toplam {} dosya", files_len)}</span>
</div>
</div>
}.into_any()
}}
</Suspense>
}
}
#[component]
fn FileRow(file: TorrentFile, hash: String, refresh_action: Action<String, Vec<TorrentFile>>) -> impl IntoView {
let f_idx = file.index;
let path_clone = file.path.clone();
let set_priority = Action::new(|req: &(String, u32, u8)| {
let (h, idx, p) = req.clone();
async move {
let res = shared::server_fns::torrent::set_file_priority(h, idx, p).await;
if let Err(e) = &res {
crate::store::show_toast(shared::NotificationLevel::Error, format!("Öncelik değiştirilemedi: {:?}", e));
} else {
crate::store::show_toast(shared::NotificationLevel::Success, "Dosya önceliği güncellendi.".to_string());
}
res
}
});
view! {
<FileContextMenu
torrent_hash=hash
file_index=f_idx
refresh_action=refresh_action
set_priority=set_priority
>
<TableRow class="hover:bg-muted/50 transition-colors group">
<TableCell class="text-center text-xs text-muted-foreground">{file.index}</TableCell>
<TableCell class="font-medium text-xs break-all max-w-[200px] md:max-w-md" attr:title=move || path_clone.clone()>
{file.path.clone()}
</TableCell>
<TableCell class="text-right text-xs text-muted-foreground whitespace-nowrap">
{format_bytes(file.size)}
</TableCell>
<TableCell class="text-right text-xs whitespace-nowrap">
<span class="text-primary font-medium">{format_bytes(file.completed_chunks)}</span>
</TableCell>
<TableCell class="text-center">
{
let (variant, label) = match file.priority {
0 => (BadgeVariant::Destructive, "İndirme"),
2 => (BadgeVariant::Success, "Yüksek"),
_ => (BadgeVariant::Secondary, "Normal"),
};
view! { <Badge variant=variant class="text-[10px] uppercase">{label}</Badge> }
}
</TableCell>
</TableRow>
</FileContextMenu>
}
}
#[component]
fn FileContextMenu(
children: Children,
torrent_hash: String,
file_index: u32,
refresh_action: Action<String, Vec<TorrentFile>>,
set_priority: Action<(String, u32, u8), Result<(), ServerFnError>>,
) -> impl IntoView {
let hash_c1 = torrent_hash.clone();
let hash_c2 = torrent_hash.clone();
let hash_c3 = torrent_hash.clone();
view! {
<ContextMenu>
<ContextMenuTrigger>
{children()}
</ContextMenuTrigger>
<ContextMenuContent class="w-48">
<ContextMenuLabel>"Dosya Önceliği"</ContextMenuLabel>
<ContextMenuGroup>
<ContextMenuItem on:click={
let h = hash_c1;
let ra = refresh_action.clone();
let sp = set_priority.clone();
move |_| {
sp.dispatch((h.clone(), file_index, 2));
ra.dispatch(h.clone());
crate::components::ui::context_menu::close_context_menu();
}
}>
<icons::ChevronsUp class="text-green-500" />
<span>"Yüksek"</span>
</ContextMenuItem>
<ContextMenuItem on:click={
let h = hash_c2;
let ra = refresh_action.clone();
let sp = set_priority.clone();
move |_| {
sp.dispatch((h.clone(), file_index, 1));
ra.dispatch(h.clone());
crate::components::ui::context_menu::close_context_menu();
}
}>
<icons::Minus class="text-blue-500" />
<span>"Normal"</span>
</ContextMenuItem>
<ContextMenuItem class="text-destructive focus:bg-destructive/10" on:click={
let h = hash_c3;
let ra = refresh_action.clone();
let sp = set_priority.clone();
move |_| {
sp.dispatch((h.clone(), file_index, 0));
ra.dispatch(h.clone());
crate::components::ui::context_menu::close_context_menu();
}
}>
<icons::X />
<span>"İndirme (Kapalı)"</span>
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
}
}
#[component]
fn FilesFallback() -> impl IntoView {
view! {
<Shimmer loading=Signal::derive(|| true) class="space-y-2">
<div class="h-10 w-full bg-muted/20 rounded-md"></div>
<div class="h-10 w-full bg-muted/20 rounded-md"></div>
<div class="h-10 w-full bg-muted/20 rounded-md"></div>
<div class="h-10 w-full bg-muted/20 rounded-md"></div>
</Shimmer>
}
}
fn format_bytes(bytes: i64) -> String {
const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
if bytes < 1024 { return format!("{} B", bytes); }
let i = (bytes as f64).log2().div_euclid(10.0) as usize;
format!("{:.1} {}", (bytes as f64) / 1024_f64.powi(i as i32), UNITS[i])
}

View File

@@ -1,3 +1,4 @@
pub mod table; pub mod table;
pub mod add_torrent; pub mod add_torrent;
pub mod details; pub mod details;
pub mod files;

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

@@ -139,7 +139,7 @@ pub fn ContextMenuTrigger(
class=trigger_class class=trigger_class
data-name="ContextMenuTrigger" data-name="ContextMenuTrigger"
data-context-trigger=ctx.target_id data-context-trigger=ctx.target_id
on:contextmenu=move |e: web_sys::MouseEvent| { on:contextmenu=move |_e: web_sys::MouseEvent| {
if let Some(cb) = on_open { if let Some(cb) = on_open {
cb.run(()); cb.run(());
} }
@@ -215,12 +215,20 @@ pub fn ContextMenuContent(
// Adjust if menu would go off right edge // Adjust if menu would go off right edge
if (x + menuRect.width > viewportWidth) {{ if (x + menuRect.width > viewportWidth) {{
left = x - menuRect.width; left = Math.max(0, x - menuRect.width);
}} }}
// Adjust if menu would go off bottom edge // Adjust if menu would go off bottom edge
if (y + menuRect.height > viewportHeight) {{ if (y + menuRect.height > viewportHeight) {{
top = y - menuRect.height; top = Math.max(0, y - menuRect.height);
}}
// Adjust for CSS transformed containing block
const offsetParent = menu.offsetParent;
if (offsetParent && offsetParent !== document.body && offsetParent !== document.documentElement) {{
const parentRect = offsetParent.getBoundingClientRect();
left -= parentRect.left;
top -= parentRect.top;
}} }}
menu.style.left = `${{left}}px`; menu.style.left = `${{left}}px`;
@@ -228,105 +236,105 @@ pub fn ContextMenuContent(
menu.style.transformOrigin = 'top left'; menu.style.transformOrigin = 'top left';
}}; }};
const openMenu = (x, y) => {{ const openMenu = (x, y) => {{
isOpen = true; isOpen = true;
// Close any other open context menus // Close any other open context menus
const allMenus = document.querySelectorAll('[data-target="target__context"]'); const allMenus = document.querySelectorAll('[data-target="target__context"]');
allMenus.forEach(m => {{ allMenus.forEach(m => {{
if (m !== menu && m.getAttribute('data-state') === 'open') {{ if (m !== menu && m.getAttribute('data-state') === 'open') {{
m.setAttribute('data-state', 'closed'); m.setAttribute('data-state', 'closed');
m.style.pointerEvents = 'none'; m.style.pointerEvents = 'none';
}}
}});
menu.setAttribute('data-state', 'open');
menu.style.visibility = 'hidden';
menu.style.pointerEvents = 'auto';
// Force reflow
menu.offsetHeight;
updatePosition(x, y);
menu.style.visibility = 'visible';
// Lock scroll
if (window.ScrollLock) {{
window.ScrollLock.lock();
}}
setTimeout(() => {{
document.addEventListener('click', handleClickOutside);
document.addEventListener('contextmenu', handleContextOutside);
}}, 0);
}};
const closeMenu = () => {{
isOpen = false;
menu.setAttribute('data-state', 'closed');
menu.style.pointerEvents = 'none';
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('contextmenu', handleContextOutside);
// Dispatch custom event for Leptos to listen to
menu.dispatchEvent(new CustomEvent('contextmenuclose', {{ bubbles: false }}));
if (window.ScrollLock) {{
window.ScrollLock.unlock(200);
}}
}};
const handleClickOutside = (e) => {{
if (!menu.contains(e.target)) {{
closeMenu();
}}
}};
const handleContextOutside = (e) => {{
if (!trigger.contains(e.target)) {{
closeMenu();
}}
}};
// Right-click on trigger
trigger.addEventListener('contextmenu', (e) => {{
e.preventDefault();
e.stopPropagation();
if (isOpen) {{
closeMenu();
}}
openMenu(e.clientX, e.clientY);
}});
// Close when action is clicked
const actions = menu.querySelectorAll('[data-context-close]');
actions.forEach(action => {{
action.addEventListener('click', () => {{
closeMenu();
}});
}});
// Handle ESC key
document.addEventListener('keydown', (e) => {{
if (e.key === 'Escape' && isOpen) {{
e.preventDefault();
closeMenu();
}} }}
}}); }});
menu.setAttribute('data-state', 'open');
menu.style.visibility = 'hidden';
menu.style.pointerEvents = 'auto';
// Force reflow
menu.offsetHeight;
updatePosition(x, y);
menu.style.visibility = 'visible';
// Lock scroll
if (window.ScrollLock) {{
window.ScrollLock.lock();
}}
setTimeout(() => {{
document.addEventListener('click', handleClickOutside);
document.addEventListener('contextmenu', handleContextOutside);
}}, 0);
}}; }};
const closeMenu = () => {{ if (document.readyState === 'loading') {{
isOpen = false; document.addEventListener('DOMContentLoaded', setupContextMenu);
menu.setAttribute('data-state', 'closed'); }} else {{
menu.style.pointerEvents = 'none'; setupContextMenu();
document.removeEventListener('click', handleClickOutside); }}
document.removeEventListener('contextmenu', handleContextOutside); }})();
"#,
// Dispatch custom event for Leptos to listen to target_id_for_script,
menu.dispatchEvent(new CustomEvent('contextmenuclose', {{ bubbles: false }})); target_id_for_script,
)}
if (window.ScrollLock) {{ </script>
window.ScrollLock.unlock(200);
}}
}};
const handleClickOutside = (e) => {{
if (!menu.contains(e.target)) {{
closeMenu();
}}
}};
const handleContextOutside = (e) => {{
if (!trigger.contains(e.target)) {{
closeMenu();
}}
}};
// Right-click on trigger
trigger.addEventListener('contextmenu', (e) => {{
e.preventDefault();
e.stopPropagation();
if (isOpen) {{
closeMenu();
}}
openMenu(e.clientX, e.clientY);
}});
// Close when action is clicked
const actions = menu.querySelectorAll('[data-context-close]');
actions.forEach(action => {{
action.addEventListener('click', () => {{
closeMenu();
}});
}});
// Handle ESC key
document.addEventListener('keydown', (e) => {{
if (e.key === 'Escape' && isOpen) {{
e.preventDefault();
closeMenu();
}}
}});
}};
if (document.readyState === 'loading') {{
document.addEventListener('DOMContentLoaded', setupContextMenu);
}} else {{
setupContextMenu();
}}
}})();
"#,
target_id_for_script,
target_id_for_script,
)}
</script>
} }
} }

View File

@@ -16,8 +16,6 @@ mod components {
clx! {SheetFooter, footer, "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"} clx! {SheetFooter, footer, "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"}
} }
pub use components::*;
/* ========================================================== */ /* ========================================================== */
/* ✨ CONTEXT ✨ */ /* ✨ CONTEXT ✨ */
/* ========================================================== */ /* ========================================================== */

View File

@@ -5,8 +5,7 @@ use leptos::task::spawn_local;
use shared::{AppEvent, GlobalStats, NotificationLevel, Torrent}; use shared::{AppEvent, GlobalStats, NotificationLevel, Torrent};
use std::collections::HashMap; use std::collections::HashMap;
use struct_patch::traits::Patch; use struct_patch::traits::Patch;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD as BASE64_URL}; use base64::{Engine as _, engine::general_purpose::{URL_SAFE_NO_PAD as BASE64_URL, STANDARD as BASE64}};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use crate::components::ui::toast::{ToastType, toast}; use crate::components::ui::toast::{ToastType, toast};
@@ -105,39 +104,29 @@ pub fn provide_torrent_store() {
} }
if let Some(data_str) = msg.data().as_string() { if let Some(data_str) = msg.data().as_string() {
match BASE64.decode(&data_str) { if let Ok(bytes) = BASE64.decode(&data_str) {
Ok(bytes) => { if let Ok(event) = rmp_serde::from_slice::<AppEvent>(&bytes) {
match rmp_serde::from_slice::<AppEvent>(&bytes) { match event {
Ok(event) => { AppEvent::FullList(list, _) => {
match event { torrents_for_sse.update(|map| {
AppEvent::FullList(list, _) => { let new_hashes: std::collections::HashSet<String> = list.iter().map(|t| t.hash.clone()).collect();
torrents_for_sse.update(|map| { map.retain(|hash, _| new_hashes.contains(hash));
let new_hashes: std::collections::HashSet<String> = list.iter().map(|t| t.hash.clone()).collect(); for new_torrent in list { map.insert(new_torrent.hash.clone(), new_torrent); }
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() {
AppEvent::Update(patch) => { torrents_for_sse.update(|map| { if let Some(t) = map.get_mut(&hash) { t.apply(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) => {
AppEvent::Stats(stats) => { global_stats_for_sse.set(stats); } show_toast(n.level.clone(), n.message.clone());
AppEvent::Notification(n) => { if n.message.contains("tamamlandı") || n.level == shared::NotificationLevel::Error {
show_toast(n.level.clone(), n.message.clone()); show_browser_notification("VibeTorrent", &n.message);
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);
} }
} }
} }
@@ -202,7 +191,7 @@ pub async fn subscribe_to_push_notifications() {
let key_array = js_sys::Uint8Array::from(&decoded_key[..]); let key_array = js_sys::Uint8Array::from(&decoded_key[..]);
// 3. Prepare Options // 3. Prepare Options
let mut options = web_sys::PushSubscriptionOptionsInit::new(); let options = web_sys::PushSubscriptionOptionsInit::new();
options.set_user_visible_only(true); options.set_user_visible_only(true);
options.set_application_server_key(&key_array.into()); options.set_application_server_key(&key_array.into());

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