Skip to content

Commit 20bbd6e

Browse files
davidtorciviaclaude
andcommitted
feat: zoom-to-video, mute, YouTube Trusted Types fix, recovery paths
## Zoom to video New global hotkey (Alt+Shift+V) and strip button that finds the largest <video> on the page and crops to its bounding rect. If the player is offscreen the page smooth-scrolls to center it first, then the crop animates in. Ephemeral (doesn't persist) so a missing/relocated video on the next visit doesn't strand a stale crop; on toggle-off any manual crop the user had saved is restored intact. If no usable video exists the button flashes red for 700ms. ## Mute Alt+Shift+M and a strip speaker button that mutes every <video> and <audio> on the page. Icon reflects real state — a volumechange listener on media elements (attached via MutationObserver so it catches lazy-loaded players too) keeps the UI in sync with page-side mute toggles like YouTube's `M` key. ## Trusted Types / YouTube compatibility YouTube (and much of Google, GitHub, many banks) ships `Content-Security-Policy: require-trusted-types-for 'script'`, which throws a TypeError on every `element.innerHTML = ...` assignment. Our IIFE died on the first such assignment, leaving the page with no strip, no hotzone, and no recovery affordances. Fix: new `setInner` helper with a two-tier strategy — registers a named Trusted Types policy if the host CSP allows it, otherwise uses DOMParser (whose output document isn't subject to the page's CSP). All 15 innerHTML assignments routed through the helper. ## Recovery affordances - `Alt+Shift+S` global hotkey: force-shows the strip. Reparents the container into the current home, un-hides it, cancels any pending hide timer, releases stuck click-through, and calls showStrip(). - Tray: new "Reload Page" item (hard Rust-side reload, the nuclear option when JS is hung) and "Show Control Strip" item (mirrors the hotkey via Rust eval). - DevTools enabled in release builds (Tauri `devtools` feature) so power users can inspect their own sessions. ## Fullscreen awareness New `fullscreenchange` listener reparents the container into the active fullscreen element. The Fullscreen API bypasses z-index (only the fullscreen element's descendants render), so without this the strip is invisible during YouTube/Plex fullscreen. ## Supporting refactors - Mute observer only calls `updateMuteIcon` when video/audio nodes are actually added or removed, not on every DOM mutation — saves significant main-thread CPU on YouTube-scale churn. - applyCrop/removeCrop now take an explicit `persist` flag so ephemeral (zoom-to-video) and saved (manual) crops don't step on each other's config state. ## Version bump 1.2.0 → 1.3.0 across Cargo.toml, tauri.conf.json, package.json. Verified: cargo test (41 pass), clippy clean, release build clean, runtime smoke-tested on YouTube including the previously-broken hijack case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e98084f commit 20bbd6e

9 files changed

Lines changed: 621 additions & 34 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "floatview",
3-
"version": "1.2.0",
3+
"version": "1.3.0",
44
"description": "A minimal floating browser window for streaming media on a secondary monitor",
55
"scripts": {
66
"tauri": "tauri",

src-tauri/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "floatview"
3-
version = "1.2.0"
3+
version = "1.3.0"
44
description = "A minimal floating browser window for streaming media"
55
authors = ["David Torcivia"]
66
license = "MIT"
@@ -10,7 +10,7 @@ edition = "2021"
1010
tauri-build = { version = "2", features = [] }
1111

1212
[dependencies]
13-
tauri = { version = "2", features = ["tray-icon"] }
13+
tauri = { version = "2", features = ["tray-icon", "devtools"] }
1414
tauri-plugin-single-instance = "2"
1515
tauri-plugin-global-shortcut = "2"
1616
tauri-plugin-updater = "2"

src-tauri/src/config.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,29 @@ pub struct HotkeyConfig {
5555
pub media_play_pause: String,
5656
pub media_next: String,
5757
pub media_previous: String,
58+
#[serde(default = "default_media_mute")]
59+
pub media_mute: String,
60+
#[serde(default = "default_zoom_video")]
61+
pub zoom_video: String,
62+
/// Emergency "force-show the control strip" hotkey. Always-works
63+
/// escape hatch for pathological page states (SPA DOM wipes, stray
64+
/// fullscreen layers, click-through left on). See also the
65+
/// [`ZOOM_VIDEO_SCRIPT`](crate::injection::ZOOM_VIDEO_SCRIPT)
66+
/// sibling scripts.
67+
#[serde(default = "default_show_strip")]
68+
pub show_strip: String,
69+
}
70+
71+
fn default_media_mute() -> String {
72+
"Alt+Shift+M".to_string()
73+
}
74+
75+
fn default_zoom_video() -> String {
76+
"Alt+Shift+V".to_string()
77+
}
78+
79+
fn default_show_strip() -> String {
80+
"Alt+Shift+S".to_string()
5881
}
5982

6083
impl Default for HotkeyConfig {
@@ -68,6 +91,9 @@ impl Default for HotkeyConfig {
6891
media_play_pause: "Alt+Shift+P".to_string(),
6992
media_next: "Alt+Shift+Right".to_string(),
7093
media_previous: "Alt+Shift+Left".to_string(),
94+
media_mute: default_media_mute(),
95+
zoom_video: default_zoom_video(),
96+
show_strip: default_show_strip(),
7197
}
7298
}
7399
}

