|
|
|
@@ -53,6 +53,44 @@ use tauri_plugin_clipboard_manager::ClipboardExt;
|
|
|
|
use tauri_plugin_opener::OpenerExt;
|
|
|
|
use tauri_plugin_opener::OpenerExt;
|
|
|
|
|
|
|
|
|
|
|
|
const SERVER_URL: &str = "https://os.labstreamsas.com";
|
|
|
|
const SERVER_URL: &str = "https://os.labstreamsas.com";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Los documentos del contador viven en Google (pestaña Contable). Se abren como PESTANAS
|
|
|
|
|
|
|
|
// de esta app, no en el navegador externo: asi el WebView guarda la sesion de Google del
|
|
|
|
|
|
|
|
// usuario (cookies persistentes) y no hay que iniciar sesion cada vez. La lista es cerrada
|
|
|
|
|
|
|
|
// a proposito: solo hosts propios de Google (docs/drive/sheets y accounts para el login).
|
|
|
|
|
|
|
|
// Todo lo demas sigue yendo al navegador del sistema.
|
|
|
|
|
|
|
|
const GOOGLE_HOSTS: &[&str] = &[
|
|
|
|
|
|
|
|
"docs.google.com",
|
|
|
|
|
|
|
|
"drive.google.com",
|
|
|
|
|
|
|
|
"sheets.google.com",
|
|
|
|
|
|
|
|
"accounts.google.com",
|
|
|
|
|
|
|
|
"myaccount.google.com",
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Cadena de navegador de escritorio. Sin esto, WebView2 se anuncia como control embebido y
|
|
|
|
|
|
|
|
// Google Sheets/Docs se quedan en blanco. Se declara por plataforma porque Google tambien
|
|
|
|
|
|
|
|
// mira la coherencia entre el sistema declarado y el resto de la cadena.
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
|
|
|
const UA_ESCRITORIO: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
|
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
|
|
|
const UA_ESCRITORIO: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
|
|
|
|
|
|
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
|
|
|
|
|
|
|
const UA_ESCRITORIO: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn is_google_host(host: &str) -> bool {
|
|
|
|
|
|
|
|
let h = host.to_ascii_lowercase();
|
|
|
|
|
|
|
|
GOOGLE_HOSTS.iter().any(|d| h == *d || h.ends_with(&format!(".{}", d)))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn es_pestana_interna(target: &Url, server: &Url) -> bool {
|
|
|
|
|
|
|
|
if target.origin() == server.origin() {
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if target.scheme() != "https" {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
target.host_str().map(is_google_host).unwrap_or(false)
|
|
|
|
|
|
|
|
}
|
|
|
|
const TAB_BAR_H: f64 = 40.0; // alto lógico de la barra de pestañas (chrome)
|
|
|
|
const TAB_BAR_H: f64 = 40.0; // alto lógico de la barra de pestañas (chrome)
|
|
|
|
const CHROME: &str = "chrome";
|
|
|
|
const CHROME: &str = "chrome";
|
|
|
|
// Hueco que ocupan los botones rojo/amarillo/verde en macOS: la barra deja libre ese
|
|
|
|
// Hueco que ocupan los botones rojo/amarillo/verde en macOS: la barra deja libre ese
|
|
|
|
@@ -205,7 +243,12 @@ const INIT_JS_TPL: &str = r#"
|
|
|
|
// descargar fallaban solo en la app de Windows, y no en Mac ni en el navegador.
|
|
|
|
// descargar fallaban solo en la app de Windows, y no en Mac ni en el navegador.
|
|
|
|
// En macOS estas dos líneas son un no-op: allí el script nunca ha corrido en subframes.
|
|
|
|
// En macOS estas dos líneas son un no-op: allí el script nunca ha corrido en subframes.
|
|
|
|
try { if (window.top !== window.self) return; } catch (e) { return; }
|
|
|
|
try { if (window.top !== window.self) return; } catch (e) { return; }
|
|
|
|
if (location.origin !== ORIGIN) return; // p. ej. el login del SSO: no intervenir
|
|
|
|
// Hosts de Google que la app abre como pestana propia (pestana Contable). En esas
|
|
|
|
|
|
|
|
// paginas solo reportamos titulo a la barra; NO tocamos clics ni window.open para no
|
|
|
|
|
|
|
|
// romper el editor de Google.
|
|
|
|
|
|
|
|
var GOOGLE_RE = /^(?:.*\.)?(?:docs|drive|sheets|accounts|myaccount)\.google\.com$/i;
|
|
|
|
|
|
|
|
var esGoogle = GOOGLE_RE.test(location.hostname);
|
|
|
|
|
|
|
|
if (location.origin !== ORIGIN && !esGoogle) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (window.__lsShell) return;
|
|
|
|
if (window.__lsShell) return;
|
|
|
|
window.__lsShell = true;
|
|
|
|
window.__lsShell = true;
|
|
|
|
@@ -216,7 +259,9 @@ const INIT_JS_TPL: &str = r#"
|
|
|
|
function abs(h) { try { return new URL(h, location.href); } catch (e) { return null; } }
|
|
|
|
function abs(h) { try { return new URL(h, location.href); } catch (e) { return null; } }
|
|
|
|
function isHttp(u) { return !!u && (u.protocol === 'http:' || u.protocol === 'https:'); }
|
|
|
|
function isHttp(u) { return !!u && (u.protocol === 'http:' || u.protocol === 'https:'); }
|
|
|
|
function isApp(u) { return isHttp(u) && u.origin === ORIGIN; }
|
|
|
|
function isApp(u) { return isHttp(u) && u.origin === ORIGIN; }
|
|
|
|
function isExt(u) { return isHttp(u) && u.origin !== ORIGIN; }
|
|
|
|
function isGoogleUrl(u) { return isHttp(u) && GOOGLE_RE.test(u.hostname); }
|
|
|
|
|
|
|
|
function isInterno(u) { return isApp(u) || isGoogleUrl(u); }
|
|
|
|
|
|
|
|
function isExt(u) { return isHttp(u) && !isInterno(u); }
|
|
|
|
function emit(name, payload) { try { T.event.emit(name, payload); } catch (e) {} }
|
|
|
|
function emit(name, payload) { try { T.event.emit(name, payload); } catch (e) {} }
|
|
|
|
function openExternal(url) {
|
|
|
|
function openExternal(url) {
|
|
|
|
try { T.core.invoke('plugin:opener|open_url', { url: url, with: null }); }
|
|
|
|
try { T.core.invoke('plugin:opener|open_url', { url: url, with: null }); }
|
|
|
|
@@ -343,6 +388,7 @@ const INIT_JS_TPL: &str = r#"
|
|
|
|
history[k] = function () { var r = o.apply(this, arguments); setTimeout(report, 0); return r; };
|
|
|
|
history[k] = function () { var r = o.apply(this, arguments); setTimeout(report, 0); return r; };
|
|
|
|
});
|
|
|
|
});
|
|
|
|
addEventListener('popstate', function () { setTimeout(report, 0); });
|
|
|
|
addEventListener('popstate', function () { setTimeout(report, 0); });
|
|
|
|
|
|
|
|
if (esGoogle) return; // en Google solo miramos el titulo
|
|
|
|
// Pulsar la luna cambia clases/estilos sin navegar: se vigilan <html> y <body> (cuando
|
|
|
|
// Pulsar la luna cambia clases/estilos sin navegar: se vigilan <html> y <body> (cuando
|
|
|
|
// existan — a document-start aún no hay), y tras la carga se re-reporta un par de veces
|
|
|
|
// existan — a document-start aún no hay), y tras la carga se re-reporta un par de veces
|
|
|
|
// por si el tema aterriza tarde con la hidratación.
|
|
|
|
// por si el tema aterriza tarde con la hidratación.
|
|
|
|
@@ -365,6 +411,9 @@ const INIT_JS_TPL: &str = r#"
|
|
|
|
if (!u) return;
|
|
|
|
if (!u) return;
|
|
|
|
if (isExt(u)) { e.preventDefault(); openExternal(u.href); return; }
|
|
|
|
if (isExt(u)) { e.preventDefault(); openExternal(u.href); return; }
|
|
|
|
var mod = e.metaKey || e.ctrlKey;
|
|
|
|
var mod = e.metaKey || e.ctrlKey;
|
|
|
|
|
|
|
|
// Enlace a Google (docs, drive, sheets, accounts): SIEMPRE como pestana propia
|
|
|
|
|
|
|
|
// aunque no lleve target=_blank, para que la sesion viva dentro de la app.
|
|
|
|
|
|
|
|
if (isGoogleUrl(u)) { e.preventDefault(); newTab(u.href); return; }
|
|
|
|
if (isApp(u) && (mod || a.target === '_blank')) { e.preventDefault(); newTab(u.href); }
|
|
|
|
if (isApp(u) && (mod || a.target === '_blank')) { e.preventDefault(); newTab(u.href); }
|
|
|
|
}, true);
|
|
|
|
}, true);
|
|
|
|
|
|
|
|
|
|
|
|
@@ -375,14 +424,14 @@ const INIT_JS_TPL: &str = r#"
|
|
|
|
if (!a) return;
|
|
|
|
if (!a) return;
|
|
|
|
var u = abs(a.getAttribute('href'));
|
|
|
|
var u = abs(a.getAttribute('href'));
|
|
|
|
if (isExt(u)) { e.preventDefault(); openExternal(u.href); return; }
|
|
|
|
if (isExt(u)) { e.preventDefault(); openExternal(u.href); return; }
|
|
|
|
if (isApp(u)) { e.preventDefault(); newTab(u.href); }
|
|
|
|
if (isInterno(u)) { e.preventDefault(); newTab(u.href); }
|
|
|
|
}, true);
|
|
|
|
}, true);
|
|
|
|
|
|
|
|
|
|
|
|
var _open = window.open;
|
|
|
|
var _open = window.open;
|
|
|
|
window.open = function (url, name, feats) {
|
|
|
|
window.open = function (url, name, feats) {
|
|
|
|
var u = url ? abs(url) : null;
|
|
|
|
var u = url ? abs(url) : null;
|
|
|
|
if (isExt(u)) { openExternal(u.href); return null; }
|
|
|
|
if (isExt(u)) { openExternal(u.href); return null; }
|
|
|
|
if (isApp(u)) { newTab(u.href); return null; }
|
|
|
|
if (isInterno(u)) { newTab(u.href); return null; }
|
|
|
|
return _open ? _open.call(window, url, name, feats) : null;
|
|
|
|
return _open ? _open.call(window, url, name, feats) : null;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
@@ -544,7 +593,7 @@ fn create_tab<R: Runtime>(app: &AppHandle<R>, url: Option<String>, activate: boo
|
|
|
|
// Solo el origen de la app puede abrirse en pestaña; cualquier otra cosa va al navegador.
|
|
|
|
// Solo el origen de la app puede abrirse en pestaña; cualquier otra cosa va al navegador.
|
|
|
|
let Ok(parsed) = Url::parse(&target) else { return };
|
|
|
|
let Ok(parsed) = Url::parse(&target) else { return };
|
|
|
|
let Ok(server) = Url::parse(SERVER_URL) else { return };
|
|
|
|
let Ok(server) = Url::parse(SERVER_URL) else { return };
|
|
|
|
if parsed.origin() != server.origin() {
|
|
|
|
if !es_pestana_interna(&parsed, &server) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -564,6 +613,7 @@ fn create_tab<R: Runtime>(app: &AppHandle<R>, url: Option<String>, activate: boo
|
|
|
|
let init = INIT_JS_TPL.replace("__LABEL__", &label).replace("__ORIGIN__", SERVER_URL);
|
|
|
|
let init = INIT_JS_TPL.replace("__LABEL__", &label).replace("__ORIGIN__", SERVER_URL);
|
|
|
|
let app_zoom = app.clone();
|
|
|
|
let app_zoom = app.clone();
|
|
|
|
let builder = WebviewBuilder::new(&label, WebviewUrl::External(parsed))
|
|
|
|
let builder = WebviewBuilder::new(&label, WebviewUrl::External(parsed))
|
|
|
|
|
|
|
|
.user_agent(UA_ESCRITORIO)
|
|
|
|
.initialization_script(&init)
|
|
|
|
.initialization_script(&init)
|
|
|
|
// Deja pasar las descargas (entregables, exportaciones) Y avisa al terminar.
|
|
|
|
// Deja pasar las descargas (entregables, exportaciones) Y avisa al terminar.
|
|
|
|
// El aviso no es un adorno: en WebView2 la interfaz de descargas queda SIEMPRE
|
|
|
|
// El aviso no es un adorno: en WebView2 la interfaz de descargas queda SIEMPRE
|
|
|
|
@@ -1400,7 +1450,7 @@ pub fn run() {
|
|
|
|
.tabs
|
|
|
|
.tabs
|
|
|
|
.iter()
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
|
|
|
.enumerate()
|
|
|
|
.filter(|(_, u)| Url::parse(u).map(|p| p.origin() == server.origin()).unwrap_or(false))
|
|
|
|
.filter(|(_, u)| Url::parse(u).map(|p| es_pestana_interna(&p, &server)).unwrap_or(false))
|
|
|
|
.take(12)
|
|
|
|
.take(12)
|
|
|
|
.map(|(i, u)| (u.clone(), saved.colors.get(i).cloned().unwrap_or_default()))
|
|
|
|
.map(|(i, u)| (u.clone(), saved.colors.get(i).cloned().unwrap_or_default()))
|
|
|
|
.collect()
|
|
|
|
.collect()
|
|
|
|
|