Comparar commits

...

3 Commits

Autor SHA1 Mensaje Fecha
Jonathan David Florez Salomón
d302591c25 v1.5.1: el shell dejaba de meterse dentro del editor de OnlyOffice en Windows
OnlyOffice fallaba al editar y al descargar SOLO en la app de escritorio de
Windows. En el navegador y en la app de macOS iba bien. La causa estaba en el
propio shell, no en el servidor de documentos.

En Windows, wry inyecta los scripts de inicio en TODOS los marcos e ignora
for_main_frame_only (wry-0.55.1/src/webview2/mod.rs:492-495, documentado en su
lib.rs:990); macOS si respeta la bandera (wkwebview/mod.rs:643-645). Y __TAURI__
viaja con ellos, asi que la guarda «if (!T || !T.event) return» no frenaba nada.
Resultado: INIT_JS_TPL corria DENTRO del iframe del Document Server con ORIGIN
apuntando todavia a la app; como alli location.href es el de docs.*, cada enlace
del editor se resolvia contra ese origen, daba isExt() = true y se desviaba al
navegador del sistema —que no tiene la sesion: «no se puede acceder»—, y
window.open devolvia null.

Cambios:

1. Guarda de marco y origen al principio del script inyectado: solo actua en el
   documento PRINCIPAL y solo en el origen de la app. En macOS es un no-op (alli
   nunca corrio en subframes). De paso arregla que en Windows el shell tambien
   secuestrara los enlaces de las paginas de SSO.

2. El manejador de atajos ignora e.altKey. AltGr enciende ctrlKey en Windows, asi
   que escribir [ o ] con teclado espanol disparaba history.back()/forward() en
   vez de escribir el caracter — en el editor y en cualquier campo de la app.

3. Aviso nativo al terminar una descarga. No es un adorno: wry trae un manejador
   de descargas por defecto (wry-0.55.1/src/lib.rs:830), de modo que en WebView2
   siempre se registra DownloadStarting y siempre responde SetHandled(true)
   (webview2/mod.rs:858-861), lo que apaga la interfaz de descargas de Windows.
   El archivo bajaba a Descargas pero nada lo decia y parecia roto. Quitar el
   manejador NO devuelve la interfaz nativa: el de wry sigue puesto.

4. Feature «devtools» de tauri: el inspector queda disponible tambien en release.
   Sin el, un fallo que solo aparece en la app instalada no se puede mirar por
   dentro.

Descartado a proposito: registrar on_new_window. En macOS el delegado ejecuta
webview.window().unwrap() y current_window.screen().unwrap() ANTES del match
(wry-0.55.1/src/wkwebview/class/wry_web_view_ui_delegate.rs:150-154), tambien en
la rama Deny; y este shell oculta la ventana al cerrar, con lo que NSWindow.screen
puede ser nil y la app entera abortaria. Cambiar un fallo de Windows por un crash
en macOS no compensa.

Verificado: cargo check limpio; sintaxis del script inyectado validada con node;
y simulacion de la guarda — marco principal + origen propio interviene, iframe de
OnlyOffice no interviene, origen ajeno no interviene.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 07:12:22 -05:00
Jonathan David Florez Salomón
e7c69fe0ea Barra al estilo Notion: una sola fila, pestanas con nombre y reordenables
La ventana gastaba DOS filas en cromo (titulo de macOS + barra de pestanas).
Ahora es una sola: se recuperan 28 px de alto en cada ventana.

- INTEGRADA AL TITULO (macOS): la ventana usa titulo en overlay y sin texto, asi
  que la barra ocupa esa fila. Deja libres 78 px para el semaforo (que sigue
  siendo el nativo) y el hueco vacio arrastra la ventana / doble clic la
  maximiza (data-tauri-drag-region + permiso core🪟allow-start-dragging).
  En Windows NO cambia nada: titulo del sistema y barra debajo, como hasta ahora
  (fusionarlas alli obligaria a dibujar a mano minimizar/maximizar/cerrar).
  Como el area util pasa a ser todo el marco, desaparece la compensacion de
  v1.4.2: los webviews vuelven a colocarse en 0 / TAB_BAR_H.
- NOMBRES QUE SE DISTINGUEN: la web pone el mismo <title> en todas las paginas
  («(25) Labstream OS»), asi que las tres pestanas se llamaban igual. Ahora el
  nombre sale de la RUTA (mismas etiquetas que la barra superior de la web) con
  su emoji: Inicio, Chats, Calendario, Cliente, Proyecto, Operaciones… Si algun
  dia la web pone titulos por pagina, mandan esos: se limpia el contador de no
  leidos y el sufijo de la app y, si queda algo propio, se usa.
- ANCHO ELASTICO: las pestanas se reparten el sitio y se encogen al abrir mas;
  por debajo de ~112 px queda solo el icono (y la x solo en la activa), con el
  nombre completo en el tooltip. Igual que Chrome.
- REORDENAR ARRASTRANDO: con eventos de PUNTERO, no drag&drop de HTML5 (en el
  webview de la barra en macOS los eventos dragstart/dragover no llegan aunque
  se desactive el manejador nativo). El nodo se mueve en vivo y al soltar se
  manda el orden a Rust, que valida que sea una permutacion exacta y lo guarda.
  El clic que cierra el arrastre no activa la pestana.
- CLIC DERECHO EN UNA PESTANA: menu NATIVO (la barra mide 40 px; uno dibujado
  dentro quedaria recortado) con Duplicar, Copiar enlace, Cerrar pestana, Cerrar
  las demas y Cerrar las de la derecha — estas dos se deshabilitan solas cuando
  no aplican. «Copiar enlace» usa tauri-plugin-clipboard-manager.
- SENAL DE CARGA: el icono de la pestana se vuelve un aro girando mientras la
  pagina carga (PageLoadEvent Started/Finished), respetando prefers-reduced-motion.

Verificado en el Mac con la app real: barra en la fila del titulo con el
semaforo intacto, ventana movida arrastrando la barra (15,53 -> 147,243),
nombres con emoji, 14 pestanas en modo solo-icono, menu contextual con sus
opciones deshabilitadas donde toca, «Cerrar las demas», duplicar, y reordenar
(tab-1,tab-2,tab-3 -> tab-3,tab-1,tab-2) persistido entre arranques.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 10:01:24 -05:00
Jonathan David Florez Salomón
bfc4e1172c La barra de pestanas ya no queda debajo del titulo nativo (macOS)
La barra existia pero estaba DIBUJADA DETRAS de la barra de titulo de macOS:
solo asomaban ~12 px por debajo, y como el titulo nativo es translucido hasta
se tenia del color de la barra. Con el rediseno estilo Chrome el sintoma se
volvio evidente (no se veian ni las pestanas ni los controles), pero el fallo
venia de antes: no lo causo el CSS.