src-tauri/src/config_io.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ pub fn sanitize_config(mut config: AppConfig) -> AppConfig {
9696
config.hotkeys.media_next = sanitize_hotkey(&config.hotkeys.media_next, "Alt+Shift+Right");
9797
config.hotkeys.media_previous =
9898
sanitize_hotkey(&config.hotkeys.media_previous, "Alt+Shift+Left");
99+
config.hotkeys.media_mute = sanitize_hotkey(&config.hotkeys.media_mute, "Alt+Shift+M");
100+
config.hotkeys.zoom_video = sanitize_hotkey(&config.hotkeys.zoom_video, "Alt+Shift+V");
101+
config.hotkeys.show_strip = sanitize_hotkey(&config.hotkeys.show_strip, "Alt+Shift+S");
99102

100103
let mut deduped_bookmarks = Vec::new();
101104
let mut seen_bookmarks = HashSet::new();

src-tauri/src/hotkeys.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ use tracing::{error, warn};
1414
use crate::actions::{
1515
do_media_action, do_opacity_change, do_toggle_always_on_top, do_toggle_locked,
1616
};
17-
use crate::injection::{MEDIA_NEXT_SCRIPT, MEDIA_PLAY_PAUSE_SCRIPT, MEDIA_PREVIOUS_SCRIPT};
17+
use crate::injection::{
18+
MEDIA_MUTE_SCRIPT, MEDIA_NEXT_SCRIPT, MEDIA_PLAY_PAUSE_SCRIPT, MEDIA_PREVIOUS_SCRIPT,
19+
SHOW_STRIP_SCRIPT, ZOOM_VIDEO_SCRIPT,
20+
};
1821
use crate::state::AppState;
1922

2023
/// Lowercased keyname → `Code` lookup. Populated lazily on first parse.
@@ -145,6 +148,21 @@ pub fn register_hotkeys(app: &AppHandle) {
145148
let app_h = app.clone();
146149
move || do_media_action(&app_h, MEDIA_PREVIOUS_SCRIPT)
147150
});
151+
152+
register_one(app, &hotkeys.media_mute, "media_mute", {
153+
let app_h = app.clone();
154+
move || do_media_action(&app_h, MEDIA_MUTE_SCRIPT)
155+
});
156+
157+
register_one(app, &hotkeys.zoom_video, "zoom_video", {
158+
let app_h = app.clone();
159+
move || do_media_action(&app_h, ZOOM_VIDEO_SCRIPT)
160+
});
161+
162+
register_one(app, &hotkeys.show_strip, "show_strip", {
163+
let app_h = app.clone();
164+
move || do_media_action(&app_h, SHOW_STRIP_SCRIPT)
165+
});
148166
}
149167

150168
/// Internal: parse + register one hotkey binding, logging on failure.

0 commit comments

Comments
 (0)