asdf98 commited on
Commit
3c96055
·
verified ·
1 Parent(s): e696850

fix: emit correct board image payload for hover ADD bridge

Browse files
Files changed (1) hide show
  1. src-tauri/src/browser/commands.rs +38 -76
src-tauri/src/browser/commands.rs CHANGED
@@ -1,6 +1,6 @@
1
  use std::collections::HashMap;
2
  use tauri::webview::{PageLoadEvent, WebviewBuilder};
3
- use tauri::{AppHandle, LogicalPosition, LogicalSize, Manager, Url, WebviewUrl};
4
 
5
  use super::layout::{hide_tab, resize_active, show_tab};
6
  use super::navigation::{navigation_blocked, resolve_url};
@@ -59,62 +59,23 @@ pub fn browser_hide_all(app: AppHandle) -> Result<(), String> {
59
  }
60
 
61
  #[tauri::command]
62
- pub async fn tab_create(app: AppHandle, url: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
63
- let resolved = resolve_url(&url);
64
- if navigation_blocked(&app, &resolved) { return snapshot(&app); }
65
- create_tab_inner(&app, &resolved, &layout).await?;
66
- snapshot(&app)
67
- }
68
-
69
  #[tauri::command]
70
- pub async fn tab_activate(app: AppHandle, tab_id: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
71
- let previous = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let previous = tabs.active.clone(); if !tabs.tabs.contains_key(&tab_id) { return Err(format!("tab not found: {tab_id}")); } tabs.active = Some(tab_id.clone()); if let Some(tab) = tabs.tabs.get_mut(&tab_id) { tab.last_active = chrono::Utc::now().timestamp(); } previous };
72
- if let Some(prev) = previous { if prev != tab_id { hide_tab(&app, &prev)?; } }
73
- if layout.width > 10.0 && layout.height > 10.0 { show_tab(&app, &tab_id, &layout)?; }
74
- emit_snapshot(&app)?; snapshot(&app)
75
- }
76
-
77
  #[tauri::command]
78
- pub async fn tab_close(app: AppHandle, tab_id: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
79
- let label = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let label = tabs.tabs.get(&tab_id).map(|t| t.label.clone()); if let Some(tab) = tabs.tabs.remove(&tab_id) { tabs.push_closed(tab); } tabs.order.retain(|id| id != &tab_id); if tabs.active.as_deref() == Some(&tab_id) { tabs.active = tabs.order.last().cloned(); } label };
80
- if let Some(label) = label { if let Some(webview) = app.get_webview(&label) { let _ = webview.close(); } }
81
- let active = { let state = app.state::<AppState>(); let tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.active.clone() };
82
- if let Some(active_id) = active { if layout.width > 10.0 && layout.height > 10.0 { show_tab(&app, &active_id, &layout)?; } }
83
- emit_snapshot(&app)?; snapshot(&app)
84
- }
85
-
86
  #[tauri::command]
87
- pub async fn tab_restore(app: AppHandle, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
88
- let url = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.pop_closed().map(|t| t.url) };
89
- if let Some(url) = url { create_tab_inner(&app, &url, &layout).await?; }
90
- snapshot(&app)
91
- }
92
-
93
  #[tauri::command]
94
- pub async fn tab_navigate(app: AppHandle, tab_id: String, url: String) -> Result<BrowserSnapshot, String> {
95
- let resolved = resolve_url(&url);
96
- if navigation_blocked(&app, &resolved) { return snapshot(&app); }
97
- let label = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let tab = tabs.tabs.get_mut(&tab_id).ok_or("tab not found")?; tab.url = resolved.clone(); tab.loading = true; tab.can_go_back = true; tab.label.clone() };
98
- let webview = app.get_webview(&label).ok_or("webview not found")?;
99
- webview.navigate(Url::parse(&resolved).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
100
- emit_snapshot(&app)?; snapshot(&app)
101
- }
102
-
103
  #[tauri::command]
