diff --git a/README.md b/README.md index df8e013..833e083 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/manifiesto.json b/manifiesto.json index 3fc6910..60fa014 100644 --- a/manifiesto.json +++ b/manifiesto.json @@ -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" } } \ No newline at end of file diff --git a/organizador.py b/organizador.py index dbd4b6f..2995e02 100644 --- a/organizador.py +++ b/organizador.py @@ -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 diff --git a/reglas.py b/reglas.py index 1ebbd4a..5b9db47 100644 --- a/reglas.py +++ b/reglas.py @@ -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} diff --git a/static/index.html b/static/index.html index 0df53db..f6092de 100644 --- a/static/index.html +++ b/static/index.html @@ -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(`
${esc(base)}\\${esc(dest)}\\${que}
`); + 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', `

${esc(ruta)}

${clientes.map(c => ` `, seguro(async () => { + const tipoDe = c => ((ESTADO.config.clientes[c] || {}).tipo) || 'contenido'; + modal('Añadir carpeta de cliente', `

${esc(ruta)}

+
+ + ${clientes.map(c => ` + Con ese nombre reúne todas sus carpetas, aunque en cada disco estén escritas distinto.
+
+ + Es lo único que decide en qué carpetas va a guardar su material. Se cambia luego en Ajustes.
+

Así va a quedar ordenada:

+
+

Al confirmar solo la lee y te arma una lista de lo que haría. No mueve ni un archivo hasta que tú lo apliques.

`, 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 ----------------