use serde::{Deserialize, Serialize}; use std::collections::HashMap; use tauri::{AppHandle, Emitter, Manager}; use crate::state::AppState; #[allow(dead_code)] pub const TAB_SLEEP_THRESHOLD_SECS: i64 = 30 * 60; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BrowserTab { pub id: String, pub label: String, pub url: String, pub title: String, pub favicon: Option, pub loading: bool, pub pinned: bool, pub sleeping: bool, pub zoom: f64, pub can_go_back: bool, pub can_go_forward: bool, pub last_active: i64, } #[derive(Default)] pub struct TabManager { pub tabs: HashMap, pub order: Vec, pub active: Option, pub closed_stack: Vec, pub zoom_memory: HashMap, } impl TabManager { pub fn push_closed(&mut self, tab: BrowserTab) { if self.closed_stack.len() >= 25 { self.closed_stack.remove(0); } self.closed_stack.push(tab); } pub fn pop_closed(&mut self) -> Option { self.closed_stack.pop() } pub fn remember_zoom(&mut self, domain: &str, zoom: f64) { self.zoom_memory.insert(domain.to_string(), zoom); } pub fn get_zoom_for_domain(&self, domain: &str) -> Option { self.zoom_memory.get(domain).copied() } } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ViewportLayout { #[serde(default)] pub x: f64, #[serde(default)] pub y: f64, pub width: f64, pub height: f64, } #[derive(Debug, Clone, Serialize)] pub struct BrowserSnapshot { pub tabs: Vec, pub active: Option, pub can_restore: bool, } pub fn snapshot(app: &AppHandle) -> Result { let state = app.state::(); let tabs = state.tabs.lock().map_err(|_| "lock")?; let ordered = tabs.order.iter().filter_map(|id| tabs.tabs.get(id).cloned()).collect(); let can_restore = !tabs.closed_stack.is_empty(); Ok(BrowserSnapshot { tabs: ordered, active: tabs.active.clone(), can_restore }) } pub fn emit_snapshot(app: &AppHandle) -> Result<(), String> { let snap = snapshot(app)?; app.emit("browser://tabs", snap).map_err(|e| e.to_string()) } pub fn update_tab_field(app: &AppHandle, id: &str, f: impl FnOnce(&mut BrowserTab)) { let state = app.state::(); if let Ok(mut tabs) = state.tabs.lock() { if let Some(tab) = tabs.tabs.get_mut(id) { f(tab); } } let _ = emit_snapshot(app); } pub fn tab_label(app: &AppHandle, tab_id: &str) -> Result { let state = app.state::(); let tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.tabs.get(tab_id).map(|t| t.label.clone()).ok_or_else(|| format!("tab not found: {tab_id}")) } pub fn eval_on_tab(app: &AppHandle, tab_id: &str, js: &str) -> Result<(), String> { let label = tab_label(app, tab_id)?; app.get_webview(&label).ok_or("webview not found")?.eval(js).map_err(|e| e.to_string()) } pub fn extract_domain(url: &str) -> String { url.split("//").nth(1) .and_then(|s| s.split('/').next()) .map(|s| s.split(':').next().unwrap_or(s)) .unwrap_or("") .trim_start_matches("www.") .to_lowercase() }