2.2.1: el diálogo de añadir carpeta se explica solo

«Cliente» y «Tipo» no decían para qué servían ni qué iba a pasar al confirmar,
y el tipo se describía por a quién se le hace en vez de por cómo queda ordenado.

- Cada campo es una pregunta con una línea debajo que dice qué hace.
- Debajo se ven, armadas con la plantilla de verdad (ESTADO.plantillas), las
  cuatro carpetas donde va a guardar el material; cambian al cambiar el tipo.
- Si el nombre es un cliente que ya existe, se preselecciona SU tipo. Antes
  salía siempre «contenido», así que una segunda carpeta de un cliente de
  pauta o de proyecto se podía registrar con la plantilla equivocada.
- El diálogo dice que al confirmar solo lee y arma una lista, sin mover nada.
- TIPOS_CLIENTE se reescribe en términos de cómo queda ordenado.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-09-16 22:12:40 -05:00
padre 99d0ba2034
commit 2930320690
Se han modificado 5 ficheros con 80 adiciones y 16 borrados

Ver fichero

@@ -409,6 +409,25 @@ lista seguía diciendo que faltaban dos cosas que en realidad ya no tenían nada
que la app hizo y por qué. En el material real, los 727 GB de las dos carpetas que mentían se resuelven
solos: lo de «02-marzo» va a diciembre de 2025 y lo de «08-Agosto» a abril de 2026.
2.2.1, el diálogo de añadir carpeta se explica solo. Al soltar una carpeta salían dos campos, «Cliente» y
«Tipo», sin decir para qué servían ni qué iba a pasar al confirmar, y el tipo se describía por a quién se le
hace («Contenido mensual (dermatólogas, marca personal): año / mes / pieza») en vez de por cómo queda
ordenado. Ahora cada campo es una pregunta en español con una línea debajo que dice qué hace, y el diálogo
enseña, con la plantilla de verdad del tipo elegido, las cuatro carpetas donde va a guardar el material:
```
V:\Galeano\2026\09- Septiembre\00_Grabacion\ lo que sale de la cámara
V:\Galeano\2026\09- Septiembre\Nombre de la pieza\ cada pieza, con su edición
V:\Galeano\2026\09- Septiembre\_Exportados\ lo que ya se entregó
V:\Galeano\_Recursos\Musica\ música y sonidos de siempre
```
El ejemplo se arma con `ESTADO.plantillas`, o sea que enseña exactamente lo que la app va a hacer y cambia
al cambiar el tipo. Además cierra un fallo: si el nombre que se escribe es un cliente que ya existe, se
preselecciona el tipo que ese cliente ya tiene; antes salía siempre «contenido», así que una segunda carpeta
de un cliente de pauta o de proyecto se podía registrar con la plantilla equivocada. Y el diálogo dice al
final que al confirmar solo lee y arma una lista: no mueve ni un archivo.
Probado el 9 sep 2026 sobre una copia de ensayo: plan → aplicar → deshacer → volver a aplicar deja el árbol
exacto; conversión ffmpeg verificada y reemplazo con original a `_Para borrar`. El puente de Resolve conectó
con Resolve Studio 21.1 pero no pudo abrir proyecto porque Resolve estaba en el Gestor de proyectos:

Ver fichero

@@ -1,19 +1,19 @@
{
"nombre": "OrganiLAB",
"version": "2.2.0",
"builtAt": "2026-09-17T02:58:48.000Z",
"archivo": "OrganizadorLabstream-portatil-2.2.0.zip",
"size": 198699833,
"sha256": "89d8437d47c05d9fcccb456b8b96fc3a2e4efddf4f48c651e992f91aba7f7092",
"version": "2.2.1",
"builtAt": "2026-09-17T03:09:00.000Z",
"archivo": "OrganizadorLabstream-portatil-2.2.1.zip",
"size": 198701797,
"sha256": "ef57218224a0f471b8bc4e6084e1900eaf81b246d8adcda85adfc7d8e2e6cd61",
"app": {
"archivo": "OrganizadorLabstream-app-2.2.0.zip",
"size": 14880084,
"sha256": "e1baad7197559579c3e202f15a2f857b973c696acbb078be19302e7e3e5ed84f",
"url": "/api/herramientas/OrganizadorLabstream-app-2.2.0.zip"
"archivo": "OrganizadorLabstream-app-2.2.1.zip",
"size": 14882048,
"sha256": "9d2d42e964717219da5f19e51ae3a82a4f1f3b010e6b3a97c35a6a69f50ce284",
"url": "/api/herramientas/OrganizadorLabstream-app-2.2.1.zip"
},
"notes": "",
"firma": {
"alg": "rsa2048-pkcs1v15-sha256",
"valor": "663c5b40876996a24ce05dfe9df6a9f71f8a7391b3f2a6ad15865b95038f12eb42836d0bdc42c14437568a387985f95dafe34f3c1e30700fe1f40ca851e56a7c78cf834bbcb21e3da244eecdd0805d8e5dcec754a4e7cfb8a10281e7eea7fde9dff788e3eac74fe57cb94a664a48008b77eb016439b8e1fa459b2d72fcbc168f96569dc0e41610e70b24b45bc2a1c7f282f9f30d76062ac045f623c7b82a415296aef45566de5628ccc3b9b3f7f537d854953a059279b3522d46285c1ed6c52fbf55de26fe2cabe774527f1de67eabf15096b6693f5454fd35835d06801d7837cf3022309ec3a5f0ec5eff77739d06be1e8cff271c715ae43142627b1e7482e4"
"valor": "5e0d56e2a07702140fc9ffdb99ccd2427f3115040760699c350ff0ab538cda5bb93ab665cce32a14087f32f2e595a1c4de548578fe9469cfd73f9821991f42c9bc526f09de5375aff79c6a063a88482d71eab6c831e78bce5536ffcba330384e8a4d522eb677f1ebb9478cda13bc7652a18279480c213798069ab1933a0dc082a1cd25dce90d890f1c6f439aa32295f4ffac7d0075254e21b0f303527e2ff0d828065d3c589e595de2a61a3c7f29314e3ecf15831a3ff7a3c44324988fe124a2c94d4ebf42c973865a4dd176b848f9cb5654065138177dd8f7b784469dff088605182bee33751428ff609731c6171efed010d59cbdf3c60976fe11c00c53b5e6"
}
}

