Comparar commits

..

3 Commits

Autor SHA1 Mensaje Fecha
Jonathan David Florez Salomón
d682b06ab9 Versión visible en la barra de la app (clic = buscar actualización)
La barra muestra la versión real del paquete (viene de Rust en el payload
ls-tabs, no quemada en el HTML) junto al menú ⋮. Clic en la etiqueta →
"Buscar actualización…" con aviso del resultado (evento ls-check-update).
Versión 1.3.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 20:02:30 -05:00
Jonathan David Florez Salomón
329ea1195e Menú de opciones ⋮ de la app (popup nativo) + preferencia de autostart
Botón ⋮ en la barra de pestañas (y clic derecho sobre ella) → menú NATIVO del
sistema, idéntico en Windows y macOS, solo del shell (la web app no se toca):

- Nueva/cerrar pestaña, recargar, acercar/alejar/tamaño real.
- "Abrir esta página en el navegador" (la pestaña activa, vía opener).
- "Iniciar con el sistema" con check: ahora la elección SE RECUERDA
  (antes el arranque re-activaba el autostart en cada apertura).
- "Buscar actualización…" manual con aviso del resultado (instalando /
  ya estás al día / sin red), además del chequeo silencioso de siempre.
- Versión visible y "Salir de Labstream OS".

El manejador de menús pasa a ser global (antes solo existía dentro del cfg de
macOS): lo comparten el menú de macOS y el popup ⋮ de ambas plataformas. El
menú de macOS gana "Buscar actualización…" y "Abrir en el navegador".

Versión 1.2.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 19:50:46 -05:00
Jonathan David Florez Salomón
2d9f14f026 Shell de pestañas: multiwebview, zoom persistente y documentos dentro de la app
La app deja de ser un envoltorio de una sola vista y pasa a ser un shell con
pestañas (multiwebview de Tauri 2, ventana "main" = barra local "chrome" +
webviews "tab-*" del servidor):

- Los enlaces internos target=_blank (documentos, entregables, OnlyOffice,
  exportaciones) abrían nada: ahora abren pestaña dentro de la app. También
  Cmd/Ctrl+clic, clic central y window.open del propio origen.
- Zoom de interfaz: Cmd/Ctrl +/−/0 y Ctrl+rueda (50%–250%), con indicador en
  la barra y persistencia entre sesiones.
- Sesión restaurada: pestañas abiertas, activa, zoom y tamaño/posición de la
  ventana vuelven al reabrir (estado.json en app_config_dir).
- Atajos: Cmd/Ctrl+T/W/R, Ctrl+Tab rota, Cmd/Ctrl+1..9; en macOS van por el
  menú nativo nuevo en español (Archivo/Edición/Vista/Ventana); en Windows
  por JS (sin barra de menú).
- macOS: clic en el Dock reabre la ventana oculta (RunEvent::Reopen).
- Una sola instancia en release (single-instance): reabrir enfoca la existente.
- Updater: avisa con notificación nativa antes de reiniciar por actualización.
- Los enlaces a OTRO origen siguen yendo al navegador del sistema y el login
  por Authentik sigue intacto (el init script se desactiva fuera del origen).

Shell ↔ Rust por eventos ls-* (core:default cubre el origen remoto; sin ACL
de comandos propios). Versión 1.1.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 15:50:04 -05:00
Se han modificado 8 ficheros con 1184 adiciones y 103 borrados

Ver fichero

