Comparar commits

..

3 Commits

Autor SHA1 Mensaje Fecha
Jonathan David Florez Salomón
d8a35f5bae Atrás/adelante en la barra + reabrir pestaña + globo de no-leídos en el icono
- ATRÁS/ADELANTE por fin: botones ← → en la barra de pestañas, ⌘[ / ⌘] en
  ambas plataformas, Alt+←/→ en Windows (fuera de campos de texto) y menú
  «Historial» en macOS. Cierra el callejón sin salida: una página sin menú
  propio (p. ej. el portal de revisión) dejaba al usuario atrapado sin forma
  de volver.
- «Reabrir pestaña cerrada» (⌘/Ctrl+Shift+T, tope de 20): en la barra ⌘⇧T
  antes caía en «nueva pestaña» por no mirar el Shift.
- Globo de NO-LEÍDOS en el icono: la web app emite `ls-badge {count}` (mismo
  conteo del «(N)» del título) y el shell pinta el número en el Dock (macOS),
  un punto rojo dibujado a mano sobre el icono de la barra de tareas (Windows,
  sin assets) y el tooltip de la bandeja («— N sin leer»).

Verificado: cargo check limpio (macOS) y firmas de las APIs solo-Windows
(set_overlay_icon, set_badge_count, Image::new_owned) cotejadas contra el
código fuente de tauri 2.11.3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 07:11:37 -05:00
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
Se han modificado 8 ficheros con 322 adiciones y 30 borrados

Ver fichero

