Cliente nativo: enlaces externos al navegador, micrófono para notas de voz, descargas

La ventana se crea ahora en Rust (lib.rs) para poder:
- Inyectar un script que manda los enlaces EXTERNOS (Drive, docs ajenos, target=_blank,
  window.open) al navegador del sistema, dejando dentro la navegación del propio
  servidor y el login por Authentik (que redirige a otro dominio y vuelve).
- Permitir descargas (entregables/exportaciones) con el diálogo nativo de guardado.
- SERVER_URL centralizado (antes en tauri.conf.json app.windows[0].url).

macOS: Info.plist con NSMicrophoneUsageDescription/NSCameraUsageDescription y
Entitlements.plist (audio-input, cámara, red, allow-jit) → las notas de voz y los
adjuntos de foto/video piden permiso y funcionan. Referenciados en bundle.macOS.

Se conserva el tray, cerrar=ocultar y el autostart. Verificado: build de macOS OK
(.app + .dmg de 3,8 MB, icono del logo, textos de permiso fusionados en Info.plist).
Los instaladores de Windows (.exe) y macOS (.dmg universal) los sigue produciendo
GitHub Actions (.github/workflows/build.yml) en cada tag de versión.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Este commit está contenido en:
Jonathan David Florez Salomón
2026-07-02 17:19:27 -05:00
padre abd18591c1
commit 8e48d888e2
Se han modificado 8 ficheros con 5437 adiciones y 21 borrados

5087
src-tauri/Cargo.lock generado Archivo normal

La diferencia del archivo ha sido suprimido porque es demasiado grande Cargar Diff

18
src-tauri/Entitlements.plist Archivo normal
Ver fichero

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Micrófono: notas de voz del chat (getUserMedia en el webview). -->
<key>com.apple.security.device.audio-input</key>
<true/>
<!-- Cámara: adjuntar foto/video en el chat. -->
<key>com.apple.security.device.camera</key>
<true/>
<!-- Cliente de red (conecta con os.labstreamsas.com). -->
<key>com.apple.security.network.client</key>
<true/>
<!-- Requerido por WKWebView (JIT de JavaScript) bajo hardened runtime. -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>

11
src-tauri/Info.plist Archivo normal
Ver fichero

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Textos que macOS muestra al pedir permiso. Tauri fusiona este Info.plist en el bundle. -->
<key>NSMicrophoneUsageDescription</key>
<string>Labstream usa el micrófono para grabar notas de voz en el chat.</string>
<key>NSCameraUsageDescription</key>
<string>Labstream usa la cámara para adjuntar fotos y video en el chat.</string>
</dict>
</plist>

Ver fichero

@@ -6,14 +6,50 @@
// - plugin `autostart`: la app arranca al iniciar sesión.
// - bandeja (tray) + "cerrar = ocultar": la app sigue corriendo y notificando
// aunque cierres la ventana.
// - la ventana se crea AQUÍ (no en tauri.conf.json) para poder inyectar un script
// que manda los enlaces externos al navegador y para permitir descargas.
use tauri::{
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
AppHandle, Manager, Runtime, WindowEvent,
AppHandle, Manager, Runtime, WebviewUrl, WebviewWindowBuilder, WindowEvent,
};
use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
const SERVER_URL: &str = "https://os.labstreamsas.com";
// Script que corre en CADA carga de página del servidor. Reenvía al NAVEGADOR del sistema los
// enlaces a OTRO origen (Drive, documentos externos, etc.): clics en `<a>` externos, `target=_blank`
// externos y `window.open(url)` externos. NO toca la navegación del propio origen, así el login por
// Authentik —que redirige a otro dominio y vuelve— sigue funcionando. Usa el comando del plugin
// opener, permitido por la capability `opener:default` para este origen remoto.
const INIT_JS: &str = r#"
(function () {
if (window.__lsExternalLinks) return;
window.__lsExternalLinks = true;
var APP_ORIGIN = location.origin;
function toAbs(href) { try { return new URL(href, location.href); } catch (e) { return null; } }
function isExternal(u) { return !!u && (u.protocol === 'http:' || u.protocol === 'https:') && u.origin !== APP_ORIGIN; }
function openExternal(url) {
try { window.__TAURI__.core.invoke('plugin:opener|open_url', { url: url, with: null }); }
catch (e) { try { window.__TAURI__.opener.openUrl(url); } catch (_) {} }
}
document.addEventListener('click', function (e) {
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
var a = e.target && e.target.closest ? e.target.closest('a[href]') : null;
if (!a) return;
var u = toAbs(a.getAttribute('href'));
if (isExternal(u)) { e.preventDefault(); openExternal(u.href); }
}, true);
var _open = window.open;
window.open = function (url, name, feats) {
var u = url ? toAbs(url) : null;
if (isExternal(u)) { openExternal(u.href); return null; }
return _open ? _open.call(window, url, name, feats) : null;
};
})();
"#;
// Muestra y enfoca la ventana principal (desde el tray o su menú).
fn show_main<R: Runtime>(app: &AppHandle<R>) {
if let Some(w) = app.get_webview_window("main") {
@@ -36,6 +72,23 @@ pub fn run() {
// Arranque automático al iniciar sesión (idempotente).
let _ = app.autolaunch().enable();
// Ventana principal: carga el servidor del NAS. Se crea aquí (no en el config) para
// poder inyectar el script de enlaces externos y permitir las descargas.
WebviewWindowBuilder::new(
app,
"main",
WebviewUrl::External(SERVER_URL.parse().expect("URL del servidor válida")),
)
.title("Labstream OS")
.inner_size(1400.0, 900.0)
.min_inner_size(1024.0, 700.0)
.resizable(true)
.center()
.initialization_script(INIT_JS)
// Deja pasar las descargas (entregables, exportaciones) con el diálogo nativo de guardado.
.on_download(|_webview, _event| true)
.build()?;
// Icono en la bandeja del sistema con menú Abrir / Salir.
let abrir = MenuItem::with_id(app, "abrir", "Abrir Labstream", true, None::<&str>)?;
let salir = MenuItem::with_id(app, "salir", "Salir", true, None::<&str>)?;

Ver fichero

@@ -8,20 +8,7 @@
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"label": "main",
"title": "Labstream OS",
"url": "https://os.labstreamsas.com",
"width": 1400,
"height": 900,
"minWidth": 1024,
"minHeight": 700,
"resizable": true,
"center": true,
"decorations": true
}
],
"windows": [],
"security": {
"csp": null
}
@@ -32,7 +19,7 @@
"publisher": "Labstream SAS",
"category": "Productivity",
"shortDescription": "Espacio de trabajo de Labstream",
"longDescription": "Cliente de escritorio de Labstream OS para Windows. Abre el espacio de trabajo de Labstream en su propia ventana.",
"longDescription": "Cliente de escritorio de Labstream OS para Windows y macOS. Abre el espacio de trabajo de Labstream en su propia ventana.",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@@ -47,7 +34,8 @@
}
},
"macOS": {
"minimumSystemVersion": "10.15"
"minimumSystemVersion": "10.15",
"entitlements": "Entitlements.plist"
}
}
}