@@ -1,21 +1,38 @@
# Labstream OS — App de escritorio (Windows + macOS) # Labstream OS — App de escritorio (Windows + macOS)
Envoltorio de escritorio para **Labstream OS**. Es un cliente delgado hecho con Cliente de escritorio para **Labstream OS**, hecho con [Tauri 2](https://tauri.app).
[Tauri 2](https://tauri.app): abre una ventana propia (sin barra de navegador) Desde la v1.1.0 no es un envoltorio de una sola vista: es un **shell con pestañas**
que carga el servidor de Labstream OS que corre en el NAS (multiwebview) sobre el servidor del NAS (`https://os.labstreamsas.com`).
(`https://os.labstreamsas.com`).
No contiene la app: el backend, la base de datos, OnlyOffice, etc. siguen en el No contiene la app: el backend, la base de datos, OnlyOffice, etc. siguen en el
NAS. Esto solo le da al equipo un ícono y una ventana propia en Windows y Mac. NAS. Esto le da al equipo un cliente nativo de verdad en Windows y Mac:
Además del ícono y la ventana, el envoltorio añade comportamiento de cliente nativo: - **Pestañas** como en un navegador: los enlaces internos con `target="_blank"`
(documentos, entregables, exportaciones, OnlyOffice…) abren **pestaña dentro de
- **Enlaces externos** (Drive, documentos ajenos…) abren en el **navegador del sistema**; la app** — antes morían en silencio. También ⌘/Ctrl+clic y clic central.
la navegación del propio servidor y el **login por Authentik** se quedan dentro. Atajos: ⌘/Ctrl+T nueva, ⌘/Ctrl+W cerrar, Ctrl+Tab rotar, ⌘/Ctrl+1…9 ir a la n.
La sesión (pestañas abiertas, activa) **se restaura** al reabrir.
- **Zoom de interfaz**: ⌘/Ctrl `+` / `−` / `0` y Ctrl+rueda, con indicador en la
barra y **persistencia** entre sesiones (50%–250%).
- **Documentos dentro de la app**: los visores/editores del propio servidor
(OnlyOffice, PDFs, reproductor de revisión) abren en pestañas; solo lo de OTRO
origen (Drive…) sale al **navegador del sistema**. El **login por Authentik**
sigue funcionando dentro.
- **Notas de voz / cámara**: pide permiso de micrófono y cámara (entitlements de macOS). - **Notas de voz / cámara**: pide permiso de micrófono y cámara (entitlements de macOS).
- **Descargas** (entregables, exportaciones) con el diálogo nativo de guardado. - **Descargas** (entregables, exportaciones) con el diálogo nativo de guardado.
- **Bandeja del sistema**: cerrar la ventana la oculta; la app sigue notificando. - **Bandeja del sistema**: cerrar la ventana la oculta; la app sigue notificando.
En macOS, el clic en el **Dock** también reabre la ventana, y hay **menú nativo**
en español (Edición/Vista/Archivo) con los atajos.
- **Arranque automático** al iniciar sesión y **sesión persistente** (no re-loguea). - **Arranque automático** al iniciar sesión y **sesión persistente** (no re-loguea).
- **Una sola instancia** (release): abrirla de nuevo enfoca la ventana existente.
- **Menú de opciones ⋮** (v1.2.0) en la barra — también con clic derecho sobre ella:
pestañas, zoom, recargar, abrir la página en el navegador, **Iniciar con el
sistema** (elección recordada) y **Buscar actualización…** con aviso del
resultado. Es un popup nativo del sistema, igual en Windows y macOS, y vive
solo en el shell (la web app no se toca).
La barra de pestañas vive en `dist/index.html` (webview local «chrome»); las
pestañas son webviews hijos `tab-*`. Shell ↔ Rust se hablan por eventos `ls-*`.
El servidor objetivo se configura en `src-tauri/src/lib.rs` → constante `SERVER_URL`. El servidor objetivo se configura en `src-tauri/src/lib.rs` → constante `SERVER_URL`.

268
dist/index.html vendido
Ver fichero

@@ -4,41 +4,263 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Labstream OS</title> <title>Labstream OS</title>
<!--
Barra de pestañas del shell de escritorio (webview "chrome", local).
Habla con Rust por eventos (__TAURI__.event):
recibe ls-tabs {tabs:[{label,title,active}], zoom}
emite ls-ready, ls-new-tab, ls-activate, ls-close-tab, ls-reload, ls-zoom, ls-cycle, ls-activate-n
-->
<style> <style>
:root {
--bg: #f4f4f5; /* zinc-100 */
--fg: #3f3f46; /* zinc-700 */
--muted: #a1a1aa; /* zinc-400 */
--tab-bg: transparent;
--tab-active-bg: #ffffff;
--tab-active-fg: #18181b;
--accent: #6366f1; /* indigo-500, color de Labstream */
--border: #e4e4e7; /* zinc-200 */
--hover: rgba(0, 0, 0, 0.05);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #18181b;
--fg: #d4d4d8;
--muted: #71717a;
--tab-active-bg: #27272a;
--tab-active-fg: #fafafa;
--border: #27272a;
--hover: rgba(255, 255, 255, 0.07);
}
}
* { box-sizing: border-box; }
html, body { html, body {
margin: 0; margin: 0;
height: 100%; height: 100%;
background: #6366f1; overflow: hidden;
color: #fff; background: var(--bg);
color: var(--fg);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif; font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
font-size: 12.5px;
user-select: none;
-webkit-user-select: none;
cursor: default;
}
#bar {
display: flex;
align-items: stretch;
gap: 4px;
height: 40px;
padding: 5px 8px 0;
border-bottom: 1px solid var(--border);
}
#tabs {
display: flex;
align-items: stretch;
gap: 2px;
flex: 1 1 auto;
min-width: 0;
overflow-x: auto;
scrollbar-width: none;
}
#tabs::-webkit-scrollbar { display: none; }
.tab {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 6px;
text-align: center; flex: 0 1 200px;
min-width: 96px;
padding: 0 6px 0 12px;
border-radius: 8px 8px 0 0;
background: var(--tab-bg);
position: relative;
overflow: hidden;
} }
.box { max-width: 420px; padding: 24px; } .tab:hover { background: var(--hover); }
h1 { font-size: 20px; font-weight: 600; margin: 0 0 8px; } .tab.active {
p { opacity: 0.85; font-size: 14px; line-height: 1.5; margin: 0; } background: var(--tab-active-bg);
.spinner { color: var(--tab-active-fg);
width: 28px; height: 28px; margin: 0 auto 20px; box-shadow: 0 -1px 0 var(--border), 1px 0 0 var(--border), -1px 0 0 var(--border);
border: 3px solid rgba(255,255,255,0.35);
border-top-color: #fff; border-radius: 50%;
animation: spin 0.8s linear infinite;
} }
@keyframes spin { to { transform: rotate(360deg); } } .tab.active::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: var(--accent);
border-radius: 2px 2px 0 0;
}
.tab .titulo {
flex: 1 1 auto;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab .cerrar {
flex: 0 0 auto;
width: 18px;
height: 18px;
display: grid;
place-items: center;
border-radius: 5px;
color: var(--muted);
font-size: 13px;
line-height: 1;
visibility: hidden;
}
.tab:hover .cerrar, .tab.active .cerrar { visibility: visible; }
.tab .cerrar:hover { background: var(--hover); color: var(--tab-active-fg); }
button {
appearance: none;
border: 0;
background: transparent;
color: var(--fg);
font: inherit;
border-radius: 7px;
cursor: default;
display: grid;
place-items: center;
}
button:hover { background: var(--hover); }
.icono {
flex: 0 0 auto;
width: 30px;
margin: 2px 0 7px;
font-size: 15px;
}
#zoom {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 1px;
margin: 2px 0 7px;
}
#zoom button { height: 100%; }
#z-menos, #z-mas { width: 26px; font-size: 14px; }
#z-pct {
min-width: 44px;
padding: 0 4px;
font-variant-numeric: tabular-nums;
color: var(--muted);
font-size: 11.5px;
}
#z-pct.activo { color: var(--accent); font-weight: 600; }
#version {
flex: 0 0 auto;
margin: 2px 0 7px;
padding: 0 7px;
color: var(--muted);
font-size: 11px;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
#version:hover { color: var(--accent); }
</style> </style>
</head> </head>
<body> <body>
<!-- <div id="bar">
Pantalla de respaldo. En condiciones normales la ventana carga directamente <div id="tabs" title=""></div>
el servidor del NAS (app.windows[].url en tauri.conf.json), así que este <button class="icono" id="nueva" title="Nueva pestaña (⌘/Ctrl+T)">+</button>
archivo casi nunca se ve. Solo aparece si se cambia la config para arrancar <button class="icono" id="recargar" title="Recargar (⌘/Ctrl+R)">⟳</button>
en local. Es obligatorio porque Tauri exige un frontendDist existente. <div id="zoom">
--> <button id="z-menos" title="Alejar (⌘/Ctrl+−)">−</button>
<div class="box"> <button id="z-pct" title="Restablecer zoom (⌘/Ctrl+0)">100%</button>
<div class="spinner"></div> <button id="z-mas" title="Acercar (⌘/Ctrl++)">+</button>
<h1>Labstream OS</h1>
<p>Conectando con el espacio de trabajo…</p>
</div> </div>
<button id="version" title="Versión de la app · clic: buscar actualización"></button>
<button class="icono" id="menu" title="Opciones de la app">⋮</button>
</div>
<script>
const T = window.__TAURI__;
const E = T && T.event;
let estado = { tabs: [], zoom: 100 };
function emitir(nombre, datos) {
try { E.emit(nombre, datos || {}); } catch (e) {}
}
const $tabs = document.getElementById("tabs");
const $pct = document.getElementById("z-pct");
const $ver = document.getElementById("version");
function render() {
$tabs.textContent = "";
for (const t of estado.tabs) {
const el = document.createElement("div");
el.className = "tab" + (t.active ? " active" : "");
el.title = t.title;
const titulo = document.createElement("span");
titulo.className = "titulo";
titulo.textContent = t.title || "Labstream OS";
el.appendChild(titulo);
const x = document.createElement("button");
x.className = "cerrar";
x.textContent = "×";
x.title = "Cerrar pestaña (⌘/Ctrl+W)";
x.addEventListener("click", (e) => {
e.stopPropagation();
emitir("ls-close-tab", { label: t.label });
});
el.appendChild(x);
el.addEventListener("click", () => emitir("ls-activate", { label: t.label }));
// Clic central sobre la pestaña = cerrarla (como en el navegador).
el.addEventListener("auxclick", (e) => {
if (e.button === 1) emitir("ls-close-tab", { label: t.label });
});
$tabs.appendChild(el);
}
const activa = $tabs.querySelector(".tab.active");
if (activa) activa.scrollIntoView({ block: "nearest", inline: "nearest" });
$pct.textContent = estado.zoom + "%";
$pct.classList.toggle("activo", estado.zoom !== 100);
$ver.textContent = estado.version ? "v" + estado.version : "";
}
if (E) {
E.listen("ls-tabs", (ev) => {
estado = ev.payload || estado;
render();
});
}
document.getElementById("nueva").addEventListener("click", () => emitir("ls-new-tab", {}));
document.getElementById("recargar").addEventListener("click", () => emitir("ls-reload", {}));
// Menú de OPCIONES de la app (nativo, lo pinta Rust): botón ⋮ o clic derecho en la barra.
document.getElementById("menu").addEventListener("click", () => emitir("ls-menu", {}));
$ver.addEventListener("click", () => emitir("ls-check-update", {}));
document.getElementById("bar").addEventListener("contextmenu", (e) => {
e.preventDefault();
emitir("ls-menu", {});
});
document.getElementById("z-menos").addEventListener("click", () => emitir("ls-zoom", { dir: -1 }));
document.getElementById("z-mas").addEventListener("click", () => emitir("ls-zoom", { dir: 1 }));
$pct.addEventListener("click", () => emitir("ls-zoom", { dir: 0 }));
// Doble clic en el hueco libre de la barra = pestaña nueva.
$tabs.addEventListener("dblclick", (e) => {
if (e.target === $tabs) emitir("ls-new-tab", {});
});
// Atajos también cuando el foco está en la barra.
addEventListener("keydown", (e) => {
const mod = e.metaKey || e.ctrlKey;
if (!mod) return;
const k = e.key;
if (k === "t" || k === "T") { e.preventDefault(); emitir("ls-new-tab", {}); }
else if (k === "w" || k === "W") { e.preventDefault(); emitir("ls-close-tab", {}); }
else if ((k === "r" || k === "R") && !e.shiftKey) { e.preventDefault(); emitir("ls-reload", {}); }
else if (k === "=" || k === "+") { e.preventDefault(); emitir("ls-zoom", { dir: 1 }); }
else if (k === "-") { e.preventDefault(); emitir("ls-zoom", { dir: -1 }); }
else if (k === "0") { e.preventDefault(); emitir("ls-zoom", { dir: 0 }); }
else if (k === "Tab") { e.preventDefault(); emitir("ls-cycle", { dir: e.shiftKey ? -1 : 1 }); }
else if (k >= "1" && k <= "9") { e.preventDefault(); emitir("ls-activate-n", { n: +k }); }
});
emitir("ls-ready", {});
</script>
</body> </body>
</html> </html>