104
  pub async fn tab_reload(app: AppHandle, tab_id: String) -> Result<(), String> { let label = tab_label(&app, &tab_id)?; app.get_webview(&label).ok_or("webview not found")?.reload().map_err(|e| e.to_string()) }
105
  #[tauri::command]
106
  pub async fn tab_back(app: AppHandle, tab_id: String) -> Result<(), String> { eval_on_tab(&app, &tab_id, "history.back()")?; update_tab_field(&app, &tab_id, |t| { t.can_go_forward = true; }); emit_snapshot(&app)?; Ok(()) }
107
  #[tauri::command]
108
  pub async fn tab_forward(app: AppHandle, tab_id: String) -> Result<(), String> { eval_on_tab(&app, &tab_id, "history.forward()")?; update_tab_field(&app, &tab_id, |t| { t.can_go_back = true; }); emit_snapshot(&app)?; Ok(()) }
109
-
110
  #[tauri::command]
111
- pub async fn tab_zoom(app: AppHandle, tab_id: String, zoom: f64) -> Result<BrowserSnapshot, String> {
112
- let zoom = zoom.clamp(0.5, 2.0);
113
- let label = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let (label, domain) = { let tab = tabs.tabs.get_mut(&tab_id).ok_or("tab not found")?; tab.zoom = zoom; let domain = extract_domain(&tab.url); (tab.label.clone(), domain) }; tabs.remember_zoom(&domain, zoom); label };
114
- app.get_webview(&label).ok_or("webview not found")?.set_zoom(zoom).map_err(|e| e.to_string())?;
115
- snapshot(&app)
116
- }
117
-
118
  #[tauri::command]
119
  pub async fn tab_resize(app: AppHandle, layout: ViewportLayout) -> Result<(), String> { resize_active(&app, &layout) }
120
  #[tauri::command]
@@ -128,40 +89,41 @@ pub async fn tab_find_clear(app: AppHandle, tab_id: String) -> Result<(), String
128
 
129
  pub(crate) async fn create_tab_inner(app: &AppHandle, url: &str, _layout: &ViewportLayout) -> Result<String, String> {
130
  let id_num = app.state::<AppState>().next_tab_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
131
- let id = format!("tab-{id_num}");
132
- let label = format!("muse-tab-{id_num}");
133
- let parsed = Url::parse(url).map_err(|e| e.to_string())?;
134
- let init_script = scripts::build_init_script(&scripts::blocked_domains_json());
135
- let full_init = format!("{init_script}\n{CONTEXT_MENU_BLOCK_JS}\n{FAVICON_SCRIPT}");
136
  let app_for_load = app.clone(); let id_for_load = id.clone(); let app_for_title = app.clone(); let id_for_title = id.clone(); let app_for_cosmetic = app.clone();
137
- let builder = WebviewBuilder::new(label.clone(), WebviewUrl::External(parsed))
138
- .initialization_script(&full_init)
139
- .on_page_load(move |webview, payload| {
140
- let url = payload.url().to_string(); let loading = matches!(payload.event(), PageLoadEvent::Started);
141
- update_tab_field(&app_for_load, &id_for_load, |t| { if !loading && t.url != url && !t.url.is_empty() { t.can_go_back = true; } t.url = url.clone(); t.loading = loading; });
142
- if matches!(payload.event(), PageLoadEvent::Finished) {
143
- let _ = webview.eval(CONTEXT_MENU_BLOCK_JS); let _ = webview.eval(scripts::hover_overlay_script());
144
- let adblock_state = app_for_cosmetic.state::<AdBlockState>(); let css = adblock_state.get_cosmetic_css(&url);
145
- if !css.is_empty() { let escaped = css.replace('\\', "\\\\").replace('`', "\\`"); let _ = webview.eval(&format!("(function(){{const s=document.createElement('style');s.id='__muse_shield';s.textContent=`{escaped}`;document.head.appendChild(s)}})();")); }
146
- let scriptlet_js = adblock_state.get_injected_script(&url); if !scriptlet_js.is_empty() { let _ = webview.eval(&format!("try{{{scriptlet_js}}}catch(e){{}}")); }
147
- let _ = webview.eval("window.__muse_report_favicon && window.__muse_report_favicon()");
148
- let title = { let state = app_for_load.state::<AppState>(); state.tabs.lock().ok().and_then(|tabs| tabs.tabs.get(&id_for_load).map(|t| t.title.clone())).unwrap_or_default() };
149
- let _ = crate::history::record_visit(&app_for_load, id_for_load.clone(), url.clone(), title);
150
- }
151
- })
152
- .on_document_title_changed(move |_wv, title| { update_tab_field(&app_for_title, &id_for_title, |t| { if !title.trim().is_empty() { t.title = title.clone(); } }); })
153
- .on_navigation({ let app_nav = app.clone(); move |url| { let s = url.as_str(); if s.starts_with("muse-action://") { handle_muse_action(&app_nav, s); return false; } !navigation_blocked(&app_nav, s) }});
154
- let window = app.get_window("main").ok_or("main window not found")?;
155
- window.add_child(builder, LogicalPosition::new(-32000.0, -32000.0), LogicalSize::new(1.0, 1.0)).map_err(|e| e.to_string())?;
156
- let domain = extract_domain(url); let saved_zoom = { let state = app.state::<AppState>(); let tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.get_zoom_for_domain(&domain) };
157
- let now = chrono::Utc::now().timestamp(); let zoom = saved_zoom.unwrap_or(1.0);
158
  let previous = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let previous = tabs.active.clone(); tabs.active = Some(id.clone()); tabs.order.push(id.clone()); tabs.tabs.insert(id.clone(), BrowserTab { id: id.clone(), label: label.clone(), url: url.to_string(), title: "New Tab".to_string(), favicon: None, loading: true, pinned: false, sleeping: false, zoom, can_go_back: false, can_go_forward: false, last_active: now }); previous };
