Comparar commits

...

3 Commits

Autor SHA1 Mensaje Fecha
Munka
3798970f74 v1.7.1: el detector de tema deja de creer y empieza a mirar
La v1.7.0 preguntaba por la clase `dark` de <html> y punto. En este
mismo PC quedó la prueba de que eso miente en WebView2: estado.json
con "dark": false y la app pintada de negro — barra clara sobre app
oscura, el bug al revés.

Ahora el reporte es a prueba de balas:

- temaOscuro(): clase `dark` en <html> o <body>; si no está, la
  LUMINANCIA del fondo realmente pintado del body (funciona sea cual
  sea el mecanismo de tema); si tampoco, el color-scheme computado.
  Con el documento a medio crear devuelve null y el campo NO viaja
  (Rust conserva el último valor bueno) — nada revienta en frío.
- Vigilancia segura: MutationObserver sobre <html> Y <body>
  (class/style/data-theme) registrado cuando existen, y dos
  re-reportes tras la carga por si el tema aterriza con la
  hidratación.

Rust y la barra quedan como en v1.7.0 (un campo `dark` ausente ya
significaba «conserva lo último»). El estado.json mentiroso se
corrige solo con el primer reporte bueno.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 11:45:01 -05:00
Munka
29f0b47371 Version 1.7.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 10:36:46 -05:00
Munka
b18c155b89 La barra obedece a la app, no a Windows: tema real + acento naranja
El bug de «siempre oscura»: la web decide su modo con su propio botón
(next-themes, defaultTheme claro, sin seguir al sistema), pero la barra
elegía tema con prefers-color-scheme — el del SISTEMA. Con Windows en
oscuro y la app en claro, franja negra sobre app blanca, siempre. La
v1.6.1 pintó bien ambos temas, pero la barra seguía eligiendo el
equivocado.

El arreglo, de punta a punta:

- El script inyectado reporta en ls-title si <html> lleva la clase
  `dark`, con un MutationObserver para que la luna se refleje al
  instante. La guarda de marco y origen queda intacta (OnlyOffice).
- Rust lo guarda (global: todas las pestañas comparten modo), lo
  reenvía en ls-tabs (`dark`) y lo PERSISTE en estado.json: la app
  abre ya del color correcto, sin fogonazo.
- La barra se pone la clase `oscuro`/`claro` al recibirlo; las media
  queries quedan solo como arranque en frío (primer arranque sin
  estado.json). Los tintes de colores de pestaña cuelgan del mismo
  interruptor.
- El acento pasa de índigo #6366f1 (comentado «color de Labstream»,
  que no lo era) al naranja de marca #F47A20: aro de carga, zoom
  activo y hover de la versión.

Verificado en navegador con el CSS y el JS reales, simulando el
payload de Rust: app clara + sistema oscuro → barra clara; app
oscura → negra; sin reporte → cae al sistema. El Rust lo compila el
CI de rama (este equipo no tiene cargo).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 10:22:46 -05:00
Se han modificado 6 ficheros con 106 adiciones y 13 borrados

39
dist/index.html vendido
Ver fichero