Causa (medida en vivo con instrumentacion): en macOS las coordenadas de los
webviews HIJOS son relativas al MARCO de la ventana (incluye el titulo) y
ademas `inner_size()` devuelve el marco, no el area de contenido
(inner_size == outer_size e inner_position == outer_position). Al colocar la
barra en y=0 quedaba bajo el titulo, y el contenido en y=40 arrancaba 12 px
por debajo, tapando el resto.

Arreglo: `top_offset()` compensa el alto del titulo (28 pt) SOLO cuando hace
falta. La deteccion es en caliente -- si el marco y el interior miden lo mismo
hay que compensar; el dia que Tauri lo corrija la resta dejara de ser 0 y no se
compensara nada -- y en Windows/Linux devuelve 0 (ahi las coordenadas ya son
del area de cliente). Se aplica en los tres sitios que colocan webviews:
layout_all, la creacion de la barra y la creacion de cada pestana.

Verificado en el Mac con la app real: barra completa y alineada bajo el
titulo, redimensionado, pestana nueva, cierre de pestana y cambio de pestana.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 08:55:56 -05:00
Se han modificado 7 ficheros con 778 adiciones y 55 borrados

226
dist/index.html vendido
Ver fichero

@@ -7,8 +7,12 @@
<!-- <!--
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,active}], zoom} recibe ls-tabs {tabs:[{label,title,url,loading,active}], zoom, version, mac}
emite ls-ready, ls-new-tab, ls-activate, ls-close-tab, ls-reload, ls-zoom, ls-cycle, ls-activate-n 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
deja libre el hueco del semáforo (clase `mac`) y el espacio vacío arrastra la ventana
(`data-tauri-drag-region`). En Windows hay título nativo aparte y no se reserva nada.
--> -->
<style> <style>
/* Paleta estilo Chrome: la barra es una FRANJA más oscura y la pestaña activa es una /* Paleta estilo Chrome: la barra es una FRANJA más oscura y la pestaña activa es una
@@ -59,6 +63,8 @@
padding: 0 6px 0 8px; padding: 0 6px 0 8px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
/* macOS: hueco para los botones rojo/amarillo/verde, que son NATIVOS y se pintan encima. */
body.mac #bar { padding-left: 78px; }
#tabs { #tabs {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
@@ -69,16 +75,16 @@
align-self: stretch; align-self: stretch;
margin-bottom: -1px; margin-bottom: -1px;
padding: 0 4px; padding: 0 4px;
overflow-x: auto; overflow: hidden;
scrollbar-width: none;
} }
#tabs::-webkit-scrollbar { display: none; }
.tab { .tab {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 7px; gap: 7px;
flex: 0 1 220px; /* Elástico como en Chrome: se reparten el ancho y se encogen al abrir más. */
min-width: 100px; flex: 1 1 220px;
min-width: 42px;
max-width: 240px;
height: 30px; height: 30px;
margin-bottom: 5px; margin-bottom: 5px;
padding: 0 6px 0 10px; padding: 0 6px 0 10px;
@@ -129,11 +135,31 @@
right: -8px; right: -8px;
background: radial-gradient(circle at 8px 0, transparent 8px, var(--tab-active-bg) 8.5px); background: radial-gradient(circle at 8px 0, transparent 8px, var(--tab-active-bg) 8.5px);
} }
/* 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. */
.tab { -webkit-user-drag: none; }
.tab.arrastrando { opacity: 0.5; }
.tab .favicon { .tab .favicon {
flex: 0 0 auto; flex: 0 0 auto;
width: 14px; width: 15px;
height: 14px; height: 15px;
border-radius: 3px; display: grid;
place-items: center;
font-size: 12.5px;
line-height: 1;
}
/* Señal de carga: el icono se sustituye por un aro girando. */
.tab .favicon.cargando {
border: 1.6px solid var(--sep);
border-top-color: var(--accent);
border-radius: 50%;
animation: giro 0.7s linear infinite;
font-size: 0;
}
@keyframes giro { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
.tab .favicon.cargando { animation-duration: 2.4s; }
} }
.tab .titulo { .tab .titulo {
flex: 1 1 auto; flex: 1 1 auto;
@@ -156,6 +182,11 @@
} }
.tab:hover .cerrar, .tab.active .cerrar { visibility: visible; } .tab:hover .cerrar, .tab.active .cerrar { visibility: visible; }
.tab .cerrar:hover { background: var(--btn-hover); color: var(--tab-active-fg); } .tab .cerrar:hover { background: var(--btn-hover); color: var(--tab-active-fg); }
/* Muchas pestañas abiertas: solo el icono (el nombre completo queda en el tooltip),
y la × únicamente en la activa — como hace Chrome al quedarse sin sitio. */
#tabs.compactas .tab { padding: 0 6px; justify-content: center; }
#tabs.compactas .tab .titulo { display: none; }
#tabs.compactas .tab:not(.active) .cerrar { display: none; }
button { button {
appearance: none; appearance: none;
border: 0; border: 0;
@@ -171,7 +202,7 @@
.icono { .icono {
flex: 0 0 auto; flex: 0 0 auto;
width: 30px; width: 30px;
margin: 2px 0 7px; height: 26px;
font-size: 15px; font-size: 15px;
} }
#zoom { #zoom {
@@ -179,35 +210,38 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1px; gap: 1px;
margin: 2px 0 7px;
} }
#zoom button { height: 100%; } #z-menos, #z-mas { width: 26px; height: 26px; font-size: 14px; }
#z-menos, #z-mas { width: 26px; font-size: 14px; }
#z-pct { #z-pct {
min-width: 44px; min-width: 44px;
height: 26px;
padding: 0 4px; padding: 0 4px;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
color: var(--muted); color: var(--muted);
font-size: 11.5px; font-size: 11.5px;
border-radius: 13px;
} }
#z-pct.activo { color: var(--accent); font-weight: 600; } #z-pct.activo { color: var(--accent); font-weight: 600; }
#version { #version {
flex: 0 0 auto; flex: 0 0 auto;
margin: 2px 0 7px; height: 26px;
padding: 0 7px; padding: 0 7px;
color: var(--muted); color: var(--muted);
font-size: 11px; font-size: 11px;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
white-space: nowrap; white-space: nowrap;
border-radius: 13px;
} }
#version:hover { color: var(--accent); } #version:hover { color: var(--accent); }
</style> </style>
</head> </head>
<body> <body>
<div id="bar"> <!-- data-tauri-drag-region: el hueco libre de la barra mueve la ventana y el doble clic
la maximiza (en macOS ya no hay barra de título nativa de la que tirar). -->
<div id="bar" data-tauri-drag-region>
<button class="icono" id="atras" title="Atrás (⌘/Ctrl+[)">←</button> <button class="icono" id="atras" title="Atrás (⌘/Ctrl+[)">←</button>
<button class="icono" id="adelante" title="Adelante (⌘/Ctrl+])">→</button> <button class="icono" id="adelante" title="Adelante (⌘/Ctrl+])">→</button>
<div id="tabs" title=""></div> <div id="tabs" data-tauri-drag-region></div>
<button class="icono" id="nueva" title="Nueva pestaña (⌘/Ctrl+T)">+</button> <button class="icono" id="nueva" title="Nueva pestaña (⌘/Ctrl+T)">+</button>
<button class="icono" id="recargar" title="Recargar (⌘/Ctrl+R)">⟳</button> <button class="icono" id="recargar" title="Recargar (⌘/Ctrl+R)">⟳</button>
<div id="zoom"> <div id="zoom">
@@ -231,31 +265,136 @@
const $pct = document.getElementById("z-pct"); const $pct = document.getElementById("z-pct");
const $ver = document.getElementById("version"); const $ver = document.getElementById("version");
// Favicon de las pestañas (look Chrome): mini logo Labstream en SVG inline — // Nombre + icono de cada pestaña a partir de su RUTA (mismas etiquetas que la barra
// cuadrado indigo con la "L" — para no depender de archivos ni de la red. // superior de la web). Sin esto todas las pestañas se llamaban «(25) Labstream OS» y
const FAVICON = "data:image/svg+xml," + encodeURIComponent( // no había forma de distinguirlas.
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'>" + const RUTAS = [
"<rect width='16' height='16' rx='4' fill='#6366f1'/>" + [/^\/$/, "🏠", "Inicio"],
"<path d='M5.2 3.8v7h5.6' stroke='#fff' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round' fill='none'/>" + [/^\/inicio/, "🏠", "Inicio"],
"</svg>", [/^\/mis-tareas/, "✅", "Mis tareas"],
); [/^\/mis-entregas/, "📦", "Mis entregas"],
[/^\/estados/, "💬", "Chat del día"],
[/^\/chat/, "💬", "Chats"],
[/^\/notas/, "📝", "Notas"],
[/^\/recordatorios/, "⏰", "Recordatorios"],
[/^\/proyectos\/nuevo/, "✨", "Nuevo proyecto"],
[/^\/proyectos\/[^/]+/, "🎬", "Proyecto"],
[/^\/proyectos/, "🎬", "Proyectos"],
[/^\/plantillas/, "🗂️", "Plantillas"],
[/^\/calendario/, "📅", "Calendario"],
[/^\/wiki\/inventario/, "📦", "Inventario"],
[/^\/wiki\/ubicacion/, "📍", "Ubicación"],
[/^\/wiki/, "📚", "Wiki"],
[/^\/clientes\/nuevo/, "✨", "Nuevo cliente"],
[/^\/clientes\/[^/]+/, "👤", "Cliente"],
[/^\/clientes/, "👤", "Clientes"],
[/^\/operaciones/, "🗄️", "Operaciones"],
[/^\/revisiones/, "🎞️", "Revisiones"],
[/^\/entregas-finales/, "🏁", "Entregas finales"],
[/^\/cotizaciones/, "🧾", "Cotizaciones"],
[/^\/propuestas/, "📄", "Propuestas"],
[/^\/facturacion/, "💵", "Facturación"],
[/^\/comercial/, "📈", "Comercial"],
[/^\/biblioteca/, "📚", "Biblioteca"],
[/^\/reportes/, "📊", "Reportes"],
[/^\/timeline/, "🧭", "Cronograma"],
[/^\/asistente/, "🤖", "Marcebot"],
[/^\/configuracion/, "⚙️", "Configuración"],
[/^\/ajustes/, "⚙️", "Ajustes"],
[/^\/perfil/, "👤", "Mi perfil"],
[/^\/papelera/, "🗑️", "Papelera"],
[/^\/docs\//, "📄", "Documento"],
];
function rutaMeta(url) {
let ruta = "/";
try { ruta = new URL(url).pathname; } catch (e) {}
for (const [re, emoji, nombre] of RUTAS) {
if (re.test(ruta)) return { emoji, nombre };
}
return { emoji: "🗂️", nombre: "Labstream OS" };
}
// El título de la web es «(25) Labstream OS» en todas las páginas: se le quita el
// contador de no leídos y el nombre de la app. Si queda algo propio, ese manda (así,
// el día que la web ponga títulos por página, la barra los aprovecha sola).
function limpiarTitulo(t) {
if (!t) return "";
let x = String(t).replace(/^\(\d+\)\s*/, "").trim();
x = x.replace(/\s*[·|—–-]\s*Labstream(\s+OS)?\s*$/i, "").trim();
return /^labstream(\s+os)?$/i.test(x) ? "" : x;
}
function nombreDe(t) {
const meta = rutaMeta(t.url || "");
return { emoji: meta.emoji, texto: limpiarTitulo(t.title) || meta.nombre };
}
// Reordenar arrastrando, con eventos de PUNTERO: el drag&drop de HTML5 no llega al
// webview de la barra en macOS. Se mueve el nodo en vivo y al soltar se manda el orden.
let arrastre = null; // {el, x0, movido}
let tragarClick = false; // el clic que cierra el arrastre no debe activar la pestaña
function ordenActual() {
return Array.from($tabs.children).map((el) => el.dataset.label);
}
addEventListener("mousemove", (e) => {
if (!arrastre) return;
if (!arrastre.movido) {
if (Math.abs(e.clientX - arrastre.x0) < 5) return; // todavía es un clic, no un arrastre
arrastre.movido = true;
arrastre.el.classList.add("arrastrando");
}
for (const otro of Array.from($tabs.children)) {
if (otro === arrastre.el) continue;
const r = otro.getBoundingClientRect();
if (e.clientX >= r.left && e.clientX <= r.right) {
const despues = e.clientX > r.left + r.width / 2;
$tabs.insertBefore(arrastre.el, despues ? otro.nextSibling : otro);
break;
}
}
});
addEventListener("mouseup", () => {
if (!arrastre) return;
const { el, movido } = arrastre;
arrastre = null;
el.classList.remove("arrastrando");
if (movido) {
tragarClick = true;
emitir("ls-reorder", { order: ordenActual().join(",") });
}
});
// Modo solo-icono: se mide el ancho REAL de una pestaña ya pintada (el reparto lo hace
// flex, así que todas miden igual) en vez de estimarlo dividiendo el contenedor.
function ajustarCompactas() {
const primera = $tabs.firstElementChild;
if (!primera) return;
const ancho = primera.getBoundingClientRect().width;
if (!ancho) return;
$tabs.classList.toggle("compactas", ancho < 112);
}
function render() { function render() {
$tabs.textContent = ""; $tabs.textContent = "";
for (const t of estado.tabs) { for (const t of estado.tabs) {
const { emoji, texto } = nombreDe(t);
const el = document.createElement("div"); const el = document.createElement("div");
el.className = "tab" + (t.active ? " active" : ""); el.className = "tab" + (t.active ? " active" : "");
el.title = t.title; el.dataset.label = t.label;
el.title = texto;
const fav = document.createElement("img"); const fav = document.createElement("span");
fav.className = "favicon"; fav.className = "favicon" + (t.loading ? " cargando" : "");
fav.src = FAVICON; fav.textContent = t.loading ? "" : emoji;
fav.alt = "";
el.appendChild(fav); el.appendChild(fav);
const titulo = document.createElement("span"); const titulo = document.createElement("span");
titulo.className = "titulo"; titulo.className = "titulo";
titulo.textContent = t.title || "Labstream OS"; titulo.textContent = texto;
el.appendChild(titulo); el.appendChild(titulo);
const x = document.createElement("button"); const x = document.createElement("button");
@@ -268,19 +407,40 @@
}); });
el.appendChild(x); el.appendChild(x);
el.addEventListener("click", () => emitir("ls-activate", { label: t.label })); el.addEventListener("click", () => {
if (tragarClick) { tragarClick = false; return; } // veníamos de reordenar
emitir("ls-activate", { label: t.label });
});
// Clic central sobre la pestaña = cerrarla (como en el navegador). // Clic central sobre la pestaña = cerrarla (como en el navegador).
el.addEventListener("auxclick", (e) => { el.addEventListener("auxclick", (e) => {
if (e.button === 1) emitir("ls-close-tab", { label: t.label }); if (e.button === 1) emitir("ls-close-tab", { label: t.label });
}); });
// Clic derecho: menú NATIVO de la pestaña (duplicar, cerrar otras, copiar enlace…).
// La barra mide 40px: un menú dibujado aquí dentro quedaría recortado.
el.addEventListener("contextmenu", (e) => {
e.preventDefault();
e.stopPropagation();
emitir("ls-tab-menu", { label: t.label });
});
// Reordenar arrastrando: lo gestionan los listeners de puntero de más arriba.
el.addEventListener("mousedown", (e) => {
if (e.button !== 0) return;
arrastre = { el, x0: e.clientX, movido: false };
});
$tabs.appendChild(el); $tabs.appendChild(el);
} }
requestAnimationFrame(ajustarCompactas);
const activa = $tabs.querySelector(".tab.active"); const activa = $tabs.querySelector(".tab.active");
if (activa) activa.scrollIntoView({ block: "nearest", inline: "nearest" }); if (activa) activa.scrollIntoView({ block: "nearest", inline: "nearest" });
$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 : "";
// En macOS la barra ES la fila del título: hay que dejar libre el semáforo.
document.body.classList.toggle("mac", !!estado.mac);
} }
if (E) { if (E) {
@@ -289,6 +449,8 @@
render(); render();
}); });
} }
// Al cambiar el ancho de la ventana se recalcula si caben los nombres.
addEventListener("resize", ajustarCompactas);
document.getElementById("atras").addEventListener("click", () => emitir("ls-nav", { dir: -1 })); document.getElementById("atras").addEventListener("click", () => emitir("ls-nav", { dir: -1 }));
document.getElementById("adelante").addEventListener("click", () => emitir("ls-nav", { dir: 1 })); document.getElementById("adelante").addEventListener("click", () => emitir("ls-nav", { dir: 1 }));