159
- if zoom != 1.0 { if let Some(webview) = app.get_webview(&label) { let _ = webview.set_zoom(zoom); } }
160
- if let Some(prev) = previous { if prev != id { hide_tab(app, &prev)?; } }
161
- emit_snapshot(app)?; Ok(id)
162
  }
163
 
164
- fn handle_muse_action(app: &AppHandle, raw: &str) { let rest = raw.trim_start_matches("muse-action://"); let (action, query) = rest.split_once('?').unwrap_or((rest, "")); let params = parse_query(query); let url = params.get("url").cloned().unwrap_or_default(); if url.is_empty() { return; } let title = params.get("title").cloned(); let source = params.get("source").cloned(); let app2 = app.clone(); let action = action.to_string(); tauri::async_runtime::spawn(async move { if let Ok(item) = crate::library::library_add_item(app2.clone(), url, source, title).await { if action == "board" { let _ = crate::board::board_add_image(app2, Some(item.id), item.data_url, 120.0, 120.0, 300.0, 200.0); } } }); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  fn parse_query(query: &str) -> HashMap<String, String> { query.split('&').filter_map(|pair| { let (k, v) = pair.split_once('=')?; Some((percent_decode(k), percent_decode(v))) }).collect() }
166
  fn percent_decode(s: &str) -> String { let bytes = s.as_bytes(); let mut out = Vec::with_capacity(bytes.len()); let mut i = 0; while i < bytes.len() { if bytes[i] == b'%' && i + 2 < bytes.len() { if let Ok(hex) = std::str::from_utf8(&bytes[i+1..i+3]) { if let Ok(v) = u8::from_str_radix(hex, 16) { out.push(v); i += 3; continue; } } } out.push(if bytes[i] == b'+' { b' ' } else { bytes[i] }); i += 1; } String::from_utf8_lossy(&out).to_string() }
167
  const FAVICON_SCRIPT: &str = r#"(function(){window.__muse_report_favicon=function(){const link=document.querySelector('link[rel~="icon"],link[rel="shortcut icon"],link[rel="apple-touch-icon"]');if(link&&link.href){try{window.__TAURI_INTERNALS__.invoke('__tab_favicon',{favicon:link.href});}catch{}}};if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',window.__muse_report_favicon);}else{window.__muse_report_favicon();}})();"#;
 
1
  use std::collections::HashMap;
2
  use tauri::webview::{PageLoadEvent, WebviewBuilder};
3
+ use tauri::{AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, Url, WebviewUrl};
4
 
5
  use super::layout::{hide_tab, resize_active, show_tab};
6
  use super::navigation::{navigation_blocked, resolve_url};
 
59
  }