@@ -7,7 +7,8 @@
<!--
Barra de pestañas del shell de escritorio (webview "chrome", local).
Habla con Rust por eventos (__TAURI__.event):
recibe ls-tabs {tabs:[{label,title,url,loading,active}], zoom, version, mac}
recibe ls-tabs {tabs:[{label,title,url,loading,active,color}], zoom, version,
mac, win, maximized, dark}
emite ls-ready, ls-new-tab, ls-activate, ls-close-tab, ls-reload, ls-zoom, ls-cycle,
ls-activate-n, ls-nav, ls-menu, ls-check-update, ls-tab-menu, ls-reorder
En macOS la ventana lleva el título en OVERLAY: esta barra ES la fila del título, así que
@@ -29,15 +30,24 @@
--muted: #5f6368;
--tab-active-bg: #ffffff; /* mismo fondo que la app → pestaña fundida con el contenido */
--tab-active-fg: #202124;
--accent: #6366f1; /* indigo-500, color de Labstream */
--accent: #F47A20; /* NARANJA Labstream (el índigo de antes no era de la marca) */
--border: #dcdee1; /* línea bajo la barra (la tapa la pestaña activa) */
/* Sobre una franja casi blanca, un hover blanco no se vería: aquí oscurece. */
--hover: rgba(0, 0, 0, 0.06);
--btn-hover: rgba(0, 0, 0, 0.08);
--sep: rgba(0, 0, 0, 0.22); /* separador fino entre pestañas inactivas */
}
/* ── Quién decide el tema ──
La APP, no Windows: la web elige su modo con su propio botón (clase `dark` en su
<html>, defaultTheme claro, sin seguir al sistema), así que obedecer aquí al
prefers-color-scheme del sistema pintaba la barra negra sobre una app clara — el
bug de siempre. El script inyectado reporta el tema real, Rust lo reenvía en
`ls-tabs` (`dark`) y este documento se pone la clase `oscuro` o `claro`.
El bloque de medios queda SOLO como arranque en frío (primer arranque, sin
estado.json y sin reporte aún): en cuanto hay clase, la clase manda.
OJO: los valores oscuros van DOS veces (media query y .oscuro) — cambiarlos a la par. */
@media (prefers-color-scheme: dark) {
:root {
:root:not(.claro) {
--bg: #0d0d0f; /* franja: negra, más oscura que el contenido */
--fg: #bdc1c6;
--muted: #80868b;
@@ -49,6 +59,17 @@
--sep: rgba(255, 255, 255, 0.25);
}
}
:root.oscuro {
--bg: #0d0d0f;
--fg: #bdc1c6;
--muted: #80868b;
--tab-active-bg: #18181b;
--tab-active-fg: #e8eaed;
--border: #26262a;
--hover: rgba(255, 255, 255, 0.08);
--btn-hover: rgba(255, 255, 255, 0.1);
--sep: rgba(255, 255, 255, 0.25);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
@@ -168,9 +189,11 @@
.tab[data-color]:not(.active)::before { display: none; }
.tab[data-color].active { box-shadow: inset 0 3px 0 rgb(var(--tc)); }
@media (prefers-color-scheme: dark) {
.tab[data-color]:not(.active) { background: rgba(var(--tc), 0.36); }
.tab[data-color]:not(.active):hover { background: rgba(var(--tc), 0.5); }
:root:not(.claro) .tab[data-color]:not(.active) { background: rgba(var(--tc), 0.36); }
:root:not(.claro) .tab[data-color]:not(.active):hover { background: rgba(var(--tc), 0.5); }
}
:root.oscuro .tab[data-color]:not(.active) { background: rgba(var(--tc), 0.36); }
:root.oscuro .tab[data-color]:not(.active):hover { background: rgba(var(--tc), 0.5); }
/* Reordenar arrastrando: NO se usa drag&drop de HTML5 (en el webview de la barra en
macOS los eventos no llegan), sino eventos de puntero. Se apaga el arrastre nativo de
WebKit para que no salga la imagen fantasma al mover una pestaña. */
@@ -508,6 +531,12 @@
$pct.textContent = estado.zoom + "%";
$pct.classList.toggle("activo", estado.zoom !== 100);
$ver.textContent = estado.version ? "v" + estado.version : "";
// Tema: manda el de la APP (payload `dark`). Mientras Rust no sepa nada (null),
// no se pone clase y siguen las media queries del sistema como arranque en frío.
if (typeof estado.dark === "boolean") {
document.documentElement.classList.toggle("oscuro", estado.dark);
document.documentElement.classList.toggle("claro", !estado.dark);
}
// En macOS la barra ES la fila del título: hay que dejar libre el semáforo.
document.body.classList.toggle("mac", !!estado.mac);
// En Windows también ES la fila del título, pero ahí los botones de ventana los

Ver fichero

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

Ver fichero

@@ -2062,7 +2062,7 @@ dependencies = [
[[package]]
name = "labstream-desktop"
version = "1.6.1"
version = "1.7.1"
dependencies = [
"serde",
"serde_json",

Ver fichero

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

Ver fichero

@@ -97,11 +97,15 @@ struct Shell {
// Último estado maximizado conocido. Solo sirve para no repintar la barra en CADA
// evento de redimensión: únicamente cuando el icono ▢/❐ tiene que cambiar.
maximized: bool,
// Tema REAL de la app (la clase `dark` de <html>, reportada por el script inyectado).
// Es global y no por pestaña: todas las pestañas del origen comparten el mismo modo
// (localStorage). None = ninguna pestaña ha reportado aún → la barra cae al del sistema.
dark: Option<bool>,
}
impl Default for Shell {
fn default() -> Self {
Self { tabs: Vec::new(), active: 0, zoom_idx: ZOOM_100, next_id: 1, win: None, autostart: None, closed: Vec::new(), menu_tab: None, maximized: false }
Self { tabs: Vec::new(), active: 0, zoom_idx: ZOOM_100, next_id: 1, win: None, autostart: None, closed: Vec::new(), menu_tab: None, maximized: false, dark: None }
}
}
@@ -132,6 +136,10 @@ struct Persisted {
active: usize,
#[serde(default)]
autostart: Option<bool>,
// Último tema conocido de la app: la barra abre ya del color correcto en vez de
// arrancar con el del sistema y dar un fogonazo al cargar la primera pestaña.
#[serde(default)]
dark: Option<bool>,
}
fn state_path<R: Runtime>(app: &AppHandle<R>) -> Option<std::path::PathBuf> {
@@ -157,6 +165,7 @@ fn save_persisted<R: Runtime>(app: &AppHandle<R>) {
colors: s.tabs.iter().map(|t| t.color.clone().unwrap_or_default()).collect(),
active: s.active,
autostart: s.autostart,
dark: s.dark,
}
};
if let Some(dir) = path.parent() {
@@ -212,9 +221,39 @@ const INIT_JS_TPL: &str = r#"
}
function newTab(url) { emit('ls-new-tab', { url: url }); }
// Título y URL → barra de pestañas (y para restaurar la sesión al reabrir).
// ── ¿La página se VE oscura? ──
// Señales, de la más directa a la más física: la clase `dark` (next-themes) y, si no
// está, el color de fondo REALMENTE pintado (luminancia del body) — vale sea cual sea
// el mecanismo de tema. Devuelve null con el documento a medio crear (los scripts de
// inicio corren antes de que exista <html>): entonces el campo NO viaja y Rust conserva
// el último valor bueno. La v1.7.0 leía la clase a secas y sin red: en WebView2 eso
// podía reventar en frío y dejar `dark` mintiendo en estado.json (barra clara sobre
// app oscura — el bug al revés).
function temaOscuro() {
try {
var doc = document.documentElement, cuerpo = document.body;
if (!doc) return null;
if (doc.classList.contains('dark') || (cuerpo && cuerpo.classList.contains('dark'))) return true;
if (!cuerpo) return null;
var fondo = getComputedStyle(cuerpo).backgroundColor || '';
var m = fondo.match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:[,\s\/]+([\d.]+))?/);
if (m && (m[4] === undefined || parseFloat(m[4]) > 0.5)) {
return 0.2126 * m[1] + 0.7152 * m[2] + 0.0722 * m[3] < 128;
}
var esquema = getComputedStyle(doc).colorScheme || '';
if (esquema.indexOf('dark') >= 0 && esquema.indexOf('light') < 0) return true;
return false;
} catch (e) { return null; }
}
// Título, URL y TEMA → barra de pestañas (y para restaurar la sesión al reabrir).
// El tema viaja aquí porque la web decide su modo con su propio botón y NO sigue al
// sistema: la barra tiene que copiarle a ELLA.
function report() {
emit('ls-title', { label: LABEL, title: document.title || 'Labstream OS', url: location.href });
var carga = { label: LABEL, title: document.title || 'Labstream OS', url: location.href };
var oscuro = temaOscuro();
if (oscuro !== null) carga.dark = oscuro;
emit('ls-title', carga);
}
function watchTitle() {
var el = document.querySelector('title');
@@ -233,6 +272,18 @@ const INIT_JS_TPL: &str = r#"
history[k] = function () { var r = o.apply(this, arguments); setTimeout(report, 0); return r; };
});
addEventListener('popstate', function () { setTimeout(report, 0); });
// 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
// por si el tema aterriza tarde con la hidratación.
function vigilarTema() {
if (!window.MutationObserver) return;
var conf = { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] };
if (document.documentElement) new MutationObserver(report).observe(document.documentElement, conf);
if (document.body) new MutationObserver(report).observe(document.body, conf);
}
if (document.body) vigilarTema();
else document.addEventListener('DOMContentLoaded', vigilarTema);
addEventListener('load', function () { setTimeout(report, 800); setTimeout(report, 2500); });
// Clics en enlaces.
document.addEventListener('click', function (e) {
@@ -368,6 +419,8 @@ fn broadcast<R: Runtime>(app: &AppHandle<R>) {
// que dibuja ella misma los botones minimizar/maximizar/cerrar (no hay nativos).
"win": cfg!(target_os = "windows"),
"maximized": maximized,
// Tema real de la app (null hasta el primer reporte: la barra usa el del sistema).
"dark": s.dark,
})
};
let _ = app.emit_to(EventTarget::webview(CHROME), "ls-tabs", payload);
@@ -945,6 +998,10 @@ fn field_i64(payload: &str, key: &str) -> Option<i64> {
serde_json::from_str::<serde_json::Value>(payload).ok()?.get(key)?.as_i64()
}
fn field_bool(payload: &str, key: &str) -> Option<bool> {
serde_json::from_str::<serde_json::Value>(payload).ok()?.get(key)?.as_bool()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let mut builder = tauri::Builder::default();
@@ -994,6 +1051,7 @@ pub fn run() {
s.zoom_idx = saved.zoom_idx.unwrap_or(ZOOM_100).min(ZOOM_STEPS.len() - 1);
s.win = saved.win;
s.autostart = saved.autostart;
s.dark = saved.dark;
}
// Ventana principal (contenedora). Tamaño/posición de la sesión anterior si los hay.
@@ -1308,10 +1366,11 @@ pub fn run() {
});
let h = handle.clone();
handle.listen_any("ls-title", move |e| {
let (label, title, url) = (
let (label, title, url, dark) = (
field(e.payload(), "label"),
field(e.payload(), "title"),
field(e.payload(), "url"),
field_bool(e.payload(), "dark"),
);
if let Some(label) = label {
let shell = h.state::<ShellState>();
@@ -1324,6 +1383,11 @@ pub fn run() {
t.url = url;
}
}
// El tema es global (todas las pestañas del origen comparten modo): el
// último reporte manda, venga de la pestaña que venga.
if let Some(dark) = dark {
s.dark = Some(dark);
}
drop(s);
broadcast(&h);
save_persisted(&h);

Ver fichero

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