Ver fichero

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

351
src-tauri/Cargo.lock generado
Ver fichero

@@ -56,6 +56,27 @@ dependencies = [
"derive_arbitrary", "derive_arbitrary",
] ]
[[package]]
name = "arboard"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
dependencies = [
"clipboard-win",
"image",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation",
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.7.2" version = "0.7.2"
@@ -354,6 +375,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.12.0" version = "1.12.0"
@@ -485,6 +512,15 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "clipboard-win"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
dependencies = [
"error-code",
]
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.7" version = "4.6.7"
@@ -587,6 +623,12 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.7" version = "0.1.7"
@@ -827,13 +869,19 @@ checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89"
dependencies = [ dependencies = [
"bit-set", "bit-set",
"cssparser", "cssparser",
"foldhash", "foldhash 0.2.0",
"html5ever", "html5ever",
"precomputed-hash", "precomputed-hash",
"selectors", "selectors",
"tendril", "tendril",
] ]
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]] [[package]]
name = "dpi" name = "dpi"
version = "0.1.2" version = "0.1.2"
@@ -959,6 +1007,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "error-code"
version = "3.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
[[package]] [[package]]
name = "event-listener" name = "event-listener"
version = "5.4.1" version = "5.4.1"
@@ -986,6 +1040,12 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]
name = "fax"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
[[package]] [[package]]
name = "fdeflate" name = "fdeflate"
version = "0.3.7" version = "0.3.7"
@@ -1021,6 +1081,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.9" version = "1.1.9"
@@ -1037,6 +1103,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]] [[package]]
name = "foldhash" name = "foldhash"
version = "0.2.0" version = "0.2.0"
@@ -1272,6 +1344,16 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "gethostname"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
dependencies = [
"rustix",
"windows-link 0.2.1",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.17" version = "0.2.17"
@@ -1454,12 +1536,32 @@ dependencies = [
"syn 2.0.118", "syn 2.0.118",
] ]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.3" version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash 0.1.5",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.1" version = "0.17.1"
@@ -1740,6 +1842,20 @@ dependencies = [
"icu_properties", "icu_properties",
] ]
[[package]]
name = "image"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png 0.18.1",
"tiff",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.3" version = "1.9.3"
@@ -1946,13 +2062,14 @@ dependencies = [
[[package]] [[package]]
name = "labstream-desktop" name = "labstream-desktop"
version = "1.4.1" version = "1.5.1"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-autostart", "tauri-plugin-autostart",
"tauri-plugin-clipboard-manager",
"tauri-plugin-notification", "tauri-plugin-notification",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-single-instance", "tauri-plugin-single-instance",
@@ -2117,6 +2234,16 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "moxcms"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
"num-traits",
"pxfm",
]
[[package]] [[package]]
name = "muda" name = "muda"
version = "0.19.3" version = "0.19.3"
@@ -2168,6 +2295,15 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "notify-rust" name = "notify-rust"
version = "4.18.0" version = "4.18.0"
@@ -2239,6 +2375,7 @@ dependencies = [
"block2", "block2",
"objc2", "objc2",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation", "objc2-foundation",
] ]
@@ -2467,6 +2604,16 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "os_pipe"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "osakit" name = "osakit"
version = "0.3.1" version = "0.3.1"
@@ -2547,6 +2694,17 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "petgraph"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
dependencies = [
"fixedbitset",
"hashbrown 0.15.5",
"indexmap 2.14.0",
]
[[package]] [[package]]
name = "phf" name = "phf"
version = "0.13.1" version = "0.13.1"
@@ -2631,7 +2789,7 @@ checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"indexmap 2.14.0", "indexmap 2.14.0",
"quick-xml", "quick-xml 0.39.4",
"serde", "serde",
"time", "time",
] ]
@@ -2768,6 +2926,18 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "pxfm"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.39.4" version = "0.39.4"
@@ -2777,6 +2947,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "quick-xml"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.45" version = "1.0.45"
@@ -3801,6 +3980,21 @@ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
] ]
[[package]]
name = "tauri-plugin-clipboard-manager"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf"
dependencies = [
"arboard",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
]
[[package]] [[package]]
name = "tauri-plugin-notification" name = "tauri-plugin-notification"
version = "2.3.3" version = "2.3.3"
@@ -4065,6 +4259,20 @@ dependencies = [
"syn 2.0.118", "syn 2.0.118",
] ]
[[package]]
name = "tiff"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
dependencies = [
"fax",
"flate2",
"half",
"quick-error",
"weezl",
"zune-jpeg",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.49" version = "0.3.49"
@@ -4375,6 +4583,17 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "tree_magic_mini"
version = "3.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
dependencies = [
"memchr",
"nom",
"petgraph",
]
[[package]] [[package]]
name = "try-lock" name = "try-lock"
version = "0.2.5" version = "0.2.5"
@@ -4646,6 +4865,76 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "wayland-backend"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016ccf01d1c58b6f8999612813e17c9b2390f7d70671428869913310f83f54b8"
dependencies = [
"cc",
"downcast-rs",
"rustix",
"smallvec",
"wayland-sys",
]
[[package]]
name = "wayland-client"
version = "0.31.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073"
dependencies = [
"bitflags 2.13.0",
"rustix",
"wayland-backend",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols"
version = "0.32.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6"
dependencies = [
"bitflags 2.13.0",
"wayland-backend",
"wayland-client",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-wlr"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
dependencies = [
"bitflags 2.13.0",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-scanner"
version = "0.31.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
dependencies = [
"proc-macro2",
"quick-xml 0.41.0",
"quote",
]
[[package]]
name = "wayland-sys"
version = "0.31.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
dependencies = [
"pkg-config",
]
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.102" version = "0.3.102"
@@ -4757,6 +5046,12 @@ dependencies = [
"windows-core 0.61.2", "windows-core 0.61.2",
] ]
[[package]]
name = "weezl"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@@ -5240,6 +5535,24 @@ version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "wl-clipboard-rs"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3"
dependencies = [
"libc",
"log",
"os_pipe",
"rustix",
"thiserror 2.0.18",
"tree_magic_mini",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols-wlr",
]
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.3" version = "0.6.3"
@@ -5311,6 +5624,23 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "x11rb"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"gethostname",
"rustix",
"x11rb-protocol",
]
[[package]]
name = "x11rb-protocol"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]] [[package]]
name = "xattr" name = "xattr"
version = "1.6.1" version = "1.6.1"
@@ -5503,6 +5833,21 @@ version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zune-core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
[[package]]
name = "zune-jpeg"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
dependencies = [
"zune-core",
]
[[package]] [[package]]
name = "zvariant" name = "zvariant"
version = "5.12.0" version = "5.12.0"