60
 
61
  #[tauri::command]
62
+ pub async fn tab_create(app: AppHandle, url: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> { let resolved = resolve_url(&url); if navigation_blocked(&app, &resolved) { return snapshot(&app); } create_tab_inner(&app, &resolved, &layout).await?; snapshot(&app) }
 
 
 
 
 
 
63
  #[tauri::command]
64
+ pub async fn tab_activate(app: AppHandle, tab_id: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> { let previous = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let previous = tabs.active.clone(); if !tabs.tabs.contains_key(&tab_id) { return Err(format!("tab not found: {tab_id}")); } tabs.active = Some(tab_id.clone()); if let Some(tab) = tabs.tabs.get_mut(&tab_id) { tab.last_active = chrono::Utc::now().timestamp(); } previous }; if let Some(prev) = previous { if prev != tab_id { hide_tab(&app, &prev)?; } } if layout.width > 10.0 && layout.height > 10.0 { show_tab(&app, &tab_id, &layout)?; } emit_snapshot(&app)?; snapshot(&app) }
 
 
 
 
 
 
65
  #[tauri::command]
66
+ pub async fn tab_close(app: AppHandle, tab_id: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> { let label = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let label = tabs.tabs.get(&tab_id).map(|t| t.label.clone()); if let Some(tab) = tabs.tabs.remove(&tab_id) { tabs.push_closed(tab); } tabs.order.retain(|id| id != &tab_id); if tabs.active.as_deref() == Some(&tab_id) { tabs.active = tabs.order.last().cloned(); } label }; if let Some(label) = label { if let Some(webview) = app.get_webview(&label) { let _ = webview.close(); } } let active = { let state = app.state::<AppState>(); let tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.active.clone() }; if let Some(active_id) = active { if layout.width > 10.0 && layout.height > 10.0 { show_tab(&app, &active_id, &layout)?; } } emit_snapshot(&app)?; snapshot(&app) }
 
 
 
 
 
 
 
67
  #[tauri::command]
68
+ pub async fn tab_restore(app: AppHandle, layout: ViewportLayout) -> Result<BrowserSnapshot, String> { let url = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.pop_closed().map(|t| t.url) }; if let Some(url) = url { create_tab_inner(&app, &url, &layout).await?; } snapshot(&app) }
 
 
 
 
 
69
  #[tauri::command]
70
+ pub async fn tab_navigate(app: AppHandle, tab_id: String, url: String) -> Result<BrowserSnapshot, String> { let resolved = resolve_url(&url); if navigation_blocked(&app, &resolved) { return snapshot(&app); } let label = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let tab = tabs.tabs.get_mut(&tab_id).ok_or("tab not found")?; tab.url = resolved.clone(); tab.loading = true; tab.can_go_back = true; tab.label.clone() }; let webview = app.get_webview(&label).ok_or("webview not found")?; webview.navigate(Url::parse(&resolved).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?; emit_snapshot(&app)?; snapshot(&app) }
 
 
 
 
 
 
 
 
71
  #[tauri::command]
72
  pub async fn tab_reload(app: AppHandle, tab_id: String) -> Result<(), String> { let label = tab_label(&app, &tab_id)?; app.get_webview(&label).ok_or("webview not found")?.reload().map_err(|e| e.to_string()) }
73
  #[tauri::command]
74
  pub async fn tab_back(app: AppHandle, tab_id: String) -> Result<(), String> { eval_on_tab(&app, &tab_id, "history.back()")?; update_tab_field(&app, &tab_id, |t| { t.can_go_forward = true; }); emit_snapshot(&app)?; Ok(()) }
