Comparar commits

...

1 Commits

Autor SHA1 Mensaje Fecha
Munka
6539f6939c v1.21.0: la pestaña del color de su página y pestañas con nombre
Para el modo edición de los sets de fotos de Labstream OS, que se abre en su
propia pestaña:

- Color de la página: una página pide el color de su pestaña con
  <html data-ls-pestana="#rrggbb">. El script de la pestaña lo manda en
  ls-title (y lo vigila con el mismo observador del tema); Rust solo acepta
  #rrggbb (color_de_pagina) y la barra pinta la pestaña de ese color, con letra
  clara u oscura según su luminancia. Activa se funde con la barra de arriba de
  la página; detrás conserva el color. Un color del menú encima va de franja.
  Una página con color propio no manda el tema: su fondo oscuro no es el de la
  app y no debe oscurecer la barra.
- Pestañas con nombre: window.open(url, "ls-…") o target="ls-…" abre la
  pestaña la primera vez; las siguientes activan ESA y le pasan la URL por
  window.__lsAbrir (el modo edición cambia de foto sin recargar); sin él,
  navega. Una descartada se revive ya en la URL nueva. Solo nombres ls-
  alfanuméricos (nombre_de_ventana); los demás abren pestaña como siempre.
- Color de página y nombre se guardan con la sesión (paginas, nombres en
  estado.json; un estado viejo carga igual).

Prueba nueva: la_pagina_solo_pide_colores_y_nombres_de_la_app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:33:20 -05:00
Se han modificado 7 ficheros con 177 adiciones y 32 borrados

Ver fichero

@@ -20,6 +20,16 @@ NAS. Esto le da al equipo un cliente nativo de verdad en Windows y Mac:
- **Colores de pestaña** (v1.6.0): clic derecho sobre una pestaña ▸ **Color**, con - **Colores de pestaña** (v1.6.0): clic derecho sobre una pestaña ▸ **Color**, con
la misma paleta de nueve tonos que usa Chrome para sus grupos. Sirve para agrupar la misma paleta de nueve tonos que usa Chrome para sus grupos. Sirve para agrupar
de un vistazo (todo lo de un cliente en verde) y **se guarda con la sesión**. de un vistazo (todo lo de un cliente en verde) y **se guarda con la sesión**.
- **Pestaña del color de su página** (v1.21.0): una página puede pedir el color de
su pestaña con `<html data-ls-pestana="#rrggbb">` (solo ese formato). El modo
edición de un set la pinta del gris del editor: activa se funde con su barra de
arriba y detrás se reconoce entre las demás. Esa página no cambia el tema de la
barra (su fondo oscuro no es el de la app).
- **Pestañas con nombre** (v1.21.0): `window.open(url, "ls-…")` o un enlace con
`target="ls-…"` abre la pestaña la primera vez y las siguientes **vuelven a
ella** en vez de abrir otra. La página recibe la URL nueva por
`window.__lsAbrir(url)` (el modo edición cambia de foto sin recargar); si no lo
tiene, navega. Nombre y color se guardan con la sesión.
- **Zoom de interfaz**: ⌘/Ctrl `+` / `−` / `0` y Ctrl+rueda, con indicador en la - **Zoom de interfaz**: ⌘/Ctrl `+` / `−` / `0` y Ctrl+rueda, con indicador en la
barra y **persistencia** entre sesiones (50%–250%). barra y **persistencia** entre sesiones (50%–250%).
- **Documentos dentro de la app**: los visores/editores del propio servidor - **Documentos dentro de la app**: los visores/editores del propio servidor

27
dist/index.html vendido
Ver fichero

