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>
Este commit está contenido en:
10
README.md
10
README.md
@@ -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
|
||||
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**.
|
||||
- **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
|
||||
barra y **persistencia** entre sesiones (50%–250%).
|
||||
- **Documentos dentro de la app**: los visores/editores del propio servidor
|
||||
|
||||
27
dist/index.html
vendido
27
dist/index.html
vendido
@@ -7,7 +7,7 @@
|
||||
<!--
|
||||
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,color}], zoom, version,
|
||||
recibe ls-tabs {tabs:[{label,title,url,loading,active,color,pagina}], 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
|
||||
@@ -208,6 +208,17 @@
|
||||
}
|
||||
: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); }
|
||||
/* ── 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
|
||||
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. */
|
||||
@@ -500,6 +511,14 @@
|
||||
$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() {
|
||||
$tabs.textContent = "";
|
||||
for (const t of estado.tabs) {
|
||||
@@ -508,6 +527,12 @@
|
||||
el.className = "tab" + (t.active ? " active" : "");
|
||||
el.dataset.label = t.label;
|
||||
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;
|
||||
|
||||
const fav = document.createElement("span");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "labstream-desktop",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"private": true,
|
||||
"description": "App de escritorio (Windows) que envuelve Labstream OS — cliente del servidor del NAS.",
|
||||
"scripts": {
|
||||
|
||||
2
src-tauri/Cargo.lock
generado
2
src-tauri/Cargo.lock
generado
@@ -2366,7 +2366,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "labstream-desktop"
|
||||
version = "1.20.0"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"active-win-pos-rs",
|
||||
"device_query",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "labstream-desktop"
|
||||
version = "1.20.0"
|
||||
version = "1.21.0"
|
||||
description = "Cliente de escritorio de Labstream OS"
|
||||
authors = ["Labstream SAS"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -81,6 +81,19 @@ fn es_pestana_interna(target: &Url, server: &Url) -> bool {
|
||||
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)]
|
||||
mod navigation_tests {
|
||||
use super::*;
|
||||
@@ -95,6 +108,20 @@ mod navigation_tests {
|
||||
assert!(is_google_host("docs.google.com"));
|
||||
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 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
|
||||
// barra con título, URL y color, y se crea otra vez al volver a ella.
|
||||
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
|
||||
@@ -198,6 +231,12 @@ struct Persisted {
|
||||
// cargar (ver `registrar_diferida`) y la barra necesita algo que enseñar mientras tanto.
|
||||
#[serde(default)]
|
||||
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)]
|
||||
active: usize,
|
||||
#[serde(default)]
|
||||
@@ -230,6 +269,8 @@ fn save_persisted<R: Runtime>(app: &AppHandle<R>) {
|
||||
tabs: s.tabs.iter().map(|t| t.url.clone()).collect(),
|
||||
colors: s.tabs.iter().map(|t| t.color.clone().unwrap_or_default()).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,
|
||||
autostart: s.autostart,
|
||||
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 }); }
|
||||
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) ──
|
||||
// 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; }
|
||||
}
|
||||
|
||||
// ── 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).
|
||||
// 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() {
|
||||
var carga = { label: LABEL, title: document.title || 'Labstream OS', url: location.href };
|
||||
var oscuro = temaOscuro();
|
||||
if (oscuro !== null) carga.dark = oscuro;
|
||||
var carga = { label: LABEL, title: document.title || 'Labstream OS', url: location.href, pestana: colorPestana() };
|
||||
if (!carga.pestana) {
|
||||
var oscuro = temaOscuro();
|
||||
if (oscuro !== null) carga.dark = oscuro;
|
||||
}
|
||||
emit('ls-title', carga);
|
||||
}
|
||||
function watchTitle() {
|
||||
@@ -404,7 +459,7 @@ const INIT_JS_TPL: &str = r#"
|
||||
// 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'] };
|
||||
var conf = { attributes: true, attributeFilter: ['class', 'style', 'data-theme', 'data-ls-pestana'] };
|
||||
if (document.documentElement) new MutationObserver(report).observe(document.documentElement, 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 (isExt(u)) { e.preventDefault(); openExternal(u.href); return; }
|
||||
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);
|
||||
|
||||
// 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) {
|
||||
var u = url ? abs(url) : 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;
|
||||
};
|
||||
|
||||
@@ -641,7 +697,7 @@ fn broadcast<R: Runtime>(app: &AppHandle<R>) {
|
||||
.tabs
|
||||
.iter()
|
||||
.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();
|
||||
json!({
|
||||
"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).
|
||||
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());
|
||||
// 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 Ok(server) = Url::parse(SERVER_URL) else { return };
|
||||
let parsed = Url::parse(&target).ok()?;
|
||||
let server = Url::parse(SERVER_URL).ok()?;
|
||||
if !es_pestana_interna(&parsed, &server) {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
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 label = format!("tab-{}", s.next_id);
|
||||
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
|
||||
};
|
||||
|
||||
@@ -839,6 +900,7 @@ fn create_tab<R: Runtime>(app: &AppHandle<R>, url: Option<String>, activate: boo
|
||||
broadcast(app);
|
||||
save_persisted(app);
|
||||
}
|
||||
Some(label)
|
||||
}
|
||||
Err(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 mut s = shell.lock().unwrap();
|
||||
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
|
||||
// (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>> {
|
||||
@@ -935,7 +1033,7 @@ fn registrar_diferida<R: Runtime>(app: &AppHandle<R>, url: &str, title: &str) {
|
||||
let label = format!("tab-{}", s.next_id);
|
||||
s.next_id += 1;
|
||||
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
|
||||
@@ -1815,7 +1913,7 @@ pub fn run() {
|
||||
// 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
|
||||
// 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");
|
||||
saved
|
||||
.tabs
|
||||
@@ -1824,7 +1922,8 @@ pub fn run() {
|
||||
.filter(|(_, u)| Url::parse(u).map(|p| es_pestana_interna(&p, &server)).unwrap_or(false))
|
||||
.take(12)
|
||||
.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()
|
||||
};
|
||||
@@ -1832,21 +1931,21 @@ pub fn run() {
|
||||
create_tab(&handle, None, true);
|
||||
} else {
|
||||
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 {
|
||||
create_tab(&handle, Some(url.clone()), false);
|
||||
} else {
|
||||
registrar_diferida(&handle, url, title);
|
||||
}
|
||||
if !color.is_empty() {
|
||||
// La pestaña se acaba de empujar al final de la lista.
|
||||
let shell = handle.state::<ShellState>();
|
||||
let mut s = shell.lock().unwrap();
|
||||
if let Some(t) = s.tabs.last_mut() {
|
||||
if t.url == *url {
|
||||
t.color = Some(color.clone());
|
||||
}
|
||||
// La pestaña se acaba de empujar al final de la lista.
|
||||
let shell = handle.state::<ShellState>();
|
||||
let mut s = shell.lock().unwrap();
|
||||
if let Some(t) = s.tabs.last_mut().filter(|t| t.url == *url) {
|
||||
if !color.is_empty() {
|
||||
t.color = Some(color.clone());
|
||||
}
|
||||
t.pagina = color_de_pagina(pagina);
|
||||
t.nombre = nombre_de_ventana(nombre);
|
||||
}
|
||||
}
|
||||
let label = {
|
||||
@@ -1881,7 +1980,13 @@ pub fn run() {
|
||||
let h = handle.clone();
|
||||
handle.listen_any("ls-new-tab", move |e| {
|
||||
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();
|
||||
handle.listen_any("ls-activate", move |e| {
|
||||
@@ -1982,11 +2087,12 @@ pub fn run() {
|
||||
});
|
||||
let h = handle.clone();
|
||||
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(), "title"),
|
||||
field(e.payload(), "url"),
|
||||
field_bool(e.payload(), "dark"),
|
||||
field(e.payload(), "pestana"),
|
||||
);
|
||||
if let Some(label) = label {
|
||||
let shell = h.state::<ShellState>();
|
||||
@@ -1998,6 +2104,10 @@ pub fn run() {
|
||||
if let Some(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
|
||||
// último reporte manda, venga de la pestaña que venga.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Labstream OS",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"identifier": "co.labstream.os",
|
||||
"build": {
|
||||
"frontendDist": "../dist"
|
||||
|
||||
Referencia en una nueva incidencia
Block a user