Comparar commits
3 Commits
v1.6.1
...
tema-robus
| Autor | SHA1 | Fecha | |
|---|---|---|---|
|
|
3798970f74 | ||
|
|
29f0b47371 | ||
|
|
b18c155b89 |
39
dist/index.html
vendido
39
dist/index.html
vendido
@@ -7,7 +7,8 @@
|
|||||||
<!--
|
<!--
|
||||||
Barra de pestañas del shell de escritorio (webview "chrome", local).
|
Barra de pestañas del shell de escritorio (webview "chrome", local).
|
||||||
Habla con Rust por eventos (__TAURI__.event):
|
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,
|
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
|
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
|
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;
|
--muted: #5f6368;
|
||||||
--tab-active-bg: #ffffff; /* mismo fondo que la app → pestaña fundida con el contenido */
|
--tab-active-bg: #ffffff; /* mismo fondo que la app → pestaña fundida con el contenido */
|
||||||
--tab-active-fg: #202124;
|
--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) */
|
--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. */
|
/* Sobre una franja casi blanca, un hover blanco no se vería: aquí oscurece. */
|
||||||
--hover: rgba(0, 0, 0, 0.06);
|
--hover: rgba(0, 0, 0, 0.06);
|
||||||
--btn-hover: rgba(0, 0, 0, 0.08);
|
--btn-hover: rgba(0, 0, 0, 0.08);
|
||||||
--sep: rgba(0, 0, 0, 0.22); /* separador fino entre pestañas inactivas */
|
--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) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root:not(.claro) {
|
||||||
--bg: #0d0d0f; /* franja: negra, más oscura que el contenido */
|
--bg: #0d0d0f; /* franja: negra, más oscura que el contenido */
|
||||||
--fg: #bdc1c6;
|
--fg: #bdc1c6;
|
||||||
--muted: #80868b;
|
--muted: #80868b;
|
||||||
@@ -49,6 +59,17 @@
|
|||||||
--sep: rgba(255, 255, 255, 0.25);
|
--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; }
|
* { box-sizing: border-box; }
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -168,9 +189,11 @@
|
|||||||
.tab[data-color]:not(.active)::before { display: none; }
|
.tab[data-color]:not(.active)::before { display: none; }
|
||||||
.tab[data-color].active { box-shadow: inset 0 3px 0 rgb(var(--tc)); }
|
.tab[data-color].active { box-shadow: inset 0 3px 0 rgb(var(--tc)); }
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.tab[data-color]:not(.active) { background: rgba(var(--tc), 0.36); }
|
:root:not(.claro) .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):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
|
/* 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
|
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. */
|
WebKit para que no salga la imagen fantasma al mover una pestaña. */
|
||||||
@@ -508,6 +531,12 @@
|
|||||||
$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 : "";
|
$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.
|
// En macOS la barra ES la fila del título: hay que dejar libre el semáforo.
|
||||||
document.body.classList.toggle("mac", !!estado.mac);
|
document.body.classList.toggle("mac", !!estado.mac);
|
||||||
// En Windows también ES la fila del título, pero ahí los botones de ventana los
|
// En Windows también ES la fila del título, pero ahí los botones de ventana los
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "labstream-desktop",
|
"name": "labstream-desktop",
|
||||||
"version": "1.6.1",
|
"version": "1.7.1",
|
||||||
"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": {
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generado
2
src-tauri/Cargo.lock
generado
@@ -2062,7 +2062,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "labstream-desktop"
|
name = "labstream-desktop"
|
||||||
version = "1.6.1"
|
version = "1.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "labstream-desktop"
|
name = "labstream-desktop"
|
||||||
version = "1.6.1"
|
version = "1.7.1"
|
||||||
description = "Cliente de escritorio de Labstream OS"
|
description = "Cliente de escritorio de Labstream OS"
|
||||||
authors = ["Labstream SAS"]
|
authors = ["Labstream SAS"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -97,11 +97,15 @@ struct Shell {
|
|||||||
// Último estado maximizado conocido. Solo sirve para no repintar la barra en CADA
|
// Último estado maximizado conocido. Solo sirve para no repintar la barra en CADA
|
||||||
// evento de redimensión: únicamente cuando el icono ▢/❐ tiene que cambiar.
|
// evento de redimensión: únicamente cuando el icono ▢/❐ tiene que cambiar.
|
||||||
maximized: bool,
|
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 {
|
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, 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,
|
active: usize,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
autostart: Option<bool>,
|
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> {
|
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(),
|
colors: s.tabs.iter().map(|t| t.color.clone().unwrap_or_default()).collect(),
|
||||||
active: s.active,
|
active: s.active,
|
||||||
autostart: s.autostart,
|
autostart: s.autostart,
|
||||||
|
dark: s.dark,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(dir) = path.parent() {
|
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 }); }
|
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() {
|
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() {
|
function watchTitle() {
|
||||||
var el = document.querySelector('title');
|
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; };
|
history[k] = function () { var r = o.apply(this, arguments); setTimeout(report, 0); return r; };
|
||||||
});
|
});
|
||||||
addEventListener('popstate', function () { setTimeout(report, 0); });
|
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.
|
// Clics en enlaces.
|
||||||
document.addEventListener('click', function (e) {
|
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).
|
// que dibuja ella misma los botones minimizar/maximizar/cerrar (no hay nativos).
|
||||||
"win": cfg!(target_os = "windows"),
|
"win": cfg!(target_os = "windows"),
|
||||||
"maximized": maximized,
|
"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);
|
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()
|
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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let mut builder = tauri::Builder::default();
|
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.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;
|
s.autostart = saved.autostart;
|
||||||
|
s.dark = saved.dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -1308,10 +1366,11 @@ pub fn run() {
|
|||||||
});
|
});
|
||||||
let h = handle.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, dark) = (
|
||||||
field(e.payload(), "label"),
|
field(e.payload(), "label"),
|
||||||
field(e.payload(), "title"),
|
field(e.payload(), "title"),
|
||||||
field(e.payload(), "url"),
|
field(e.payload(), "url"),
|
||||||
|
field_bool(e.payload(), "dark"),
|
||||||
);
|
);
|
||||||
if let Some(label) = label {
|
if let Some(label) = label {
|
||||||
let shell = h.state::<ShellState>();
|
let shell = h.state::<ShellState>();
|
||||||
@@ -1324,6 +1383,11 @@ pub fn run() {
|
|||||||
t.url = url;
|
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);
|
drop(s);
|
||||||
broadcast(&h);
|
broadcast(&h);
|
||||||
save_persisted(&h);
|
save_persisted(&h);
|
||||||
|
|||||||
@@ -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.6.1",
|
"version": "1.7.1",
|
||||||
"identifier": "co.labstream.os",
|
"identifier": "co.labstream.os",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist"
|
"frontendDist": "../dist"
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user