fix: dedupe muse-action captures by captureId to prevent duplicate adds
Browse files- src-tauri/src/browser/commands.rs +41 -134
src-tauri/src/browser/commands.rs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
use tauri::webview::{PageLoadEvent, WebviewBuilder};
|
| 2 |
use tauri::{AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, Url, WebviewUrl};
|
| 3 |
|
|
@@ -8,22 +10,15 @@ use crate::adblock::engine::AdBlockState;
|
|
| 8 |
use crate::adblock::scripts;
|
| 9 |
use crate::state::AppState;
|
| 10 |
|
|
|
|
|
|
|
| 11 |
const CONTEXT_MENU_BLOCK_JS: &str = r#"
|
| 12 |
(function(){
|
| 13 |
if(window.__muse_ctx_installed) return;
|
| 14 |
window.__muse_ctx_installed = true;
|
| 15 |
document.addEventListener('contextmenu', function(e) {
|
| 16 |
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
|
| 17 |
-
try {
|
| 18 |
-
var t = e.target;
|
| 19 |
-
window.__TAURI_INTERNALS__.invoke('browser_context_menu', {
|
| 20 |
-
x: e.screenX, y: e.screenY, clientX: e.clientX, clientY: e.clientY,
|
| 21 |
-
tagName: t.tagName || null, src: t.src || t.currentSrc || null,
|
| 22 |
-
href: t.closest && t.closest('a') ? t.closest('a').href : null,
|
| 23 |
-
text: (window.getSelection() || '').toString().slice(0, 200) || null,
|
| 24 |
-
pageUrl: location.href
|
| 25 |
-
});
|
| 26 |
-
} catch(ex) {}
|
| 27 |
return false;
|
| 28 |
}, true);
|
| 29 |
window.addEventListener('contextmenu', function(e){ e.preventDefault(); return false; }, true);
|
|
@@ -31,149 +26,61 @@ const CONTEXT_MENU_BLOCK_JS: &str = r#"
|
|
| 31 |
"#;
|
| 32 |
|
| 33 |
#[tauri::command]
|
| 34 |
-
pub async fn browser_init(app: AppHandle, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
|
| 35 |
-
let existing = snapshot(&app)?;
|
| 36 |
-
if !existing.tabs.is_empty() { if layout.width > 10.0 && layout.height > 10.0 { resize_active(&app, &layout)?; } return Ok(existing); }
|
| 37 |
-
create_tab_inner(&app, "https://duckduckgo.com", &layout).await?;
|
| 38 |
-
snapshot(&app)
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
#[tauri::command]
|
| 42 |
-
pub async fn browser_set_visible(app: AppHandle, visible: bool, layout: ViewportLayout) -> Result<(), String> {
|
| 43 |
-
let active = { let state = app.state::<AppState>(); let tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.active.clone() };
|
| 44 |
-
if let Some(active_id) = active { if visible && layout.width > 10.0 && layout.height > 10.0 { show_tab(&app, &active_id, &layout)?; } else { hide_tab(&app, &active_id)?; } }
|
| 45 |
-
Ok(())
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
#[tauri::command]
|
| 49 |
-
pub fn browser_hide_all(app: AppHandle) -> Result<(), String> {
|
| 50 |
-
let ids = { let state = app.state::<AppState>(); let tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.order.clone() };
|
| 51 |
-
for id in ids { let _ = hide_tab(&app, &id); }
|
| 52 |
-
Ok(())
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
#[tauri::command]
|
| 56 |
-
pub async fn tab_create(app: AppHandle, url: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> { let resolved
|
| 57 |
#[tauri::command]
|
| 58 |
-
pub async fn tab_activate(app: AppHandle, tab_id: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> { let previous
|
| 59 |
#[tauri::command]
|
| 60 |
-
pub async fn tab_close(app: AppHandle, tab_id: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> { let label
|
| 61 |
#[tauri::command]
|
| 62 |
-
pub async fn tab_restore(app: AppHandle, layout: ViewportLayout) -> Result<BrowserSnapshot, String> { let url
|
| 63 |
#[tauri::command]
|
| 64 |
-
pub async fn tab_navigate(app: AppHandle, tab_id: String, url: String) -> Result<BrowserSnapshot, String> { let resolved
|
| 65 |
#[tauri::command]
|
| 66 |
-
pub async fn tab_reload(app: AppHandle, tab_id: String) -> Result<(), String> { let label
|
| 67 |
#[tauri::command]
|
| 68 |
-
pub async fn tab_back(app: AppHandle, tab_id: String) -> Result<(), String> { eval_on_tab(&app,
|
| 69 |
#[tauri::command]
|
| 70 |
-
pub async fn tab_forward(app: AppHandle, tab_id: String) -> Result<(), String> { eval_on_tab(&app,
|
| 71 |
#[tauri::command]
|
| 72 |
-
pub async fn tab_zoom(app: AppHandle, tab_id: String, zoom: f64) -> Result<BrowserSnapshot, String> { let zoom
|
| 73 |
#[tauri::command]
|
| 74 |
-
pub async fn tab_resize(app: AppHandle, layout: ViewportLayout) -> Result<(), String> { resize_active(&app,
|
| 75 |
#[tauri::command]
|
| 76 |
pub fn tab_get_all(app: AppHandle) -> Result<BrowserSnapshot, String> { snapshot(&app) }
|
| 77 |
#[tauri::command]
|
| 78 |
-
pub fn tab_pin(app: AppHandle, tab_id: String, pinned: bool) -> Result<BrowserSnapshot, String> { update_tab_field(&app,
|
| 79 |
#[tauri::command]
|
| 80 |
-
pub async fn tab_find(app: AppHandle, tab_id: String, query: String) -> Result<u32, String> { let q
|
| 81 |
#[tauri::command]
|
| 82 |
-
pub async fn tab_find_clear(app: AppHandle, tab_id: String) -> Result<(), String> { eval_on_tab(&app,
|
| 83 |
-
|
| 84 |
-
pub(crate) async fn create_tab_inner(app: &AppHandle, url: &str, _layout: &ViewportLayout) -> Result<String, String> {
|
| 85 |
-
let id_num = app.state::<AppState>().next_tab_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
| 86 |
-
let id = format!("tab-{id_num}"); let label = format!("muse-tab-{id_num}");
|
| 87 |
-
let parsed = Url::parse(url).map_err(|e| e.to_string())?;
|
| 88 |
-
let init_script = scripts::build_init_script(&scripts::blocked_domains_json());
|
| 89 |
-
let full_init = format!("{init_script}\n{CONTEXT_MENU_BLOCK_JS}\n{FAVICON_SCRIPT}");
|
| 90 |
-
let app_for_load = app.clone(); let id_for_load = id.clone();
|
| 91 |
-
let app_for_title = app.clone(); let id_for_title = id.clone();
|
| 92 |
-
let app_for_cosmetic = app.clone();
|
| 93 |
-
|
| 94 |
-
let builder = WebviewBuilder::new(label.clone(), WebviewUrl::External(parsed))
|
| 95 |
-
.initialization_script(&full_init)
|
| 96 |
-
.on_page_load(move |webview, payload| {
|
| 97 |
-
let url = payload.url().to_string();
|
| 98 |
-
let loading = matches!(payload.event(), PageLoadEvent::Started);
|
| 99 |
-
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; });
|
| 100 |
-
if matches!(payload.event(), PageLoadEvent::Finished) {
|
| 101 |
-
let _ = webview.eval(CONTEXT_MENU_BLOCK_JS);
|
| 102 |
-
let _ = webview.eval(scripts::hover_overlay_script());
|
| 103 |
-
let adblock_state = app_for_cosmetic.state::<AdBlockState>();
|
| 104 |
-
let css = adblock_state.get_cosmetic_css(&url);
|
| 105 |
-
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)}})();")); }
|
| 106 |
-
let scriptlet_js = adblock_state.get_injected_script(&url);
|
| 107 |
-
if !scriptlet_js.is_empty() { let _ = webview.eval(&format!("try{{{scriptlet_js}}}catch(e){{}}")); }
|
| 108 |
-
let _ = webview.eval("window.__muse_report_favicon && window.__muse_report_favicon()");
|
| 109 |
-
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() };
|
| 110 |
-
let _ = crate::history::record_visit(&app_for_load, id_for_load.clone(), url.clone(), title);
|
| 111 |
-
}
|
| 112 |
-
})
|
| 113 |
-
.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(); } }); })
|
| 114 |
-
.on_navigation({ let app_nav = app.clone(); move |url| {
|
| 115 |
-
let s = url.as_str();
|
| 116 |
-
// Process muse-action:// HERE because register_uri_scheme_protocol does NOT fire
|
| 117 |
-
// for navigations that are blocked (returning false prevents the request from
|
| 118 |
-
// reaching the network layer where the scheme protocol lives).
|
| 119 |
-
if s.starts_with("muse-action://") {
|
| 120 |
-
handle_muse_action(&app_nav, s);
|
| 121 |
-
return false; // Block navigation — page stays on current URL
|
| 122 |
-
}
|
| 123 |
-
if navigation_blocked(&app_nav, s) { return false; }
|
| 124 |
-
true
|
| 125 |
-
}});
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
let
|
| 131 |
-
let
|
| 132 |
-
let
|
| 133 |
-
let
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
emit_snapshot(app)?; Ok(id)
|
| 137 |
}
|
| 138 |
|
| 139 |
-
|
| 140 |
-
//
|
| 141 |
-
|
| 142 |
-
let
|
| 143 |
-
|
| 144 |
-
let
|
| 145 |
-
let
|
| 146 |
-
|
| 147 |
-
let source = params.get("source").cloned().unwrap_or_default();
|
| 148 |
-
let title = params.get("title").cloned().unwrap_or_else(|| "Web Reference".to_string());
|
| 149 |
-
let w = params.get("w").and_then(|s| s.parse::<u32>().ok()).unwrap_or(300);
|
| 150 |
-
let h = params.get("h").and_then(|s| s.parse::<u32>().ok()).unwrap_or(200);
|
| 151 |
-
let action = action.trim_end_matches('/').to_string();
|
| 152 |
-
let app2 = app.clone();
|
| 153 |
-
|
| 154 |
-
tauri::async_runtime::spawn(async move {
|
| 155 |
-
match crate::library::library_add_item(app2.clone(), url.clone(), Some(source.clone()), Some(title.clone())).await {
|
| 156 |
-
Ok(item) => {
|
| 157 |
-
if action == "board" {
|
| 158 |
-
let _ = crate::board::board_add_image(app2.clone(), Some(item.id.clone()), item.data_url.clone(), 120.0, 120.0, 300.0, 200.0);
|
| 159 |
-
}
|
| 160 |
-
let _ = app2.emit("board://image_added", serde_json::json!({
|
| 161 |
-
"libraryId": item.id, "dataUrl": item.data_url, "url": item.url,
|
| 162 |
-
"sourceUrl": item.source_url, "title": item.title,
|
| 163 |
-
"width": item.width, "height": item.height, "colors": item.colors
|
| 164 |
-
}));
|
| 165 |
-
}
|
| 166 |
-
Err(_e) => {
|
| 167 |
-
// Fallback: emit with raw URL so image still appears on canvas (as remote img)
|
| 168 |
-
let _ = app2.emit("board://image_added", serde_json::json!({
|
| 169 |
-
"url": url, "dataUrl": url, "sourceUrl": source, "title": title,
|
| 170 |
-
"width": w, "height": h, "colors": []
|
| 171 |
-
}));
|
| 172 |
-
}
|
| 173 |
-
}
|
| 174 |
-
});
|
| 175 |
}
|
| 176 |
|
| 177 |
-
fn percent_decode(s:
|
| 178 |
-
|
| 179 |
-
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, HashSet};
|
| 2 |
+
use std::sync::{Mutex, OnceLock};
|
| 3 |
use tauri::webview::{PageLoadEvent, WebviewBuilder};
|
| 4 |
use tauri::{AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, Url, WebviewUrl};
|
| 5 |
|
|
|
|
| 10 |
use crate::adblock::scripts;
|
| 11 |
use crate::state::AppState;
|
| 12 |
|
| 13 |
+
static SEEN_CAPTURE_IDS: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
|
| 14 |
+
|
| 15 |
const CONTEXT_MENU_BLOCK_JS: &str = r#"
|
| 16 |
(function(){
|
| 17 |
if(window.__muse_ctx_installed) return;
|
| 18 |
window.__muse_ctx_installed = true;
|
| 19 |
document.addEventListener('contextmenu', function(e) {
|
| 20 |
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
|
| 21 |
+
try { var t=e.target; window.__TAURI_INTERNALS__.invoke('browser_context_menu',{x:e.screenX,y:e.screenY,clientX:e.clientX,clientY:e.clientY,tagName:t.tagName||null,src:t.src||t.currentSrc||null,href:t.closest&&t.closest('a')?t.closest('a').href:null,text:(window.getSelection()||'').toString().slice(0,200)||null,pageUrl:location.href}); } catch(ex) {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
return false;
|
| 23 |
}, true);
|
| 24 |
window.addEventListener('contextmenu', function(e){ e.preventDefault(); return false; }, true);
|
|
|
|
| 26 |
"#;
|
| 27 |
|
| 28 |
#[tauri::command]
|
| 29 |
+
pub async fn browser_init(app: AppHandle, layout: ViewportLayout) -> Result<BrowserSnapshot, String> { let existing=snapshot(&app)?; if !existing.tabs.is_empty(){ if layout.width>10.0&&layout.height>10.0{resize_active(&app,&layout)?;} return Ok(existing);} create_tab_inner(&app,"https://duckduckgo.com",&layout).await?; snapshot(&app) }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
#[tauri::command]
|
| 31 |
+
pub async fn browser_set_visible(app: AppHandle, visible: bool, layout: ViewportLayout) -> Result<(), String> { 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 visible&&layout.width>10.0&&layout.height>10.0{show_tab(&app,&active_id,&layout)?;}else{hide_tab(&app,&active_id)?;} } Ok(()) }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
#[tauri::command]
|
| 33 |
+
pub fn browser_hide_all(app: AppHandle) -> Result<(), String> { let ids={let state=app.state::<AppState>();let tabs=state.tabs.lock().map_err(|_|"lock")?;tabs.order.clone()}; for id in ids{let _=hide_tab(&app,&id);} Ok(()) }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
#[tauri::command]
|
| 35 |
+
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) }
|
| 36 |
#[tauri::command]
|
| 37 |
+
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) }
|
| 38 |
#[tauri::command]
|
| 39 |
+
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) }
|
| 40 |
#[tauri::command]
|
| 41 |
+
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) }
|
| 42 |
#[tauri::command]
|
| 43 |
+
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) }
|
| 44 |
#[tauri::command]
|
| 45 |
+
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()) }
|
| 46 |
#[tauri::command]
|
| 47 |
+
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(()) }
|
| 48 |
#[tauri::command]
|
| 49 |
+
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(()) }
|
| 50 |
#[tauri::command]
|
| 51 |
+
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) }
|
| 52 |
#[tauri::command]
|
| 53 |
+
pub async fn tab_resize(app: AppHandle, layout: ViewportLayout) -> Result<(), String> { resize_active(&app,&layout) }
|
| 54 |
#[tauri::command]
|
| 55 |
pub fn tab_get_all(app: AppHandle) -> Result<BrowserSnapshot, String> { snapshot(&app) }
|
| 56 |
#[tauri::command]
|
| 57 |
+
pub fn tab_pin(app: AppHandle, tab_id: String, pinned: bool) -> Result<BrowserSnapshot, String> { update_tab_field(&app,&tab_id,|t|{t.pinned=pinned;}); snapshot(&app) }
|
| 58 |
#[tauri::command]
|
| 59 |
+
pub async fn tab_find(app: AppHandle, tab_id: String, query: String) -> Result<u32, String> { let q=serde_json::to_string(&query).unwrap_or("\"\"".into()); eval_on_tab(&app,&tab_id,&format!(r#"(function(){{window.__muse_find_cleanup&&window.__muse_find_cleanup();if(!{q})return 0;const text={q};const walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);let count=0;const marks=[];while(walker.nextNode()){{const node=walker.currentNode;const idx=node.textContent.toLowerCase().indexOf(text.toLowerCase());if(idx>=0){{const range=document.createRange();range.setStart(node,idx);range.setEnd(node,idx+text.length);const mark=document.createElement('mark');mark.style.cssText='background:#C49A3C;color:#100E0B;border-radius:2px;padding:0 1px';range.surroundContents(mark);marks.push(mark);count++;}}}}if(marks.length>0)marks[0].scrollIntoView({{block:'center'}});window.__muse_find_cleanup=()=>marks.forEach(m=>{{const p=m.parentNode;if(p){{p.replaceChild(document.createTextNode(m.textContent||''),m);p.normalize();}}}});return count;}})()"#))?; Ok(0) }
|
| 60 |
#[tauri::command]
|
| 61 |
+
pub async fn tab_find_clear(app: AppHandle, tab_id: String) -> Result<(), String> { eval_on_tab(&app,&tab_id,"window.__muse_find_cleanup && window.__muse_find_cleanup()") }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
pub(crate) async fn create_tab_inner(app:&AppHandle,url:&str,_layout:&ViewportLayout)->Result<String,String>{
|
| 64 |
+
let id_num=app.state::<AppState>().next_tab_id.fetch_add(1,std::sync::atomic::Ordering::Relaxed);
|
| 65 |
+
let id=format!("tab-{id_num}"); let label=format!("muse-tab-{id_num}"); let parsed=Url::parse(url).map_err(|e|e.to_string())?;
|
| 66 |
+
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}");
|
| 67 |
+
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();
|
| 68 |
+
let builder=WebviewBuilder::new(label.clone(),WebviewUrl::External(parsed)).initialization_script(&full_init).on_page_load(move|webview,payload|{let url=payload.url().to_string();let loading=matches!(payload.event(),PageLoadEvent::Started);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;});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);}}).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;}if navigation_blocked(&app_nav,s){return false;}true}});
|
| 69 |
+
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())?;
|
| 70 |
+
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);
|
| 71 |
+
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};
|
| 72 |
+
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)
|
| 73 |
}
|
| 74 |
|
| 75 |
+
fn handle_muse_action(app:&AppHandle,raw:&str){
|
| 76 |
+
let rest=raw.trim_start_matches("muse-action://"); let (action,query)=rest.split_once('?').unwrap_or((rest,""));
|
| 77 |
+
let params:HashMap<String,String>=query.split('&').filter_map(|pair|{let(k,v)=pair.split_once('=')?;Some((percent_decode(k),percent_decode(v)))}).collect();
|
| 78 |
+
let capture_id=params.get("captureId").cloned().unwrap_or_default();
|
| 79 |
+
if !capture_id.is_empty(){let seen=SEEN_CAPTURE_IDS.get_or_init(||Mutex::new(HashSet::new()));if let Ok(mut s)=seen.lock(){if s.contains(&capture_id){return;}s.insert(capture_id);if s.len()>512{if let Some(first)=s.iter().next().cloned(){s.remove(&first);}}}}
|
| 80 |
+
let url=params.get("url").cloned().unwrap_or_default(); if url.is_empty(){return;}
|
| 81 |
+
let source=params.get("source").cloned().unwrap_or_default(); let title=params.get("title").cloned().unwrap_or_else(||"Web Reference".to_string()); let w=params.get("w").and_then(|s|s.parse::<u32>().ok()).unwrap_or(300); let h=params.get("h").and_then(|s|s.parse::<u32>().ok()).unwrap_or(200); let action=action.trim_end_matches('/').to_string(); let app2=app.clone();
|
| 82 |
+
tauri::async_runtime::spawn(async move{match crate::library::library_add_item(app2.clone(),url.clone(),Some(source.clone()),Some(title.clone())).await{Ok(item)=>{if action=="board"{let _=crate::board::board_add_image(app2.clone(),Some(item.id.clone()),item.data_url.clone(),120.0,120.0,300.0,200.0);}let _=app2.emit("board://image_added",serde_json::json!({"libraryId":item.id,"dataUrl":item.data_url,"url":item.url,"sourceUrl":item.source_url,"title":item.title,"width":item.width,"height":item.height,"colors":item.colors,"captureId":capture_id}));}Err(_)=>{let _=app2.emit("board://image_added",serde_json::json!({"url":url,"dataUrl":url,"sourceUrl":source,"title":title,"width":w,"height":h,"colors":[],"captureId":capture_id}));}}});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
+
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()}
|
| 86 |
+
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();}})();"#;
|
|
|