Ver fichero

@@ -1,6 +1,6 @@
{ {
"name": "labstream-desktop", "name": "labstream-desktop",
"version": "1.0.0", "version": "1.3.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": {

19
src-tauri/Cargo.lock generado
Ver fichero

@@ -1946,7 +1946,7 @@ dependencies = [
[[package]] [[package]]
name = "labstream-desktop" name = "labstream-desktop"
version = "1.0.0" version = "1.3.0"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@@ -1955,6 +1955,7 @@ dependencies = [
"tauri-plugin-autostart", "tauri-plugin-autostart",
"tauri-plugin-notification", "tauri-plugin-notification",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-single-instance",
"tauri-plugin-updater", "tauri-plugin-updater",
] ]
@@ -3841,6 +3842,22 @@ dependencies = [
"zbus", "zbus",
] ]
[[package]]
name = "tauri-plugin-single-instance"
version = "2.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3214becf9ef5783c0ae99a3bb25adf5353a7a16ebf53e74b909e29205735c6c"
dependencies = [
"serde",
"serde_json",
"tauri",
"thiserror 2.0.18",
"tokio",
"tracing",
"windows-sys 0.60.2",
"zbus",
]
[[package]] [[package]]
name = "tauri-plugin-updater" name = "tauri-plugin-updater"
version = "2.10.1" version = "2.10.1"

Ver fichero

@@ -1,6 +1,6 @@
[package] [package]
name = "labstream-desktop" name = "labstream-desktop"
version = "1.0.0" version = "1.3.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"
@@ -14,10 +14,12 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = ["tray-icon"] } # `unstable` habilita multiwebview: varios webviews (pestañas) dentro de UNA ventana.
tauri = { version = "2", features = ["tray-icon", "unstable"] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
tauri-plugin-notification = "2" tauri-plugin-notification = "2"
tauri-plugin-autostart = "2" tauri-plugin-autostart = "2"
tauri-plugin-updater = "2" tauri-plugin-updater = "2"
tauri-plugin-single-instance = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"

Ver fichero

@@ -1,8 +1,9 @@
{ {
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Permisos por defecto del envoltorio de escritorio.", "description": "Permisos del shell de escritorio: la barra local (chrome) y las pestañas remotas (tab-*).",
"windows": ["main"], "windows": ["main"],
"webviews": ["chrome", "tab-*"],
"remote": { "remote": {
"urls": ["https://os.labstreamsas.com"] "urls": ["https://os.labstreamsas.com"]
}, },

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

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.0.0", "version": "1.3.0",
"identifier": "co.labstream.os", "identifier": "co.labstream.os",
"build": { "build": {
"frontendDist": "../dist" "frontendDist": "../dist"