@@ -25,6 +25,11 @@ NAS. Esto le da al equipo un cliente nativo de verdad en Windows y Mac:
en español (Edición/Vista/Archivo) con los atajos. 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. - **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 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-*`. pestañas son webviews hijos `tab-*`. Shell ↔ Rust se hablan por eventos `ls-*`.

29
dist/index.html vendido
Ver fichero

@@ -146,10 +146,22 @@
font-size: 11.5px; font-size: 11.5px;
} }
#z-pct.activo { color: var(--accent); font-weight: 600; } #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"> <div id="bar">
<button class="icono" id="atras" title="Atrás (⌘/Ctrl+[)">←</button>
<button class="icono" id="adelante" title="Adelante (⌘/Ctrl+])">→</button>
<div id="tabs" title=""></div> <div id="tabs" title=""></div>
<button class="icono" id="nueva" title="Nueva pestaña (⌘/Ctrl+T)">+</button> <button class="icono" id="nueva" title="Nueva pestaña (⌘/Ctrl+T)">+</button>
<button class="icono" id="recargar" title="Recargar (⌘/Ctrl+R)">⟳</button> <button class="icono" id="recargar" title="Recargar (⌘/Ctrl+R)">⟳</button>
@@ -158,6 +170,8 @@
<button id="z-pct" title="Restablecer zoom (⌘/Ctrl+0)">100%</button> <button id="z-pct" title="Restablecer zoom (⌘/Ctrl+0)">100%</button>
<button id="z-mas" title="Acercar (⌘/Ctrl++)">+</button> <button id="z-mas" title="Acercar (⌘/Ctrl++)">+</button>
</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> </div>
<script> <script>
const T = window.__TAURI__; const T = window.__TAURI__;
@@ -170,6 +184,7 @@
const $tabs = document.getElementById("tabs"); const $tabs = document.getElementById("tabs");
const $pct = document.getElementById("z-pct"); const $pct = document.getElementById("z-pct");
const $ver = document.getElementById("version");
function render() { function render() {
$tabs.textContent = ""; $tabs.textContent = "";
@@ -205,6 +220,7 @@
$pct.textContent = estado.zoom + "%"; $pct.textContent = estado.zoom + "%";
$pct.classList.toggle("activo", estado.zoom !== 100); $pct.classList.toggle("activo", estado.zoom !== 100);
$ver.textContent = estado.version ? "v" + estado.version : "";
} }
if (E) { if (E) {
@@ -214,8 +230,17 @@
}); });
} }
document.getElementById("atras").addEventListener("click", () => emitir("ls-nav", { dir: -1 }));
document.getElementById("adelante").addEventListener("click", () => emitir("ls-nav", { dir: 1 }));
document.getElementById("nueva").addEventListener("click", () => emitir("ls-new-tab", {})); document.getElementById("nueva").addEventListener("click", () => emitir("ls-new-tab", {}));
document.getElementById("recargar").addEventListener("click", () => emitir("ls-reload", {})); 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-menos").addEventListener("click", () => emitir("ls-zoom", { dir: -1 }));
document.getElementById("z-mas").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 })); $pct.addEventListener("click", () => emitir("ls-zoom", { dir: 0 }));
@@ -229,9 +254,11 @@
const mod = e.metaKey || e.ctrlKey; const mod = e.metaKey || e.ctrlKey;
if (!mod) return; if (!mod) return;
const k = e.key; const k = e.key;
if (k === "t" || k === "T") { e.preventDefault(); emitir("ls-new-tab", {}); } if (k === "t" || k === "T") { e.preventDefault(); emitir(e.shiftKey ? "ls-reopen" : "ls-new-tab", {}); }
else if (k === "w" || k === "W") { e.preventDefault(); emitir("ls-close-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 === "r" || k === "R") && !e.shiftKey) { e.preventDefault(); emitir("ls-reload", {}); }
else if (k === "[") { e.preventDefault(); emitir("ls-nav", { dir: -1 }); }
else if (k === "]") { e.preventDefault(); emitir("ls-nav", { dir: 1 }); }
else if (k === "=" || k === "+") { e.preventDefault(); emitir("ls-zoom", { dir: 1 }); } else if (k === "=" || k === "+") { e.preventDefault(); emitir("ls-zoom", { dir: 1 }); }
else if (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 === "0") { e.preventDefault(); emitir("ls-zoom", { dir: 0 }); }

4
package-lock.json generado
Ver fichero

@@ -1,12 +1,12 @@
{ {
"name": "labstream-desktop", "name": "labstream-desktop",
"version": "1.0.0", "version": "1.4.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "labstream-desktop", "name": "labstream-desktop",
"version": "1.0.0", "version": "1.4.0",
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2" "@tauri-apps/cli": "^2"
} }

Ver fichero

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

@@ -1946,7 +1946,7 @@ dependencies = [
[[package]] [[package]]
name = "labstream-desktop" name = "labstream-desktop"
version = "1.1.0" version = "1.4.0"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",

Ver fichero

@@ -1,6 +1,6 @@
[package] [package]
name = "labstream-desktop" name = "labstream-desktop"
version = "1.1.0" version = "1.4.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

@@ -17,6 +17,11 @@
// - plugin `updater`: busca versión nueva en los Releases de GitHub y se actualiza sola. // - plugin `updater`: busca versión nueva en los Releases de GitHub y se actualiza sola.
// - bandeja (tray) + "cerrar = ocultar": sigue corriendo y notificando en segundo plano. // - bandeja (tray) + "cerrar = ocultar": sigue corriendo y notificando en segundo plano.
// Y lo nuevo: // Y lo nuevo:
// - ATRÁS/ADELANTE: botones ← → en la barra, ⌘[ / ⌘] (Alt+←/→ en Windows) y menú
// Historial (macOS). Antes una página sin menú propio era un callejón sin salida.
// - «Reabrir pestaña cerrada» (⌘/Ctrl+Shift+T), como en el navegador.
// - Globo de NO-LEÍDOS en el icono (evento `ls-badge` desde la web app): número en el
// Dock (macOS), punto rojo en la barra de tareas (Windows) y tooltip de la bandeja.
// - ZOOM de interfaz (Cmd/Ctrl +/−/0 y Ctrl+rueda) con persistencia entre sesiones. // - ZOOM de interfaz (Cmd/Ctrl +/−/0 y Ctrl+rueda) con persistencia entre sesiones.
// - Sesión restaurada: al reabrir vuelve con las mismas pestañas y tamaño de ventana. // - Sesión restaurada: al reabrir vuelve con las mismas pestañas y tamaño de ventana.
// - single-instance (solo release): abrir la app de nuevo enfoca la ventana existente. // - single-instance (solo release): abrir la app de nuevo enfoca la ventana existente.
@@ -32,7 +37,7 @@ use std::sync::Mutex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use tauri::{ use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem, Submenu}, menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
webview::{PageLoadEvent, Webview, WebviewBuilder}, webview::{PageLoadEvent, Webview, WebviewBuilder},
window::{Window, WindowBuilder}, window::{Window, WindowBuilder},
@@ -41,6 +46,7 @@ use tauri::{
}; };
use tauri_plugin_autostart::{MacosLauncher, ManagerExt}; use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
use tauri_plugin_notification::NotificationExt; use tauri_plugin_notification::NotificationExt;
use tauri_plugin_opener::OpenerExt;
const SERVER_URL: &str = "https://os.labstreamsas.com"; const SERVER_URL: &str = "https://os.labstreamsas.com";
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)
@@ -64,11 +70,13 @@ struct Shell {
zoom_idx: usize, zoom_idx: usize,
next_id: u64, next_id: u64,
win: Option<WinRect>, win: Option<WinRect>,
autostart: Option<bool>, // elección del usuario en el menú ⋮ (None = por defecto, activado)
closed: Vec<String>, // URLs de pestañas cerradas (para «Reabrir pestaña cerrada», ⌘⇧T)
} }
impl Default for Shell { impl Default for Shell {
fn default() -> Self { fn default() -> Self {
Self { tabs: Vec::new(), active: 0, zoom_idx: ZOOM_100, next_id: 1, win: None } Self { tabs: Vec::new(), active: 0, zoom_idx: ZOOM_100, next_id: 1, win: None, autostart: None, closed: Vec::new() }
} }
} }
@@ -93,6 +101,8 @@ struct Persisted {
tabs: Vec<String>, tabs: Vec<String>,
#[serde(default)] #[serde(default)]
active: usize, active: usize,
#[serde(default)]
autostart: Option<bool>,
} }
fn state_path<R: Runtime>(app: &AppHandle<R>) -> Option<std::path::PathBuf> { fn state_path<R: Runtime>(app: &AppHandle<R>) -> Option<std::path::PathBuf> {
@@ -116,6 +126,7 @@ fn save_persisted<R: Runtime>(app: &AppHandle<R>) {
win: s.win, win: s.win,
tabs: s.tabs.iter().map(|t| t.url.clone()).collect(), tabs: s.tabs.iter().map(|t| t.url.clone()).collect(),
active: s.active, active: s.active,
autostart: s.autostart,
} }
}; };
if let Some(dir) = path.parent() { if let Some(dir) = path.parent() {
@@ -211,9 +222,14 @@ const INIT_JS_TPL: &str = r#"
var mod = e.metaKey || e.ctrlKey; var mod = e.metaKey || e.ctrlKey;
if (!mod) return; if (!mod) return;
var k = e.key; var k = e.key;
if (k === 't' || k === 'T') { e.preventDefault(); newTab(null); } if (k === 't' || k === 'T') {
e.preventDefault();
if (e.shiftKey) emit('ls-reopen', {}); else newTab(null);
}
else if (k === 'w' || k === 'W') { e.preventDefault(); emit('ls-close-tab', { label: LABEL }); } else if (k === 'w' || k === 'W') { e.preventDefault(); emit('ls-close-tab', { label: LABEL }); }
else if ((k === 'r' || k === 'R') && !e.shiftKey) { e.preventDefault(); location.reload(); } else if ((k === 'r' || k === 'R') && !e.shiftKey) { e.preventDefault(); location.reload(); }
else if (k === '[') { e.preventDefault(); history.back(); }
else if (k === ']') { e.preventDefault(); history.forward(); }
else if (k === '=' || k === '+') { e.preventDefault(); emit('ls-zoom', { dir: 1 }); } else if (k === '=' || k === '+') { e.preventDefault(); emit('ls-zoom', { dir: 1 }); }
else if (k === '-') { e.preventDefault(); emit('ls-zoom', { dir: -1 }); } else if (k === '-') { e.preventDefault(); emit('ls-zoom', { dir: -1 }); }
else if (k === '0') { e.preventDefault(); emit('ls-zoom', { dir: 0 }); } else if (k === '0') { e.preventDefault(); emit('ls-zoom', { dir: 0 }); }
@@ -221,6 +237,18 @@ const INIT_JS_TPL: &str = r#"
else if (k >= '1' && k <= '9') { e.preventDefault(); emit('ls-activate-n', { n: +k }); } else if (k >= '1' && k <= '9') { e.preventDefault(); emit('ls-activate-n', { n: +k }); }
}, true); }, true);
// Alt+←/→ = atrás/adelante (convención de Windows/Linux; en Mac es ⌘[ / ⌘] y Alt+flecha
// se respeta para saltar palabras al escribir). Fuera de campos de texto solamente.
var MAC = /Mac/i.test(navigator.platform || '');
function editable(el) {
return !!el && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName || ''));
}
addEventListener('keydown', function (e) {
if (MAC || !e.altKey || e.metaKey || e.ctrlKey || e.shiftKey || editable(e.target)) return;
if (e.key === 'ArrowLeft') { e.preventDefault(); history.back(); }
else if (e.key === 'ArrowRight') { e.preventDefault(); history.forward(); }
}, true);
// Ctrl+rueda = zoom (convención de navegador; en Mac también con Ctrl). // Ctrl+rueda = zoom (convención de navegador; en Mac también con Ctrl).
addEventListener('wheel', function (e) { addEventListener('wheel', function (e) {
if (!e.ctrlKey) return; if (!e.ctrlKey) return;
@@ -264,7 +292,7 @@ fn current_zoom<R: Runtime>(app: &AppHandle<R>) -> f64 {
ZOOM_STEPS[s.zoom_idx.min(ZOOM_STEPS.len() - 1)] ZOOM_STEPS[s.zoom_idx.min(ZOOM_STEPS.len() - 1)]
} }
// Manda el estado de pestañas + zoom a la barra (webview "chrome"). // Manda el estado de pestañas + zoom + versión a la barra (webview "chrome").
fn broadcast<R: Runtime>(app: &AppHandle<R>) { fn broadcast<R: Runtime>(app: &AppHandle<R>) {
let payload = { let payload = {
let shell = app.state::<ShellState>(); let shell = app.state::<ShellState>();
@@ -275,7 +303,11 @@ fn broadcast<R: Runtime>(app: &AppHandle<R>) {
.enumerate() .enumerate()
.map(|(i, t)| json!({ "label": t.label, "title": t.title, "active": i == s.active })) .map(|(i, t)| json!({ "label": t.label, "title": t.title, "active": i == s.active }))
.collect(); .collect();
json!({ "tabs": tabs, "zoom": (ZOOM_STEPS[s.zoom_idx.min(ZOOM_STEPS.len()-1)] * 100.0).round() as u32 }) json!({
"tabs": tabs,
"zoom": (ZOOM_STEPS[s.zoom_idx.min(ZOOM_STEPS.len()-1)] * 100.0).round() as u32,
"version": app.package_info().version.to_string(),
})
}; };
let _ = app.emit_to(EventTarget::webview(CHROME), "ls-tabs", payload); let _ = app.emit_to(EventTarget::webview(CHROME), "ls-tabs", payload);
} }
@@ -374,6 +406,12 @@ fn close_tab<R: Runtime>(app: &AppHandle<R>, label: &str) {
let mut s = shell.lock().unwrap(); let mut s = shell.lock().unwrap();
let Some(i) = s.tabs.iter().position(|t| t.label == label) else { return }; let Some(i) = s.tabs.iter().position(|t| t.label == label) else { return };
let was_active = i == s.active; let was_active = i == s.active;
// Recuerda la URL para «Reabrir pestaña cerrada» (tope de 20, las más recientes).
let url = s.tabs[i].url.clone();
s.closed.push(url);
if s.closed.len() > 20 {
s.closed.remove(0);
}
s.tabs.remove(i); s.tabs.remove(i);
if s.tabs.is_empty() { if s.tabs.is_empty() {
s.active = 0; s.active = 0;
@@ -474,6 +512,160 @@ fn activate_nth<R: Runtime>(app: &AppHandle<R>, n: usize) {
activate_tab(app, &label); activate_tab(app, &label);
} }
fn active_url<R: Runtime>(app: &AppHandle<R>) -> Option<String> {
let shell = app.state::<ShellState>();
let s = shell.lock().unwrap();
s.tabs.get(s.active).map(|t| t.url.clone())
}
// Atrás/adelante en el HISTORIAL de la pestaña activa. Antes no existía por ningún lado y
// una página sin menú propio (p. ej. el portal de revisión) era un callejón sin salida.
fn nav_tab<R: Runtime>(app: &AppHandle<R>, dir: i32) {
if let Some(l) = active_label(app) {
if let Some(wv) = get_webview(app, &l) {
let _ = wv.eval(if dir < 0 { "history.back()" } else { "history.forward()" });
}
}
}
// «Reabrir pestaña cerrada» (⌘/Ctrl+Shift+T): la más reciente primero, como en el navegador.
fn reopen_tab<R: Runtime>(app: &AppHandle<R>) {
let url = {
let shell = app.state::<ShellState>();
let mut s = shell.lock().unwrap();
s.closed.pop()
};
if let Some(u) = url {
create_tab(app, Some(u), true);
}
}
// Punto rojo para el icono de la barra de tareas de Windows (dibujado a mano: sin assets).
#[cfg(target_os = "windows")]
fn red_dot() -> tauri::image::Image<'static> {
const S: usize = 16;
let mut px = vec![0u8; S * S * 4];
let c = (S as f64 - 1.0) / 2.0;
for y in 0..S {
for x in 0..S {
let (dx, dy) = (x as f64 - c, y as f64 - c);
if (dx * dx + dy * dy).sqrt() <= c {
let i = (y * S + x) * 4;
px[i] = 225;
px[i + 1] = 29;
px[i + 2] = 72;
px[i + 3] = 255;
}
}
}
tauri::image::Image::new_owned(px, S as u32, S as u32)
}
// Globo de NO-LEÍDOS en el icono, alimentado por la web app (evento `ls-badge {count}`):
// macOS = número en el icono del Dock; Windows = punto rojo sobre el icono de la barra de
// tareas; y en ambos, el tooltip de la bandeja dice cuántos hay.
fn set_badge<R: Runtime>(app: &AppHandle<R>, count: i64) {
if let Some(win) = main_window(app) {
#[cfg(target_os = "macos")]
let _ = win.set_badge_count(if count > 0 { Some(count) } else { None });
#[cfg(target_os = "windows")]
let _ = win.set_overlay_icon(if count > 0 { Some(red_dot()) } else { None });
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let _ = &win;
}
if let Some(tray) = app.tray_by_id("main-tray") {
let tip = if count > 0 { format!("Labstream OS — {count} sin leer") } else { "Labstream OS".to_string() };
let _ = tray.set_tooltip(Some(tip));
}
}
// Menú de OPCIONES de la app (botón ⋮ / clic derecho en la barra): un popup NATIVO del sistema,
// igual en Windows y macOS. Se construye fresco en cada apertura para que el check de
// "Iniciar con el sistema" y la versión estén siempre al día. Solo opciones del SHELL — la web
// app no se toca.
fn show_app_menu<R: Runtime>(app: &AppHandle<R>) {
let Some(win) = main_window(app) else { return };
let autostart_on = app.autolaunch().is_enabled().unwrap_or(false);
let version = format!("Versión {}", app.package_info().version);
let build = || -> tauri::Result<Menu<R>> {
Ok(Menu::with_items(
app,
&[
&MenuItem::with_id(app, "nav-back", "Atrás", true, Some("CmdOrCtrl+["))?,
&MenuItem::with_id(app, "nav-forward", "Adelante", true, Some("CmdOrCtrl+]"))?,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "new-tab", "Nueva pestaña", true, Some("CmdOrCtrl+T"))?,
&MenuItem::with_id(app, "close-tab", "Cerrar pestaña", true, Some("CmdOrCtrl+W"))?,
&MenuItem::with_id(app, "reopen-tab", "Reabrir pestaña cerrada", true, Some("CmdOrCtrl+Shift+T"))?,
&MenuItem::with_id(app, "reload", "Recargar", true, Some("CmdOrCtrl+R"))?,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "zoom-in", "Acercar", true, Some("CmdOrCtrl+="))?,
&MenuItem::with_id(app, "zoom-out", "Alejar", true, Some("CmdOrCtrl+-"))?,
&MenuItem::with_id(app, "zoom-0", "Tamaño real", true, Some("CmdOrCtrl+0"))?,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "open-browser", "Abrir esta página en el navegador", true, None::<&str>)?,
&CheckMenuItem::with_id(app, "autostart-toggle", "Iniciar con el sistema", true, autostart_on, None::<&str>)?,
&MenuItem::with_id(app, "check-update", "Buscar actualización…", true, None::<&str>)?,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "version", &version, false, None::<&str>)?,
&MenuItem::with_id(app, "quit-app", "Salir de Labstream OS", true, None::<&str>)?,
],
)?)
};
if let Ok(menu) = build() {
let _ = win.popup_menu(&menu);
}
}
// Activa/desactiva el arranque con el sistema y RECUERDA la elección (si no, el arranque
// la re-activaba en cada apertura).
fn toggle_autostart<R: Runtime>(app: &AppHandle<R>) {
let mgr = app.autolaunch();
let was_on = mgr.is_enabled().unwrap_or(false);
let _ = if was_on { mgr.disable() } else { mgr.enable() };
{
let shell = app.state::<ShellState>();
shell.lock().unwrap().autostart = Some(!was_on);
}
save_persisted(app);
let _ = app
.notification()
.builder()
.title("Labstream OS")
.body(if was_on {
"Ya no se iniciará con el sistema."
} else {
"Se iniciará automáticamente con el sistema."
})
.show();
}
// "Buscar actualización…" del menú: como la silenciosa, pero SIEMPRE responde con un aviso
// (instalando / ya estás al día / sin red).
#[cfg(desktop)]
async fn check_update_manual(handle: tauri::AppHandle) {
use tauri_plugin_updater::UpdaterExt;
let notify = |body: String| {
let _ = handle.notification().builder().title("Labstream OS").body(body).show();
};
let Ok(updater) = handle.updater() else {
notify("El actualizador no está disponible en esta instalación.".into());
return;
};
match updater.check().await {
Ok(Some(update)) => {
notify(format!("Instalando la versión {}…", update.version));
if update.download_and_install(|_, _| {}, || {}).await.is_ok() {
handle.restart();
} else {
notify("No se pudo instalar la actualización. Intenta de nuevo más tarde.".into());
}
}
Ok(None) => notify(format!("Estás en la última versión ({}).", handle.package_info().version)),
Err(_) => notify("No se pudo comprobar la actualización (revisa la red).".into()),
}
}
// Muestra y enfoca la ventana principal (tray, dock, segunda instancia). // Muestra y enfoca la ventana principal (tray, dock, segunda instancia).
fn show_main<R: Runtime>(app: &AppHandle<R>) { fn show_main<R: Runtime>(app: &AppHandle<R>) {
if let Some(w) = main_window(app) { if let Some(w) = main_window(app) {
@@ -538,8 +730,19 @@ pub fn run() {
let app = builder let app = builder
.manage(ShellState::default()) .manage(ShellState::default())
.setup(|app| { .setup(|app| {
// Arranque automático al iniciar sesión (idempotente). let handle = app.handle().clone();
let saved = load_persisted(&handle);
// Arranque automático al iniciar sesión: por defecto activado, pero se respeta
// lo que el usuario elija en el menú ⋮ ("Iniciar con el sistema").
match saved.autostart {
Some(false) => {
let _ = app.autolaunch().disable();
}
_ => {
let _ = app.autolaunch().enable(); let _ = app.autolaunch().enable();
}
}
// Al abrir, busca una actualización en segundo plano (no bloquea el arranque). // Al abrir, busca una actualización en segundo plano (no bloquea el arranque).
#[cfg(desktop)] #[cfg(desktop)]
@@ -548,13 +751,12 @@ pub fn run() {
tauri::async_runtime::spawn(check_update(handle)); tauri::async_runtime::spawn(check_update(handle));
} }
let handle = app.handle().clone();
let saved = load_persisted(&handle);
{ {
let shell = handle.state::<ShellState>(); let shell = handle.state::<ShellState>();
let mut s = shell.lock().unwrap(); let mut s = shell.lock().unwrap();
s.zoom_idx = saved.zoom_idx.unwrap_or(ZOOM_100).min(ZOOM_STEPS.len() - 1); s.zoom_idx = saved.zoom_idx.unwrap_or(ZOOM_100).min(ZOOM_STEPS.len() - 1);
s.win = saved.win; s.win = saved.win;
s.autostart = saved.autostart;
} }
// Ventana principal (contenedora). Tamaño/posición de la sesión anterior si los hay. // Ventana principal (contenedora). Tamaño/posición de la sesión anterior si los hay.
@@ -582,6 +784,8 @@ pub fn run() {
"Labstream OS", "Labstream OS",
true, true,
&[ &[
&MenuItem::with_id(h, "check-update", "Buscar actualización…", true, None::<&str>)?,
&PredefinedMenuItem::separator(h)?,
&PredefinedMenuItem::hide(h, Some("Ocultar Labstream OS"))?, &PredefinedMenuItem::hide(h, Some("Ocultar Labstream OS"))?,
&PredefinedMenuItem::hide_others(h, Some("Ocultar otros"))?, &PredefinedMenuItem::hide_others(h, Some("Ocultar otros"))?,
&PredefinedMenuItem::show_all(h, Some("Mostrar todo"))?, &PredefinedMenuItem::show_all(h, Some("Mostrar todo"))?,
@@ -596,6 +800,9 @@ pub fn run() {
&[ &[
&MenuItem::with_id(h, "new-tab", "Nueva pestaña", true, Some("CmdOrCtrl+T"))?, &MenuItem::with_id(h, "new-tab", "Nueva pestaña", true, Some("CmdOrCtrl+T"))?,
&MenuItem::with_id(h, "close-tab", "Cerrar pestaña", true, Some("CmdOrCtrl+W"))?, &MenuItem::with_id(h, "close-tab", "Cerrar pestaña", true, Some("CmdOrCtrl+W"))?,
&MenuItem::with_id(h, "reopen-tab", "Reabrir pestaña cerrada", true, Some("CmdOrCtrl+Shift+T"))?,
&PredefinedMenuItem::separator(h)?,
&MenuItem::with_id(h, "open-browser", "Abrir esta página en el navegador", true, None::<&str>)?,
], ],
)?; )?;
let edicion = Submenu::with_items( let edicion = Submenu::with_items(
@@ -626,6 +833,15 @@ pub fn run() {
&PredefinedMenuItem::fullscreen(h, Some("Pantalla completa"))?, &PredefinedMenuItem::fullscreen(h, Some("Pantalla completa"))?,
], ],
)?; )?;
let historial = Submenu::with_items(
h,
"Historial",
true,
&[
&MenuItem::with_id(h, "nav-back", "Atrás", true, Some("CmdOrCtrl+["))?,
&MenuItem::with_id(h, "nav-forward", "Adelante", true, Some("CmdOrCtrl+]"))?,
],
)?;
let ventana = Submenu::with_items( let ventana = Submenu::with_items(
h, h,
"Ventana", "Ventana",
@@ -635,22 +851,41 @@ pub fn run() {
&PredefinedMenuItem::maximize(h, Some("Zoom"))?, &PredefinedMenuItem::maximize(h, Some("Zoom"))?,
], ],
)?; )?;
let menu = Menu::with_items(h, &[&app_menu, &archivo, &edicion, &vista, &ventana])?; let menu = Menu::with_items(h, &[&app_menu, &archivo, &edicion, &vista, &historial, &ventana])?;
app.set_menu(menu)?; app.set_menu(menu)?;
}
// Manejador de TODOS los menús (el de macOS y el popup ⋮ de ambas plataformas).
app.on_menu_event(|app, event| match event.id().as_ref() { app.on_menu_event(|app, event| match event.id().as_ref() {
"nav-back" => nav_tab(app, -1),
"nav-forward" => nav_tab(app, 1),
"new-tab" => create_tab(app, None, true), "new-tab" => create_tab(app, None, true),
"close-tab" => { "close-tab" => {
if let Some(l) = active_label(app) { if let Some(l) = active_label(app) {
close_tab(app, &l); close_tab(app, &l);
} }
} }
"reopen-tab" => reopen_tab(app),
"reload" => reload_tab(app, None), "reload" => reload_tab(app, None),
"zoom-in" => zoom_step(app, 1), "zoom-in" => zoom_step(app, 1),
"zoom-out" => zoom_step(app, -1), "zoom-out" => zoom_step(app, -1),
"zoom-0" => zoom_step(app, 0), "zoom-0" => zoom_step(app, 0),
"open-browser" => {
if let Some(url) = active_url(app) {
let _ = app.opener().open_url(url, None::<&str>);
}
}
"autostart-toggle" => toggle_autostart(app),
"check-update" => {
#[cfg(desktop)]
tauri::async_runtime::spawn(check_update_manual(app.clone()));
}
"quit-app" => {
save_persisted(app);
app.exit(0);
}
_ => {} _ => {}
}); });
}
// Barra de pestañas (webview local, arriba). // Barra de pestañas (webview local, arriba).
let scale = win.scale_factor().unwrap_or(1.0); let scale = win.scale_factor().unwrap_or(1.0);
@@ -734,6 +969,31 @@ pub fn run() {
} }
}); });
let h = handle.clone(); let h = handle.clone();
handle.listen_any("ls-nav", move |e| {
let dir = field_i64(e.payload(), "dir").unwrap_or(-1) as i32;
nav_tab(&h, dir);
});
let h = handle.clone();
handle.listen_any("ls-reopen", move |_| {
reopen_tab(&h);
});
// Globo de no-leídos: lo emite la web app cuando cambia el conteo del chat.
let h = handle.clone();
handle.listen_any("ls-badge", move |e| {
let n = field_i64(e.payload(), "count").unwrap_or(0);
set_badge(&h, n);
});
let h = handle.clone();
handle.listen_any("ls-menu", move |_| {
show_app_menu(&h);
});
// Clic en la etiqueta de versión de la barra → buscar actualización (con aviso).
let h = handle.clone();
handle.listen_any("ls-check-update", move |_| {
#[cfg(desktop)]
tauri::async_runtime::spawn(check_update_manual(h.clone()));
});
let h = handle.clone();
handle.listen_any("ls-title", move |e| { handle.listen_any("ls-title", move |e| {
let (label, title, url) = ( let (label, title, url) = (
field(e.payload(), "label"), field(e.payload(), "label"),

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