75
  #[tauri::command]
76
  pub async fn tab_forward(app: AppHandle, tab_id: String) -> Result<(), String> { eval_on_tab(&app, &tab_id, "history.forward()")?; update_tab_field(&app, &tab_id, |t| { t.can_go_back = true; }); emit_snapshot(&app)?; Ok(()) }
 
77
  #[tauri::command]
78
+ pub async fn tab_zoom(app: AppHandle, tab_id: String, zoom: f64) -> Result<BrowserSnapshot, String> { let zoom = zoom.clamp(0.5, 2.0); let label = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let (label, domain) = { let tab = tabs.tabs.get_mut(&tab_id).ok_or("tab not found")?; tab.zoom = zoom; let domain = extract_domain(&tab.url); (tab.label.clone(), domain) }; tabs.remember_zoom(&domain, zoom); label }; app.get_webview(&label).ok_or("webview not found")?.set_zoom(zoom).map_err(|e| e.to_string())?; snapshot(&app) }
 
 
 
 
 
 
79
  #[tauri::command]
80
  pub async fn tab_resize(app: AppHandle, layout: ViewportLayout) -> Result<(), String> { resize_active(&app, &layout) }
81
  #[tauri::command]
 
89
 
90
  pub(crate) async fn create_tab_inner(app: &AppHandle, url: &str, _layout: &ViewportLayout) -> Result<String, String> {
91
  let id_num = app.state::<AppState>().next_tab_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
92
+ let id = format!("tab-{id_num}"); let label = format!("muse-tab-{id_num}"); let parsed = Url::parse(url).map_err(|e| e.to_string())?;
93
+ let init_script = scripts::build_init_script(&scripts::blocked_domains_json()); let full_init = format!("{init_script}\n{CONTEXT_MENU_BLOCK_JS}\n{FAVICON_SCRIPT}");
 
 
 
94
  let app_for_load = app.clone(); let id_for_load = id.clone(); let app_for_title = app.clone(); let id_for_title = id.clone(); let app_for_cosmetic = app.clone();
95
+ let builder = WebviewBuilder::new(label.clone(), WebviewUrl::External(parsed)).initialization_script(&full_init).on_page_load(move |webview, payload| {
96
+ let url = payload.url().to_string(); let loading = matches!(payload.event(), PageLoadEvent::Started);
97
+ update_tab_field(&app_for_load, &id_for_load, |t| { if !loading && t.url != url && !t.url.is_empty() { t.can_go_back = true; } t.url = url.clone(); t.loading = loading; });
98
+ if matches!(payload.event(), PageLoadEvent::Finished) { let _ = webview.eval(CONTEXT_MENU_BLOCK_JS); let _ = webview.eval(scripts::hover_overlay_script()); let adblock_state = app_for_cosmetic.state::<AdBlockState>(); let css = adblock_state.get_cosmetic_css(&url); if !css.is_empty() { let escaped = css.replace('\\', "\\\\").replace('`', "\\`"); let _ = webview.eval(&format!("(function(){{const s=document.createElement('style');s.id='__muse_shield';s.textContent=`{escaped}`;document.head.appendChild(s)}})();")); } let scriptlet_js = adblock_state.get_injected_script(&url); if !scriptlet_js.is_empty() { let _ = webview.eval(&format!("try{{{scriptlet_js}}}catch(e){{}}")); } let _ = webview.eval("window.__muse_report_favicon && window.__muse_report_favicon()"); let title = { let state = app_for_load.state::<AppState>(); state.tabs.lock().ok().and_then(|tabs| tabs.tabs.get(&id_for_load).map(|t| t.title.clone())).unwrap_or_default() }; let _ = crate::history::record_visit(&app_for_load, id_for_load.clone(), url.clone(), title); }
99
+ }).on_document_title_changed(move |_wv, title| { update_tab_field(&app_for_title, &id_for_title, |t| { if !title.trim().is_empty() { t.title = title.clone(); } }); }).on_navigation({ let app_nav = app.clone(); move |url| { let s = url.as_str(); if s.starts_with("muse-action://") { handle_muse_action(&app_nav, s); return false; } !navigation_blocked(&app_nav, s) }});
100
+ let window = app.get_window("main").ok_or("main window not found")?; window.add_child(builder, LogicalPosition::new(-32000.0, -32000.0), LogicalSize::new(1.0, 1.0)).map_err(|e| e.to_string())?;
101
+ let domain = extract_domain(url); let saved_zoom = { let state = app.state::<AppState>(); let tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.get_zoom_for_domain(&domain) }; let now = chrono::Utc::now().timestamp(); let zoom = saved_zoom.unwrap_or(1.0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  let previous = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let previous = tabs.active.clone(); tabs.active = Some(id.clone()); tabs.order.push(id.clone()); tabs.tabs.insert(id.clone(), BrowserTab { id: id.clone(), label: label.clone(), url: url.to_string(), title: "New Tab".to_string(), favicon: None, loading: true, pinned: false, sleeping: false, zoom, can_go_back: false, can_go_forward: false, last_active: now }); previous };
103
+ if zoom != 1.0 { if let Some(webview) = app.get_webview(&label) { let _ = webview.set_zoom(zoom); } } if let Some(prev) = previous { if prev != id { hide_tab(app, &prev)?; } } emit_snapshot(app)?; Ok(id)
 
 
104
  }
