fix: emit correct board image payload for hover ADD bridge
Browse files
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
|
| 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 |
-
.
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 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) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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();}})();"#;
|