Ver fichero

@@ -1,6 +1,6 @@
[package] [package]
name = "labstream-desktop" name = "labstream-desktop"
version = "1.4.1" version = "1.5.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"
@@ -15,8 +15,13 @@ tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
# `unstable` habilita multiwebview: varios webviews (pestañas) dentro de UNA ventana. # `unstable` habilita multiwebview: varios webviews (pestañas) dentro de UNA ventana.
tauri = { version = "2", features = ["tray-icon", "unstable"] } # `devtools` deja el inspector disponible TAMBIÉN en release (por defecto solo va en debug:
# wry-0.55.1/src/lib.rs:834-837). Sin él, un fallo que solo aparece en la app instalada —como
# el de OnlyOffice en Windows— no se puede mirar por dentro y hay que deducirlo leyendo
# fuentes. Con él, clic derecho → Inspeccionar sobre el marco que falle y se acabó.
tauri = { version = "2", features = ["tray-icon", "unstable", "devtools"] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-notification = "2" tauri-plugin-notification = "2"
tauri-plugin-autostart = "2" tauri-plugin-autostart = "2"
tauri-plugin-updater = "2" tauri-plugin-updater = "2"

Ver fichero

@@ -2,14 +2,22 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Permisos del shell de escritorio: la barra local (chrome) y las pestañas remotas (tab-*).", "description": "Permisos del shell de escritorio: la barra local (chrome) y las pestañas remotas (tab-*).",
"windows": ["main"], "windows": [
"webviews": ["chrome", "tab-*"], "main"
],
"webviews": [
"chrome",
"tab-*"
],
"remote": { "remote": {
"urls": ["https://os.labstreamsas.com"] "urls": [
"https://os.labstreamsas.com"
]
}, },
"permissions": [ "permissions": [
"core:default", "core:default",
"opener:default", "opener:default",
"notification:default" "notification:default",
"core:window:allow-start-dragging"
] ]
} }

