2.0.0: una sola lista de acciones por carpeta
La pantalla «Carpeta» pasa a ser la lista de todo lo que se puede hacer ahora mismo, en orden, con la cifra real de cada cosa y su botón. Era la simplificación de fondo de la auditoría: antes había que bajar tres niveles de navegación para saber qué faltaba en una carpeta. - acciones() y GET /api/carpeta/acciones: todas las cifras salen de consultas baratas (0,04-0,23 s en las carpetas reales); el plan solo se usa si ya estaba calculado. - Se conecta la detección de repetidos, que funcionaba pero no tenía botón. - _gb da terabytes; el peso de «por nombrar» deja de contarse dos veces al anidarse (salían totales mayores que el propio disco). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Este commit está contenido en:
12
README.md
12
README.md
@@ -367,6 +367,18 @@ diálogo en un PowerShell aparte para no bloquear el servidor y devuelve la ruta
|
|||||||
perdía el foco); el buscador de Ajustes ya no borra lo que estabas editando; la lista de stock no salta al
|
perdía el foco); el buscador de Ajustes ya no borra lo que estabas editando; la lista de stock no salta al
|
||||||
principio en cada clic.
|
principio en cada clic.
|
||||||
|
|
||||||
|
2.0.0, una sola lista por carpeta. La pantalla «Carpeta» deja de ser un resumen y pasa a ser la lista de
|
||||||
|
todo lo que se puede hacer ahora mismo, en orden, cada cosa con su cifra real y su botón: apartar basura y
|
||||||
|
proxies, decir qué son las carpetas que no reconoce, ordenar por fecha, ordenar lo demás, apartar stock
|
||||||
|
viejo, juntar repetidos, aligerar los clips y respaldar. Lo que ya está hecho aparece tachado con un visto.
|
||||||
|
Era la simplificación de fondo que salió de la auditoría: antes había que bajar tres niveles de navegación
|
||||||
|
(pestaña, sub-pestaña, paso) para saber qué faltaba en una carpeta.
|
||||||
|
|
||||||
|
Endpoint nuevo `GET /api/carpeta/acciones` (`acciones()`): todas las cifras salen de consultas baratas sobre
|
||||||
|
el inventario, así que tarda entre 0,04 y 0,23 s en las carpetas reales; el plan de orden solo se consulta si
|
||||||
|
ya estaba calculado, y si no el botón dice «Calcular». De paso se conecta a la interfaz la detección de
|
||||||
|
material repetido, que funcionaba por dentro pero no tenía dónde pulsarse, y `_gb` pasa a dar terabytes.
|
||||||
|
|
||||||
Probado el 9 sep 2026 sobre una copia de ensayo: plan → aplicar → deshacer → volver a aplicar deja el árbol
|
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ó
|
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:
|
con Resolve Studio 21.1 pero no pudo abrir proyecto porque Resolve estaba en el Gestor de proyectos:
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"nombre": "OrganiLAB",
|
"nombre": "OrganiLAB",
|
||||||
"version": "1.9.0",
|
"version": "2.0.0",
|
||||||
"builtAt": "2026-09-16T16:06:18.000Z",
|
"builtAt": "2026-09-16T16:44:23.000Z",
|
||||||
"archivo": "OrganizadorLabstream-portatil-1.9.0.zip",
|
"archivo": "OrganizadorLabstream-portatil-2.0.0.zip",
|
||||||
"size": 198688972,
|
"size": 198696162,
|
||||||
"sha256": "288861f0e16f6cfd4b081cd7bd65632d65de45453dd1d1a291d9dabdc47624c9",
|
"sha256": "dbde9ca9d11f8b2620cea2ba216cf4705c3ddd66b1310cbaf0935a7a8a0345c1",
|
||||||
"app": {
|
"app": {
|
||||||
"archivo": "OrganizadorLabstream-app-1.9.0.zip",
|
"archivo": "OrganizadorLabstream-app-2.0.0.zip",
|
||||||
"size": 14869223,
|
"size": 14876413,
|
||||||
"sha256": "bd2786385a7793a54cdc94a13514732b8c4c60f4a5b5f1cfd226e9768e7e5a65",
|
"sha256": "f27d51517adaad20dd43a07697049a5b44469b846db65d47f0b0be97e8e4294e",
|
||||||
"url": "/api/herramientas/OrganizadorLabstream-app-1.9.0.zip"
|
"url": "/api/herramientas/OrganizadorLabstream-app-2.0.0.zip"
|
||||||
},
|
},
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"firma": {
|
"firma": {
|
||||||
"alg": "rsa2048-pkcs1v15-sha256",
|
"alg": "rsa2048-pkcs1v15-sha256",
|
||||||
"valor": "4dfb460c2e17cb978e82089c2206cf273e41fdef2ae0d7b46c128d2bc8c1e65caf6b74cb0e88c13b5ffb01b06a23d3255a747a4f7e2503bfa19a7f80cba25faf3f461349dbbb5e4b987f2f7749e1169fd62ae07083cb726a4494242a7acc81b21469e3b086a73e69c010d9f393c16bfea65b58cf029f5f4b07b5a4267a4f52ac3d65a91e568b16960c926573adfd7c45ab57e0be83054f18261631f18027371ceb48a8a9b7baaef0d8d848cd6099764d3b69248f8e7e03d58f459ff1be92ae9e5a0acb6e30cf17b161cdf34eb47f28b3bffd31c47da661f8c8660898aba6536559436cbf8997fcc575c449495f1993ac2367217fd0e94f60cdfec1bc93000127"
|
"valor": "09b248add12c0bc1ed332398801452ebf3dba6a74bf6e6f94909c5dfbb74a00b85f8aa393ee3edffa700d95b381ba918dbaa0b991d9646963d0b0508a76f4904f952ed2bdf2a4bc8aa66e7ea4edc1b6ef11c04bc1d959a93aca309937db7b662c698d9356984dec3f4a1033e4f844cdc86730a1cd5228a32d40c9ce997453379da96e3055a24fd511e3062cbea029cccf54f05fc95e16cc257abd2fc53cfdcda7bf81695daabbb2e905bcfb378fa1c394b341b70f8e3048c0afb1bba4a99ce53b19433c50ea419e6420a6e17d279b721b56a565d408c133b8cb71de22fdd6b67ead86db2fa80f2c9749d584e7b8b17f2c42d631881695a6efd5d6daf76ecff22"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
155
organizador.py
155
organizador.py
@@ -34,7 +34,7 @@ import actualizador
|
|||||||
asistente.O = sys.modules[__name__]
|
asistente.O = sys.modules[__name__]
|
||||||
actualizador.O = sys.modules[__name__]
|
actualizador.O = sys.modules[__name__]
|
||||||
|
|
||||||
VERSION = "1.9.0"
|
VERSION = "2.0.0"
|
||||||
|
|
||||||
CONGELADO = getattr(sys, "frozen", False)
|
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
|
SIN_VENTANA = getattr(subprocess, "CREATE_NO_WINDOW", 0) # ffmpeg/ffprobe/net sin abrir consolas negras # empaquetado como .exe con PyInstaller
|
||||||
@@ -2699,6 +2699,152 @@ def estado_general():
|
|||||||
"cubetas": R.CUBETAS, "tipos": R.TIPOS_CLIENTE, "perfiles": PERFILES, "formatos": {k: {"nombre": v["nombre"], "que": v["que"], "ext": v["ext"]} for k, v in FORMATOS.items()}, "plantillas": PLANTILLAS}
|
"cubetas": R.CUBETAS, "tipos": R.TIPOS_CLIENTE, "perfiles": PERFILES, "formatos": {k: {"nombre": v["nombre"], "que": v["que"], "ext": v["ext"]} for k, v in FORMATOS.items()}, "plantillas": PLANTILLAS}
|
||||||
|
|
||||||
|
|
||||||
|
def _plural(n, uno, varios):
|
||||||
|
return f"{n:,}".replace(",", ".") + " " + (uno if n == 1 else varios)
|
||||||
|
|
||||||
|
|
||||||
|
def acciones(raiz):
|
||||||
|
"""Todo lo que se puede hacer AHORA MISMO en esta carpeta, en orden, cada cosa con su cifra.
|
||||||
|
|
||||||
|
Es la vista principal: en lugar de repartir el trabajo entre pestañas dentro de pestañas, una sola
|
||||||
|
lista dice qué falta y cuánto pesa. Todo sale de consultas baratas sobre el inventario; lo único
|
||||||
|
que puede costar es el plan de orden, que solo se usa si ya estaba calculado."""
|
||||||
|
raiz = _clave(raiz)
|
||||||
|
r = raiz_cfg(raiz)
|
||||||
|
if not r:
|
||||||
|
return {"error": "carpeta no registrada"}
|
||||||
|
con = db()
|
||||||
|
esc = con.execute("SELECT * FROM escaneos WHERE raiz = ?", (raiz,)).fetchone()
|
||||||
|
out = []
|
||||||
|
|
||||||
|
def añadir(**kw):
|
||||||
|
kw.setdefault("estado", "toca") # toca | hecho | espera
|
||||||
|
out.append(kw)
|
||||||
|
|
||||||
|
if not esc:
|
||||||
|
con.close()
|
||||||
|
añadir(clave="escanear", titulo="Escanear la carpeta", estado="toca", boton="Escanear",
|
||||||
|
que="Lee nombres, tamaños y fechas de todos los archivos. No mueve nada.",
|
||||||
|
cifra="", detalle="En una carpeta grande del NAS tarda unos minutos.")
|
||||||
|
return {"raiz": raiz, "cliente": r["cliente"], "acciones": out, "escaneo": None}
|
||||||
|
|
||||||
|
en_papelera = " AND " + " AND ".join(
|
||||||
|
["ruta NOT LIKE ? ESCAPE '!'"] * len(R.PAPELERAS))
|
||||||
|
args_pap = [esc_like(os.path.join(raiz, pp)) + "%" for pp in R.PAPELERAS]
|
||||||
|
|
||||||
|
# --- 1. basura y proxies que siguen fuera de la papelera ---
|
||||||
|
fila = con.execute("SELECT COUNT(*) n, COALESCE(SUM(bytes),0) b FROM archivos WHERE raiz = ? "
|
||||||
|
"AND (es_basura = 1 OR es_proxy = 1)" + en_papelera, [raiz] + args_pap).fetchone()
|
||||||
|
if fila["n"]:
|
||||||
|
añadir(clave="limpiar", titulo="Apartar basura y proxies", boton="Ver y apartar",
|
||||||
|
que="Archivos de sistema y proxies, que Resolve y Premiere regeneran solos.",
|
||||||
|
cifra=f"{_plural(fila['n'], 'archivo', 'archivos')} · {_gb(fila['b'])}",
|
||||||
|
detalle=f"Van a «{R.PAPELERA}» dentro de la misma carpeta. No se borra nada.",
|
||||||
|
ir="plan", paso="limpiar")
|
||||||
|
else:
|
||||||
|
añadir(clave="limpiar", titulo="Basura y proxies", estado="hecho",
|
||||||
|
que="No queda nada suelto que apartar.", cifra="", detalle="")
|
||||||
|
|
||||||
|
# --- 2. carpetas que la app no reconoce ---
|
||||||
|
voc = vocabulario(raiz)
|
||||||
|
sin = [t for t in voc if not t.get("cubeta") and not t.get("dentro_pieza")]
|
||||||
|
seguras = [t for t in sin if t.get("sug_conf") == "alta"]
|
||||||
|
if sin:
|
||||||
|
# sin sumar bytes: una carpeta sin reconocer dentro de otra sin reconocer contaría dos veces
|
||||||
|
# el mismo material, y salían totales mayores que el propio disco
|
||||||
|
con_sug = [t for t in sin if t.get("sugerencia")]
|
||||||
|
añadir(clave="nombrar", titulo="Decir qué son " + _plural(len(sin), "carpeta", "carpetas"), boton="Nombrarlas" if len(sin) > 1 else "Nombrarla",
|
||||||
|
que="Sin esto la app no las mueve: se quedan donde están.",
|
||||||
|
cifra=_plural(len(sin), "carpeta", "carpetas") + (f" · {len(seguras)} de un clic" if seguras else ""),
|
||||||
|
detalle=(f"La app propone qué son {len(con_sug)} de ellas mirando lo que tienen dentro"
|
||||||
|
+ (f", y {len(seguras)} con bastante seguridad como para aceptarlas todas juntas." if seguras else ".")
|
||||||
|
if con_sug else "Puedes dejarlas sin nombrar: simplemente no se moverán."),
|
||||||
|
ir="nombrar")
|
||||||
|
else:
|
||||||
|
añadir(clave="nombrar", titulo="Carpetas reconocidas", estado="hecho",
|
||||||
|
que="La app sabe qué es cada carpeta.", cifra="", detalle="")
|
||||||
|
|
||||||
|
# --- 3 y 4. ordenar (usa el plan solo si ya estaba calculado) ---
|
||||||
|
plan = PLANES.get(raiz)
|
||||||
|
for clave, titulo, que in (("fechas", "Ordenar por año y mes",
|
||||||
|
"Meses con nombre completo, grabaciones repartidas por mes y cámara, piezas numeradas."),
|
||||||
|
("resto", "Ordenar lo demás",
|
||||||
|
"Exportados, proyectos, música y marca a su carpeta.")):
|
||||||
|
if plan:
|
||||||
|
fs = [f for f in plan["filas"] if f.get("paso") == clave]
|
||||||
|
marc = [f for f in fs if f.get("marcado")]
|
||||||
|
if fs:
|
||||||
|
añadir(clave=clave, titulo=titulo, boton="Ver y aplicar", que=que,
|
||||||
|
cifra=f"{_plural(len(fs), 'cambio', 'cambios')} · {_gb(sum(f['bytes'] for f in fs))}",
|
||||||
|
detalle=(f"{len(marc)} están marcados; el resto son dudosos y no se mueven salvo que los marques."
|
||||||
|
if len(marc) < len(fs) else "Todo se mueve dentro de la misma carpeta."),
|
||||||
|
ir="plan", paso=clave)
|
||||||
|
else:
|
||||||
|
añadir(clave=clave, titulo=titulo, estado="hecho", que="Nada que mover.", cifra="", detalle="")
|
||||||
|
else:
|
||||||
|
añadir(clave=clave, titulo=titulo, boton="Calcular", que=que, cifra="",
|
||||||
|
detalle="Hay que calcular el plan para saber cuánto es.", ir="plan", paso=clave)
|
||||||
|
|
||||||
|
# --- 5. stock de Envato viejo ---
|
||||||
|
tope = limite_meses(3)
|
||||||
|
viejo = [f for f in con.execute(
|
||||||
|
"SELECT bytes, fecha, anio_ruta, mes_ruta FROM archivos WHERE raiz = ? AND es_stock = 1 AND es_basura = 0"
|
||||||
|
+ en_papelera, [raiz] + args_pap).fetchall() if es_viejo(f, tope)]
|
||||||
|
if viejo:
|
||||||
|
añadir(clave="stock", titulo="Apartar stock de Envato viejo", boton="Ver y apartar",
|
||||||
|
que="De proyectos ya publicados: se vuelve a descargar con la suscripción.",
|
||||||
|
cifra=f"{_plural(len(viejo), 'archivo', 'archivos')} · {_gb(sum(f['bytes'] for f in viejo))}",
|
||||||
|
detalle="Son los de proyectos de más de 3 meses. Puedes ajustar el corte.",
|
||||||
|
ir="stock")
|
||||||
|
|
||||||
|
# --- 6. material repetido ---
|
||||||
|
rep_fila = con.execute(
|
||||||
|
"SELECT COALESCE(SUM(n - 1), 0) n, COALESCE(SUM((n - 1) * bytes), 0) b FROM "
|
||||||
|
"(SELECT nombre, bytes, COUNT(*) n FROM archivos WHERE raiz = ? AND es_basura = 0 AND es_proxy = 0 "
|
||||||
|
"AND bytes > 5000000" + en_papelera + " GROUP BY nombre, bytes HAVING COUNT(*) > 1)",
|
||||||
|
[raiz] + args_pap).fetchone()
|
||||||
|
if rep_fila["n"]:
|
||||||
|
añadir(clave="repetidos", titulo="Juntar material repetido", boton="Ver repetidos",
|
||||||
|
que="Mismo nombre y mismo peso en varias carpetas.",
|
||||||
|
cifra=f"{_plural(rep_fila['n'], 'copia de más', 'copias de más')} · {_gb(rep_fila['b'])}",
|
||||||
|
detalle="Se comprueba el contenido antes de tocar nada; la copia buena se queda donde está.",
|
||||||
|
ir="repetidos")
|
||||||
|
|
||||||
|
# --- 7. convertir clips viejos ---
|
||||||
|
o = CONFIG["optimizar"]
|
||||||
|
cam = con.execute(
|
||||||
|
"SELECT COUNT(*) n, COALESCE(SUM(bytes),0) b, SUM(analizado = 1) ana FROM archivos WHERE raiz = ? "
|
||||||
|
"AND rol IN ('camara_raw','camara_video') AND es_proxy = 0 AND es_basura = 0 AND bytes >= ?" + en_papelera,
|
||||||
|
[raiz, int(o.get("min_mb", 300)) * 1024 * 1024] + args_pap).fetchone()
|
||||||
|
hechos = con.execute("SELECT COUNT(*) n FROM trabajos WHERE lower(raiz) = ? AND estado = 'reemplazado'",
|
||||||
|
(raiz.lower(),)).fetchone()["n"]
|
||||||
|
if cam["n"]:
|
||||||
|
if not (cam["ana"] or 0):
|
||||||
|
añadir(clave="convertir", titulo="Aligerar los clips de cámara", boton="Leer los clips",
|
||||||
|
que="Pasan a un formato unas 10 veces más ligero. El original no se toca hasta que reemplaces.",
|
||||||
|
cifra=f"{_plural(cam['n'], 'clip', 'clips')} · {_gb(cam['b'])}",
|
||||||
|
detalle="Primero hay que leerlos para saber cuáles conviene convertir. Va en segundo plano.",
|
||||||
|
ir="convertir")
|
||||||
|
else:
|
||||||
|
añadir(clave="convertir", titulo="Aligerar los clips de cámara", boton="Ver clips",
|
||||||
|
que="Pasan a un formato unas 10 veces más ligero. El original no se toca hasta que reemplaces.",
|
||||||
|
cifra=f"{_plural(cam['n'], 'clip', 'clips')} · {_gb(cam['b'])}",
|
||||||
|
detalle=f"{cam['ana']} ya están leídos" + (f" y {hechos} reemplazados." if hechos else "."),
|
||||||
|
ir="convertir")
|
||||||
|
|
||||||
|
# --- 8. respaldo, al final ---
|
||||||
|
dest = CONFIG.get("respaldo", {}).get("destino", "")
|
||||||
|
añadir(clave="respaldar", titulo="Copiar el respaldo a otro disco",
|
||||||
|
boton="Preparar respaldo", que="Una copia compacta con los clips ya convertidos, y un índice.",
|
||||||
|
cifra="", estado="espera" if any(a["estado"] == "toca" for a in out) else "toca",
|
||||||
|
detalle=(f"Destino: {dest}" if dest else "Falta indicar el disco de destino.") +
|
||||||
|
" Conviene hacerlo cuando lo demás esté hecho.",
|
||||||
|
ir="respaldar")
|
||||||
|
|
||||||
|
con.close()
|
||||||
|
return {"raiz": raiz, "cliente": r["cliente"], "acciones": out, "escaneo": dict(esc)}
|
||||||
|
|
||||||
|
|
||||||
def estado_carpeta(ruta):
|
def estado_carpeta(ruta):
|
||||||
"""El recorrido de una carpeta: qué paso está hecho, cuál toca y qué lo bloquea."""
|
"""El recorrido de una carpeta: qué paso está hecho, cuál toca y qué lo bloquea."""
|
||||||
raiz = _clave(ruta)
|
raiz = _clave(ruta)
|
||||||
@@ -2815,6 +2961,8 @@ class Manejador(BaseHTTPRequestHandler):
|
|||||||
if ruta == "/api/soltado":
|
if ruta == "/api/soltado":
|
||||||
r = soltado(q.get("nombre", ""))
|
r = soltado(q.get("nombre", ""))
|
||||||
return self._json({"ruta": r, "es_archivo": bool(r) and os.path.isfile(r)})
|
return self._json({"ruta": r, "es_archivo": bool(r) and os.path.isfile(r)})
|
||||||
|
if ruta == "/api/carpeta/acciones":
|
||||||
|
return self._json(acciones(q["raiz"]))
|
||||||
if ruta == "/api/carpeta/estado":
|
if ruta == "/api/carpeta/estado":
|
||||||
return self._json(estado_carpeta(q["raiz"]))
|
return self._json(estado_carpeta(q["raiz"]))
|
||||||
if ruta == "/api/mapa":
|
if ruta == "/api/mapa":
|
||||||
@@ -3104,7 +3252,10 @@ La app NUNCA borra: lo que sobra va a _Para borrar dentro de la misma carpeta (l
|
|||||||
|
|
||||||
|
|
||||||
def _gb(b):
|
def _gb(b):
|
||||||
return f"{(b or 0) / 1e9:.1f} GB" if (b or 0) >= 1e8 else f"{(b or 0) / 1e6:.0f} MB"
|
b = b or 0
|
||||||
|
if b >= 1e12:
|
||||||
|
return f"{b / 1e12:.2f} TB"
|
||||||
|
return f"{b / 1e9:.1f} GB" if b >= 1e8 else f"{b / 1e6:.0f} MB"
|
||||||
|
|
||||||
|
|
||||||
def _opcion(args, nombre, defecto=None):
|
def _opcion(args, nombre, defecto=None):
|
||||||
|
|||||||
@@ -177,6 +177,22 @@
|
|||||||
.actualizacion{border-color:var(--acc)!important;background:var(--acc2)!important;margin-bottom:18px}
|
.actualizacion{border-color:var(--acc)!important;background:var(--acc2)!important;margin-bottom:18px}
|
||||||
/* nombrar */
|
/* nombrar */
|
||||||
.nombrar{list-style:none;margin:0;padding:0}
|
.nombrar{list-style:none;margin:0;padding:0}
|
||||||
|
.acc{display:flex;flex-direction:column;gap:8px;margin-bottom:18px}
|
||||||
|
.ac{display:grid;grid-template-columns:26px minmax(0,1fr) auto;gap:14px;align-items:center;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:12px 14px}
|
||||||
|
.ac.hecho{background:transparent;border-style:dashed;opacity:.62}
|
||||||
|
.ac.espera{opacity:.72}
|
||||||
|
.ac .ico{width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;flex:none;background:var(--acc2);color:var(--acc)}
|
||||||
|
.ac.hecho .ico{background:var(--ok2);color:var(--ok)}
|
||||||
|
.ac.espera .ico{background:var(--panel2);color:var(--mut)}
|
||||||
|
.ac .tx{min-width:0}
|
||||||
|
.ac .tx b{font-size:14px;display:block}
|
||||||
|
.ac .tx .q{font-size:12.5px;color:var(--mut);margin-top:1px}
|
||||||
|
.ac .tx .d{font-size:12px;color:var(--mut);margin-top:3px}
|
||||||
|
.ac .der{display:flex;flex-direction:column;align-items:flex-end;gap:5px;flex:none}
|
||||||
|
.ac .cifra{font-size:13px;font-weight:700;white-space:nowrap;font-variant-numeric:tabular-nums}
|
||||||
|
.ac.hecho .cifra{font-weight:500;color:var(--mut)}
|
||||||
|
.ac button{white-space:nowrap}
|
||||||
|
@media (max-width:820px){ .ac{grid-template-columns:26px minmax(0,1fr)} .ac .der{grid-column:2;align-items:flex-start} }
|
||||||
.paso{background:var(--panel);border:1px solid var(--line);border-radius:8px;margin-bottom:10px}
|
.paso{background:var(--panel);border:1px solid var(--line);border-radius:8px;margin-bottom:10px}
|
||||||
.paso .pc{display:flex;gap:12px;align-items:center;padding:12px 14px}
|
.paso .pc{display:flex;gap:12px;align-items:center;padding:12px 14px}
|
||||||
.paso .num{width:26px;height:26px;border-radius:50%;background:var(--acc2);color:var(--acc);font-weight:700;display:flex;align-items:center;justify-content:center;flex:none;font-size:13px}
|
.paso .num{width:26px;height:26px;border-radius:50%;background:var(--acc2);color:var(--acc);font-weight:700;display:flex;align-items:center;justify-content:center;flex:none;font-size:13px}
|
||||||
@@ -312,7 +328,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="inicioLleno">
|
<div id="inicioLleno">
|
||||||
<div class="cab"><div><h2 class="v" id="cabTitulo">…</h2><div class="sub" id="cabSub"></div></div><div class="der"><button class="lnk" id="btnEscanear">Volver a escanear</button><button class="lnk" id="btnAsistente">Asistente paso a paso</button><button class="lnk" id="btnDesconectar" title="La carpeta deja de aparecer en la lista de la izquierda. No borra ni mueve nada del disco.">Quitar de la lista</button></div></div>
|
<div class="cab"><div><h2 class="v" id="cabTitulo">…</h2><div class="sub" id="cabSub"></div></div><div class="der"><button class="lnk" id="btnEscanear">Volver a escanear</button><button class="lnk" id="btnAsistente">Asistente paso a paso</button><button class="lnk" id="btnDesconectar" title="La carpeta deja de aparecer en la lista de la izquierda. No borra ni mueve nada del disco.">Quitar de la lista</button></div></div>
|
||||||
<div class="sig" id="siguiente"></div>
|
<div id="acciones" class="acc"></div>
|
||||||
|
<div class="sig" id="siguiente" style="display:none"></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="box plano"><h3>Qué hay en la carpeta</h3><div id="pesoBarra" class="peso"></div><div id="pesoLinea" class="peso-linea"></div><div id="inicioDiag" style="margin-top:8px;font-size:12.5px"></div></div>
|
<div class="box plano"><h3>Qué hay en la carpeta</h3><div id="pesoBarra" class="peso"></div><div id="pesoLinea" class="peso-linea"></div><div id="inicioDiag" style="margin-top:8px;font-size:12.5px"></div></div>
|
||||||
<div class="box plano"><h3>Últimos cambios en esta carpeta</h3><div id="inicioLotes" class="mut">Todavía no se ha movido nada.</div></div>
|
<div class="box plano"><h3>Últimos cambios en esta carpeta</h3><div id="inicioLotes" class="mut">Todavía no se ha movido nada.</div></div>
|
||||||
@@ -689,6 +706,7 @@ async function cargarCarpeta(){
|
|||||||
const e = CARPETA.escaneo;
|
const e = CARPETA.escaneo;
|
||||||
$('#cabSub').innerHTML = `<span class="path">${esc(r.ruta)}</span> · ${esc(ESTADO.tipos[r.tipo] || r.tipo)}${e ? ` · ${num(e.n)} archivos · ${gb(e.bytes)} · escaneada ${fecha(e.ts)}` : ' · sin escanear'}`;
|
$('#cabSub').innerHTML = `<span class="path">${esc(r.ruta)}</span> · ${esc(ESTADO.tipos[r.tipo] || r.tipo)}${e ? ` · ${num(e.n)} archivos · ${gb(e.bytes)} · escaneada ${fecha(e.ts)}` : ' · sin escanear'}`;
|
||||||
pintarIndice(); pintarSiguiente(pasosDe(CARPETA, r));
|
pintarIndice(); pintarSiguiente(pasosDe(CARPETA, r));
|
||||||
|
pintarAcciones().catch(e => console.warn(e));
|
||||||
const d = CARPETA.diagnostico;
|
const d = CARPETA.diagnostico;
|
||||||
if (!d) { $('#pesoBarra').innerHTML = ''; $('#pesoLinea').innerHTML = ''; $('#inicioDiag').innerHTML = '<span class="mut">Escanea la carpeta para ver qué hay.</span>'; }
|
if (!d) { $('#pesoBarra').innerHTML = ''; $('#pesoLinea').innerHTML = ''; $('#inicioDiag').innerHTML = '<span class="mut">Escanea la carpeta para ver qué hay.</span>'; }
|
||||||
else {
|
else {
|
||||||
@@ -700,6 +718,28 @@ async function cargarCarpeta(){
|
|||||||
$('#inicioLotes').innerHTML = lotes.length ? lotes.map(l => `<div class="ev"><span class="h">${esc(fechaCorta(l.ts))}</span><span class="q"><b>${esc(queSeHizo(l.lote))}</b> · ${l.movimientos} mov.${l.errores ? ` <span class="bad">· ${l.errores} avisos</span>` : ''}</span>${l.lote && l.lote.startsWith('optimizar') ? '' : `<button data-l="${esc(l.lote)}">Deshacer</button>`}</div>`).join('') : '<span class="mut">Todavía no se ha movido nada en esta carpeta.</span>';
|
$('#inicioLotes').innerHTML = lotes.length ? lotes.map(l => `<div class="ev"><span class="h">${esc(fechaCorta(l.ts))}</span><span class="q"><b>${esc(queSeHizo(l.lote))}</b> · ${l.movimientos} mov.${l.errores ? ` <span class="bad">· ${l.errores} avisos</span>` : ''}</span>${l.lote && l.lote.startsWith('optimizar') ? '' : `<button data-l="${esc(l.lote)}">Deshacer</button>`}</div>`).join('') : '<span class="mut">Todavía no se ha movido nada en esta carpeta.</span>';
|
||||||
$$('#inicioLotes button[data-l]').forEach(bt => bt.onclick = () => deshacer(bt.dataset.l));
|
$$('#inicioLotes button[data-l]').forEach(bt => bt.onclick = () => deshacer(bt.dataset.l));
|
||||||
}
|
}
|
||||||
|
// ---------------- lo que se puede hacer en esta carpeta ----------------
|
||||||
|
// Una sola lista, en orden, con la cifra de cada cosa y un botón. Sustituye a repartir el trabajo
|
||||||
|
// entre pestañas dentro de pestañas: el usuario ve de un vistazo qué falta y cuánto pesa.
|
||||||
|
const IR_ACCION = {
|
||||||
|
plan: p => { irA('revisar'); subRev('plan'); if (!PLAN) $('#btnPlan').click(); else if (p) { PLAN_ABIERTOS = new Set([p]); pintarPlan(); } },
|
||||||
|
nombrar: () => { irA('revisar'); subRev('nombrar'); },
|
||||||
|
stock: () => { irA('revisar'); subRev('stock'); if (!STOCK.length) $('#btnStock').click(); },
|
||||||
|
repetidos: () => { irA('revisar'); subRev('hay'); const d = $('#dups'); if (d) d.scrollIntoView({block: 'center', behavior: 'smooth'}); },
|
||||||
|
convertir: () => { irA('convertir'); $('#btnBuscarClips').click(); },
|
||||||
|
respaldar: () => irA('respaldar'),
|
||||||
|
escanear: () => $('#btnEscanear').click(),
|
||||||
|
};
|
||||||
|
async function pintarAcciones(){
|
||||||
|
const raiz = raizActual(); if (!raiz) return;
|
||||||
|
const d = await api('/api/carpeta/acciones?raiz=' + encodeURIComponent(raiz));
|
||||||
|
if (raizActual() !== raiz) return; // cambió de carpeta mientras llegaba
|
||||||
|
ACCIONES = d.acciones || [];
|
||||||
|
const quedan = ACCIONES.filter(a => a.estado === 'toca').length;
|
||||||
|
$('#acciones').innerHTML = `<div class="cabsec">${quedan ? `Lo que se puede hacer ahora · ${quedan}` : 'Esta carpeta está al día'}</div>` +
|
||||||
|
ACCIONES.map((a, i) => `<div class="ac ${a.estado}"><span class="ico">${a.estado === 'hecho' ? '✓' : a.estado === 'espera' ? '·' : i + 1}</span><div class="tx"><b>${esc(a.titulo)}</b><div class="q">${esc(a.que)}</div>${a.detalle ? `<div class="d">${esc(a.detalle)}</div>` : ''}</div><div class="der">${a.cifra ? `<span class="cifra">${esc(a.cifra)}</span>` : ''}${a.boton ? `<button class="${a.estado === 'espera' ? 'lnk' : 'p'}" data-a="${i}">${esc(a.boton)}</button>` : ''}</div></div>`).join('');
|
||||||
|
$$('#acciones [data-a]').forEach(b => b.onclick = () => { const a = ACCIONES[+b.dataset.a]; (IR_ACCION[a.ir] || (() => {}))(a.paso); });
|
||||||
|
}
|
||||||
function queSeHizo(lote){
|
function queSeHizo(lote){
|
||||||
if (!lote) return 'Cambio';
|
if (!lote) return 'Cambio';
|
||||||
if (lote === 'optimizar' || lote.startsWith('optimizar-')) return 'Reemplazó originales por la versión convertida';
|
if (lote === 'optimizar' || lote.startsWith('optimizar-')) return 'Reemplazó originales por la versión convertida';
|
||||||
@@ -866,7 +906,7 @@ const PASOS_PLAN = [
|
|||||||
{k: 'resto', t: 'Ordenar lo demás', q: 'Exportados, proyectos, música, marca y demás van a su carpeta. Lo dudoso queda sin marcar: la app no lo mueve salvo que lo marques tú.', vacio: 'Nada más que ordenar.'},
|
{k: 'resto', t: 'Ordenar lo demás', q: 'Exportados, proyectos, música, marca y demás van a su carpeta. Lo dudoso queda sin marcar: la app no lo mueve salvo que lo marques tú.', vacio: 'Nada más que ordenar.'},
|
||||||
];
|
];
|
||||||
const SEG_TXT = {alta: 'seguro', media: 'probable', baja: 'dudoso'};
|
const SEG_TXT = {alta: 'seguro', media: 'probable', baja: 'dudoso'};
|
||||||
let PLAN_ABIERTOS = new Set();
|
let PLAN_ABIERTOS = new Set(), ACCIONES = [];
|
||||||
function estadoPlan(){
|
function estadoPlan(){
|
||||||
const hay = !!PLAN;
|
const hay = !!PLAN;
|
||||||
$('#btnArbol').disabled = !hay;
|
$('#btnArbol').disabled = !hay;
|
||||||
@@ -941,7 +981,7 @@ function aplicarFilas(m, btn, titulo){
|
|||||||
PLAN = null; MAPA = null;
|
PLAN = null; MAPA = null;
|
||||||
aviso(`${res.movimientos} movimiento${res.movimientos !== 1 ? 's' : ''} hecho${res.movimientos !== 1 ? 's' : ''}.` + (res.errores.length ? ` ${res.errores.length} no se pudieron.` : ''), res.errores.length ? 'bad' : 'ok', {texto: 'Deshacer esto', ir: () => deshacer(res.lote)});
|
aviso(`${res.movimientos} movimiento${res.movimientos !== 1 ? 's' : ''} hecho${res.movimientos !== 1 ? 's' : ''}.` + (res.errores.length ? ` ${res.errores.length} no se pudieron.` : ''), res.errores.length ? 'bad' : 'ok', {texto: 'Deshacer esto', ir: () => deshacer(res.lote)});
|
||||||
if (res.errores.length) modal('Algunos no se pudieron mover', `<pre>${esc(res.errores.join('\n'))}</pre>`, null);
|
if (res.errores.length) modal('Algunos no se pudieron mover', `<pre>${esc(res.errores.join('\n'))}</pre>`, null);
|
||||||
await cargarEstado(); await calcularPlan();
|
await cargarEstado(); await calcularPlan(); pintarAcciones().catch(() => {});
|
||||||
}));
|
}));
|
||||||
$('#modalSi').textContent = 'Aplicar';
|
$('#modalSi').textContent = 'Aplicar';
|
||||||
}
|
}
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user