Ver fichero

@@ -34,7 +34,7 @@ import actualizador
asistente.O = sys.modules[__name__]
actualizador.O = sys.modules[__name__]
VERSION = "2.2.0"
VERSION = "2.2.1"
CONGELADO = getattr(sys, "frozen", False)
SIN_VENTANA = getattr(subprocess, "CREATE_NO_WINDOW", 0) # ffmpeg/ffprobe/net sin abrir consolas negras # empaquetado como .exe con PyInstaller

Ver fichero

@@ -473,10 +473,10 @@ def etiqueta_grabacion(nombre: str) -> str:
# --------------------------------------------------------------------------
TIPOS_CLIENTE = {
"contenido": "Contenido mensual (dermatólogas, marca personal): año / mes / pieza",
"pauta": "Pauta y anuncios (campañas por tandas, ganchos, VSL)",
"proyecto": "Proyecto puntual corporativo (un trabajo, una entrega)",
"interno": "Interno de Labstream (recursos compartidos)",
"contenido": "Le hago piezas mes a mes · se ordena por año y mes",
"pauta": "Le hago anuncios y campañas · se ordena por campaña",
"proyecto": "Un trabajo puntual con su entrega · se ordena por fases",
"interno": "Es material de Labstream · se ordena por tipo de recurso",
}
# Destinos relativos a la raíz del cliente. {anio} {mes} = "08-Agosto" {nombre} {fuente}

Ver fichero

