diff --git a/crates/beaver_shell/src/browser_window.rs b/crates/beaver_shell/src/browser_window.rs index 8fac0b6..e50ffcd 100644 --- a/crates/beaver_shell/src/browser_window.rs +++ b/crates/beaver_shell/src/browser_window.rs @@ -343,6 +343,10 @@ impl BrowserWindow { if let Some(webview) = self.first_webview() { webview.resize(new_size); } + self.save_window_state(); + }, + WindowEvent::Moved(_) => { + self.save_window_state(); }, WindowEvent::ModifiersChanged(modifiers) => { self.modifiers_state.set(modifiers.state()); @@ -355,6 +359,18 @@ impl BrowserWindow { } } + fn save_window_state(&self) { + let size = self.window.inner_size(); + let position = self.window.outer_position().unwrap_or_default(); + crate::window_state::WindowState { + width: size.width, + height: size.height, + x: position.x, + y: position.y, + } + .save(&crate::config_dir()); + } + fn maybe_notify_keyboard_event(&self, keyboard_event: KeyboardEvent) { let focused_webview_id = self.focused_webview_id.get(); let system_webview_id = self.system_webview_id.get(); diff --git a/crates/beaver_shell/src/content_blocker.rs b/crates/beaver_shell/src/content_blocker.rs index 7d33b52..033bd4f 100644 --- a/crates/beaver_shell/src/content_blocker.rs +++ b/crates/beaver_shell/src/content_blocker.rs @@ -15,7 +15,7 @@ use servo::{ UrlWithBlobClaim, fetch_async, }; -const CACHE_FILE: &str = "adblock-engine.bin"; +const CACHE_FILE: &str = "adblock_engine.bin"; const DB_FILE: &str = "content_blocker.db"; const EASYLIST_URL: &str = "https://easylist.to/easylist/easylist.txt"; const BUNDLED_EASYLIST: &str = include_str!("../resources/easylist.txt"); diff --git a/crates/beaver_shell/src/main.rs b/crates/beaver_shell/src/main.rs index 2bb6ba9..744b867 100644 --- a/crates/beaver_shell/src/main.rs +++ b/crates/beaver_shell/src/main.rs @@ -11,6 +11,7 @@ mod resources; mod touch_event_simulator; #[cfg(feature = "status-tray")] mod tray; +mod window_state; use std::cell::{Cell, RefCell}; use std::collections::HashMap; @@ -70,7 +71,7 @@ fn config_dir() -> Option { } } -/// Load Servo preferences from `servo-prefs.json` in the config directory, +/// Load Servo preferences from `servo_prefs.json` in the config directory, /// merged with hardcoded defaults for beaver. fn load_servo_prefs(config_dir: Option) -> Preferences { // Start with beaver's defaults. @@ -83,7 +84,7 @@ fn load_servo_prefs(config_dir: Option) -> Preferences { }; // Try to load saved preferences and merge on top. - if let Some(path) = config_dir.map(|d| d.join("servo-prefs.json")) { + if let Some(path) = config_dir.map(|d| d.join("servo_prefs.json")) { if let Ok(content) = std::fs::read_to_string(&path) { match serde_json::from_str::(&content) { Ok(saved) => { @@ -91,7 +92,7 @@ fn load_servo_prefs(config_dir: Option) -> Preferences { info!("Loaded Servo preferences from {}", path.display()); }, Err(e) => { - warn!("Failed to parse servo-prefs.json: {e}"); + warn!("Failed to parse servo_prefs.json: {e}"); }, } } @@ -180,6 +181,17 @@ impl AppState { /// 1. Clear all windows (which hold WebViews that reference Servo) /// 2. Set a dummy delegate to break the AppState <-> Servo cycle fn shutdown(&self) { + if let Some(window) = self.focused_window.borrow().as_ref() { + let size = window.window.inner_size(); + let position = window.window.outer_position().unwrap_or_default(); + window_state::WindowState { + width: size.width, + height: size.height, + x: position.x, + y: position.y, + } + .save(&config_dir()); + } // Clear focused_window reference *self.focused_window.borrow_mut() = None; // Clear windows - this drops WebViews which hold Servo references @@ -251,7 +263,21 @@ impl AppState { Some(PrefValue::Bool(true)) ); - if simulate_touch { + if let Some(state) = window_state::WindowState::load(&config_dir()) { + info!( + "[WindowState] Restoring {}x{} at ({}, {})", + state.width, state.height, state.x, state.y + ); + window_attributes = window_attributes + .with_inner_size(winit::dpi::PhysicalSize { + width: state.width, + height: state.height, + }) + .with_position(winit::dpi::PhysicalPosition { + x: state.x, + y: state.y, + }); + } else if simulate_touch { window_attributes = window_attributes.with_inner_size(winit::dpi::PhysicalSize { width: 480, height: 860, @@ -910,7 +936,7 @@ impl ApplicationHandler for App { servo.setup_logging(); // Register observer to persist Servo preferences on change. - if let Some(path) = config_dir().map(|d| d.join("servo-prefs.json")) { + if let Some(path) = config_dir().map(|d| d.join("servo_prefs.json")) { servo::prefs::add_observer(Box::new(ServoPrefsPersister { path })); } diff --git a/crates/beaver_shell/src/window_state.rs b/crates/beaver_shell/src/window_state.rs new file mode 100644 index 0000000..1e2f440 --- /dev/null +++ b/crates/beaver_shell/src/window_state.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::fs; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct WindowState { + pub width: u32, + pub height: u32, + pub x: i32, + pub y: i32, +} + +impl WindowState { + pub fn load(config_dir: &Option) -> Option { + let path = config_dir.as_ref()?.join("window_state.json"); + let content = fs::read_to_string(path).ok()?; + serde_json::from_str(&content).ok() + } + + pub fn save(&self, config_dir: &Option) { + let Some(dir) = config_dir else { return }; + let path = dir.join("window_state.json"); + if let Ok(json) = serde_json::to_string(self) { + let _ = fs::write(path, json); + } + } +} diff --git a/ui/system/remote/index.css b/ui/system/remote/index.css index bdc3570..509bef3 100644 --- a/ui/system/remote/index.css +++ b/ui/system/remote/index.css @@ -1,8 +1,9 @@ /* SPDX-License-Identifier: AGPL-3.0-or-later */ /* - * Beaver Remote Control — handheld touch UI for controlling a media center. - * Uses the standard Beaver theme tokens so it adapts to the user's chosen theme. + * Beaver Remote Control — a warm, tactile phone interface + * for controlling The Hearth media center. + * Feels like holding a handcrafted wooden remote. */ * { @@ -20,6 +21,7 @@ html, body { overflow: hidden; user-select: none; -webkit-user-select: none; + -webkit-tap-highlight-color: transparent; } .screen { @@ -36,22 +38,23 @@ html, body { /* ===== Device Selector ===== */ .selector-header { - padding: 4em 2em 2em; + padding: 3.5em 2em 1.5em; text-align: center; } .app-title { font-family: Pacifico, cursive; - font-size: 2.8em; + font-size: 2.4em; color: var(--color-text); line-height: 1.1; } .app-subtitle { - font-size: 0.95em; + font-size: 0.85em; color: var(--color-text-tertiary); - margin-top: 0.5em; - letter-spacing: 0.03em; + margin-top: 0.4em; + letter-spacing: 0.04em; + text-transform: uppercase; } .device-list { @@ -67,7 +70,7 @@ html, body { gap: 1.2em; padding: 4em 2em; color: var(--color-text-tertiary); - font-size: 0.95em; + font-size: 0.9em; } .spinner { @@ -83,15 +86,15 @@ html, body { .device-item { display: flex; align-items: center; - gap: 1.2em; - padding: 1.3em 1.5em; + gap: 1em; + padding: 1.2em 1.4em; background: var(--bg-surface); border-radius: var(--radius-md); - margin-bottom: 0.6em; + margin-bottom: 0.5em; cursor: pointer; transition: - background var(--transition-fast), - transform 0.1s cubic-bezier(0.16, 1, 0.3, 1); + background 0.18s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.12s cubic-bezier(0.16, 1, 0.3, 1); border: 1px solid var(--color-border); } @@ -101,7 +104,7 @@ html, body { } .device-item lucide-icon { - font-size: 1.6em; + font-size: 1.4em; color: var(--color-primary); } @@ -111,13 +114,13 @@ html, body { .device-item-name { font-weight: var(--font-weight-bold); - font-size: 1.1em; + font-size: 1em; } .device-item-status { font-size: var(--font-size-sm); color: var(--color-text-secondary); - margin-top: 0.15em; + margin-top: 0.1em; } .no-devices { @@ -125,22 +128,23 @@ html, body { padding: 4em 2em; color: var(--color-text-tertiary); line-height: 1.6; + font-size: 0.9em; } -/* ===== Guest Connect (device list) ===== */ +/* ===== Guest Connect ===== */ .device-item-guest { display: flex; align-items: center; - gap: 1.2em; - padding: 1.3em 1.5em; + gap: 1em; + padding: 1.2em 1.4em; background: none; border-radius: var(--radius-md); - margin-bottom: 0.6em; + margin-bottom: 0.5em; cursor: pointer; transition: - background var(--transition-fast), - transform 0.1s cubic-bezier(0.16, 1, 0.3, 1); + background 0.18s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.12s cubic-bezier(0.16, 1, 0.3, 1); border: 1px dashed var(--color-border); } @@ -150,11 +154,11 @@ html, body { } .device-item-guest lucide-icon { - font-size: 1.6em; + font-size: 1.4em; color: var(--color-text-secondary); } -/* ===== PIN Entry Screen ===== */ +/* ===== PIN Entry ===== */ .pin-form { display: flex; @@ -177,7 +181,7 @@ html, body { font-family: var(--font-family-base); font-weight: var(--font-weight-bold); caret-color: var(--color-primary); - transition: border-color var(--transition-fast); + transition: border-color 0.18s cubic-bezier(0.16, 1, 0.3, 1); } .pin-input:focus { @@ -207,7 +211,7 @@ html, body { font-size: 1em; cursor: pointer; transition: - background var(--transition-fast), + background 0.18s cubic-bezier(0.16, 1, 0.3, 1), transform 0.1s cubic-bezier(0.16, 1, 0.3, 1); } @@ -236,15 +240,15 @@ html, body { .connection-bar { display: flex; align-items: center; - gap: 0.8em; - padding: 0.9em 1.5em; + gap: 0.6em; + padding: 0.7em 1.2em; background: var(--bg-header); border-bottom: 1px solid var(--color-border); } .connected-icon { color: var(--color-primary); - font-size: 1em; + font-size: 0.9em; } #connected-name { @@ -261,7 +265,7 @@ html, body { padding: 0.5em; cursor: pointer; border-radius: 50%; - transition: background var(--transition-fast); + transition: background 0.18s cubic-bezier(0.16, 1, 0.3, 1); } .disconnect-btn:active { @@ -271,19 +275,19 @@ html, body { /* ===== Now Playing ===== */ .now-playing { - padding: 1.2em 1.5em; + padding: 1em 1.4em; background: var(--bg-menu); border-bottom: 1px solid var(--color-border); } .now-playing-info { text-align: center; - margin-bottom: 1em; + margin-bottom: 0.8em; } .np-title { font-weight: var(--font-weight-bold); - font-size: 1.05em; + font-size: 1em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -292,14 +296,14 @@ html, body { .np-artist { font-size: var(--font-size-sm); color: var(--color-text-secondary); - margin-top: 0.15em; + margin-top: 0.1em; } .transport-controls { display: flex; align-items: center; justify-content: center; - gap: 1em; + gap: 0.8em; } .transport-btn { @@ -314,19 +318,28 @@ html, body { color: var(--color-text); cursor: pointer; transition: - background 0.12s, - transform 0.1s cubic-bezier(0.16, 1, 0.3, 1); + background 0.05s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.05s cubic-bezier(0.16, 1, 0.3, 1); } .transport-btn:active { background: var(--bg-hover); - transform: scale(0.9); + transform: translateY(1px); } .transport-btn.play-btn { width: 3.5em; height: 3.5em; background: var(--bg-surface); + border: 1px solid var(--color-border); + box-shadow: + 0 2px 0 var(--color-border), + 0 2px 4px var(--color-shadow); +} + +.transport-btn.play-btn:active { + box-shadow: none; + transform: translateY(2px); } .transport-btn lucide-icon { @@ -334,7 +347,7 @@ html, body { } .transport-btn.play-btn lucide-icon { - font-size: 1.5em; + font-size: 1.4em; } /* ===== D-Pad ===== */ @@ -351,8 +364,8 @@ html, body { display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr; - gap: 0.6em; - width: min(300px, 75vw); + gap: 6px; + width: min(280px, 72vw); aspect-ratio: 1; } @@ -362,30 +375,52 @@ html, body { justify-content: center; background: var(--bg-surface); border: 1px solid var(--color-border); - border-radius: var(--radius-md); color: var(--color-text); cursor: pointer; font-family: var(--font-family-base); font-size: 1em; + box-shadow: + 0 2px 0 var(--color-border), + 0 2px 4px var(--color-shadow); transition: - background 0.1s, - transform 0.08s cubic-bezier(0.16, 1, 0.3, 1); - -webkit-tap-highlight-color: transparent; + background 0.05s cubic-bezier(0.16, 1, 0.3, 1), + box-shadow 0.05s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.05s cubic-bezier(0.16, 1, 0.3, 1); } .dpad-btn:active { background: var(--color-menu-item-hover); - transform: scale(0.92); + box-shadow: none; + transform: translateY(2px); } .dpad-btn lucide-icon { - font-size: 2em; + font-size: 1.8em; +} + +.dpad-btn.up { + grid-column: 2; + grid-row: 1; + border-radius: var(--radius-md) var(--radius-md) 4px 4px; } -.dpad-btn.up { grid-column: 2; grid-row: 1; border-radius: var(--radius-md) var(--radius-md) 4px 4px; } -.dpad-btn.down { grid-column: 2; grid-row: 3; border-radius: 4px 4px var(--radius-md) var(--radius-md); } -.dpad-btn.left { grid-column: 1; grid-row: 2; border-radius: var(--radius-md) 4px 4px var(--radius-md); } -.dpad-btn.right { grid-column: 3; grid-row: 2; border-radius: 4px var(--radius-md) var(--radius-md) 4px; } +.dpad-btn.down { + grid-column: 2; + grid-row: 3; + border-radius: 4px 4px var(--radius-md) var(--radius-md); +} + +.dpad-btn.left { + grid-column: 1; + grid-row: 2; + border-radius: var(--radius-md) 4px 4px var(--radius-md); +} + +.dpad-btn.right { + grid-column: 3; + grid-row: 2; + border-radius: 4px var(--radius-md) var(--radius-md) 4px; +} .dpad-btn.ok { grid-column: 2; @@ -394,13 +429,18 @@ html, body { border-color: var(--color-primary); color: var(--color-text-on-header); font-weight: var(--font-weight-bold); - font-size: 1.2em; + font-size: 1.1em; border-radius: 50%; + letter-spacing: 0.05em; + box-shadow: + 0 2px 0 oklch(35% 0.08 155), + 0 2px 6px var(--color-shadow); } .dpad-btn.ok:active { - opacity: 0.85; - transform: scale(0.9); + box-shadow: none; + transform: translateY(2px); + opacity: 0.9; } /* ===== Utility Bar ===== */ @@ -408,33 +448,37 @@ html, body { .utility-bar { display: flex; justify-content: center; - gap: 1em; - padding: 1em 1.5em 2.5em; + gap: 0.8em; + padding: 0.8em 1.5em 2em; } .utility-btn { display: flex; align-items: center; justify-content: center; - width: 3.5em; - height: 3.5em; + width: 3.2em; + height: 3.2em; background: var(--bg-surface); border: 1px solid var(--color-border); color: var(--color-text-secondary); cursor: pointer; border-radius: var(--radius-md); font-family: var(--font-family-base); + box-shadow: + 0 2px 0 var(--color-border), + 0 2px 4px var(--color-shadow); transition: - background 0.1s, - transform 0.08s cubic-bezier(0.16, 1, 0.3, 1); - -webkit-tap-highlight-color: transparent; + background 0.05s cubic-bezier(0.16, 1, 0.3, 1), + box-shadow 0.05s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.05s cubic-bezier(0.16, 1, 0.3, 1); } .utility-btn:active { background: var(--color-menu-item-hover); - transform: scale(0.9); + box-shadow: none; + transform: translateY(2px); } .utility-btn lucide-icon { - font-size: 1.5em; + font-size: 1.3em; }