Compare commits
1 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2dfa7963e |
110
Cargo.lock
generated
110
Cargo.lock
generated
@@ -566,6 +566,15 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codee"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d3ad3122b0001c7f140cf4d605ef9a9e2c24d96ab0b4fb4347b76de2425f445"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collection_literals"
|
name = "collection_literals"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -790,6 +799,41 @@ version = "1.1.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
|
checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"darling_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "5.5.3"
|
version = "5.5.3"
|
||||||
@@ -823,6 +867,18 @@ version = "2.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "default-struct-builder"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0df63c21a4383f94bd5388564829423f35c316aed85dc4f8427aded372c7c0d"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.4.5"
|
version = "0.4.5"
|
||||||
@@ -1144,7 +1200,9 @@ dependencies = [
|
|||||||
name = "frontend"
|
name = "frontend"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"codee",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"console_log",
|
"console_log",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -1152,9 +1210,11 @@ dependencies = [
|
|||||||
"gloo-timers",
|
"gloo-timers",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"leptos",
|
"leptos",
|
||||||
|
"leptos-use",
|
||||||
"leptos_router",
|
"leptos_router",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"shared",
|
"shared",
|
||||||
"tailwind_fuse",
|
"tailwind_fuse",
|
||||||
@@ -1819,6 +1879,12 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -1986,6 +2052,30 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-use"
|
||||||
|
version = "0.13.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "789bf9f4337e6ebd8f1b407e3f762fdc538d48dc145f9d1dce2338014b38f4dd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"chrono",
|
||||||
|
"codee",
|
||||||
|
"cookie",
|
||||||
|
"default-struct-builder",
|
||||||
|
"futures-util",
|
||||||
|
"gloo-timers",
|
||||||
|
"js-sys",
|
||||||
|
"lazy_static",
|
||||||
|
"leptos",
|
||||||
|
"paste",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"unic-langid",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos_config"
|
name = "leptos_config"
|
||||||
version = "0.6.15"
|
version = "0.6.15"
|
||||||
@@ -3934,6 +4024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
|
"serde_core",
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4298,6 +4389,24 @@ version = "1.19.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-langid"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05"
|
||||||
|
dependencies = [
|
||||||
|
"unic-langid-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-langid-impl"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658"
|
||||||
|
dependencies = [
|
||||||
|
"tinystr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@@ -5055,6 +5164,7 @@ version = "0.11.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"serde",
|
||||||
"yoke",
|
"yoke",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
"zerovec-derive",
|
"zerovec-derive",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_use::on_click_outside;
|
|
||||||
use leptos_use::storage::use_local_storage;
|
use leptos_use::storage::use_local_storage;
|
||||||
use codee::string::FromToStringCodec;
|
use codee::string::FromToStringCodec;
|
||||||
use shared::GlobalLimitRequest;
|
use shared::GlobalLimitRequest;
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ pub fn provide_torrent_store() {
|
|||||||
let notifications = create_rw_signal(Vec::<NotificationItem>::new());
|
let notifications = create_rw_signal(Vec::<NotificationItem>::new());
|
||||||
let user = create_rw_signal(Option::<String>::None);
|
let user = create_rw_signal(Option::<String>::None);
|
||||||
|
|
||||||
|
// Browser notification hook
|
||||||
|
let show_browser_notification = crate::utils::notification::use_app_notification();
|
||||||
|
|
||||||
let store = TorrentStore {
|
let store = TorrentStore {
|
||||||
torrents,
|
torrents,
|
||||||
filter,
|
filter,
|
||||||
@@ -149,6 +152,7 @@ pub fn provide_torrent_store() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let show_browser_notification = show_browser_notification.clone();
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let mut backoff_ms: u32 = 1000; // Start with 1 second
|
let mut backoff_ms: u32 = 1000; // Start with 1 second
|
||||||
let max_backoff_ms: u32 = 30000; // Max 30 seconds
|
let max_backoff_ms: u32 = 30000; // Max 30 seconds
|
||||||
@@ -247,7 +251,7 @@ pub fn provide_torrent_store() {
|
|||||||
shared::NotificationLevel::Info => "ℹ️ VibeTorrent",
|
shared::NotificationLevel::Info => "ℹ️ VibeTorrent",
|
||||||
};
|
};
|
||||||
|
|
||||||
crate::utils::notification::show_notification_if_enabled(
|
show_browser_notification(
|
||||||
title,
|
title,
|
||||||
&n.message
|
&n.message
|
||||||
);
|
);
|
||||||
@@ -329,49 +333,21 @@ pub async fn subscribe_to_push_notifications() {
|
|||||||
// First, request notification permission if not already granted
|
// First, request notification permission if not already granted
|
||||||
let window = web_sys::window().expect("window should exist");
|
let window = web_sys::window().expect("window should exist");
|
||||||
|
|
||||||
// Notification.permission is a static property, but web_sys exposes it via the Notification class instance or we check it manually.
|
let permission = crate::utils::notification::get_notification_permission();
|
||||||
// Actually, Notification::permission() is a static method in web_sys.
|
if permission == "default" {
|
||||||
match web_sys::Notification::permission() {
|
|
||||||
web_sys::NotificationPermission::Granted => {
|
|
||||||
log::info!("Notification permission already granted");
|
|
||||||
}
|
|
||||||
web_sys::NotificationPermission::Denied => {
|
|
||||||
log::warn!("Notification permission was denied");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
web_sys::NotificationPermission::Default => {
|
|
||||||
log::info!("Requesting notification permission...");
|
log::info!("Requesting notification permission...");
|
||||||
let permission_promise = match web_sys::Notification::request_permission() {
|
if !crate::utils::notification::request_notification_permission().await {
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to request notification permission: {:?}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match wasm_bindgen_futures::JsFuture::from(permission_promise).await {
|
|
||||||
Ok(val) => {
|
|
||||||
let permission = val.as_string().unwrap_or_default();
|
|
||||||
if permission != "granted" {
|
|
||||||
log::warn!("Notification permission denied by user");
|
log::warn!("Notification permission denied by user");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log::info!("Notification permission granted by user");
|
} else if permission == "denied" {
|
||||||
}
|
log::warn!("Notification permission was denied");
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to await notification permission: {:?}", e);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
log::warn!("Unknown notification permission status");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Notification permission granted! Proceeding with push subscription...");
|
log::info!("Notification permission granted! Proceeding with push subscription...");
|
||||||
|
|
||||||
|
|
||||||
// Get VAPID public key from backend
|
// Get VAPID public key from backend
|
||||||
let public_key_response = match Request::get("/api/push/public-key").send().await {
|
let public_key_response = match Request::get("/api/push/public-key").send().await {
|
||||||
Ok(resp) => resp,
|
Ok(resp) => resp,
|
||||||
|
|||||||
@@ -1,37 +1,19 @@
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use web_sys::{Notification, NotificationOptions};
|
use web_sys::{Notification, NotificationOptions};
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_use::{use_web_notification, UseWebNotificationReturn, NotificationPermission};
|
||||||
|
|
||||||
/// Request browser notification permission from user
|
/// Request browser notification permission from user
|
||||||
pub async fn request_notification_permission() -> bool {
|
pub async fn request_notification_permission() -> bool {
|
||||||
let window = web_sys::window().expect("no global window");
|
if let Ok(promise) = Notification::request_permission() {
|
||||||
|
if let Ok(result) = wasm_bindgen_futures::JsFuture::from(promise).await {
|
||||||
// Check if Notification API is available
|
return result.as_string().unwrap_or_default() == "granted";
|
||||||
if js_sys::Reflect::has(&window, &JsValue::from_str("Notification")).unwrap_or(false) {
|
|
||||||
let notification = js_sys::Reflect::get(&window, &JsValue::from_str("Notification"))
|
|
||||||
.expect("Notification should exist");
|
|
||||||
|
|
||||||
// Request permission
|
|
||||||
let promise = js_sys::Reflect::get(¬ification, &JsValue::from_str("requestPermission"))
|
|
||||||
.expect("requestPermission should exist");
|
|
||||||
|
|
||||||
if let Ok(function) = promise.dyn_into::<js_sys::Function>() {
|
|
||||||
if let Ok(promise) = function.call0(¬ification) {
|
|
||||||
if let Ok(promise) = promise.dyn_into::<js_sys::Promise>() {
|
|
||||||
let result = wasm_bindgen_futures::JsFuture::from(promise).await;
|
|
||||||
|
|
||||||
if let Ok(permission) = result {
|
|
||||||
let permission_str = permission.as_string().unwrap_or_default();
|
|
||||||
return permission_str == "granted";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if browser notifications are supported and permitted
|
/// Check if browser notifications are supported
|
||||||
pub fn is_notification_supported() -> bool {
|
pub fn is_notification_supported() -> bool {
|
||||||
let window = web_sys::window().expect("no global window");
|
let window = web_sys::window().expect("no global window");
|
||||||
js_sys::Reflect::has(&window, &JsValue::from_str("Notification")).unwrap_or(false)
|
js_sys::Reflect::has(&window, &JsValue::from_str("Notification")).unwrap_or(false)
|
||||||
@@ -39,67 +21,61 @@ pub fn is_notification_supported() -> bool {
|
|||||||
|
|
||||||
/// Get current notification permission status
|
/// Get current notification permission status
|
||||||
pub fn get_notification_permission() -> String {
|
pub fn get_notification_permission() -> String {
|
||||||
if !is_notification_supported() {
|
match Notification::permission() {
|
||||||
return "unsupported".to_string();
|
web_sys::NotificationPermission::Granted => "granted".to_string(),
|
||||||
|
web_sys::NotificationPermission::Denied => "denied".to_string(),
|
||||||
|
web_sys::NotificationPermission::Default => "default".to_string(),
|
||||||
|
_ => "default".to_string(),
|
||||||
}
|
}
|
||||||
|
|
||||||
let window = web_sys::window().expect("no global window");
|
|
||||||
let notification = js_sys::Reflect::get(&window, &JsValue::from_str("Notification"))
|
|
||||||
.expect("Notification should exist");
|
|
||||||
|
|
||||||
let permission = js_sys::Reflect::get(¬ification, &JsValue::from_str("permission"))
|
|
||||||
.unwrap_or(JsValue::from_str("default"));
|
|
||||||
|
|
||||||
permission.as_string().unwrap_or("default".to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show a browser notification
|
/// Hook for using browser notifications within Leptos components or effects.
|
||||||
/// Returns true if notification was shown successfully
|
/// This uses leptos-use for reactive permission tracking.
|
||||||
pub fn show_browser_notification(title: &str, body: &str, icon: Option<&str>) -> bool {
|
pub fn use_app_notification() -> impl Fn(&str, &str) + Clone {
|
||||||
// Check permission first
|
let UseWebNotificationReturn { permission, .. } = use_web_notification();
|
||||||
let permission = get_notification_permission();
|
|
||||||
if permission != "granted" {
|
move |title: &str, body: &str| {
|
||||||
log::warn!("Notification permission not granted: {}", permission);
|
// Check user preference from localStorage
|
||||||
|
let window = web_sys::window().expect("no global window");
|
||||||
|
let storage = window.local_storage().ok().flatten();
|
||||||
|
let enabled = storage
|
||||||
|
.and_then(|s| s.get_item("vibetorrent_browser_notifications").ok().flatten())
|
||||||
|
.unwrap_or_else(|| "true".to_string());
|
||||||
|
|
||||||
|
// Use the reactive permission signal from leptos-use
|
||||||
|
if enabled == "true" && permission.get() == NotificationPermission::Granted {
|
||||||
|
show_browser_notification(title, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a browser notification (non-reactive version)
|
||||||
|
pub fn show_browser_notification(title: &str, body: &str) -> bool {
|
||||||
|
if get_notification_permission() != "granted" {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create notification options
|
|
||||||
let opts = NotificationOptions::new();
|
let opts = NotificationOptions::new();
|
||||||
opts.set_body(body);
|
opts.set_body(body);
|
||||||
opts.set_icon(icon.unwrap_or("/icon-192.png"));
|
opts.set_icon("/icon-192.png");
|
||||||
opts.set_badge("/icon-192.png");
|
|
||||||
opts.set_tag("vibetorrent");
|
opts.set_tag("vibetorrent");
|
||||||
opts.set_require_interaction(false);
|
|
||||||
opts.set_silent(Some(false));
|
|
||||||
|
|
||||||
// Create and show notification
|
|
||||||
match Notification::new_with_options(title, &opts) {
|
match Notification::new_with_options(title, &opts) {
|
||||||
Ok(_notification) => {
|
Ok(_) => true,
|
||||||
true
|
Err(_) => false,
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to create notification: {:?}", e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show notification only if enabled in settings and permission granted
|
/// Legacy helper for showing notification if enabled in settings
|
||||||
pub fn show_notification_if_enabled(title: &str, body: &str) -> bool {
|
pub fn show_notification_if_enabled(title: &str, body: &str) -> bool {
|
||||||
// Check localStorage for user preference
|
|
||||||
let window = web_sys::window().expect("no global window");
|
let window = web_sys::window().expect("no global window");
|
||||||
let storage = window.local_storage().ok().flatten();
|
let storage = window.local_storage().ok().flatten();
|
||||||
|
|
||||||
if let Some(storage) = storage {
|
|
||||||
let enabled = storage
|
let enabled = storage
|
||||||
.get_item("vibetorrent_browser_notifications")
|
.and_then(|s| s.get_item("vibetorrent_browser_notifications").ok().flatten())
|
||||||
.ok()
|
.unwrap_or_else(|| "true".to_string());
|
||||||
.flatten()
|
|
||||||
.unwrap_or("true".to_string());
|
|
||||||
|
|
||||||
if enabled == "true" {
|
if enabled == "true" {
|
||||||
return show_browser_notification(title, body, None);
|
return show_browser_notification(title, body);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
|
|||||||
Reference in New Issue
Block a user