@@ -143,6 +143,13 @@
.modal.on{display:flex}
.modal .box{max-width:640px;width:92%;max-height:90vh;overflow:auto;box-shadow:0 20px 60px rgba(0,0,0,.18)}
.modal h3{font-size:15px;text-transform:none;letter-spacing:0;color:var(--tx);font-weight:700}
.preg{margin:14px 0 0}
.preg>label{display:block;font-weight:600;margin-bottom:5px}
.preg>span{display:block;color:var(--mut);font-size:12px;margin-top:4px}
.ejtipo{display:grid;gap:5px;margin:6px 0 0;padding:10px 12px;background:var(--panel2);border-radius:8px}
.ejtipo div{display:flex;gap:4px 10px;align-items:baseline;flex-wrap:wrap}
.ejtipo b{font-family:ui-monospace,Consolas,monospace;font-size:11.5px;font-weight:600;word-break:break-all}
.ejtipo span{font-size:12px;color:var(--mut)}
pre{background:var(--panel2);padding:10px;border-radius:6px;overflow:auto;font-size:11.5px;max-height:280px}
.tree{font-family:ui-monospace,Consolas,monospace;font-size:11.5px;white-space:pre;line-height:1.5;overflow:auto;max-height:420px}
.hint{background:var(--acc2);border:0;border-left:3px solid var(--acc);border-radius:0 6px 6px 0;padding:9px 12px;color:var(--tx);font-size:12.5px;margin-bottom:14px}
@@ -1234,12 +1241,43 @@ $('#btnElegir').onclick = () => elegirCarpeta($('#btnElegir'));
$('#btnElegir2').onclick = e => { e.stopPropagation(); elegirCarpeta($('#btnElegir2')); };
zona.onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); zona.onclick(); } };
$('#vacioCaja').onclick = () => zona.onclick(); $('#vacioCaja').onkeydown = zona.onkeydown;
const MESES_UI = ['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'];
const EJEMPLO_CUBETAS = [['GRABACION', 'lo que sale de la cámara'], ['PIEZA', 'cada pieza, con su edición'],
['EXPORTADOS', 'lo que ya se entregó'], ['MUSICA', 'música y sonidos de siempre'],
['BASURA', 'lo que sobra, para que lo borres tú']];
function ejemploTipo(ruta, tipo){
// se arma con la plantilla de verdad, así que el ejemplo es exactamente lo que la app va a hacer
const pl = (ESTADO.plantillas || {})[tipo] || {}, h = new Date();
const val = {anio: h.getFullYear(), mes: `${String(h.getMonth() + 1).padStart(2, '0')}- ${MESES_UI[h.getMonth()]}`,
nombre: 'Nombre de la pieza', sub: '', fuente: '', rel: ''};
const base = (ruta || '').replace(/[\\/]+$/, ''), visto = new Set(), filas = [];
for (const [cub, que] of EJEMPLO_CUBETAS){
const dest = (pl[cub] || '').replace(/\{(\w+)\}/g, (_, k) => val[k] !== undefined ? String(val[k]) : '')
.replace(/\/+$/, '').replace(/\/+/g, '\\');
if (!dest || visto.has(dest)) continue;
visto.add(dest);
filas.push(`<div><b>${esc(base)}\\${esc(dest)}\\</b><span>${que}</span></div>`);
if (filas.length === 4) break;
}
return filas.join('');
}
function proponerRaiz(ruta){
if (!ruta) return;
const clientes = Object.keys(ESTADO.config.clientes);
const nombre = ruta.replace(/[\\/]+$/, '').split(/[\\/]/).pop();
const sugerido = clientes.find(c => (ESTADO.config.clientes[c].alias || []).concat(c).some(a => a.toLowerCase() === nombre.toLowerCase())) || nombre;
modal('Añadir carpeta de cliente', `<p class="path">${esc(ruta)}</p><label>Cliente <input type="text" id="nuevoCliente" value="${esc(sugerido)}" list="lcli2"></label><datalist id="lcli2">${clientes.map(c => `<option value="${esc(c)}">`).join('')}</datalist> <label>Tipo <select id="nuevoTipo">${Object.entries(ESTADO.tipos).map(([k,v]) => `<option value="${k}">${k} · ${esc(v)}</option>`).join('')}</select></label>`, seguro(async () => {
const tipoDe = c => ((ESTADO.config.clientes[c] || {}).tipo) || 'contenido';
modal('Añadir carpeta de cliente', `<p class="path">${esc(ruta)}</p>
<div class="preg"><label for="nuevoCliente">¿De quién es este material?</label>
<input type="text" id="nuevoCliente" value="${esc(sugerido)}" list="lcli2" style="width:100%">
<datalist id="lcli2">${clientes.map(c => `<option value="${esc(c)}">`).join('')}</datalist>
<span>Con ese nombre reúne todas sus carpetas, aunque en cada disco estén escritas distinto.</span></div>
<div class="preg"><label for="nuevoTipo">¿Qué clase de trabajo le haces?</label>
<select id="nuevoTipo" style="width:100%">${Object.entries(ESTADO.tipos).map(([k,v]) => `<option value="${k}">${esc(v)}</option>`).join('')}</select>
<span>Es lo único que decide en qué carpetas va a guardar su material. Se cambia luego en Ajustes.</span></div>
<p style="margin:14px 0 0"><b>Así va a quedar ordenada:</b></p>
<div class="ejtipo" id="ejemploTipo"></div>
<p class="mut" style="margin-top:12px">Al confirmar solo la lee y te arma una lista de lo que haría. <b>No mueve ni un archivo</b> hasta que tú lo apliques.</p>`, seguro(async () => {
const cliente = $('#nuevoCliente').value.trim() || nombre, tipo = $('#nuevoTipo').value;
const raices = ESTADO.config.raices.filter(r => r.ruta.toLowerCase() !== ruta.toLowerCase()); raices.push({ruta, cliente, tipo});
await api('/api/raices', {raices});
@@ -1248,6 +1286,13 @@ function proponerRaiz(ruta){
await api('/api/escanear', {raiz: raizActual()}); await cargarEstado(); irA('carpeta');
aviso('Carpeta añadida. Escaneando…', 'ok');
}));
const sel = $('#nuevoTipo'), inp = $('#nuevoCliente');
const pinta = () => $('#ejemploTipo').innerHTML = ejemploTipo(ruta, sel.value);
sel.value = tipoDe(sugerido); sel.onchange = pinta;
// si escribe el nombre de un cliente que ya existe, manda el tipo que ese cliente ya tiene: dos
// carpetas del mismo cliente ordenadas con plantillas distintas serían un lío
inp.oninput = () => { const c = ESTADO.config.clientes[inp.value.trim()]; if (c && c.tipo && c.tipo !== sel.value) { sel.value = c.tipo; pinta(); } };
pinta();
}
// ---------------- asistente ----------------