Ver fichero

@@ -39,18 +39,21 @@ use serde_json::json;
use tauri::{ use tauri::{
menu::{CheckMenuItem, 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::{DownloadEvent, PageLoadEvent, Webview, WebviewBuilder},
window::{Window, WindowBuilder}, window::{Window, WindowBuilder},
AppHandle, Emitter, EventTarget, Listener, LogicalPosition, LogicalSize, Manager, Runtime, AppHandle, Emitter, EventTarget, Listener, LogicalPosition, LogicalSize, Manager, Runtime,
Url, WebviewUrl, WindowEvent, Url, WebviewUrl, WindowEvent,
}; };
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_clipboard_manager::ClipboardExt;
use tauri_plugin_opener::OpenerExt; 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)
const CHROME: &str = "chrome"; const CHROME: &str = "chrome";
// Hueco que ocupan los botones rojo/amarillo/verde en macOS: la barra deja libre ese
// espacio a la izquierda (lo aplica el CSS de la barra con la bandera `mac` del payload).
// Escalones de zoom (como un navegador). 1.0 = 100%. // Escalones de zoom (como un navegador). 1.0 = 100%.
const ZOOM_STEPS: &[f64] = &[0.5, 0.67, 0.75, 0.8, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0, 2.5]; const ZOOM_STEPS: &[f64] = &[0.5, 0.67, 0.75, 0.8, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0, 2.5];
@@ -62,6 +65,7 @@ struct TabRec {
label: String, label: String,
title: String, title: String,
url: String, url: String,
loading: bool, // la barra pinta un giro en el icono mientras carga
} }
struct Shell { struct Shell {
@@ -72,11 +76,12 @@ struct Shell {
win: Option<WinRect>, win: Option<WinRect>,
autostart: Option<bool>, // elección del usuario en el menú ⋮ (None = por defecto, activado) autostart: Option<bool>, // elección del usuario en el menú ⋮ (None = por defecto, activado)
closed: Vec<String>, // URLs de pestañas cerradas (para «Reabrir pestaña cerrada», ⌘⇧T) closed: Vec<String>, // URLs de pestañas cerradas (para «Reabrir pestaña cerrada», ⌘⇧T)
menu_tab: Option<String>, // pestaña sobre la que se abrió el menú de clic derecho
} }
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() } Self { tabs: Vec::new(), active: 0, zoom_idx: ZOOM_100, next_id: 1, win: None, autostart: None, closed: Vec::new(), menu_tab: None }
} }
} }
@@ -143,16 +148,33 @@ fn save_persisted<R: Runtime>(app: &AppHandle<R>) {
// - Reporta título/URL a la barra (observer del <title> + pushState del SPA). // - Reporta título/URL a la barra (observer del <title> + pushState del SPA).
// - Atajos de teclado (en macOS los consume antes el menú nativo; esto cubre Windows y extras) // - Atajos de teclado (en macOS los consume antes el menú nativo; esto cubre Windows y extras)
// y Ctrl+rueda para zoom. // y Ctrl+rueda para zoom.
// En páginas de OTRO origen (p. ej. el login de Authentik) __TAURI__ no está inyectado y el // SOLO debe gobernar el documento principal de NUESTRO origen; ver la guarda de abajo.
// script sale al principio sin tocar nada — la navegación del SSO sigue intacta.
const INIT_JS_TPL: &str = r#" const INIT_JS_TPL: &str = r#"
(function () { (function () {
var ORIGIN = "__ORIGIN__";
// ── GUARDA DE MARCO Y ORIGEN ──
// Antes aquí solo estaba `if (!T || !T.event) return;`, confiando en que __TAURI__ no
// existiría fuera del origen permitido. Eso es cierto en macOS y FALSO en Windows:
// · wry inyecta los scripts de inicio en TODOS los marcos e IGNORA for_main_frame_only
// (wry-0.55.1/src/webview2/mod.rs:492-495, documentado en su lib.rs:990), mientras que
// WKWebView sí la respeta (wry-0.55.1/src/wkwebview/mod.rs:643-645);
// · y __TAURI__ viaja con ellos (withGlobalTauri, tauri/src/manager/webview.rs:159-165).
// Consecuencia real: dentro del iframe del editor de OnlyOffice (docs.labstreamsas.com)
// este script SÍ corría en Windows, con ORIGIN apuntando todavía a la app. Como allí
// `location.href` es el del Document Server, abs() resolvía CADA enlace del editor contra
// docs.* → isExt() cierto → preventDefault() + mandarlo al navegador del sistema (que no
// tiene la sesión: «no se puede acceder»), y window.open devolvía null. Por eso editar y
// descargar fallaban solo en la app de Windows, y no en Mac ni en el navegador.
// En macOS estas dos líneas son un no-op: allí el script nunca ha corrido en subframes.
try { if (window.top !== window.self) return; } catch (e) { return; }
if (location.origin !== ORIGIN) return; // p. ej. el login del SSO: no intervenir
if (window.__lsShell) return; if (window.__lsShell) return;
window.__lsShell = true; window.__lsShell = true;
var T = window.__TAURI__; var T = window.__TAURI__;
if (!T || !T.event) return; // página fuera del origen permitido (p. ej. SSO): no intervenir if (!T || !T.event) return;
var LABEL = "__LABEL__"; var LABEL = "__LABEL__";
var ORIGIN = "__ORIGIN__";
function abs(h) { try { return new URL(h, location.href); } catch (e) { return null; } } function abs(h) { try { return new URL(h, location.href); } catch (e) { return null; } }
function isHttp(u) { return !!u && (u.protocol === 'http:' || u.protocol === 'https:'); } function isHttp(u) { return !!u && (u.protocol === 'http:' || u.protocol === 'https:'); }
@@ -219,6 +241,11 @@ const INIT_JS_TPL: &str = r#"
// Atajos de teclado. // Atajos de teclado.
addEventListener('keydown', function (e) { addEventListener('keydown', function (e) {
// AltGr enciende ctrlKey en Windows. Sin este filtro, escribir [ o ] con teclado
// español —que se hacen con AltGr— entraba por aquí y disparaba history.back() /
// history.forward() en vez de escribir el carácter: en el editor, en el chat, en una
// nota y en cualquier campo de la app. En macOS no aplica (Option no toca ctrlKey).
if (e.altKey) return;
var mod = e.metaKey || e.ctrlKey; var mod = e.metaKey || e.ctrlKey;
if (!mod) return; if (!mod) return;
var k = e.key; var k = e.key;
@@ -301,17 +328,34 @@ fn broadcast<R: Runtime>(app: &AppHandle<R>) {
.tabs .tabs
.iter() .iter()
.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, "url": t.url, "loading": t.loading, "active": i == s.active }))
.collect(); .collect();
json!({ json!({
"tabs": tabs, "tabs": tabs,
"zoom": (ZOOM_STEPS[s.zoom_idx.min(ZOOM_STEPS.len()-1)] * 100.0).round() as u32, "zoom": (ZOOM_STEPS[s.zoom_idx.min(ZOOM_STEPS.len()-1)] * 100.0).round() as u32,
"version": app.package_info().version.to_string(), "version": app.package_info().version.to_string(),
// En macOS la barra ocupa la fila del título (ventana con título en overlay) y
// debe dejar libre el hueco del semáforo; en Windows hay título nativo aparte.
"mac": cfg!(target_os = "macos"),
}) })
}; };
let _ = app.emit_to(EventTarget::webview(CHROME), "ls-tabs", payload); let _ = app.emit_to(EventTarget::webview(CHROME), "ls-tabs", payload);
} }
// Marca una pestaña como «cargando» y refresca la barra (giro en el icono).
fn set_loading<R: Runtime>(app: &AppHandle<R>, label: &str, loading: bool) {
{
let shell = app.state::<ShellState>();
let mut s = shell.lock().unwrap();
let Some(t) = s.tabs.iter_mut().find(|t| t.label == label) else { return };
if t.loading == loading {
return; // sin cambios: no repintar la barra
}
t.loading = loading;
}
broadcast(app);
}
// Muestra la pestaña `label` y esconde las demás. Devuelve el foco al contenido. // Muestra la pestaña `label` y esconde las demás. Devuelve el foco al contenido.
fn activate_tab<R: Runtime>(app: &AppHandle<R>, label: &str) { fn activate_tab<R: Runtime>(app: &AppHandle<R>, label: &str) {
{ {
@@ -354,7 +398,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() }); s.tabs.push(TabRec { label: label.clone(), title: "Labstream OS".into(), url: target.clone(), loading: true });
label label
}; };
@@ -366,13 +410,46 @@ fn create_tab<R: Runtime>(app: &AppHandle<R>, url: Option<String>, activate: boo
let app_zoom = app.clone(); let app_zoom = app.clone();
let builder = WebviewBuilder::new(&label, WebviewUrl::External(parsed)) let builder = WebviewBuilder::new(&label, WebviewUrl::External(parsed))
.initialization_script(&init) .initialization_script(&init)
// Deja pasar las descargas (entregables, exportaciones) con el diálogo nativo de guardado. // Deja pasar las descargas (entregables, exportaciones) Y avisa al terminar.
.on_download(|_webview, _event| true) // El aviso no es un adorno: en WebView2 la interfaz de descargas queda SIEMPRE
// apagada. wry trae un manejador de descargas por defecto (wry-0.55.1/src/lib.rs:830),
// asi que DownloadStarting se registra siempre y siempre responde SetHandled(true)
// (wry-0.55.1/src/webview2/mod.rs:858-861), lo que le dice a WebView2 que del aviso
// se encarga la app: ni barra de progreso, ni globo de «descarga terminada». El
// archivo bajaba bien a la carpeta de Descargas, pero NADA lo decia y parecia roto.
// Quitar este manejador NO devuelve la interfaz nativa: el de wry sigue puesto.
.on_download(|webview, event| {
if let DownloadEvent::Finished { path, success, .. } = &event {
let cuerpo = if !*success {
"No se pudo completar la descarga.".to_string()
} else if let Some(p) = path {
// En macOS `path` llega siempre vacio (limite de WKDownload:
// wry-0.55.1/src/wkwebview/download.rs:98), asi que en la practica
// este brazo con nombre de archivo es el de Windows.
match p.file_name() {
Some(n) => format!("Descargado: {}", n.to_string_lossy()),
None => "Descarga terminada.".to_string(),
}
} else {
"Descarga terminada. Esta en tu carpeta de Descargas.".to_string()
};
let _ = webview
.app_handle()
.notification()
.builder()
.title("Labstream OS")
.body(cuerpo)
.show();
}
true
})
// El zoom guardado se re-aplica en cada carga (el factor no siempre sobrevive a la navegación). // El zoom guardado se re-aplica en cada carga (el factor no siempre sobrevive a la navegación).
.on_page_load(move |wv, payload| { .on_page_load(move |wv, payload| {
if let PageLoadEvent::Finished = payload.event() { let cargando = matches!(payload.event(), PageLoadEvent::Started);
if !cargando {
let _ = wv.set_zoom(current_zoom(&app_zoom)); let _ = wv.set_zoom(current_zoom(&app_zoom));
} }
set_loading(&app_zoom, wv.label(), cargando);
}); });
let created = win.add_child( let created = win.add_child(
@@ -617,6 +694,101 @@ fn show_app_menu<R: Runtime>(app: &AppHandle<R>) {
} }
} }
// Menú de CLIC DERECHO sobre una pestaña. Nativo (la barra mide 40 px: un menú HTML dentro
// del webview quedaría recortado). Recuerda en el estado sobre qué pestaña se abrió.
fn show_tab_menu<R: Runtime>(app: &AppHandle<R>, label: &str) {
let Some(win) = main_window(app) else { return };
let (idx, total) = {
let shell = app.state::<ShellState>();
let mut s = shell.lock().unwrap();
let Some(i) = s.tabs.iter().position(|t| t.label == label) else { return };
s.menu_tab = Some(label.to_string());
(i, s.tabs.len())
};
let build = || -> tauri::Result<Menu<R>> {
Ok(Menu::with_items(
app,
&[
&MenuItem::with_id(app, "tab-duplicate", "Duplicar pestaña", true, None::<&str>)?,
&MenuItem::with_id(app, "tab-copy-url", "Copiar enlace", true, None::<&str>)?,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "tab-close", "Cerrar pestaña", true, None::<&str>)?,
&MenuItem::with_id(app, "tab-close-others", "Cerrar las demás", total > 1, None::<&str>)?,
&MenuItem::with_id(app, "tab-close-right", "Cerrar las de la derecha", idx + 1 < total, None::<&str>)?,
],
)?)
};
if let Ok(menu) = build() {
let _ = win.popup_menu(&menu);
}
}
// Acciones del menú de pestaña. `menu_tab` guarda sobre cuál se abrió (puede no ser la activa).
fn tab_menu_action<R: Runtime>(app: &AppHandle<R>, action: &str) {
let target = {
let shell = app.state::<ShellState>();
let s = shell.lock().unwrap();
s.menu_tab.clone()
};
let Some(label) = target else { return };
// Las que hay que cerrar se calculan ANTES de cerrar nada (los índices se mueven).
let (url, otras, derecha) = {
let shell = app.state::<ShellState>();
let s = shell.lock().unwrap();
let Some(i) = s.tabs.iter().position(|t| t.label == label) else { return };
(
s.tabs[i].url.clone(),
s.tabs.iter().filter(|t| t.label != label).map(|t| t.label.clone()).collect::<Vec<_>>(),
s.tabs.iter().skip(i + 1).map(|t| t.label.clone()).collect::<Vec<_>>(),
)
};
match action {
"tab-duplicate" => create_tab(app, Some(url), true),
"tab-copy-url" => {
let _ = app.clipboard().write_text(url);
}
"tab-close" => close_tab(app, &label),
"tab-close-others" => {
for l in otras {
close_tab(app, &l);
}
}
"tab-close-right" => {
for l in derecha {
close_tab(app, &l);
}
}
_ => {}
}
}
// Reordena las pestañas al soltar una en otra posición (arrastrar en la barra). Llega el
// orden completo de etiquetas; se conserva cuál estaba activa y se guarda la sesión.
fn reorder_tabs<R: Runtime>(app: &AppHandle<R>, order: Vec<String>) {
{
let shell = app.state::<ShellState>();
let mut s = shell.lock().unwrap();
// Solo se acepta una permutación EXACTA de las pestañas actuales (nada de perder o
// duplicar si la barra manda algo raro).
if order.len() != s.tabs.len() || !order.iter().all(|l| s.tabs.iter().any(|t| &t.label == l)) {
return;
}
let activa = s.tabs.get(s.active).map(|t| t.label.clone());
let mut nuevas: Vec<TabRec> = Vec::with_capacity(order.len());
for l in &order {
if let Some(i) = s.tabs.iter().position(|t| &t.label == l) {
nuevas.push(s.tabs.remove(i));
}
}
s.tabs = nuevas;
if let Some(a) = activa {
s.active = s.tabs.iter().position(|t| t.label == a).unwrap_or(0);
}
}
broadcast(app);
save_persisted(app);
}
// Activa/desactiva el arranque con el sistema y RECUERDA la elección (si no, el arranque // Activa/desactiva el arranque con el sistema y RECUERDA la elección (si no, el arranque
// la re-activaba en cada apertura). // la re-activaba en cada apertura).
fn toggle_autostart<R: Runtime>(app: &AppHandle<R>) { fn toggle_autostart<R: Runtime>(app: &AppHandle<R>) {
@@ -723,6 +895,7 @@ pub fn run() {
builder = builder builder = builder
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, None)); .plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, None));
@@ -760,10 +933,18 @@ pub fn run() {
} }
// 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.
// macOS: título en OVERLAY y sin texto → el contenido (y por tanto la barra de
// pestañas) ocupa también la fila del título, al estilo de Notion/VS Code. Los
// botones del semáforo siguen siendo los nativos; la barra les deja su hueco.
// En Windows se conserva el título del sistema con la barra debajo.
let mut wb = WindowBuilder::new(app, "main") let mut wb = WindowBuilder::new(app, "main")
.title("Labstream OS") .title("Labstream OS")
.resizable(true) .resizable(true)
.min_inner_size(1024.0, 700.0); .min_inner_size(1024.0, 700.0);
#[cfg(target_os = "macos")]
{
wb = wb.title_bar_style(tauri::TitleBarStyle::Overlay).hidden_title(true);
}
match saved.win { match saved.win {
Some(r) if r.w >= 600.0 && r.h >= 400.0 => { Some(r) if r.w >= 600.0 && r.h >= 400.0 => {
wb = wb.inner_size(r.w, r.h).position(r.x, r.y); wb = wb.inner_size(r.w, r.h).position(r.x, r.y);
@@ -875,6 +1056,9 @@ pub fn run() {
let _ = app.opener().open_url(url, None::<&str>); let _ = app.opener().open_url(url, None::<&str>);
} }
} }
id @ ("tab-duplicate" | "tab-copy-url" | "tab-close" | "tab-close-others" | "tab-close-right") => {
tab_menu_action(app, id)
}
"autostart-toggle" => toggle_autostart(app), "autostart-toggle" => toggle_autostart(app),
"check-update" => { "check-update" => {
#[cfg(desktop)] #[cfg(desktop)]
@@ -891,7 +1075,11 @@ pub fn run() {
let scale = win.scale_factor().unwrap_or(1.0); let scale = win.scale_factor().unwrap_or(1.0);
let size = win.inner_size()?.to_logical::<f64>(scale); let size = win.inner_size()?.to_logical::<f64>(scale);
win.add_child( win.add_child(
WebviewBuilder::new(CHROME, WebviewUrl::App("index.html".into())), WebviewBuilder::new(CHROME, WebviewUrl::App("index.html".into()))
// Sin esto Tauri instala su manejador de arrastre del SISTEMA en el
// webview y se traga los eventos HTML5 → no se podrían reordenar las
// pestañas arrastrando. La barra no recibe archivos, así que sobra.
.disable_drag_drop_handler(),
LogicalPosition::new(0.0, 0.0), LogicalPosition::new(0.0, 0.0),
LogicalSize::new(size.width, TAB_BAR_H), LogicalSize::new(size.width, TAB_BAR_H),
)?; )?;
@@ -984,6 +1172,21 @@ pub fn run() {
set_badge(&h, n); set_badge(&h, n);
}); });
let h = handle.clone(); let h = handle.clone();
let h_tabmenu = handle.clone();
handle.listen_any("ls-tab-menu", move |e| {
if let Some(label) = field(e.payload(), "label") {
show_tab_menu(&h_tabmenu, &label);
}
});
let h_reorder = handle.clone();
handle.listen_any("ls-reorder", move |e| {
// `order` llega como lista de etiquetas separadas por coma (el payload de los
// eventos del shell es plano: strings y números).
if let Some(orden) = field(e.payload(), "order") {
let v: Vec<String> = orden.split(',').map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect();
reorder_tabs(&h_reorder, v);
}
});
handle.listen_any("ls-menu", move |_| { handle.listen_any("ls-menu", move |_| {
show_app_menu(&h); show_app_menu(&h);
}); });

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