| 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<String>,
|
| 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<String, BrowserTab>,
|
| pub order: Vec<String>,
|
| pub active: Option<String>,
|
| pub closed_stack: Vec<BrowserTab>,
|
| pub zoom_memory: HashMap<String, f64>,
|
| }
|
|
|
| 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<BrowserTab> { 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<f64> { 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<BrowserTab>,
|
| pub active: Option<String>,
|
| pub can_restore: bool,
|
| }
|
|
|
| pub fn snapshot(app: &AppHandle) -> Result<BrowserSnapshot, String> {
|
| let state = app.state::<AppState>();
|
| 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::<AppState>();
|
| 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<String, String> {
|
| let state = app.state::<AppState>();
|
| 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()
|
| }
|
|
|