Comparar commits

...

2 Commits

Autor SHA1 Mensaje Fecha
Munka
d287b4e647 v1.13.0: los documentos del contador se abren dentro de la app
Bump de version. La novedad esta en el commit anterior: hosts de Google (docs, drive,
sheets, accounts, myaccount) se abren como pestanas propias del shell en vez de mandarse
al navegador del sistema, para que la sesion de Google viva dentro de la app y las hojas
del contador se vean en la pestana Contable sin pedir cookies cada vez.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-14 11:46:02 -05:00
Munka
d847aca9a7 Los documentos del contador (Google) se abren como pestanas de esta app
La app de escritorio es un navegador aparte sin la sesion de Google del usuario. Antes
un enlace de docs.google.com se abria en el navegador del sistema y Google pedia
Permitir cookies dentro de un marco que nadie podia contestar. Ahora hosts de Google
(docs / drive / sheets / accounts / myaccount) se abren como PESTANAS internas del shell,
y el WebView2 guarda la sesion de Google como cualquier otra cookie: se inicia sesion
UNA vez y las hojas del contador se ven y editan en la pestana Contable.

- create_tab acepta URLs con scheme https y host en GOOGLE_HOSTS ademas del origen propio.
- La restauracion de sesion respeta el mismo criterio (una pestana de Google guardada
  reabre como pestana de Google).
- El script inyectado detecta docs.google.com/... y hace newTab en vez de openExternal;
  en paginas de Google solo reporta el titulo (sin tocar clics, atajos ni window.open)
  para no interferir con el editor de Google.

Falta probar el instalador de prueba antes de etiquetar la version.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-14 11:45:40 -05:00
Se han modificado 5 ficheros con 49 adiciones y 10 borrados

Ver fichero

@@ -1,6 +1,6 @@
{ {
"name": "labstream-desktop", "name": "labstream-desktop",
"version": "1.12.0", "version": "1.13.0",
"private": true, "private": true,
"description": "App de escritorio (Windows) que envuelve Labstream OS — cliente del servidor del NAS.", "description": "App de escritorio (Windows) que envuelve Labstream OS — cliente del servidor del NAS.",
"scripts": { "scripts": {

Ver fichero

@@ -2366,7 +2366,7 @@ dependencies = [
[[package]] [[package]]
name = "labstream-desktop" name = "labstream-desktop"
version = "1.12.0" version = "1.13.0"
dependencies = [ dependencies = [
"active-win-pos-rs", "active-win-pos-rs",
"device_query", "device_query",

Ver fichero

@@ -1,6 +1,6 @@
[package] [package]
name = "labstream-desktop" name = "labstream-desktop"
version = "1.12.0" version = "1.13.0"
description = "Cliente de escritorio de Labstream OS" description = "Cliente de escritorio de Labstream OS"
authors = ["Labstream SAS"] authors = ["Labstream SAS"]
edition = "2021" edition = "2021"

Ver fichero

@@ -53,6 +53,34 @@ 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",
];
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 +233,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 +249,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 +378,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 +401,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 +414,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 +583,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;
} }
@@ -1400,7 +1439,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()

Ver fichero

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Labstream OS", "productName": "Labstream OS",
"version": "1.12.0", "version": "1.13.0",
"identifier": "co.labstream.os", "identifier": "co.labstream.os",
"build": { "build": {
"frontendDist": "../dist" "frontendDist": "../dist"