Comparar commits

...

2 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
Se han modificado 7 ficheros con 189 adiciones y 25 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-*`.

21
dist/index.html vendido
Ver fichero

@@ -146,6 +146,16 @@
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>
@@ -158,6 +168,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 +182,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 +218,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) {
@@ -216,6 +230,13 @@
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 }));

Ver fichero

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

Ver fichero

@@ -1946,7 +1946,7 @@ dependencies = [
[[package]] [[package]]
name = "labstream-desktop" name = "labstream-desktop"
version = "1.1.0" version = "1.3.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.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"

Ver fichero

@@ -32,7 +32,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 +41,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 +65,12 @@ 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)
} }
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 }
} }
} }
@@ -93,6 +95,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 +120,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() {
@@ -264,7 +269,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 +280,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);
} }
@@ -474,6 +483,95 @@ 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())
}
// 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, "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, "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 +636,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 +657,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 +690,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 +706,8 @@ 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"))?,
&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(
@@ -637,6 +749,9 @@ pub fn run() {
)?; )?;
let menu = Menu::with_items(h, &[&app_menu, &archivo, &edicion, &vista, &ventana])?; let menu = Menu::with_items(h, &[&app_menu, &archivo, &edicion, &vista, &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() {
"new-tab" => create_tab(app, None, true), "new-tab" => create_tab(app, None, true),
"close-tab" => { "close-tab" => {
@@ -648,9 +763,22 @@ pub fn run() {
"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 +862,16 @@ pub fn run() {
} }
}); });
let h = handle.clone(); 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.3.0",
"identifier": "co.labstream.os", "identifier": "co.labstream.os",
"build": { "build": {
"frontendDist": "../dist" "frontendDist": "../dist"