use futures::StreamExt; use gloo_net::eventsource::futures::EventSource; use leptos::prelude::*; use shared::{AppEvent, GlobalStats, NotificationLevel, SystemNotification, Torrent}; use std::collections::HashMap; use serde::{Serialize, Deserialize}; #[derive(Clone, Debug, PartialEq)] pub struct NotificationItem { pub id: u64, pub notification: SystemNotification, } // ============================================================================ // Toast Helper Functions // ============================================================================ pub fn show_toast_with_signal( notifications: RwSignal>, level: NotificationLevel, message: impl Into, ) { let id = js_sys::Date::now() as u64; let notification = SystemNotification { level, message: message.into(), }; let item = NotificationItem { id, notification }; notifications.update(|list| list.push(item)); // Auto-remove after 5 seconds leptos::prelude::set_timeout( move || { notifications.update(|list| list.retain(|i| i.id != id)); }, std::time::Duration::from_secs(5), ); } pub fn show_toast(level: NotificationLevel, message: impl Into) { if let Some(store) = use_context::() { show_toast_with_signal(store.notifications, level, message); } } pub fn toast_success(message: impl Into) { show_toast(NotificationLevel::Success, message); } pub fn toast_error(message: impl Into) { show_toast(NotificationLevel::Error, message); } // ============================================================================ // Action Message Mapping // ============================================================================ pub fn get_action_messages(action: &str) -> (&'static str, &'static str) { match action { "start" => ("Torrent başlatıldı", "Torrent başlatılamadı"), "stop" => ("Torrent durduruldu", "Torrent durdurulamadı"), "pause" => ("Torrent duraklatıldı", "Torrent duraklatılamadı"), "delete" => ("Torrent silindi", "Torrent silinemedi"), "delete_with_data" => ("Torrent ve verileri silindi", "Torrent silinemedi"), "recheck" => ("Torrent kontrol ediliyor", "Kontrol başlatılamadı"), _ => ("İşlem tamamlandı", "İşlem başarısız"), } } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct PushSubscriptionData { pub endpoint: String, pub keys: PushKeys, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct PushKeys { pub p256dh: String, pub auth: String, } // ============================================================================ // Store Definition // ============================================================================ #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FilterStatus { All, Downloading, Seeding, Completed, Paused, Inactive, Active, Error, } #[derive(Clone, Copy, Debug)] pub struct TorrentStore { pub torrents: RwSignal>, pub filter: RwSignal, pub search_query: RwSignal, pub global_stats: RwSignal, pub notifications: RwSignal>, pub user: RwSignal>, } pub fn provide_torrent_store() { let torrents = RwSignal::new(HashMap::new()); let filter = RwSignal::new(FilterStatus::All); let search_query = RwSignal::new(String::new()); let global_stats = RwSignal::new(GlobalStats::default()); let notifications = RwSignal::new(Vec::::new()); let user = RwSignal::new(Option::::None); let show_browser_notification = crate::utils::notification::use_app_notification(); let store = TorrentStore { torrents, filter, search_query, global_stats, notifications, user }; provide_context(store); // SSE Connection Effect::new(move |_| { if user.get().is_none() { return; } let show_browser_notification = show_browser_notification.clone(); leptos::task::spawn_local(async move { let mut backoff_ms: u32 = 1000; let mut was_connected = false; let mut disconnect_notified = false; loop { let es_result = EventSource::new("/api/events"); match es_result { Ok(mut es) => { if let Ok(mut stream) = es.subscribe("message") { let mut got_first_message = false; while let Some(Ok((_, msg))) = stream.next().await { if !got_first_message { got_first_message = true; backoff_ms = 1000; if was_connected && disconnect_notified { show_toast_with_signal(notifications, NotificationLevel::Success, "Sunucu bağlantısı yeniden kuruldu"); disconnect_notified = false; } was_connected = true; } if let Some(data_str) = msg.data().as_string() { if let Ok(event) = serde_json::from_str::(&data_str) { match event { AppEvent::FullList { torrents: list, .. } => { torrents.update(|map| { let new_hashes: std::collections::HashSet = 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(update) => { torrents.update(|map| { if let Some(t) = map.get_mut(&update.hash) { if let Some(v) = update.name { t.name = v; } if let Some(v) = update.size { t.size = v; } if let Some(v) = update.down_rate { t.down_rate = v; } if let Some(v) = update.up_rate { t.up_rate = v; } if let Some(v) = update.percent_complete { t.percent_complete = v; } if let Some(v) = update.completed { t.completed = v; } if let Some(v) = update.eta { t.eta = v; } if let Some(v) = update.status { t.status = v; } if let Some(v) = update.error_message { t.error_message = v; } if let Some(v) = update.label { t.label = Some(v); } } }); } AppEvent::Stats(stats) => { global_stats.set(stats); } AppEvent::Notification(n) => { show_toast_with_signal(notifications, n.level.clone(), n.message.clone()); if n.message.contains("tamamlandı") || n.level == shared::NotificationLevel::Error { show_browser_notification("VibeTorrent", &n.message); } } } } } } if was_connected && !disconnect_notified { show_toast_with_signal(notifications, NotificationLevel::Warning, "Sunucu bağlantısı kesildi, yeniden bağlanılıyor..."); disconnect_notified = true; } } } Err(_) => { if was_connected && !disconnect_notified { show_toast_with_signal(notifications, NotificationLevel::Warning, "Sunucu bağlantısı kurulamıyor..."); disconnect_notified = true; } } } gloo_timers::future::TimeoutFuture::new(backoff_ms).await; backoff_ms = std::cmp::min(backoff_ms * 2, 30000); } }); }); } pub async fn subscribe_to_push_notifications() { // ... }