@@ -7,7 +7,7 @@
<!-- <!--
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,color}], zoom, version, recibe ls-tabs {tabs:[{label,title,url,loading,active,color,pagina}], zoom, version,
mac, win, maximized, dark} 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
@@ -208,6 +208,17 @@
} }
:root.oscuro .tab[data-color]:not(.active) { background: rgba(var(--tc), 0.36); } :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); } :root.oscuro .tab[data-color]:not(.active):hover { background: rgba(var(--tc), 0.5); }
/* ── La pestaña del COLOR DE SU PÁGINA (1.21) ──
La página lo pide (`<html data-ls-pestana="#262626">`): el modo edición de un set pinta la
suya del gris del editor. Activa, es de ese color y se funde con la barra de arriba de la
página, como la de siempre se funde con el blanco de la app. Detrás, el mismo color: entre
las demás se reconoce de un vistazo. Con color del menú encima, ese va de franja arriba.
(#tabs delante: gana a las reglas de tema y de color del menú sin !important.) */
#tabs .tab.pagina { --muted: color-mix(in srgb, var(--tp-fg) 60%, transparent); background: var(--tp); color: var(--tp-fg); }
#tabs .tab.pagina:not(.active) { box-shadow: none; }
#tabs .tab.pagina:not(.active):hover { background: color-mix(in srgb, var(--tp) 86%, var(--tp-fg)); color: var(--tp-fg); }
#tabs .tab.pagina .cerrar:hover { background: color-mix(in srgb, var(--tp-fg) 18%, transparent); color: var(--tp-fg); }
#tabs .tab.pagina[data-color] { box-shadow: inset 0 3px 0 rgb(var(--tc)); }
/* 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. */
@@ -500,6 +511,14 @@
$tabs.classList.toggle("compactas", ancho < 112); $tabs.classList.toggle("compactas", ancho < 112);
} }
// ¿Un #rrggbb claro? (luminancia relativa): decide si la letra encima va oscura o clara.
function esClaro(hex) {
const n = parseInt(hex.slice(1), 16);
const lin = (c) => { c /= 255; return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; };
const l = 0.2126 * lin((n >> 16) & 255) + 0.7152 * lin((n >> 8) & 255) + 0.0722 * lin(n & 255);
return l > 0.35;
}
function render() { function render() {
$tabs.textContent = ""; $tabs.textContent = "";
for (const t of estado.tabs) { for (const t of estado.tabs) {
@@ -508,6 +527,12 @@
el.className = "tab" + (t.active ? " active" : ""); el.className = "tab" + (t.active ? " active" : "");
el.dataset.label = t.label; el.dataset.label = t.label;
if (t.color) el.dataset.color = t.color; // teñido desde el menú de la pestaña if (t.color) el.dataset.color = t.color; // teñido desde el menú de la pestaña
// El color que pide la página (ya validado #rrggbb en el shell), con letra que se lea encima.
if (t.pagina) {
el.classList.add("pagina");
el.style.setProperty("--tp", t.pagina);
el.style.setProperty("--tp-fg", esClaro(t.pagina) ? "#202124" : "#ececec");
}
el.title = texto; el.title = texto;
const fav = document.createElement("span"); const fav = document.createElement("span");

Ver fichero

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

@@ -2366,7 +2366,7 @@ dependencies = [
[[package]] [[package]]
name = "labstream-desktop" name = "labstream-desktop"
version = "1.20.0" version = "1.21.0"
dependencies = [ dependencies = [
"active-win-pos-rs", "active-win-pos-rs",
"device_query", "device_query",

Ver fichero

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

@@ -81,6 +81,19 @@ fn es_pestana_interna(target: &Url, server: &Url) -> bool {
target.origin() == server.origin() target.origin() == server.origin()
} }
// El color que pide una página para su pestaña: solo `#rrggbb` (va a un estilo de la barra; nada
// que no sea un color llega allí).
fn color_de_pagina(c: &str) -> Option<String> {
let hex = c.strip_prefix('#')?;
(hex.len() == 6 && hex.chars().all(|ch| ch.is_ascii_hexdigit())).then(|| c.to_ascii_lowercase())
}
// Un nombre de ventana que identifica pestaña: los de la app (`ls-…`, p. ej. `ls-edicion-<set>`).
// Los reservados del navegador (_blank, _self…) y cualquier otro abren pestaña nueva como siempre.
fn nombre_de_ventana(n: &str) -> Option<String> {
(n.starts_with("ls-") && n.len() <= 80 && n.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')).then(|| n.to_string())
}
#[cfg(test)] #[cfg(test)]
mod navigation_tests { mod navigation_tests {
use super::*; use super::*;
@@ -95,6 +108,20 @@ mod navigation_tests {
assert!(is_google_host("docs.google.com")); assert!(is_google_host("docs.google.com"));
assert!(!is_google_host("docs.google.com.evil.example")); assert!(!is_google_host("docs.google.com.evil.example"));
} }
#[test]
fn la_pagina_solo_pide_colores_y_nombres_de_la_app() {
assert_eq!(color_de_pagina("#262626").as_deref(), Some("#262626"));
assert_eq!(color_de_pagina("#A0b1C2").as_deref(), Some("#a0b1c2"));
for malo in ["", "262626", "#26262", "#2626266", "red", "#26262g", "#262626;background:url(x)"] {
assert_eq!(color_de_pagina(malo), None, "{malo}");
}
assert_eq!(nombre_de_ventana("ls-edicion-cmg123").as_deref(), Some("ls-edicion-cmg123"));
for malo in ["", "_blank", "_self", "edicion", "ls-a b", "ls-\"<x>"] {
assert_eq!(nombre_de_ventana(malo), None, "{malo}");
}
assert_eq!(nombre_de_ventana(&format!("ls-{}", "a".repeat(80))), None);
}
} }
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)
const CHROME: &str = "chrome"; const CHROME: &str = "chrome";
@@ -122,6 +149,12 @@ struct TabRec {
// Ahorro de memoria (memoria.rs): su webview se cerró tras DESCARTE_TRAS dormida. Sigue en la // Ahorro de memoria (memoria.rs): su webview se cerró tras DESCARTE_TRAS dormida. Sigue en la
// barra con título, URL y color, y se crea otra vez al volver a ella. // barra con título, URL y color, y se crea otra vez al volver a ella.
descartada: bool, descartada: bool,
// El color que PIDE la página (`<html data-ls-pestana="#262626">`, ver `color_de_pagina`): el
// modo edición de un set pinta su pestaña del gris del editor. Lo manda la página, no el menú.
pagina: Option<String>,
// El nombre de ventana con que se abrió (`window.open(url, nombre)`): abrir otra vez con el
// mismo nombre lleva a esta pestaña en vez de abrir otra (ver `reusar_pestana`).
nombre: Option<String>,
} }
// Paleta de colores de pestaña, la misma idea que los grupos de Chrome: sirve para agrupar de // Paleta de colores de pestaña, la misma idea que los grupos de Chrome: sirve para agrupar de
@@ -198,6 +231,12 @@ struct Persisted {
// cargar (ver `registrar_diferida`) y la barra necesita algo que enseñar mientras tanto. // cargar (ver `registrar_diferida`) y la barra necesita algo que enseñar mientras tanto.
#[serde(default)] #[serde(default)]
titles: Vec<String>, titles: Vec<String>,
// El color que pidió la página y el nombre de ventana de cada una (mismo orden; "" = ninguno):
// la pestaña del modo edición vuelve del gris del editor y se sigue reutilizando.
#[serde(default)]
paginas: Vec<String>,
#[serde(default)]
nombres: Vec<String>,
#[serde(default)] #[serde(default)]
active: usize, active: usize,
#[serde(default)] #[serde(default)]
@@ -230,6 +269,8 @@ fn save_persisted<R: Runtime>(app: &AppHandle<R>) {
tabs: s.tabs.iter().map(|t| t.url.clone()).collect(), tabs: s.tabs.iter().map(|t| t.url.clone()).collect(),
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(),
titles: s.tabs.iter().map(|t| t.title.clone()).collect(), titles: s.tabs.iter().map(|t| t.title.clone()).collect(),
paginas: s.tabs.iter().map(|t| t.pagina.clone().unwrap_or_default()).collect(),
nombres: s.tabs.iter().map(|t| t.nombre.clone().unwrap_or_default()).collect(),
active: s.active, active: s.active,
autostart: s.autostart, autostart: s.autostart,
dark: s.dark, dark: s.dark,
@@ -287,7 +328,9 @@ const INIT_JS_TPL: &str = r#"
try { T.core.invoke('plugin:opener|open_url', { url: url, with: null }); } try { T.core.invoke('plugin:opener|open_url', { url: url, with: null }); }
catch (e) { try { T.opener.openUrl(url); } catch (_) {} } catch (e) { try { T.opener.openUrl(url); } catch (_) {} }
} }
function newTab(url) { emit('ls-new-tab', { url: url }); } function newTab(url, nombre) { emit('ls-new-tab', nombre ? { url: url, name: nombre } : { url: url }); }
// Una ventana con nombre de la app (`ls-…`): reabrirla lleva a la pestaña que ya la tiene.
function nombreVentana(n) { return typeof n === 'string' && /^ls-[A-Za-z0-9_-]{1,77}$/.test(n) ? n : null; }
// ── Errores de la página → diario de salud del shell (salud.log) ── // ── Errores de la página → diario de salud del shell (salud.log) ──
// Solo el mensaje, el archivo:línea y la ruta (sin datos del formulario ni de la sesión). Como // Solo el mensaje, el archivo:línea y la ruta (sin datos del formulario ni de la sesión). Como
@@ -373,13 +416,25 @@ const INIT_JS_TPL: &str = r#"
} catch (e) { return null; } } catch (e) { return null; }
} }
// ── El color que la página pide para SU pestaña ── `<html data-ls-pestana='#262626'>` (el modo
// edición de un set, del gris del editor). "" = ninguno.
function colorPestana() {
try {
var c = document.documentElement && document.documentElement.getAttribute('data-ls-pestana');
return c && /^#[0-9a-fA-F]{6}$/.test(c) ? c : '';
} catch (e) { return ''; }
}
// Título, URL y TEMA → barra de pestañas (y para restaurar la sesión al reabrir). // 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 // 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. // sistema: la barra tiene que copiarle a ELLA. Una página con color propio no lo dice: su
// fondo oscuro es el de su pestaña, no el tema de la app.
function report() { function report() {
var carga = { label: LABEL, title: document.title || 'Labstream OS', url: location.href }; var carga = { label: LABEL, title: document.title || 'Labstream OS', url: location.href, pestana: colorPestana() };
if (!carga.pestana) {
var oscuro = temaOscuro(); var oscuro = temaOscuro();
if (oscuro !== null) carga.dark = oscuro; if (oscuro !== null) carga.dark = oscuro;
}
emit('ls-title', carga); emit('ls-title', carga);
} }
function watchTitle() { function watchTitle() {
@@ -404,7 +459,7 @@ const INIT_JS_TPL: &str = r#"
// por si el tema aterriza tarde con la hidratación. // por si el tema aterriza tarde con la hidratación.
function vigilarTema() { function vigilarTema() {
if (!window.MutationObserver) return; if (!window.MutationObserver) return;
var conf = { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] }; var conf = { attributes: true, attributeFilter: ['class', 'style', 'data-theme', 'data-ls-pestana'] };
if (document.documentElement) new MutationObserver(report).observe(document.documentElement, conf); if (document.documentElement) new MutationObserver(report).observe(document.documentElement, conf);
if (document.body) new MutationObserver(report).observe(document.body, conf); if (document.body) new MutationObserver(report).observe(document.body, conf);
} }
@@ -421,7 +476,8 @@ const INIT_JS_TPL: &str = r#"
if (!u) return; if (!u) return;
if (isExt(u)) { e.preventDefault(); openExternal(u.href); return; } if (isExt(u)) { e.preventDefault(); openExternal(u.href); return; }
var mod = e.metaKey || e.ctrlKey; var mod = e.metaKey || e.ctrlKey;
if (isApp(u) && (mod || a.target === '_blank')) { e.preventDefault(); newTab(u.href); } var nombre = mod ? null : nombreVentana(a.target);
if (isApp(u) && (mod || a.target === '_blank' || nombre)) { e.preventDefault(); newTab(u.href, nombre); }
}, true); }, true);
// Clic con la rueda (botón central) en un enlace interno → pestaña nueva, como en el navegador. // Clic con la rueda (botón central) en un enlace interno → pestaña nueva, como en el navegador.
@@ -438,7 +494,7 @@ const INIT_JS_TPL: &str = r#"
window.open = function (url, name, feats) { window.open = function (url, name, feats) {
var u = url ? abs(url) : null; var u = url ? abs(url) : null;
if (isExt(u)) { openExternal(u.href); return null; } if (isExt(u)) { openExternal(u.href); return null; }
if (isInterno(u)) { newTab(u.href); return null; } if (isInterno(u)) { newTab(u.href, nombreVentana(name)); return null; }
return _open ? _open.call(window, url, name, feats) : null; return _open ? _open.call(window, url, name, feats) : null;
}; };
@@ -641,7 +697,7 @@ fn broadcast<R: Runtime>(app: &AppHandle<R>) {
.tabs .tabs
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, t)| json!({ "label": t.label, "title": t.title, "url": t.url, "loading": t.loading, "active": i == s.active, "color": t.color })) .map(|(i, t)| json!({ "label": t.label, "title": t.title, "url": t.url, "loading": t.loading, "active": i == s.active, "color": t.color, "pagina": t.pagina }))
.collect(); .collect();
json!({ json!({
"tabs": tabs, "tabs": tabs,
@@ -814,12 +870,17 @@ fn avisar_descarga_terminada<R: Runtime>(webview: &Webview<R>, url: &Url, path:
// Crea una pestaña nueva cargando `url` (o el servidor si viene vacío). // Crea una pestaña nueva cargando `url` (o el servidor si viene vacío).
fn create_tab<R: Runtime>(app: &AppHandle<R>, url: Option<String>, activate: bool) { fn create_tab<R: Runtime>(app: &AppHandle<R>, url: Option<String>, activate: bool) {
crear_pestana(app, url, activate, None);
}
// Lo mismo, con su nombre de ventana (ver `reusar_pestana`). Devuelve la etiqueta de la nueva.
fn crear_pestana<R: Runtime>(app: &AppHandle<R>, url: Option<String>, activate: bool, nombre: Option<String>) -> Option<String> {
let target = url.unwrap_or_else(|| SERVER_URL.to_string()); let target = url.unwrap_or_else(|| SERVER_URL.to_string());
// Solo el origen de la app puede abrirse en pestaña; cualquier otra cosa va al navegador. // Solo el origen de la app puede abrirse en pestaña; cualquier otra cosa va al navegador.
let Ok(parsed) = Url::parse(&target) else { return }; let parsed = Url::parse(&target).ok()?;
let Ok(server) = Url::parse(SERVER_URL) else { return }; let server = Url::parse(SERVER_URL).ok()?;
if !es_pestana_interna(&parsed, &server) { if !es_pestana_interna(&parsed, &server) {
return; return None;
} }
let label = { let label = {
@@ -827,7 +888,7 @@ fn create_tab<R: Runtime>(app: &AppHandle<R>, url: Option<String>, activate: boo
let mut s = shell.lock().unwrap(); let mut s = shell.lock().unwrap();
let label = format!("tab-{}", s.next_id); let label = format!("tab-{}", s.next_id);
s.next_id += 1; s.next_id += 1;
s.tabs.push(TabRec { label: label.clone(), title: "Labstream OS".into(), url: target.clone(), loading: true, color: None, oculta_desde: if activate { None } else { Some(Instant::now()) }, dormida: false, descartada: false }); s.tabs.push(TabRec { label: label.clone(), title: "Labstream OS".into(), url: target.clone(), loading: true, color: None, oculta_desde: if activate { None } else { Some(Instant::now()) }, dormida: false, descartada: false, pagina: None, nombre });
label label
}; };
@@ -839,6 +900,7 @@ fn create_tab<R: Runtime>(app: &AppHandle<R>, url: Option<String>, activate: boo
broadcast(app); broadcast(app);
save_persisted(app); save_persisted(app);
} }
Some(label)
} }
Err(e) => { Err(e) => {
salud::anotar("ERROR", "pestaña", &format!("no se pudo crear el webview: {e}")); salud::anotar("ERROR", "pestaña", &format!("no se pudo crear el webview: {e}"));
@@ -846,10 +908,46 @@ fn create_tab<R: Runtime>(app: &AppHandle<R>, url: Option<String>, activate: boo
let shell = app.state::<ShellState>(); let shell = app.state::<ShellState>();
let mut s = shell.lock().unwrap(); let mut s = shell.lock().unwrap();
s.tabs.retain(|t| t.label != label); s.tabs.retain(|t| t.label != label);
None
} }
} }
} }
// ── Pestañas con NOMBRE (1.21) ──
// `window.open(url, "ls-edicion-<set>")` (el modo edición de un set) abre la pestaña la primera
// vez; las siguientes llevan a ESA pestaña, como hace el navegador con una ventana con nombre, en
// vez de amontonar una por foto. A la página se le pasa la URL nueva por `__lsAbrir` (el modo
// edición cambia de foto sin recargar, guardando lo de la que tenía); si no lo conoce, navega.
// Devuelve false si no hay pestaña con ese nombre (entonces se crea).
fn reusar_pestana<R: Runtime>(app: &AppHandle<R>, nombre: &str, url: Option<&str>) -> bool {
let Ok(server) = Url::parse(SERVER_URL) else { return false };
let destino = match url.map(Url::parse) {
None => None,
Some(Ok(u)) if es_pestana_interna(&u, &server) => Some(u),
Some(_) => return false,
};
let (label, descartada) = {
let shell = app.state::<ShellState>();
let mut s = shell.lock().unwrap();
let Some(t) = s.tabs.iter_mut().find(|t| t.nombre.as_deref() == Some(nombre)) else { return false };
// Descartada (sin webview): se crea ya en la URL nueva al activarla.
if t.descartada {
if let Some(d) = &destino {
t.url = d.to_string();
}
}
(t.label.clone(), t.descartada)
};
activate_tab(app, &label);
if let (Some(d), false) = (destino, descartada) {
if let Some(wv) = get_webview(app, &label) {
let u = serde_json::to_string(d.as_str()).unwrap_or_else(|_| "\"\"".into());
let _ = wv.eval(format!("(function(u){{try{{if(window.__lsAbrir&&window.__lsAbrir(u))return}}catch(e){{}}location.assign(u)}})({u})"));
}
}
true
}
// Crea el webview de una pestaña: una nueva (create_tab) o una descartada que vuelve // Crea el webview de una pestaña: una nueva (create_tab) o una descartada que vuelve
// (revivir_pestana, al activarla). Lo vigila salud.rs: si su proceso muere, se recarga sola. // (revivir_pestana, al activarla). Lo vigila salud.rs: si su proceso muere, se recarga sola.
fn montar_webview<R: Runtime>(app: &AppHandle<R>, label: &str, parsed: Url) -> tauri::Result<Webview<R>> { fn montar_webview<R: Runtime>(app: &AppHandle<R>, label: &str, parsed: Url) -> tauri::Result<Webview<R>> {
@@ -935,7 +1033,7 @@ fn registrar_diferida<R: Runtime>(app: &AppHandle<R>, url: &str, title: &str) {
let label = format!("tab-{}", s.next_id); let label = format!("tab-{}", s.next_id);
s.next_id += 1; s.next_id += 1;
let title = if title.is_empty() { "Labstream OS".to_string() } else { title.to_string() }; let title = if title.is_empty() { "Labstream OS".to_string() } else { title.to_string() };
s.tabs.push(TabRec { label, title, url: url.to_string(), loading: false, color: None, oculta_desde: Some(Instant::now()), dormida: false, descartada: true }); s.tabs.push(TabRec { label, title, url: url.to_string(), loading: false, color: None, oculta_desde: Some(Instant::now()), dormida: false, descartada: true, pagina: None, nombre: None });
} }
// Vuelve a crear el webview de una pestaña descartada, en su última URL. Devuelve false si no se // Vuelve a crear el webview de una pestaña descartada, en su última URL. Devuelve false si no se
@@ -1815,7 +1913,7 @@ pub fn run() {
// registran descartadas (título, URL y color en la barra) y se crean al hacer clic en // registran descartadas (título, URL y color en la barra) y se crean al hacer clic en
// ellas, como hacen Chrome y Edge. Antes se cargaban todas a la vez: con 5 pestañas, 5 // ellas, como hacen Chrome y Edge. Antes se cargaban todas a la vez: con 5 pestañas, 5
// copias enteras de la web app arrancando juntas. // copias enteras de la web app arrancando juntas.
let restore: Vec<(String, String, String)> = { let restore: Vec<(String, String, String, String, String)> = {
let server = Url::parse(SERVER_URL).expect("URL del servidor válida"); let server = Url::parse(SERVER_URL).expect("URL del servidor válida");
saved saved
.tabs .tabs
@@ -1824,7 +1922,8 @@ pub fn run() {
.filter(|(_, u)| Url::parse(u).map(|p| es_pestana_interna(&p, &server)).unwrap_or(false)) .filter(|(_, u)| Url::parse(u).map(|p| es_pestana_interna(&p, &server)).unwrap_or(false))
.take(12) .take(12)
.map(|(i, u)| { .map(|(i, u)| {
(u.clone(), saved.colors.get(i).cloned().unwrap_or_default(), saved.titles.get(i).cloned().unwrap_or_default()) let de = |v: &Vec<String>| v.get(i).cloned().unwrap_or_default();
(u.clone(), de(&saved.colors), de(&saved.titles), de(&saved.paginas), de(&saved.nombres))
}) })
.collect() .collect()
}; };
@@ -1832,21 +1931,21 @@ pub fn run() {
create_tab(&handle, None, true); create_tab(&handle, None, true);
} else { } else {
let idx = saved.active.min(restore.len() - 1); let idx = saved.active.min(restore.len() - 1);
for (i, (url, color, title)) in restore.iter().enumerate() { for (i, (url, color, title, pagina, nombre)) in restore.iter().enumerate() {
if i == idx { if i == idx {
create_tab(&handle, Some(url.clone()), false); create_tab(&handle, Some(url.clone()), false);
} else { } else {
registrar_diferida(&handle, url, title); registrar_diferida(&handle, url, title);
} }
if !color.is_empty() {
// La pestaña se acaba de empujar al final de la lista. // La pestaña se acaba de empujar al final de la lista.
let shell = handle.state::<ShellState>(); let shell = handle.state::<ShellState>();
let mut s = shell.lock().unwrap(); let mut s = shell.lock().unwrap();
if let Some(t) = s.tabs.last_mut() { if let Some(t) = s.tabs.last_mut().filter(|t| t.url == *url) {
if t.url == *url { if !color.is_empty() {
t.color = Some(color.clone()); t.color = Some(color.clone());
} }
} t.pagina = color_de_pagina(pagina);
t.nombre = nombre_de_ventana(nombre);
} }
} }
let label = { let label = {
@@ -1881,7 +1980,13 @@ pub fn run() {
let h = handle.clone(); let h = handle.clone();
handle.listen_any("ls-new-tab", move |e| { handle.listen_any("ls-new-tab", move |e| {
let url = field(e.payload(), "url"); let url = field(e.payload(), "url");
create_tab(&h, url, true); let nombre = field(e.payload(), "name").and_then(|n| nombre_de_ventana(&n));
if let Some(n) = &nombre {
if reusar_pestana(&h, n, url.as_deref()) {
return;
}
}
crear_pestana(&h, url, true, nombre);
}); });
let h = handle.clone(); let h = handle.clone();
handle.listen_any("ls-activate", move |e| { handle.listen_any("ls-activate", move |e| {
@@ -1982,11 +2087,12 @@ 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, dark) = ( let (label, title, url, dark, pestana) = (
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"), field_bool(e.payload(), "dark"),
field(e.payload(), "pestana"),
); );
if let Some(label) = label { if let Some(label) = label {
let shell = h.state::<ShellState>(); let shell = h.state::<ShellState>();
@@ -1998,6 +2104,10 @@ pub fn run() {
if let Some(url) = url { if let Some(url) = url {
t.url = url; t.url = url;
} }
// (una página de antes de la 1.21 no lo manda: se deja como estaba)
if let Some(p) = pestana {
t.pagina = color_de_pagina(&p);
}
} }
// El tema es global (todas las pestañas del origen comparten modo): el // El tema es global (todas las pestañas del origen comparten modo): el
// último reporte manda, venga de la pestaña que venga. // último reporte manda, venga de la pestaña que venga.

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