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>
Este commit está contenido en:
226
dist/index.html
vendido
226
dist/index.html
vendido
@@ -7,8 +7,12 @@
|
||||
<!--
|
||||
Barra de pestañas del shell de escritorio (webview "chrome", local).
|
||||
Habla con Rust por eventos (__TAURI__.event):
|
||||
recibe ls-tabs {tabs:[{label,title,active}], zoom}
|
||||
emite ls-ready, ls-new-tab, ls-activate, ls-close-tab, ls-reload, ls-zoom, ls-cycle, ls-activate-n
|
||||
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, 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>
|
||||
/* 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;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
@@ -69,16 +75,16 @@
|
||||
align-self: stretch;
|
||||
margin-bottom: -1px;
|
||||
padding: 0 4px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
#tabs::-webkit-scrollbar { display: none; }
|
||||
.tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
flex: 0 1 220px;
|
||||
min-width: 100px;
|
||||
/* Elástico como en Chrome: se reparten el ancho y se encogen al abrir más. */
|
||||
flex: 1 1 220px;
|
||||
min-width: 42px;
|
||||
max-width: 240px;
|
||||
height: 30px;
|
||||
margin-bottom: 5px;
|
||||
padding: 0 6px 0 10px;
|
||||
@@ -129,11 +135,31 @@
|
||||
right: -8px;
|
||||
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 {
|
||||
flex: 0 0 auto;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 3px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
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 {
|
||||
flex: 1 1 auto;
|
||||
@@ -156,6 +182,11 @@
|
||||
}
|
||||
.tab:hover .cerrar, .tab.active .cerrar { visibility: visible; }
|
||||
.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 {
|
||||
appearance: none;
|
||||
border: 0;
|
||||
@@ -171,7 +202,7 @@
|
||||
.icono {
|
||||
flex: 0 0 auto;
|
||||
width: 30px;
|
||||
margin: 2px 0 7px;
|
||||
height: 26px;
|
||||
font-size: 15px;
|
||||
}
|
||||
#zoom {
|
||||
@@ -179,35 +210,38 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
margin: 2px 0 7px;
|
||||
}
|
||||
#zoom button { height: 100%; }
|
||||
#z-menos, #z-mas { width: 26px; font-size: 14px; }
|
||||
#z-menos, #z-mas { width: 26px; height: 26px; font-size: 14px; }
|
||||
#z-pct {
|
||||
min-width: 44px;
|
||||
height: 26px;
|
||||
padding: 0 4px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--muted);
|
||||
font-size: 11.5px;
|
||||
border-radius: 13px;
|
||||
}
|
||||
#z-pct.activo { color: var(--accent); font-weight: 600; }
|
||||
#version {
|
||||
flex: 0 0 auto;
|
||||
margin: 2px 0 7px;
|
||||
height: 26px;
|
||||
padding: 0 7px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
border-radius: 13px;
|
||||
}
|
||||
#version:hover { color: var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
<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="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="recargar" title="Recargar (⌘/Ctrl+R)">⟳</button>
|
||||
<div id="zoom">
|
||||
@@ -231,31 +265,136 @@
|
||||
const $pct = document.getElementById("z-pct");
|
||||
const $ver = document.getElementById("version");
|
||||
|
||||
// Favicon de las pestañas (look Chrome): mini logo Labstream en SVG inline —
|
||||
// cuadrado indigo con la "L" — para no depender de archivos ni de la red.
|
||||
const FAVICON = "data:image/svg+xml," + encodeURIComponent(
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'>" +
|
||||
"<rect width='16' height='16' rx='4' fill='#6366f1'/>" +
|
||||
"<path d='M5.2 3.8v7h5.6' stroke='#fff' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round' fill='none'/>" +
|
||||
"</svg>",
|
||||
);
|
||||
// Nombre + icono de cada pestaña a partir de su RUTA (mismas etiquetas que la barra
|
||||
// superior de la web). Sin esto todas las pestañas se llamaban «(25) Labstream OS» y
|
||||
// no había forma de distinguirlas.
|
||||
const RUTAS = [
|
||||
[/^\/$/, "🏠", "Inicio"],
|
||||
[/^\/inicio/, "🏠", "Inicio"],
|
||||
[/^\/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() {
|
||||
$tabs.textContent = "";
|
||||
for (const t of estado.tabs) {
|
||||
const { emoji, texto } = nombreDe(t);
|
||||
const el = document.createElement("div");
|
||||
el.className = "tab" + (t.active ? " active" : "");
|
||||
el.title = t.title;
|
||||
el.dataset.label = t.label;
|
||||
el.title = texto;
|
||||
|
||||
const fav = document.createElement("img");
|
||||
fav.className = "favicon";
|
||||
fav.src = FAVICON;
|
||||
fav.alt = "";
|
||||
const fav = document.createElement("span");
|
||||
fav.className = "favicon" + (t.loading ? " cargando" : "");
|
||||
fav.textContent = t.loading ? "" : emoji;
|
||||
el.appendChild(fav);
|
||||
|
||||
const titulo = document.createElement("span");
|
||||
titulo.className = "titulo";
|
||||
titulo.textContent = t.title || "Labstream OS";
|
||||
titulo.textContent = texto;
|
||||
el.appendChild(titulo);
|
||||
|
||||
const x = document.createElement("button");
|
||||
@@ -268,19 +407,40 @@
|
||||
});
|
||||
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).
|
||||
el.addEventListener("auxclick", (e) => {
|
||||
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);
|
||||
}
|
||||
requestAnimationFrame(ajustarCompactas);
|
||||
|
||||
const activa = $tabs.querySelector(".tab.active");
|
||||
if (activa) activa.scrollIntoView({ block: "nearest", inline: "nearest" });
|
||||
|
||||
$pct.textContent = estado.zoom + "%";
|
||||
$pct.classList.toggle("activo", estado.zoom !== 100);
|
||||
$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) {
|
||||
@@ -289,6 +449,8 @@
|
||||
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("adelante").addEventListener("click", () => emitir("ls-nav", { dir: 1 }));
|
||||
|
||||
351
src-tauri/Cargo.lock
generado
351
src-tauri/Cargo.lock
generado
@@ -56,6 +56,27 @@ dependencies = [
|
||||
"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]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
@@ -354,6 +375,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.0"
|
||||
@@ -485,6 +512,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -587,6 +623,12 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
@@ -827,13 +869,19 @@ checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"cssparser",
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
"html5ever",
|
||||
"precomputed-hash",
|
||||
"selectors",
|
||||
"tendril",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "dpi"
|
||||
version = "0.1.2"
|
||||
@@ -959,6 +1007,12 @@ dependencies = [
|
||||
"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]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
@@ -986,6 +1040,12 @@ version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "fax"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
@@ -1021,6 +1081,12 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
@@ -1037,6 +1103,12 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
@@ -1272,6 +1344,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
@@ -1454,12 +1536,32 @@ dependencies = [
|
||||
"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]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
@@ -1740,6 +1842,20 @@ dependencies = [
|
||||
"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]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@@ -1946,13 +2062,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "labstream-desktop"
|
||||
version = "1.4.2"
|
||||
version = "1.5.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-autostart",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-single-instance",
|
||||
@@ -2117,6 +2234,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "muda"
|
||||
version = "0.19.3"
|
||||
@@ -2168,6 +2295,15 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-rust"
|
||||
version = "4.18.0"
|
||||
@@ -2239,6 +2375,7 @@ dependencies = [
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
@@ -2467,6 +2604,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
@@ -2547,6 +2694,17 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "phf"
|
||||
version = "0.13.1"
|
||||
@@ -2631,7 +2789,7 @@ checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"indexmap 2.14.0",
|
||||
"quick-xml",
|
||||
"quick-xml 0.39.4",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
@@ -2768,6 +2926,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "quick-xml"
|
||||
version = "0.39.4"
|
||||
@@ -2777,6 +2947,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
@@ -3801,6 +3980,21 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tauri-plugin-notification"
|
||||
version = "2.3.3"
|
||||
@@ -4065,6 +4259,20 @@ dependencies = [
|
||||
"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]]
|
||||
name = "time"
|
||||
version = "0.3.49"
|
||||
@@ -4375,6 +4583,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
@@ -4646,6 +4865,76 @@ dependencies = [
|
||||
"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]]
|
||||
name = "web-sys"
|
||||
version = "0.3.102"
|
||||
@@ -4757,6 +5046,12 @@ dependencies = [
|
||||
"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]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -5240,6 +5535,24 @@ version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "writeable"
|
||||
version = "0.6.3"
|
||||
@@ -5311,6 +5624,23 @@ dependencies = [
|
||||
"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]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
@@ -5503,6 +5833,21 @@ version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "zvariant"
|
||||
version = "5.12.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "labstream-desktop"
|
||||
version = "1.4.2"
|
||||
version = "1.5.0"
|
||||
description = "Cliente de escritorio de Labstream OS"
|
||||
authors = ["Labstream SAS"]
|
||||
edition = "2021"
|
||||
@@ -17,6 +17,7 @@ tauri-build = { version = "2", features = [] }
|
||||
# `unstable` habilita multiwebview: varios webviews (pestañas) dentro de UNA ventana.
|
||||
tauri = { version = "2", features = ["tray-icon", "unstable"] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-clipboard-manager = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
tauri-plugin-autostart = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
@@ -2,14 +2,22 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Permisos del shell de escritorio: la barra local (chrome) y las pestañas remotas (tab-*).",
|
||||
"windows": ["main"],
|
||||
"webviews": ["chrome", "tab-*"],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"webviews": [
|
||||
"chrome",
|
||||
"tab-*"
|
||||
],
|
||||
"remote": {
|
||||
"urls": ["https://os.labstreamsas.com"]
|
||||
"urls": [
|
||||
"https://os.labstreamsas.com"
|
||||
]
|
||||
},
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"notification:default"
|
||||
"notification:default",
|
||||
"core:window:allow-start-dragging"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -46,14 +46,14 @@ use tauri::{
|
||||
};
|
||||
use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
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 CHROME: &str = "chrome";
|
||||
// Alto de la barra de título nativa de macOS (ventana estándar). Ver `top_offset`.
|
||||
#[cfg(target_os = "macos")]
|
||||
const MACOS_TITLEBAR_H: f64 = 28.0;
|
||||
// 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%.
|
||||
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];
|
||||
@@ -65,6 +65,7 @@ struct TabRec {
|
||||
label: String,
|
||||
title: String,
|
||||
url: String,
|
||||
loading: bool, // la barra pinta un giro en el icono mientras carga
|
||||
}
|
||||
|
||||
struct Shell {
|
||||
@@ -75,11 +76,12 @@ struct Shell {
|
||||
win: Option<WinRect>,
|
||||
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)
|
||||
menu_tab: Option<String>, // pestaña sobre la que se abrió el menú de clic derecho
|
||||
}
|
||||
|
||||
impl Default for Shell {
|
||||
fn default() -> Self {
|
||||
Self { tabs: Vec::new(), active: 0, zoom_idx: ZOOM_100, next_id: 1, win: None, autostart: None, closed: Vec::new() }
|
||||
Self { tabs: Vec::new(), active: 0, zoom_idx: ZOOM_100, next_id: 1, win: None, autostart: None, closed: Vec::new(), menu_tab: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,40 +273,19 @@ fn get_webview<R: Runtime>(app: &AppHandle<R>, label: &str) -> Option<Webview<R>
|
||||
main_window(app).and_then(|w| w.webviews().into_iter().find(|v| v.label() == label))
|
||||
}
|
||||
|
||||
// En macOS las coordenadas de los webviews HIJOS son relativas al MARCO de la ventana
|
||||
// (incluye la barra de título) y `inner_size()` devuelve también el marco. Sin compensar,
|
||||
// la barra de pestañas se dibuja DEBAJO del título nativo y solo asoman ~12 px (además el
|
||||
// título translúcido se teñía del color de la barra). Se detecta en caliente: si el marco y
|
||||
// el interior miden lo mismo hay que compensar; si Tauri lo corrige, la resta deja de ser 0
|
||||
// y no se compensa nada. En Windows/Linux siempre es 0 (ahí las coordenadas ya son del área
|
||||
// de cliente).
|
||||
fn top_offset<R: Runtime>(win: &Window<R>, scale: f64) -> f64 {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let (Ok(outer), Ok(inner)) = (win.outer_size(), win.inner_size()) {
|
||||
let o = outer.to_logical::<f64>(scale).height;
|
||||
let i = inner.to_logical::<f64>(scale).height;
|
||||
if (o - i).abs() < 1.0 {
|
||||
return MACOS_TITLEBAR_H;
|
||||
}
|
||||
}
|
||||
let _ = (win, scale);
|
||||
0.0
|
||||
}
|
||||
|
||||
// Geometría: barra arriba (alto fijo), contenido debajo ocupando el resto.
|
||||
fn layout_all<R: Runtime>(app: &AppHandle<R>) {
|
||||
let Some(win) = main_window(app) else { return };
|
||||
let scale = win.scale_factor().unwrap_or(1.0);
|
||||
let Ok(size) = win.inner_size() else { return };
|
||||
let size = size.to_logical::<f64>(scale);
|
||||
let top = top_offset(&win, scale);
|
||||
let content_h = (size.height - top - TAB_BAR_H).max(0.0);
|
||||
let content_h = (size.height - TAB_BAR_H).max(0.0);
|
||||
for wv in win.webviews() {
|
||||
if wv.label() == CHROME {
|
||||
let _ = wv.set_position(LogicalPosition::new(0.0, top));
|
||||
let _ = wv.set_position(LogicalPosition::new(0.0, 0.0));
|
||||
let _ = wv.set_size(LogicalSize::new(size.width, TAB_BAR_H));
|
||||
} else {
|
||||
let _ = wv.set_position(LogicalPosition::new(0.0, top + TAB_BAR_H));
|
||||
let _ = wv.set_position(LogicalPosition::new(0.0, TAB_BAR_H));
|
||||
let _ = wv.set_size(LogicalSize::new(size.width, content_h));
|
||||
}
|
||||
}
|
||||
@@ -325,17 +306,34 @@ fn broadcast<R: Runtime>(app: &AppHandle<R>) {
|
||||
.tabs
|
||||
.iter()
|
||||
.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();
|
||||
json!({
|
||||
"tabs": tabs,
|
||||
"zoom": (ZOOM_STEPS[s.zoom_idx.min(ZOOM_STEPS.len()-1)] * 100.0).round() as u32,
|
||||
"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);
|
||||
}
|
||||
|
||||
// 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.
|
||||
fn activate_tab<R: Runtime>(app: &AppHandle<R>, label: &str) {
|
||||
{
|
||||
@@ -378,7 +376,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() });
|
||||
s.tabs.push(TabRec { label: label.clone(), title: "Labstream OS".into(), url: target.clone(), loading: true });
|
||||
label
|
||||
};
|
||||
|
||||
@@ -394,16 +392,17 @@ fn create_tab<R: Runtime>(app: &AppHandle<R>, url: Option<String>, activate: boo
|
||||
.on_download(|_webview, _event| true)
|
||||
// El zoom guardado se re-aplica en cada carga (el factor no siempre sobrevive a la navegación).
|
||||
.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));
|
||||
}
|
||||
set_loading(&app_zoom, wv.label(), cargando);
|
||||
});
|
||||
|
||||
let top = top_offset(&win, scale);
|
||||
let created = win.add_child(
|
||||
builder,
|
||||
LogicalPosition::new(0.0, top + TAB_BAR_H),
|
||||
LogicalSize::new(size.width, (size.height - top - TAB_BAR_H).max(0.0)),
|
||||
LogicalPosition::new(0.0, TAB_BAR_H),
|
||||
LogicalSize::new(size.width, (size.height - TAB_BAR_H).max(0.0)),
|
||||
);
|
||||
|
||||
match created {
|
||||
@@ -642,6 +641,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
|
||||
// la re-activaba en cada apertura).
|
||||
fn toggle_autostart<R: Runtime>(app: &AppHandle<R>) {
|
||||
@@ -748,6 +842,7 @@ pub fn run() {
|
||||
|
||||
builder = builder
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, None));
|
||||
@@ -785,10 +880,18 @@ pub fn run() {
|
||||
}
|
||||
|
||||
// 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")
|
||||
.title("Labstream OS")
|
||||
.resizable(true)
|
||||
.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 {
|
||||
Some(r) if r.w >= 600.0 && r.h >= 400.0 => {
|
||||
wb = wb.inner_size(r.w, r.h).position(r.x, r.y);
|
||||
@@ -900,6 +1003,9 @@ pub fn run() {
|
||||
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),
|
||||
"check-update" => {
|
||||
#[cfg(desktop)]
|
||||
@@ -916,8 +1022,12 @@ pub fn run() {
|
||||
let scale = win.scale_factor().unwrap_or(1.0);
|
||||
let size = win.inner_size()?.to_logical::<f64>(scale);
|
||||
win.add_child(
|
||||
WebviewBuilder::new(CHROME, WebviewUrl::App("index.html".into())),
|
||||
LogicalPosition::new(0.0, top_offset(&win, scale)),
|
||||
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),
|
||||
LogicalSize::new(size.width, TAB_BAR_H),
|
||||
)?;
|
||||
|
||||
@@ -1009,6 +1119,21 @@ pub fn run() {
|
||||
set_badge(&h, n);
|
||||
});
|
||||
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 |_| {
|
||||
show_app_menu(&h);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Labstream OS",
|
||||
"version": "1.4.2",
|
||||
"version": "1.5.0",
|
||||
"identifier": "co.labstream.os",
|
||||
"build": {
|
||||
"frontendDist": "../dist"
|
||||
|
||||
Referencia en una nueva incidencia
Block a user