105
 
106
+ fn handle_muse_action(app: &AppHandle, raw: &str) {
107
+ let rest = raw.trim_start_matches("muse-action://"); let (action, query) = rest.split_once('?').unwrap_or((rest, "")); let params = parse_query(query); let url = params.get("url").cloned().unwrap_or_default(); if url.is_empty() { return; }
108
+ let title = params.get("title").cloned(); let source_page = params.get("source").cloned(); let app2 = app.clone(); let action = action.to_string();
109
+ tauri::async_runtime::spawn(async move {
110
+ if let Ok(item) = crate::library::library_add_item(app2.clone(), url.clone(), source_page.clone(), title).await {
111
+ if action == "board" {
112
+ let _ = crate::board::board_add_image(app2.clone(), Some(item.id.clone()), item.data_url.clone(), 120.0, 120.0, 300.0, 200.0);
113
+ let _ = app2.emit("board://image_added", serde_json::json!({
114
+ "libraryId": item.id,
115
+ "dataUrl": item.data_url,
116
+ "url": item.url,
117
+ "sourceUrl": item.source_url,
118
+ "title": item.title,
119
+ "width": item.width,
120
+ "height": item.height,
121
+ "colors": item.colors
122
+ }));
123
+ }
124
+ }
125
+ });
126
+ }
127
  fn parse_query(query: &str) -> HashMap<String, String> { query.split('&').filter_map(|pair| { let (k, v) = pair.split_once('=')?; Some((percent_decode(k), percent_decode(v))) }).collect() }
128
  fn percent_decode(s: &str) -> String { let bytes = s.as_bytes(); let mut out = Vec::with_capacity(bytes.len()); let mut i = 0; while i < bytes.len() { if bytes[i] == b'%' && i + 2 < bytes.len() { if let Ok(hex) = std::str::from_utf8(&bytes[i+1..i+3]) { if let Ok(v) = u8::from_str_radix(hex, 16) { out.push(v); i += 3; continue; } } } out.push(if bytes[i] == b'+' { b' ' } else { bytes[i] }); i += 1; } String::from_utf8_lossy(&out).to_string() }
129
  const FAVICON_SCRIPT: &str = r#"(function(){window.__muse_report_favicon=function(){const link=document.querySelector('link[rel~="icon"],link[rel="shortcut icon"],link[rel="apple-touch-icon"]');if(link&&link.href){try{window.__TAURI_INTERNALS__.invoke('__tab_favicon',{favicon:link.href});}catch{}}};if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',window.__muse_report_favicon);}else{window.__muse_report_favicon();}})();"#;