Plan por pasos en frases, sugerencias en Por nombrar, stock por antigüedad, respaldo por lotes, arrastre con ruta real, barra de tareas
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
123
organizador.py
123
organizador.py
@@ -525,6 +525,10 @@ class Plan:
|
||||
"archivos": [a[0] for a in archivos] if archivos else None,
|
||||
"marcado": (confianza in ("alta", "media")) if marcado is None else marcado,
|
||||
"cosmetico": marcado is False and accion == "renombrar",
|
||||
# en qué paso del recorrido se aplica: 1 limpiar (basura/proxies), 2 por fecha (años, meses, grabaciones,
|
||||
# piezas), 3 el resto (exportados, proyectos, marca, música, dudosos)
|
||||
"paso": "limpiar" if accion == "papelera" or cubeta in ("BASURA", "PROXY") else
|
||||
"fechas" if cubeta in ("MES", "ANIO", "GRABACION", "PIEZA", "CELULAR") else "resto",
|
||||
})
|
||||
|
||||
def suelto(self, fila, cubeta, destino_rel, motivo, confianza):
|
||||
@@ -1081,10 +1085,65 @@ def vocabulario(raiz):
|
||||
t["bytes"] += c["bytes"] or 0
|
||||
if len(t["rutas"]) < 4:
|
||||
t["rutas"].append(rel)
|
||||
if tokens:
|
||||
con = db()
|
||||
for t in tokens.values():
|
||||
if not t["cubeta"]:
|
||||
_sugerir(con, raiz, t)
|
||||
con.close()
|
||||
out = sorted(tokens.values(), key=lambda t: (-t["bytes"], t["token"]))
|
||||
return out
|
||||
|
||||
|
||||
_ROL_FRASE = {"camara": "clips de cámara", "proyecto": "proyectos de edición / diseño", "audio": "audio",
|
||||
"imagen": "imágenes", "documento": "documentos", "video": "videos", "otro": "otros archivos"}
|
||||
|
||||
|
||||
def _sugerir(con, raiz, t):
|
||||
"""Qué es probablemente esta carpeta, mirando lo que tiene dentro y a qué se parece su nombre.
|
||||
Deja t["sugerencia"], t["sug_conf"], t["por_que"] y t["contenido"] (resumen legible)."""
|
||||
import difflib
|
||||
conteo = defaultdict(lambda: [0, 0])
|
||||
total = [0, 0]
|
||||
for rel in t["rutas"][:3]:
|
||||
for f in archivos_bajo(con, os.path.join(raiz, rel)):
|
||||
if f["es_basura"] or f["es_proxy"]:
|
||||
continue
|
||||
rol = f["rol"]
|
||||
k = ("camara" if rol in ("camara_raw", "camara_video", "camara_sidecar", "audio_grabado") else
|
||||
"proyecto" if rol in ("proyecto", "diseno") else
|
||||
"audio" if rol in ("audio",) else
|
||||
"imagen" if rol in ("imagen", "foto_camara", "celular_foto") else
|
||||
"documento" if rol == "documento" else
|
||||
"video" if rol in ("video", "celular_video") else "otro")
|
||||
conteo[k][0] += 1
|
||||
conteo[k][1] += f["bytes"]
|
||||
total[0] += 1
|
||||
total[1] += f["bytes"]
|
||||
orden = sorted(conteo.items(), key=lambda kv: -kv[1][1])
|
||||
t["contenido"] = " · ".join(f"{n} {_ROL_FRASE[k]}" for k, (n, b) in orden[:3]) if orden else "vacía o solo subcarpetas"
|
||||
sug, conf, por_que = "", "", ""
|
||||
# a) por el nombre: se parece a una palabra del diccionario
|
||||
dicc = DICC["carpetas"]
|
||||
cerca = difflib.get_close_matches(t["token"], list(dicc.keys()), n=1, cutoff=0.82)
|
||||
if cerca and dicc[cerca[0]] not in ("CONTENEDOR", "IGNORAR"):
|
||||
sug, conf, por_que = dicc[cerca[0]], "media", f"el nombre se parece a «{cerca[0]}»"
|
||||
# b) por el contenido (manda si es contundente)
|
||||
if total[0] >= 3 and orden:
|
||||
k, (n, b) = orden[0]
|
||||
frac = b / total[1] if total[1] else n / total[0]
|
||||
if frac >= 0.7:
|
||||
por_cont = {"camara": "GRABACION", "proyecto": "PROYECTOS", "audio": "MUSICA", "imagen": "FOTOS",
|
||||
"documento": "DOCUMENTOS", "video": "EXPORTADOS"}.get(k)
|
||||
if por_cont:
|
||||
if sug and sug != por_cont:
|
||||
por_que += f"; pero dentro casi todo son {_ROL_FRASE[k]}"
|
||||
conf = "baja"
|
||||
else:
|
||||
sug, conf, por_que = por_cont, ("alta" if frac >= 0.9 and n >= 3 else "media"), f"dentro casi todo son {_ROL_FRASE[k]} ({round(frac * 100)} %)"
|
||||
t["sugerencia"], t["sug_conf"], t["por_que"] = sug, conf, por_que
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# Mapa
|
||||
# ==========================================================================
|
||||
@@ -1359,7 +1418,7 @@ def encolar(rutas, perfil, chroma, color, raiz):
|
||||
return ids
|
||||
|
||||
|
||||
COLA = {"activa": False, "actual": None, "cancelar": False, "hilo": None, "mensaje": ""}
|
||||
COLA = {"activa": False, "actual": None, "cancelar": False, "hilo": None, "mensaje": "", "hechos": 0, "inicio": 0}
|
||||
|
||||
|
||||
def set_trabajo(id_, **kw):
|
||||
@@ -1537,6 +1596,7 @@ def bucle_cola():
|
||||
except Exception as ex: # noqa
|
||||
set_trabajo(t["id"], estado="error", log=f"fallo inesperado: {ex}")
|
||||
_verificar_listos([t["id"]])
|
||||
COLA["hechos"] += 1
|
||||
elif lote_resolve:
|
||||
COLA["actual"] = lote_resolve[0]["id"]
|
||||
try:
|
||||
@@ -1545,6 +1605,7 @@ def bucle_cola():
|
||||
for r in lote_resolve:
|
||||
set_trabajo(r["id"], estado="error", log=f"fallo inesperado del puente: {ex}")
|
||||
_verificar_listos([r["id"] for r in lote_resolve])
|
||||
COLA["hechos"] += len(lote_resolve)
|
||||
else:
|
||||
break
|
||||
COLA["actual"] = None
|
||||
@@ -1590,6 +1651,8 @@ def iniciar_cola():
|
||||
recuperar_huerfanas()
|
||||
COLA["activa"] = True
|
||||
COLA["cancelar"] = False
|
||||
COLA["hechos"] = 0
|
||||
COLA["inicio"] = time.time()
|
||||
COLA["hilo"] = threading.Thread(target=bucle_cola, daemon=True)
|
||||
COLA["hilo"].start()
|
||||
return True
|
||||
@@ -1905,7 +1968,7 @@ def apartar_duplicados(raiz, grupos=None):
|
||||
RESPALDO = {"estado": "", "hechos": 0, "total": 0, "bytes": 0, "bytes_total": 0, "actual": "", "errores": [], "destino": "", "cancelar": False, "inicio": 0, "copiados": 0, "saltados": 0}
|
||||
|
||||
|
||||
def plan_respaldo(raiz, destino, por_tipo=True, incluir_raw=False, sin_stock=True):
|
||||
def plan_respaldo(raiz, destino, por_tipo=True, incluir_raw=False, sin_stock=True, meses=0):
|
||||
con = db()
|
||||
rc = raiz_cfg(raiz)
|
||||
cliente = rc["cliente"] if rc else os.path.basename(raiz.rstrip("\\/")) or "Raiz"
|
||||
@@ -1920,6 +1983,8 @@ def plan_respaldo(raiz, destino, por_tipo=True, incluir_raw=False, sin_stock=Tru
|
||||
marca_a = os.sep + "_Archivo" + os.sep
|
||||
items, vistos = [], set()
|
||||
omit_raw, omit_stock_b, omit_stock_n, convertidos = [], 0, 0, 0
|
||||
tope = limite_meses(meses) if int(meses or 0) > 0 else None # solo lo anterior a este (año, mes)
|
||||
omit_reciente_n, omit_reciente_b = 0, 0
|
||||
|
||||
def agregar(ruta, rol, ext, bytes_, mtime):
|
||||
k = os.path.normpath(ruta).lower()
|
||||
@@ -1938,6 +2003,10 @@ def plan_respaldo(raiz, destino, por_tipo=True, incluir_raw=False, sin_stock=Tru
|
||||
omit_stock_b += f["bytes"]
|
||||
omit_stock_n += 1
|
||||
continue
|
||||
if tope and not es_viejo(f, tope):
|
||||
omit_reciente_n += 1
|
||||
omit_reciente_b += f["bytes"]
|
||||
continue
|
||||
k = os.path.normpath(f["ruta"]).lower()
|
||||
if k in conv:
|
||||
sal = conv[k]
|
||||
@@ -1979,6 +2048,7 @@ def plan_respaldo(raiz, destino, por_tipo=True, incluir_raw=False, sin_stock=Tru
|
||||
"por_tipo": [{"tipo": k, **v} for k, v in sorted(por_tipo_res.items(), key=lambda kv: -kv[1]["bytes"])],
|
||||
"convertidos": convertidos, "raw_omitidos": {"n": len(omit_raw), "bytes": sum(x["bytes"] for x in omit_raw), "ejemplos": omit_raw[:15]},
|
||||
"stock_omitido": {"n": omit_stock_n, "bytes": omit_stock_b},
|
||||
"reciente_omitido": {"n": omit_reciente_n, "bytes": omit_reciente_b, "meses": int(meses or 0)},
|
||||
"muestra": [{"de": i["rel"], "a": os.path.relpath(i["destino"], destino)} for i in items[:25]],
|
||||
"items": items,
|
||||
}
|
||||
@@ -2128,7 +2198,14 @@ def estado_general():
|
||||
con = db()
|
||||
esc = {r["raiz"]: dict(r) for r in con.execute("SELECT * FROM escaneos").fetchall()}
|
||||
cola_n = {r["estado"]: r["n"] for r in con.execute("SELECT estado, COUNT(*) n FROM trabajos GROUP BY estado").fetchall()}
|
||||
actual = None
|
||||
if COLA["actual"]:
|
||||
t = con.execute("SELECT id, ruta, raiz, pct, bytes_in, motor, estado FROM trabajos WHERE id = ?", (COLA["actual"],)).fetchone()
|
||||
if t:
|
||||
actual = dict(t)
|
||||
con.close()
|
||||
# «clip 3 de 12»: los hechos en esta tanda + el que va + los que esperan
|
||||
total_tanda = COLA["hechos"] + (1 if actual else 0) + cola_n.get("pendiente", 0)
|
||||
esc_ci = {k.lower(): v for k, v in esc.items()}
|
||||
raices = []
|
||||
for r in CONFIG["raices"]:
|
||||
@@ -2136,7 +2213,8 @@ def estado_general():
|
||||
raices.append({**r, "clave": k, "escaneo": esc_ci.get(k.lower()), "progreso": PROGRESO.get(k), "analisis": ANALISIS.get(k)})
|
||||
return {"config": CONFIG, "raices": raices, "herramientas": herramientas(),
|
||||
"version": VERSION, "actualizacion": actualizador.ESTADO, "congelado": CONGELADO,
|
||||
"cola": {"activa": COLA["activa"], "actual": COLA["actual"], "mensaje": COLA["mensaje"], "n": cola_n},
|
||||
"cola": {"activa": COLA["activa"], "actual": actual, "mensaje": COLA["mensaje"], "n": cola_n,
|
||||
"hechos": COLA["hechos"], "total": total_tanda, "inicio": COLA["inicio"]},
|
||||
"aplicando": APLICANDO, "asistente": asistente.ESTADO,
|
||||
"respaldo": {k: v for k, v in RESPALDO.items() if k != "errores"} | {"errores_n": len(RESPALDO.get("errores") or [])},
|
||||
"cubetas": R.CUBETAS, "tipos": R.TIPOS_CLIENTE, "perfiles": PERFILES, "plantillas": PLANTILLAS}
|
||||
@@ -2253,6 +2331,9 @@ class Manejador(BaseHTTPRequestHandler):
|
||||
return
|
||||
if ruta == "/api/estado":
|
||||
return self._json(estado_general())
|
||||
if ruta == "/api/soltado":
|
||||
r = soltado(q.get("nombre", ""))
|
||||
return self._json({"ruta": r, "es_archivo": bool(r) and os.path.isfile(r)})
|
||||
if ruta == "/api/carpeta/estado":
|
||||
return self._json(estado_carpeta(q["raiz"]))
|
||||
if ruta == "/api/mapa":
|
||||
@@ -2324,7 +2405,7 @@ class Manejador(BaseHTTPRequestHandler):
|
||||
destino = q.get("destino") or CONFIG.get("respaldo", {}).get("destino", "")
|
||||
if not destino:
|
||||
return self._json({"error": "indica la carpeta de destino del respaldo"}, 400)
|
||||
plan = plan_respaldo(raiz, destino, q.get("por_tipo", "1") == "1", q.get("incluir_raw") == "1", q.get("sin_stock", "1") == "1")
|
||||
plan = plan_respaldo(raiz, destino, q.get("por_tipo", "1") == "1", q.get("incluir_raw") == "1", q.get("sin_stock", "1") == "1", int(q.get("meses", "0") or 0))
|
||||
PLANES_RESPALDO[raiz] = plan
|
||||
CONFIG.setdefault("respaldo", {})["destino"] = destino
|
||||
guardar_json(CONFIG_PATH, CONFIG)
|
||||
@@ -2724,6 +2805,31 @@ def _asegurar_consola():
|
||||
pass
|
||||
|
||||
|
||||
SOLTADOS = {"ts": 0, "rutas": []}
|
||||
|
||||
|
||||
def _al_soltar(e):
|
||||
try:
|
||||
rutas = [f.get("pywebviewFullPath") for f in (e.get("dataTransfer") or {}).get("files", []) if f.get("pywebviewFullPath")]
|
||||
except Exception: # noqa
|
||||
rutas = []
|
||||
if rutas:
|
||||
SOLTADOS.update(ts=time.time(), rutas=[os.path.normpath(r) for r in rutas])
|
||||
|
||||
|
||||
def soltado(nombre, espera=2.5):
|
||||
"""Ruta completa de lo que se acaba de soltar en la ventana (llega por el puente WebView2). Espera un poco
|
||||
porque el drop del navegador y el aviso del puente no llegan en el mismo orden."""
|
||||
fin = time.time() + espera
|
||||
while time.time() < fin:
|
||||
if time.time() - SOLTADOS["ts"] < 8:
|
||||
for r in SOLTADOS["rutas"]:
|
||||
if not nombre or os.path.basename(r.rstrip("\\/")).lower() == nombre.lower():
|
||||
return r
|
||||
time.sleep(0.1)
|
||||
return None
|
||||
|
||||
|
||||
def _ventana(url):
|
||||
"""Ventana propia con WebView2 (Edge). Si no está disponible, se abre en el navegador."""
|
||||
try:
|
||||
@@ -2733,6 +2839,15 @@ def _ventana(url):
|
||||
return False
|
||||
try:
|
||||
v = webview.create_window("OrganiLAB", url, width=1280, height=820, min_size=(960, 640), background_color="#ffffff")
|
||||
|
||||
def _al_cargar():
|
||||
# WebView2 entrega la ruta completa de lo que se suelta solo por este puente (el navegador no la da):
|
||||
# se guarda y la interfaz la pide en /api/soltado en cuanto ve el drop.
|
||||
try:
|
||||
v.dom.document.events.drop += _al_soltar
|
||||
except Exception as ex: # noqa
|
||||
print("sin puente de arrastre:", ex)
|
||||
v.events.loaded += _al_cargar
|
||||
webview.start(gui="edgechromium", private_mode=False, storage_path=os.path.join(DATOS, "ventana"))
|
||||
return True
|
||||
except Exception as ex: # noqa
|
||||
|
||||
@@ -173,6 +173,32 @@
|
||||
.actualizacion{border-color:var(--acc)!important;background:var(--acc2)!important;margin-bottom:18px}
|
||||
/* nombrar */
|
||||
.nombrar{list-style:none;margin:0;padding:0}
|
||||
.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 .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.vacio .num{background:var(--ok2);color:var(--ok)}
|
||||
.paso .pc .tx{flex:1;min-width:0}
|
||||
.paso .pc .tx b{font-size:14px}
|
||||
.paso .pc .tx .mut{font-size:12.5px;margin-top:2px}
|
||||
.paso .pc .der{display:flex;gap:8px;align-items:center;flex:none}
|
||||
.paso .res{font-size:12.5px;color:var(--mut);white-space:nowrap}
|
||||
.paso .detalle{border-top:1px solid var(--line2);padding:6px 14px 10px;display:none}
|
||||
.paso.abierto .detalle{display:block}
|
||||
.paso .fila{display:grid;grid-template-columns:22px minmax(0,1fr) auto;gap:10px;align-items:start;padding:7px 0;border-bottom:1px solid var(--line2);font-size:13px}
|
||||
.paso .fila:last-child{border-bottom:0}
|
||||
.paso .fila .de{color:var(--mut);font-size:12px;margin-top:2px}
|
||||
.paso .fila.baja .frase{color:var(--mut)}
|
||||
.paso .fila .ed{border:0;background:none;color:var(--acc);cursor:pointer;font-size:12px;padding:0 4px}
|
||||
.paso .grp{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--mut);padding:10px 0 4px;font-weight:700}
|
||||
#planIntro b.grande{font-size:14px}
|
||||
.nombrar li{grid-template-columns:minmax(0,1fr) auto 110px}
|
||||
.nombrar li .r{display:block}
|
||||
.nombrar li .sg{display:block;font-size:12px;color:var(--acc);margin-top:3px;white-space:normal}
|
||||
.nombrar li .sg b{display:inline}
|
||||
.nombrar li .acts button{white-space:nowrap}
|
||||
.nombrar li .acts{display:flex;gap:6px;align-items:center;justify-content:flex-end}
|
||||
.nombrar li .acts select{max-width:230px}
|
||||
.stk-grp td{background:var(--panel2);font-weight:600;font-size:12.5px}
|
||||
.nombrar li{display:grid;grid-template-columns:minmax(0,1fr) 260px 130px;gap:10px;align-items:center;padding:7px 0;border-bottom:1px solid var(--line);font-size:13px}
|
||||
.nombrar li b{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.nombrar li small{color:var(--mut);white-space:nowrap}
|
||||
@@ -281,7 +307,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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="Quitar esta carpeta de la lista; no borra nada del disco">Desconectar</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 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>
|
||||
@@ -313,24 +339,21 @@
|
||||
</div>
|
||||
|
||||
<div class="sub" id="s-plan">
|
||||
<div class="toolbar">
|
||||
<button id="btnPlan" class="p">Calcular</button>
|
||||
<button id="btnAplicar" class="ok" disabled>Aplicar</button><span class="por-que" id="btnAplicarPorQue">Calcula primero</span>
|
||||
<div id="planIntro" class="hint" style="margin-bottom:12px">Pulsa <b>Calcular</b> para ver, paso a paso, qué haría la app en esta carpeta.</div>
|
||||
<div class="toolbar" style="margin-bottom:12px">
|
||||
<button id="btnPlan" class="p">Calcular qué va a pasar</button>
|
||||
<button id="btnAplicarSeguros" class="ok" disabled>Aplicar todo lo seguro</button>
|
||||
<span class="sep"></span>
|
||||
<span id="planResumen" class="mut"></span>
|
||||
<button class="lnk" id="btnMarcarAlta" disabled>Solo los seguros</button>
|
||||
<button class="lnk" id="btnMarcarTodo" disabled>Todo</button>
|
||||
<button class="lnk" id="btnCosmeticos" disabled title="Renombrados de mes o pieza que solo cambian el nombre">+ cosméticos</button>
|
||||
<button class="lnk" id="btnDesmarcar" disabled>Nada</button>
|
||||
<button class="lnk" id="btnArbol" disabled>Cómo quedaría</button>
|
||||
<input type="text" id="filtroPlan" placeholder="filtrar…" style="min-width:140px">
|
||||
<input type="text" id="filtroPlan" placeholder="buscar en el plan…" style="min-width:160px">
|
||||
</div>
|
||||
<div id="planAvisos"></div>
|
||||
<div class="box"><table id="tablaPlan"><thead><tr><th></th><th>Ahora está en</th><th></th><th>Pasa a (editable)</th><th>Por qué</th><th>Seguridad</th><th class="num">N</th><th class="num">Peso</th></tr></thead><tbody><tr><td colspan="8" class="mut">Pulsa «Calcular».</td></tr></tbody></table></div>
|
||||
<div id="planPasos"></div>
|
||||
</div>
|
||||
|
||||
<div class="sub" id="s-nombrar">
|
||||
<div class="toolbar"><input type="text" id="filtroVoc" placeholder="buscar carpeta…" style="min-width:220px"><label><input type="checkbox" id="soloSin" checked> solo sin reconocer</label><label><input type="checkbox" id="sinPiezas" checked> ocultar lo de dentro de piezas</label><span class="sep"></span><span id="vocResumen" class="mut"></span></div>
|
||||
<p class="ayuda">La app mira qué hay dentro de cada carpeta y propone qué es. Confirma con un clic o elige otra cosa; se lo aprende para todos los discos. <span class="q">?</span><span class="mas">Solo se ordenan solas las carpetas reconocidas. Las que dejes sin nombrar no se mueven. Una sugerencia «segura» sale cuando casi todo el contenido es de un mismo tipo (por ejemplo, 90 % clips de cámara).</span></p>
|
||||
<div class="toolbar"><button id="btnAceptarSug" class="p" disabled>Aceptar las sugerencias seguras</button><input type="text" id="filtroVoc" placeholder="buscar carpeta…" style="min-width:200px"><label><input type="checkbox" id="soloSin" checked> solo sin reconocer</label><label><input type="checkbox" id="sinPiezas" checked> ocultar lo de dentro de piezas</label><span class="sep"></span><span id="vocResumen" class="mut"></span></div>
|
||||
<div class="box"><ul class="nombrar" id="nombrar"><li class="mut">cargando…</li></ul></div>
|
||||
<details class="mas"><summary>Reglas exactas aprendidas al editar un destino en el plan</summary><div id="reglas" class="mut" style="margin-top:8px">ninguna todavía</div></details>
|
||||
</div>
|
||||
@@ -338,7 +361,8 @@
|
||||
<div class="sub" id="s-stock">
|
||||
<p class="ayuda">Las tomas de Envato de proyectos ya publicados sobran: se vuelven a descargar con la suscripción. <span class="q">?</span><span class="mas">Se detectan por el nombre (…-2026-01-22-22-34-00-utc) o por estar en una carpeta «Elements». Van a <span class="path">_Para borrar/stock</span>; el borrado definitivo lo hace una persona. Se deshace desde Historial.</span></p>
|
||||
<div class="toolbar">
|
||||
<label>proyectos de más de <input type="number" id="stockMeses" value="3" min="0" style="width:60px"> meses</label>
|
||||
<label>marcar lo de más de <input type="number" id="stockMeses" value="3" min="0" style="width:56px"> meses</label>
|
||||
<span class="mut" style="font-size:12px">rápido:</span><button class="lnk" data-m="1">1</button><button class="lnk" data-m="2">2</button><button class="lnk" data-m="3">3</button><button class="lnk" data-m="6">6</button><button class="lnk" data-m="12">12</button>
|
||||
<label><input type="checkbox" id="stockVideo" checked> video</label>
|
||||
<label><input type="checkbox" id="stockAudio"> música</label>
|
||||
<label><input type="checkbox" id="stockImg"> imágenes</label>
|
||||
@@ -346,7 +370,7 @@
|
||||
<span class="sep"></span><span id="stockResumen" class="mut"></span>
|
||||
<button id="btnStockPapelera" class="bad" disabled>Apartar a _Para borrar/stock</button><span class="por-que" id="btnStockPapeleraPorQue">Busca primero</span>
|
||||
</div>
|
||||
<div class="box" id="stockCaja" style="display:none;max-height:420px;overflow:auto"><table id="tablaStock"><thead><tr><th></th><th>Carpeta</th><th>Mes</th><th class="num">Archivos</th><th class="num">Peso</th><th>Ejemplos</th></tr></thead><tbody></tbody></table></div>
|
||||
<div class="box" id="stockCaja" style="display:none;max-height:420px;overflow:auto"><table id="tablaStock"><thead><tr><th></th><th>Carpeta</th><th>Mes de la pieza</th><th class="num">Archivos</th><th class="num">Peso</th><th>Ejemplos</th></tr></thead><tbody></tbody></table></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -382,10 +406,11 @@
|
||||
<!-- ============ RESPALDAR ============ -->
|
||||
<section id="t-respaldar">
|
||||
<h2 class="v">Respaldar</h2>
|
||||
<p class="ayuda">Copia a otro disco una versión <b>compacta</b> del cliente, con los clips ya convertidos en vez de los originales. <span class="q">?</span><span class="mas">Misma estructura de carpetas, cada una separada por tipo de medio (Video, Audio, Imagenes, Diseño, Proyectos, Documentos). Deja <span class="path">INDICE.csv</span> y <span class="path">_MAPA.md</span> en la raíz del respaldo. Es incremental: lo que ya está igual no se vuelve a copiar. Los originales de cámara sin convertir no se copian salvo que marques «incluir BRAW / RAW».</span></p>
|
||||
<p class="ayuda">Último paso: cuando la carpeta ya está limpia, ordenada y convertida, copia a otro disco una versión <b>compacta</b> del cliente. Puedes ir por lotes: solo lo de más de 3, 6 o 12 meses. <span class="q">?</span><span class="mas">Misma estructura de carpetas, cada una separada por tipo de medio (Video, Audio, Imagenes, Diseño, Proyectos, Documentos). Deja <span class="path">INDICE.csv</span> y <span class="path">_MAPA.md</span> en la raíz del respaldo. Es incremental: lo que ya está igual no se vuelve a copiar. Los originales de cámara sin convertir no se copian salvo que marques «incluir BRAW / RAW».</span></p>
|
||||
<div id="respAviso"></div>
|
||||
<div class="toolbar">
|
||||
<label>destino <input type="text" id="respDestino" placeholder="T:\Archivo Labstream" style="min-width:320px"></label>
|
||||
<label>destino <input type="text" id="respDestino" placeholder="T:\Archivo Labstream" style="min-width:300px"></label>
|
||||
<label>qué copiar <select id="respMeses"><option value="0">todo el cliente</option><option value="3">solo lo de más de 3 meses</option><option value="6">solo lo de más de 6 meses</option><option value="12">solo lo de más de 12 meses</option></select></label>
|
||||
<label><input type="checkbox" id="respTipo" checked> subcarpeta por tipo de medio</label>
|
||||
<label><input type="checkbox" id="respSinStock" checked> sin stock de Envato</label>
|
||||
<label><input type="checkbox" id="respRaw"> incluir BRAW / RAW sin convertir</label>
|
||||
@@ -394,7 +419,7 @@
|
||||
<button id="btnRespDetener" class="bad" disabled>Detener</button>
|
||||
<span id="respEstado" class="mut"></span>
|
||||
</div>
|
||||
<div class="cards" id="respCards" style="grid-template-columns:repeat(5,1fr)"></div>
|
||||
<div class="cards" id="respCards" style="grid-template-columns:repeat(auto-fit,minmax(150px,1fr))"></div>
|
||||
<div class="bar" style="margin-bottom:14px"><i id="respBarra" style="width:0%"></i></div>
|
||||
<div class="row">
|
||||
<div class="box"><h3>Por tipo de medio</h3><div id="respTipos" class="mut">calcula para verlo</div></div>
|
||||
@@ -407,7 +432,7 @@
|
||||
<!-- ============ HISTORIAL ============ -->
|
||||
<section id="t-historial">
|
||||
<div class="cab"><h2 class="v">Historial</h2><div class="der"><label class="mut" style="font-size:12.5px"><input type="checkbox" id="bitSoloRaiz" checked> solo esta carpeta</label></div></div>
|
||||
<p class="ayuda">Cada vez que la app mueve algo lo apunta como un cambio, y <b>Deshacer</b> devuelve cada archivo a donde estaba. <span class="q">?</span><span class="mas">Si alguien movió un archivo después, ese se salta y se avisa. La app nunca borra: lo apartado en <span class="path">_Para borrar</span> lo revisa y lo borra una persona desde el Explorador.</span></p>
|
||||
<p class="ayuda">Cada vez que la app mueve algo lo apunta aquí, con ejemplos de qué fue a dónde. <span class="q">?</span><span class="mas">La app nunca borra: lo apartado en <span class="path">_Para borrar</span> lo revisa y lo borra una persona desde el Explorador.</span></p>
|
||||
<div id="papeleraLinea" class="hint" style="display:none"></div>
|
||||
<div id="historial"></div>
|
||||
</section>
|
||||
@@ -491,7 +516,7 @@ function cfgRaiz(){ return (ESTADO.raices||[]).find(r => r.clave === raizActual(
|
||||
function irA(t){ $$('nav a').forEach(x => x.classList.toggle('on', x.dataset.t === t)); $$('section').forEach(s => s.classList.toggle('on', s.id === 't-' + t)); pintarIndice(); cargarVista(t); }
|
||||
function vistaActual(){ return ($$('nav a').find(a => a.classList.contains('on')) || {}).dataset?.t || 'carpeta'; }
|
||||
$$('nav a').forEach(a => { a.onclick = () => irA(a.dataset.t); a.onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); irA(a.dataset.t); } }; });
|
||||
function subRev(s){ $$('#tabsRev a').forEach(x => x.classList.toggle('on', x.dataset.s === s)); $$('#t-revisar .sub').forEach(x => x.classList.toggle('on', x.id === 's-' + s)); if (s === 'plan' && !PLAN && CARPETA && CARPETA.plan && MAPA && MAPA.escaneado) $('#btnPlan').click(); }
|
||||
function subRev(s){ $$('#tabsRev a').forEach(x => x.classList.toggle('on', x.dataset.s === s)); $$('#t-revisar .sub').forEach(x => x.classList.toggle('on', x.id === 's-' + s)); if (s === 'plan' && !PLAN && MAPA && MAPA.escaneado) $('#btnPlan').click(); }
|
||||
$$('#tabsRev a').forEach(a => a.onclick = () => subRev(a.dataset.s));
|
||||
const cargarVista = seguro(async t => {
|
||||
t = t || vistaActual();
|
||||
@@ -549,7 +574,7 @@ async function sondear(){ if (SONDEO) return; SONDEO = cargarEstado().catch(e =>
|
||||
setInterval(sondear, 2500);
|
||||
$('#raiz').onchange = () => { PLAN = null; CAND = []; STOCK = []; RESP = null; MAPA = null; CARPETA = null; limpiarTablas(); cargarEstado().then(() => cargarVista()).catch(e => aviso(e.message, 'bad')); };
|
||||
function limpiarTablas(){
|
||||
$('#tablaPlan tbody').innerHTML = '<tr><td colspan="8" class="mut">Pulsa «Calcular».</td></tr>'; $('#planResumen').textContent = ''; $('#planAvisos').innerHTML = ''; estadoPlan();
|
||||
$('#planPasos').innerHTML = ''; $('#planAvisos').innerHTML = ''; estadoPlan();
|
||||
$('#optCards').innerHTML = ''; $('#anaEstado').textContent = ''; $('#convLista').innerHTML = '<span class="mut">Pulsa «Buscar clips» para ver qué se puede convertir en esta carpeta.</span>'; bloquear('btnEncolar', 'x'); $('#btnEncolar').textContent = 'Convertir';
|
||||
$('#tablaStock tbody').innerHTML = ''; $('#stockCaja').style.display = 'none'; $('#stockResumen').textContent = ''; bloquear('btnStockPapelera', 'Busca primero');
|
||||
$('#respCards').innerHTML = ''; $('#respTipos').innerHTML = '<span class="mut">calcula para verlo</span>'; $('#respRawLista').innerHTML = '<span class="mut">calcula para verlo</span>'; $('#respMuestra').innerHTML = '<span class="mut">calcula para verlo</span>'; $('#respEstado').textContent = ''; bloquear('btnRespCopiar', 'Calcula primero');
|
||||
@@ -563,7 +588,7 @@ function tareasDe(E){
|
||||
if (r.progreso && r.progreso.estado === 'corriendo') { const p = r.progreso; const est = r.escaneo && r.escaneo.n ? Math.min(99, Math.round(p.n * 100 / r.escaneo.n)) : null; T.push({raiz: r.clave, q: 'Escaneando', d: `${r.cliente} · ${num(p.n)} archivos · ${gb(p.bytes)}`, pct: est}); }
|
||||
if (r.analisis && r.analisis.estado === 'corriendo') { const a = r.analisis; T.push({raiz: r.clave, q: 'Leyendo clips', d: `${r.cliente} · ${a.hechos} de ${a.total}`, pct: a.total ? Math.round(a.hechos * 100 / a.total) : null}); }
|
||||
});
|
||||
if (E.cola.activa) { const n = E.cola.n || {}; T.push({raiz: (E.cola.actual || {}).raiz, q: 'Convirtiendo', d: `${E.cola.actual ? (E.cola.actual.ruta || '').split(/[\\/]/).pop() + ' · ' : ''}${n.pendiente || 0} en espera${E.cola.mensaje ? ' · ' + E.cola.mensaje : ''}`, pct: E.cola.actual ? Math.round(E.cola.actual.pct || 0) : null, cancelar: () => api('/api/optimizar/control', {accion: 'detener'})}); }
|
||||
if (E.cola.activa) { const n = E.cola.n || {}; const a = E.cola.actual; const tot = E.cola.total || 0, hechos = E.cola.hechos || 0; const pctClip = a ? (a.pct || 0) : 0; const pctTot = tot ? Math.round((hechos + pctClip / 100) / tot * 100) : null; T.push({raiz: a ? a.raiz : null, q: tot ? `Convirtiendo ${Math.min(hechos + 1, tot)} de ${tot}` : 'Convirtiendo', d: `${a ? (a.ruta || '').split(/[\/]/).pop() + ' · ' + Math.round(pctClip) + ' % de este clip · ' : ''}${n.pendiente || 0} en espera${E.cola.mensaje ? ' · ' + E.cola.mensaje : ''}`, pct: pctTot, cancelar: () => api('/api/optimizar/control', {accion: 'detener'})}); }
|
||||
if (E.asistente.activo) { const a = E.asistente; T.push({raiz: a.raiz, q: 'Asistente: ' + (NOMBRE_PASO[a.paso] || a.paso || '…'), d: `${porNombre(a.raiz)} · ${a.total ? num(a.hechas) + ' de ' + num(a.total) + ' · ' : ''}${(a.texto || '').slice(-60)}`, pct: a.total ? a.pct : null}); }
|
||||
else if (E.aplicando && E.aplicando.total && E.aplicando.hechas < E.aplicando.total) T.push({q: 'Moviendo', d: E.aplicando.actual || '', pct: Math.round(E.aplicando.hechas * 100 / E.aplicando.total)});
|
||||
if (E.respaldo.estado === 'corriendo') { const e = E.respaldo; T.push({q: 'Copiando respaldo', d: `${num(e.hechos)} de ${num(e.total)} · ${gb(e.bytes)} de ${gb(e.bytes_total)}`, pct: e.bytes_total ? Math.round(e.bytes / e.bytes_total * 100) : null, cancelar: () => api('/api/respaldo/detener', {})}); }
|
||||
@@ -605,7 +630,7 @@ function pintarIndice(){
|
||||
const P = pasosDe(CARPETA, r);
|
||||
riel = `<div class="riel">${P.map((p, i) => `<div class="${p.e === 'hecho' ? 'ok' : p.e === 'curso' || p.e === 'listo' ? 'cur' : p.e} ${aqui.includes(i) ? 'aqui' : ''}" data-i="${i}" title="${esc(p.t)}: ${esc(p.d)}"><span class="pto ${p.e === 'hecho' ? 'ok' : p.e === 'curso' ? 'run' : p.e === 'listo' ? 'cur' : ''}"></span>${esc(p.t)}<small>${esc(p.corto || '')}</small></div>`).join('')}</div>`;
|
||||
}
|
||||
return `<div class="cl ${r.clave === act ? 'on' : ''}" data-r="${esc(r.clave)}" title="${esc(r.ruta)}" tabindex="0"><div class="n"><span>${esc(r.cliente)}</span><em>${esc(disco)}</em></div><div class="e"><span class="pto ${e[0]}"></span>${esc(e[1])}<button class="x" data-x="${esc(r.clave)}" title="Desconectar esta carpeta de la lista" aria-label="Desconectar">✕</button></div>${riel}</div>`;
|
||||
return `<div class="cl ${r.clave === act ? 'on' : ''}" data-r="${esc(r.clave)}" title="${esc(r.ruta)}" tabindex="0"><div class="n"><span>${esc(r.cliente)}</span><em>${esc(disco)}</em></div><div class="e"><span class="pto ${e[0]}"></span>${esc(e[1])}<button class="x" data-x="${esc(r.clave)}" title="Quitar de la lista (no borra nada del disco)" aria-label="Quitar de la lista">✕</button></div>${riel}</div>`;
|
||||
}).join('') || '<div class="mut" style="font-size:12.5px">Ninguna todavía.</div>';
|
||||
$$('#indice .cl').forEach(el => { const ir = () => { if ($('#raiz').value === el.dataset.r) { irA('carpeta'); return; } $('#raiz').value = el.dataset.r; $('#raiz').onchange(); }; el.onclick = ir; el.onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); ir(); } }; });
|
||||
$$('#indice .x').forEach(el => el.onclick = e => { e.stopPropagation(); desconectar(el.dataset.x); });
|
||||
@@ -617,9 +642,9 @@ function desconectar(clave){
|
||||
const r = (ESTADO.raices || []).find(x => x.clave === clave); if (!r) return;
|
||||
const ocupada = tareasDe(ESTADO).some(t => (t.raiz || '').toLowerCase() === clave.toLowerCase());
|
||||
if (ocupada) { aviso('Esa carpeta tiene una tarea en curso. Espera a que termine.', 'bad'); return; }
|
||||
modal('Desconectar «' + r.cliente + '»', `<p>La carpeta <span class="path">${esc(r.ruta)}</span> desaparece de la lista de la izquierda. <b>No se borra ni se mueve nada del disco</b>; puedes volver a soltarla cuando quieras.</p><label style="display:flex;gap:8px;align-items:flex-start;margin-top:10px;font-size:13px"><input type="checkbox" id="mDatos" style="margin-top:3px"><span>Olvidar también el inventario escaneado${r.escaneo ? ` (${num(r.escaneo.n)} archivos · ${gb(r.escaneo.bytes)})` : ''}. Si la vuelves a conectar habrá que escanearla de nuevo.</span></label>`, async () => {
|
||||
modal('Quitar «' + r.cliente + '» de la lista', `<p>La carpeta <span class="path">${esc(r.ruta)}</span> desaparece de la lista de la izquierda. <b>No se borra ni se mueve nada del disco</b>; puedes volver a soltarla cuando quieras.</p><label style="display:flex;gap:8px;align-items:flex-start;margin-top:10px;font-size:13px"><input type="checkbox" id="mDatos" style="margin-top:3px"><span>Olvidar también el inventario escaneado${r.escaneo ? ` (${num(r.escaneo.n)} archivos · ${gb(r.escaneo.bytes)})` : ''}. Si la vuelves a conectar habrá que escanearla de nuevo.</span></label>`, async () => {
|
||||
const olvidar = $('#mDatos').checked;
|
||||
const listo = async (n) => { aviso(n ? `Carpeta desconectada; ${n} conversión${n > 1 ? 'es' : ''} descartada${n > 1 ? 's' : ''}.` : 'Carpeta desconectada.', 'ok'); await cargarEstado(); const s = $('#raiz'); if (s.value === clave) { s.value = ESTADO.raices[0] ? ESTADO.raices[0].clave : ''; s.onchange(); } else { pintarIndice(); cargarVista(); } };
|
||||
const listo = async (n) => { aviso(n ? `Carpeta quitada de la lista; ${n} conversión${n > 1 ? 'es' : ''} descartada${n > 1 ? 's' : ''}.` : 'Carpeta desconectada.', 'ok'); await cargarEstado(); const s = $('#raiz'); if (s.value === clave) { s.value = ESTADO.raices[0] ? ESTADO.raices[0].clave : ''; s.onchange(); } else { pintarIndice(); cargarVista(); } };
|
||||
try {
|
||||
await api('/api/raiz/quitar', {raiz: clave, datos: olvidar});
|
||||
await listo(0);
|
||||
@@ -629,9 +654,9 @@ function desconectar(clave){
|
||||
// Conversiones sin terminar: se enseñan (cuáles, en qué estado) y, si ninguna está convirtiéndose ahora,
|
||||
// se ofrece descartarlas y desconectar de una vez. Antes solo salía un globo sin decir cuáles.
|
||||
apuntarAviso(e.message, c.archivos.join('\n'));
|
||||
modal('No se pudo desconectar «' + r.cliente + '»', `<p>${esc(e.message)}.</p><p><b>${c.n}</b> conversión${c.n > 1 ? 'es' : ''} sin terminar${c.por_estado.corriendo ? ` (${c.por_estado.corriendo} convirtiéndose ahora)` : ''}:</p><pre>${esc(c.archivos.join('\n'))}${c.n > c.archivos.length ? `\n… y ${c.n - c.archivos.length} más` : ''}</pre><p class="mut" style="font-size:12.5px">${c.en_curso ? 'Detén la cola en <b>Convertir a 10 bits</b> y vuelve a intentarlo.' : 'Descartarlas las quita de la cola sin tocar ningún archivo: los originales siguen donde están, y si vuelves a conectar la carpeta puedes encolarlas otra vez.'}</p>`,
|
||||
modal('No se pudo quitar «' + r.cliente + '»', `<p>${esc(e.message)}.</p><p><b>${c.n}</b> conversión${c.n > 1 ? 'es' : ''} sin terminar${c.por_estado.corriendo ? ` (${c.por_estado.corriendo} convirtiéndose ahora)` : ''}:</p><pre>${esc(c.archivos.join('\n'))}${c.n > c.archivos.length ? `\n… y ${c.n - c.archivos.length} más` : ''}</pre><p class="mut" style="font-size:12.5px">${c.en_curso ? 'Detén la cola en <b>Convertir a 10 bits</b> y vuelve a intentarlo.' : 'Descartarlas las quita de la cola sin tocar ningún archivo: los originales siguen donde están, y si vuelves a conectar la carpeta puedes encolarlas otra vez.'}</p>`,
|
||||
c.en_curso ? null : async () => { try { await api('/api/raiz/quitar', {raiz: clave, datos: olvidar, descartar_cola: true}); await listo(c.n); } catch (e2) { aviso(e2.message, 'bad', null, detalleDe(e2)); } });
|
||||
if (!c.en_curso) $('#modalSi').textContent = 'Descartar y desconectar';
|
||||
if (!c.en_curso) $('#modalSi').textContent = 'Descartar y quitar';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -659,8 +684,7 @@ async function cargarCarpeta(){
|
||||
$('#inicioDiag').innerHTML = d.frases.slice(0, 3).map(f => `<div>${esc(f)}</div>`).join('') + (d.frases.length > 3 ? `<div class="mut">Más en Revisar y ordenar.</div>` : '');
|
||||
}
|
||||
const lotes = CARPETA.lotes || [];
|
||||
$('#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 === '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').forEach(bt => bt.onclick = () => deshacer(bt.dataset.l));
|
||||
$('#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></div>`).join('') : '<span class="mut">Todavía no se ha movido nada en esta carpeta.</span>';
|
||||
}
|
||||
function queSeHizo(lote){
|
||||
if (!lote) return 'Cambio';
|
||||
@@ -792,13 +816,22 @@ async function cargarVoc(){
|
||||
const opcionesCubeta = c => `<option value="">— no sé / dejar como está —</option>` + Object.entries(ESTADO.cubetas).map(([k,v]) => `<option value="${k}" ${k===c?'selected':''}>${esc(NOMBRE_CUBETA[k] || k)} · ${esc(v)}</option>`).join('');
|
||||
function pintarNombrar(){
|
||||
const f = ($('#filtroVoc').value || '').toLowerCase(); const solo = $('#soloSin').checked; const sinP = $('#sinPiezas').checked;
|
||||
const rows = VOC.filter(t => (!f || t.token.includes(f) || (t.rutas||[]).join(' ').toLowerCase().includes(f)) && (!solo || !t.cubeta) && (!sinP || !t.dentro_pieza)).sort((a,b) => (!!a.cubeta - !!b.cubeta) || b.bytes - a.bytes);
|
||||
const rows = VOC.filter(t => (!f || t.token.includes(f) || (t.rutas || []).join(' ').toLowerCase().includes(f)) && (!solo || !t.cubeta) && (!sinP || !t.dentro_pieza)).sort((a, b) => (!!a.cubeta - !!b.cubeta) || ((b.sug_conf === 'alta') - (a.sug_conf === 'alta')) || b.bytes - a.bytes);
|
||||
const sin = VOC.filter(t => !t.cubeta && !t.dentro_pieza).length;
|
||||
$('#vocResumen').textContent = `${rows.length} nombres · ${sin} sin reconocer`;
|
||||
const seguras = VOC.filter(t => !t.cubeta && !t.dentro_pieza && t.sugerencia && t.sug_conf === 'alta');
|
||||
$('#vocResumen').textContent = `${rows.length} carpetas · ${sin} sin reconocer${seguras.length ? ` · ${seguras.length} con sugerencia segura` : ''}`;
|
||||
const bd = $('#badgeNombrar'); bd.textContent = sin; bd.classList.toggle('cero', !sin);
|
||||
$('#nombrar').innerHTML = rows.slice(0, 300).map(t => `<li><span><b title="${esc((t.rutas||[]).join('\n'))}">${esc(t.ejemplo)}${t.aprendido ? ' <span class="tag alta">aprendido</span>' : ''}</b><span class="r">${esc((t.rutas||[]).slice(0,2).join(' · '))}</span></span><select data-tok="${esc(t.token)}">${opcionesCubeta(t.cubeta)}</select><small>${t.n} carpeta${t.n > 1 ? 's' : ''} · ${gb(t.bytes)}</small></li>`).join('') + (rows.length > 300 ? `<li class="mut">… y ${rows.length - 300} más: usa el buscador</li>` : '') || (sin ? '<li class="mut">Nada con ese filtro.</li>' : '<li class="ok">Todas las carpetas están reconocidas.</li>');
|
||||
bloquear('btnAceptarSug', seguras.length ? '' : 'x'); $('#btnAceptarSug').textContent = seguras.length ? `Aceptar ${seguras.length} sugerencia${seguras.length > 1 ? 's' : ''} segura${seguras.length > 1 ? 's' : ''}` : 'Aceptar las sugerencias seguras';
|
||||
$('#nombrar').innerHTML = rows.slice(0, 300).map(t => `<li><span><b title="${esc((t.rutas || []).join('\n'))}">${esc(t.ejemplo)}${t.aprendido ? ' <span class="tag alta">aprendido</span>' : ''}${t.n > 1 ? ` <span class="mut" style="font-weight:500">· ${t.n} carpetas con este nombre</span>` : ''}</b><span class="r">${esc((t.rutas || []).slice(0, 2).join(' · '))}</span>${t.contenido ? `<span class="r">Dentro: ${esc(t.contenido)}</span>` : ''}${!t.cubeta && t.sugerencia ? `<span class="sg">Parece <b>${esc(NOMBRE_CUBETA[t.sugerencia] || t.sugerencia)}</b> (${SEG_TXT[t.sug_conf] || t.sug_conf}): ${esc(t.por_que)}</span>` : !t.cubeta ? `<span class="sg" style="color:var(--mut)">Sin pista: elige qué es, o déjala y no se moverá.</span>` : ''}</span><span class="acts">${!t.cubeta && t.sugerencia ? `<button class="p" data-si="${esc(t.token)}" data-cub="${esc(t.sugerencia)}">Sí, es ${esc(NOMBRE_CUBETA[t.sugerencia] || t.sugerencia)}</button>` : ''}<select data-tok="${esc(t.token)}" aria-label="qué es">${opcionesCubeta(t.cubeta)}</select></span><small>${gb(t.bytes)}</small></li>`).join('') + (rows.length > 300 ? `<li class="mut">… y ${rows.length - 300} más: usa el buscador</li>` : '') || (sin ? '<li class="mut">Nada con ese filtro.</li>' : '<li class="ok">Todas las carpetas están reconocidas.</li>');
|
||||
$$('#nombrar select').forEach(s => s.onchange = () => nombrarToken(s.dataset.tok, s.value));
|
||||
$$('#nombrar [data-si]').forEach(b => b.onclick = () => nombrarToken(b.dataset.si, b.dataset.cub));
|
||||
}
|
||||
$('#btnAceptarSug').onclick = () => accion($('#btnAceptarSug'), async () => {
|
||||
const seguras = VOC.filter(t => !t.cubeta && !t.dentro_pieza && t.sugerencia && t.sug_conf === 'alta');
|
||||
for (const t of seguras) { await api('/api/diccionario', {token: t.token, cubeta: t.sugerencia}); t.cubeta = t.sugerencia; t.aprendido = true; }
|
||||
PLAN = null; estadoPlan(); if (CARPETA) { CARPETA.sin_nombre_n = VOC.filter(x => !x.cubeta && !x.dentro_pieza).length; pintarIndice(); }
|
||||
pintarNombrar(); aviso(`${seguras.length} carpetas nombradas. Calcula el plan para verlas ordenadas.`, 'ok');
|
||||
});
|
||||
const nombrarToken = seguro(async (tok, cub) => {
|
||||
await api('/api/diccionario', {token: tok, cubeta: cub});
|
||||
const t = VOC.find(x => x.token === tok); if (t) { t.cubeta = cub; t.aprendido = !!cub; t.motivo = 'lo dijiste tú'; }
|
||||
@@ -812,59 +845,110 @@ function pintarReglas(reglas){
|
||||
$$('#reglas button').forEach(b => b.onclick = () => accion(b, async () => { const r = reglas[+b.dataset.i]; const d = await api('/api/regla', {cliente: r.cliente, origen: r.origen, destino: null}); pintarReglas(d.reglas); PLAN = null; estadoPlan(); }));
|
||||
}
|
||||
|
||||
// ---------------- revisar: plan ----------------
|
||||
// ---------------- revisar: plan (paso a paso, en frases) ----------------
|
||||
const PASOS_PLAN = [
|
||||
{k: 'limpiar', t: 'Apartar basura y proxies', q: 'Archivos de sistema (.DS_Store, Thumbs.db, cachés) y proxies se apartan en «_Para borrar». Los proxies se regeneran desde Resolve o Premiere. Nada se borra.', vacio: 'No hay basura ni proxies que apartar.'},
|
||||
{k: 'fechas', t: 'Ordenar por año y mes', q: 'Los meses toman el nombre «03- Marzo», las grabaciones se reparten por mes y por cámara, y las piezas quedan numeradas dentro de su mes.', vacio: 'Las fechas ya están en orden.'},
|
||||
{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'};
|
||||
let PLAN_ABIERTOS = new Set();
|
||||
function estadoPlan(){
|
||||
const hay = !!PLAN;
|
||||
['btnMarcarAlta','btnMarcarTodo','btnCosmeticos','btnDesmarcar','btnArbol'].forEach(id => $('#' + id).disabled = !hay);
|
||||
if (!hay) { bloquear('btnAplicar', 'Calcula primero'); $('#btnAplicar').textContent = 'Aplicar'; return; }
|
||||
const m = PLAN.filas.filter(r => r.marcado);
|
||||
bloquear('btnAplicar', PLAN.filas.length ? (m.length ? '' : 'Marca algún movimiento') : 'No hay nada que mover');
|
||||
$('#btnAplicar').textContent = m.length ? `Aplicar ${m.length} movimiento${m.length > 1 ? 's' : ''}` : 'Aplicar';
|
||||
$('#planResumen').textContent = PLAN.filas.length ? `${m.length} de ${PLAN.filas.length} marcados · ${gb(m.reduce((s,r)=>s+r.bytes,0))} · ${num(m.reduce((s,r)=>s+r.n,0))} archivos` : '';
|
||||
$('#btnArbol').disabled = !hay;
|
||||
const seg = hay ? PLAN.filas.filter(r => r.marcado) : [];
|
||||
bloquear('btnAplicarSeguros', hay ? (seg.length ? '' : 'x') : 'x'); $('#btnAplicarSeguros').textContent = seg.length ? `Aplicar todo lo marcado (${seg.length})` : 'Aplicar todo lo seguro';
|
||||
if (CARPETA) { CARPETA.plan = hay ? {filas: PLAN.filas.length, marcadas: seg.length, baja: PLAN.filas.filter(r => r.confianza === 'baja').length} : null; pintarIndice(); }
|
||||
}
|
||||
async function calcularPlan(){
|
||||
$('#planIntro').innerHTML = '<span class="spin"></span> mirando la carpeta…';
|
||||
PLAN = await api('/api/plan?raiz=' + encodeURIComponent(raizActual()));
|
||||
pintarPlan();
|
||||
}
|
||||
$('#btnPlan').onclick = () => accion($('#btnPlan'), calcularPlan);
|
||||
function motivoLlano(m){
|
||||
return (m || '').replace(/diccionario: palabra «([^»]+)»/g, 'por la palabra «$1» del nombre').replace(/diccionario: contiene «([^»]+)»/g, 'porque el nombre contiene «$1»').replace(/diccionario: «([^»]+)»/g, 'por su nombre («$1»)').replace(/regla aprendida/g, 'lo dijiste tú la vez pasada').replace(/^no reconocida: /, 'la app no reconoce el nombre: ').replace(/\(fecha de modificación\)/g, '(por la fecha de modificación: puede fallar)');
|
||||
}
|
||||
function fraseDe(r){
|
||||
const nom = x => `«${esc(x.split(/[\\/]/).pop())}»`;
|
||||
if (r.origen_rel === '(toda la raíz)') return `${r.archivos.length} archivos de ${r.cubeta === 'PROXY' ? 'proxy' : 'basura'} repartidos por la carpeta se apartan en «_Para borrar»`;
|
||||
if (r.accion === 'papelera') return `${nom(r.origen_rel)} se aparta en «_Para borrar»`;
|
||||
if (r.accion === 'renombrar') return `${nom(r.origen_rel)} pasa a llamarse ${nom(r.destino_rel)}`;
|
||||
if (r.archivos) return `${r.archivos.length} archivo${r.archivos.length > 1 ? 's' : ''} de ${nom(r.origen_rel)} van a «${esc(r.destino_rel)}»`;
|
||||
return `${nom(r.origen_rel)} se mueve a «${esc(r.destino_rel)}»`;
|
||||
}
|
||||
$('#btnPlan').onclick = () => accion($('#btnPlan'), async () => { $('#planResumen').innerHTML = '<span class="spin"></span> calculando…'; PLAN = await api('/api/plan?raiz=' + encodeURIComponent(raizActual())); ABIERTOS = new Set(); pintarPlan(); if (CARPETA) { CARPETA.plan = {filas: PLAN.filas.length, marcadas: PLAN.filas.filter(r => r.marcado).length}; pintarIndice(); } });
|
||||
let ABIERTOS = new Set();
|
||||
function pintarPlan(){
|
||||
if (!PLAN) { estadoPlan(); return; }
|
||||
const f = ($('#filtroPlan').value || '').toLowerCase();
|
||||
$('#planAvisos').innerHTML = (PLAN.avisos||[]).map(a => `<div class="hint warn">${esc(a)}</div>`).join('');
|
||||
const filas = PLAN.filas.filter(r => !f || (r.origen_rel + ' ' + r.destino_rel + ' ' + r.motivo).toLowerCase().includes(f));
|
||||
const grupos = {}; filas.forEach(r => (grupos[r.cubeta] = grupos[r.cubeta] || []).push(r));
|
||||
const ordenG = Object.keys(grupos).sort((a, b) => grupos[b].reduce((s,r)=>s+r.bytes,0) - grupos[a].reduce((s,r)=>s+r.bytes,0));
|
||||
let html = '';
|
||||
for (const g of ordenG) {
|
||||
const rs = grupos[g].sort((a,b) => ({alta:0,media:1,baja:2}[a.confianza]-{alta:0,media:1,baja:2}[b.confianza]) || b.bytes - a.bytes);
|
||||
const marc = rs.filter(r => r.marcado).length, abierto = ABIERTOS.has(g) || !!f;
|
||||
const dud = rs.filter(r => r.confianza === 'baja').length;
|
||||
html += `<tr class="grp" data-g="${esc(g)}"><td colspan="8"><span class="caret">${abierto ? '▾' : '▸'}</span><span class="tag" style="border-color:${COLORES[g]||''};color:${COLORES[g]||''}">${esc(NOMBRE_CUBETA[g] || g)}</span> <b>${rs.length} movimiento${rs.length > 1 ? 's' : ''}</b><span class="cnt">${num(rs.reduce((s,r)=>s+r.n,0))} archivos · ${gb(rs.reduce((s,r)=>s+r.bytes,0))} · ${marc} marcado${marc !== 1 ? 's' : ''}${dud ? ` · <span class="bad">${dud} dudoso${dud > 1 ? 's' : ''}</span>` : ''}</span></td></tr>`;
|
||||
if (abierto) for (const r of rs) html += `<tr data-id="${r.id}"><td><input type="checkbox" ${r.marcado ? 'checked' : ''} aria-label="marcar"></td><td class="path">${esc(r.origen_rel)}${r.archivos ? `<div class="cell-small">${r.archivos.length} archivos: ${r.archivos.slice(0,3).map(a => esc(a.split(/[\\/]/).pop())).join(', ')}${r.archivos.length > 3 ? '…' : ''}</div>` : ''}</td><td class="arrow">→</td><td><input class="dest" type="text" value="${esc(r.destino_rel)}" aria-label="destino"><div class="cell-small">${esc(r.accion)}</div></td><td class="cell-small">${esc(r.motivo)}</td><td><span class="tag ${r.confianza}">${SEGURIDAD[r.confianza] || r.confianza}</span></td><td class="num">${r.n}</td><td class="num">${gb(r.bytes)}</td></tr>`;
|
||||
}
|
||||
$('#tablaPlan tbody').innerHTML = html || '<tr><td colspan="8" class="mut">Nada que mover: la carpeta ya está en orden.</td></tr>';
|
||||
$$('#tablaPlan tr.grp').forEach(tr => tr.onclick = () => { const g = tr.dataset.g; if (ABIERTOS.has(g)) ABIERTOS.delete(g); else ABIERTOS.add(g); pintarPlan(); });
|
||||
$$('#tablaPlan tr[data-id]').forEach(tr => { const r = PLAN.filas.find(x => x.id === +tr.dataset.id); tr.querySelector('input[type=checkbox]').onchange = e => { r.marcado = e.target.checked; estadoPlan(); }; const inp = tr.querySelector('.dest'); inp.onchange = seguro(async () => { r.destino_rel = inp.value.trim(); r.editado = true; r.marcado = true; tr.querySelector('input[type=checkbox]').checked = true; if (!r.archivos) await api('/api/regla', {cliente: PLAN.cliente, origen: r.origen_rel, destino: r.destino_rel, cubeta: r.cubeta}); estadoPlan(); }); });
|
||||
$('#planAvisos').innerHTML = (PLAN.avisos || []).map(a => `<div class="hint warn">${esc(a)}</div>`).join('');
|
||||
const porPaso = {}; PASOS_PLAN.forEach(p => porPaso[p.k] = []);
|
||||
PLAN.filas.forEach(r => (porPaso[r.paso || 'resto'] = porPaso[r.paso || 'resto'] || []).push(r));
|
||||
const tot = PLAN.filas.length, totB = PLAN.filas.reduce((a, r) => a + r.bytes, 0), dud = PLAN.filas.filter(r => r.confianza === 'baja').length;
|
||||
const partes = PASOS_PLAN.filter(p => porPaso[p.k].length).map(p => `${p.t.toLowerCase()} (${porPaso[p.k].length})`);
|
||||
$('#planIntro').innerHTML = tot ? `<b class="grande">Qué va a hacer la app:</b> ${partes.join(', ')}. En total ${tot} cambio${tot > 1 ? 's' : ''} · ${gb(totB)}. <b>Todo se queda dentro de</b> <span class="path">${esc(PLAN.raiz)}</span> <b>y nada se borra.</b>${dud ? ` ${dud} cambio${dud > 1 ? 's son dudosos y quedan' : ' es dudoso y queda'} sin marcar.` : ''} Puedes aplicar cada paso por separado, en orden.` : `<b class="grande">La carpeta ya está en orden.</b> No hay nada que apartar ni mover.`;
|
||||
$('#planPasos').innerHTML = PASOS_PLAN.map((p, i) => {
|
||||
const rs = porPaso[p.k].filter(r => !f || (r.origen_rel + ' ' + r.destino_rel + ' ' + r.motivo).toLowerCase().includes(f)).sort((a, b) => ({alta: 0, media: 1, baja: 2}[a.confianza] - {alta: 0, media: 1, baja: 2}[b.confianza]) || b.bytes - a.bytes);
|
||||
const marc = rs.filter(r => r.marcado), abierto = PLAN_ABIERTOS.has(p.k) || !!f;
|
||||
const vacio = !porPaso[p.k].length;
|
||||
let det = '';
|
||||
if (abierto && rs.length) {
|
||||
const grupos = {}; rs.forEach(r => (grupos[r.cubeta] = grupos[r.cubeta] || []).push(r));
|
||||
det = Object.keys(grupos).map(g => `<div class="grp">${esc(NOMBRE_CUBETA[g] || g)} · ${grupos[g].length}</div>` + grupos[g].map(r => `<div class="fila ${r.confianza}" data-id="${r.id}"><input type="checkbox" ${r.marcado ? 'checked' : ''} aria-label="marcar" style="margin-top:2px"><div><div class="frase">${fraseDe(r)}${r.editado ? ' <span class="tag">destino cambiado</span>' : ''}</div><div class="de">${esc(motivoLlano(r.motivo))}${r.n ? ` · ${num(r.n)} archivo${r.n > 1 ? 's' : ''} · ${gb(r.bytes)}` : ''}</div></div><div><span class="tag ${r.confianza}">${SEG_TXT[r.confianza] || r.confianza}</span> ${r.archivos ? '' : `<button class="ed" data-ed="${r.id}" title="Cambiar a dónde va">cambiar destino</button>`}</div></div>`).join('')).join('');
|
||||
}
|
||||
return `<div class="paso ${vacio ? 'vacio' : ''} ${abierto ? 'abierto' : ''}" data-k="${p.k}"><div class="pc"><span class="num">${vacio ? '✓' : i + 1}</span><div class="tx"><b>${p.t}</b><div class="mut">${vacio ? p.vacio : p.q}</div></div><div class="der">${vacio ? '' : `<span class="res">${rs.length} cambio${rs.length > 1 ? 's' : ''} · ${gb(rs.reduce((a, r) => a + r.bytes, 0))}${marc.length < rs.length ? ` · ${marc.length} marcado${marc.length !== 1 ? 's' : ''}` : ''}</span><button class="lnk" data-ver="${p.k}">${abierto ? 'ocultar' : 'ver qué'}</button><button class="p" data-aplicar="${p.k}" ${marc.length ? '' : 'disabled'}>Aplicar${marc.length ? ' ' + marc.length : ''}</button>`}</div></div><div class="detalle">${det}</div></div>`;
|
||||
}).join('');
|
||||
$$('#planPasos [data-ver]').forEach(b => b.onclick = () => { const k = b.dataset.ver; if (PLAN_ABIERTOS.has(k)) PLAN_ABIERTOS.delete(k); else PLAN_ABIERTOS.add(k); pintarPlan(); });
|
||||
$$('#planPasos [data-aplicar]').forEach(b => b.onclick = () => aplicarPaso(b.dataset.aplicar, b));
|
||||
$$('#planPasos .fila').forEach(el => { const r = PLAN.filas.find(x => x.id === +el.dataset.id); el.querySelector('input[type=checkbox]').onchange = e => { r.marcado = e.target.checked; pintarPlan(); }; });
|
||||
$$('#planPasos [data-ed]').forEach(b => b.onclick = () => { const r = PLAN.filas.find(x => x.id === +b.dataset.ed); modal('Cambiar destino', `<p>${fraseDe(r)}</p><p class="mut">Escribe la ruta dentro de la carpeta del cliente a la que debe ir. La app se lo aprende para la próxima vez.</p><input type="text" id="mDest" style="width:100%" value="${esc(r.destino_rel)}">`, seguro(async () => { const v = $('#mDest').value.trim(); if (!v) return; r.destino_rel = v; r.editado = true; r.marcado = true; if (!r.archivos) await api('/api/regla', {cliente: PLAN.cliente, origen: r.origen_rel, destino: v, cubeta: r.cubeta}); pintarPlan(); })); });
|
||||
estadoPlan();
|
||||
}
|
||||
$('#filtroPlan').oninput = pintarPlan;
|
||||
$('#btnMarcarAlta').onclick = () => { PLAN.filas.forEach(r => r.marcado = r.confianza === 'alta'); pintarPlan(); };
|
||||
$('#btnMarcarTodo').onclick = () => { PLAN.filas.forEach(r => r.marcado = true); pintarPlan(); };
|
||||
$('#btnCosmeticos').onclick = () => { PLAN.filas.forEach(r => { if (r.cosmetico) r.marcado = true; }); pintarPlan(); };
|
||||
$('#btnDesmarcar').onclick = () => { PLAN.filas.forEach(r => r.marcado = false); pintarPlan(); };
|
||||
$('#btnArbol').onclick = () => { const arbol = {}; PLAN.filas.filter(r => r.marcado).forEach(r => { const partes = r.destino_rel.split(/[\\/]/); let n = arbol; partes.forEach(p => { n[p] = n[p] || {}; n = n[p]; }); }); const lineas = []; const dibuja = (n, pref) => { const ks = Object.keys(n).sort(); ks.forEach((k,i) => { const ult = i === ks.length-1; lineas.push(pref + (ult ? '└─ ' : '├─ ') + k); dibuja(n[k], pref + (ult ? ' ' : '│ ')); }); }; dibuja(arbol, ''); modal('Cómo quedaría (solo lo marcado)', `<div class="tree">${esc(PLAN.raiz)}\n${esc(lineas.join('\n'))}</div>`, null); };
|
||||
$('#btnAplicar').onclick = () => { const m = PLAN.filas.filter(r => r.marcado); modal('Aplicar los movimientos', `<p>Se van a mover <b>${m.length}</b> cosas (${gb(m.reduce((s,r)=>s+r.bytes,0))}, ${num(m.reduce((s,r)=>s+r.n,0))} archivos) dentro de <span class="path">${esc(PLAN.raiz)}</span>.</p><p><b>Nada se borra.</b> Es un cambio de sitio dentro del mismo disco: rápido y reversible desde Historial. <b>Cierra Resolve y Premiere si tienen abierto material de esta carpeta</b> para que no se bloqueen archivos.</p>`, () => accion($('#btnAplicar'), async () => { const destinos = {}; m.forEach(r => { if (r.editado) destinos[r.id] = r.destino_rel; }); const res = await api('/api/aplicar', {raiz: PLAN.raiz, ids: m.map(r => r.id), destinos}); PLAN = null; MAPA = null; $('#tablaPlan tbody').innerHTML = ''; estadoPlan(); $('#planResumen').innerHTML = `<span class="ok">${res.movimientos} movimientos hechos</span>${res.errores.length ? ` · <span class="bad">${res.errores.length} avisos</span>` : ''}`; aviso(`Listo: ${res.movimientos} movimientos.` + (res.errores.length ? ` ${res.errores.length} no se pudieron.` : ''), res.errores.length ? 'bad' : 'ok', {texto: 'Ver historial', ir: () => irA('historial')}); if (res.errores.length) modal('Avisos al aplicar', `<pre>${esc(res.errores.join('\n'))}</pre>`, null); await cargarEstado(); cargarVista(); })); $('#modalSi').textContent = 'Mover'; };
|
||||
function aplicarFilas(m, btn, titulo){
|
||||
if (!m.length) return;
|
||||
modal(titulo, `<p>Se mueven <b>${m.length}</b> cosa${m.length > 1 ? 's' : ''} (${gb(m.reduce((a, r) => a + r.bytes, 0))}, ${num(m.reduce((a, r) => a + r.n, 0))} archivos) dentro de <span class="path">${esc(PLAN.raiz)}</span>.</p><p><b>Nada se borra.</b> Es un cambio de sitio dentro del mismo disco. <b>Cierra Resolve y Premiere si tienen abierto material de esta carpeta</b> para que no bloqueen archivos.</p>`, () => accion(btn, async () => {
|
||||
const destinos = {}; m.forEach(r => { if (r.editado) destinos[r.id] = r.destino_rel; });
|
||||
const res = await api('/api/aplicar', {raiz: PLAN.raiz, ids: m.map(r => r.id), destinos});
|
||||
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: 'Ver historial', ir: () => irA('historial')});
|
||||
if (res.errores.length) modal('Algunos no se pudieron mover', `<pre>${esc(res.errores.join('\n'))}</pre>`, null);
|
||||
await cargarEstado(); await calcularPlan();
|
||||
}));
|
||||
$('#modalSi').textContent = 'Aplicar';
|
||||
}
|
||||
function aplicarPaso(k, btn){ const p = PASOS_PLAN.find(x => x.k === k); aplicarFilas(PLAN.filas.filter(r => (r.paso || 'resto') === k && r.marcado), btn, 'Paso: ' + p.t); }
|
||||
$('#btnAplicarSeguros').onclick = () => aplicarFilas(PLAN.filas.filter(r => r.marcado), $('#btnAplicarSeguros'), 'Aplicar todo lo marcado');
|
||||
$('#btnArbol').onclick = () => { const arbol = {}; PLAN.filas.filter(r => r.marcado).forEach(r => { const partes = r.destino_rel.split(/[\\/]/); let n = arbol; partes.forEach(p => { n[p] = n[p] || {}; n = n[p]; }); }); const lineas = []; const dibuja = (n, pref) => { const ks = Object.keys(n).sort(); ks.forEach((k, i) => { const ult = i === ks.length - 1; lineas.push(pref + (ult ? '└─ ' : '├─ ') + k); dibuja(n[k], pref + (ult ? ' ' : '│ ')); }); }; dibuja(arbol, ''); modal('Cómo quedaría (solo lo marcado)', `<div class="tree">${esc(PLAN.raiz)}\n${esc(lineas.join('\n'))}</div>`, null); };
|
||||
|
||||
// ---------------- revisar: stock viejo ----------------
|
||||
$$('#s-stock button[data-m]').forEach(b => b.onclick = () => { $('#stockMeses').value = b.dataset.m; if (STOCK.length) { STOCK.forEach(g => g.sel = g.meses >= +b.dataset.m); pintarStock(); } });
|
||||
$('#stockMeses').onchange = () => { if (STOCK.length) { STOCK.forEach(g => g.sel = g.meses >= (+$('#stockMeses').value || 0)); pintarStock(); } };
|
||||
const TRAMOS = [[12, 'Más de 12 meses'], [6, 'Entre 6 y 12 meses'], [3, 'Entre 3 y 6 meses'], [1, 'Entre 1 y 3 meses'], [0, 'Menos de 1 mes']];
|
||||
$('#btnStock').onclick = () => accion($('#btnStock'), async () => {
|
||||
const tipos = [$('#stockVideo').checked && 'Video', $('#stockAudio').checked && 'Audio', $('#stockImg').checked && 'Imagenes'].filter(Boolean).join(',');
|
||||
const d = await api(`/api/stock?raiz=${encodeURIComponent(raizActual())}&meses=${+$('#stockMeses').value || 0}&tipos=${tipos}`);
|
||||
STOCK = d.grupos.map(g => ({...g, sel: true, raiz: raizActual()}));
|
||||
$('#stockResumen').textContent = `${d.n} archivos · ${gb(d.bytes)} en proyectos anteriores a ${d.tope}`;
|
||||
const d = await api(`/api/stock?raiz=${encodeURIComponent(raizActual())}&meses=0&tipos=${tipos}`);
|
||||
const hoy = new Date(); const ahoraM = hoy.getFullYear() * 12 + hoy.getMonth();
|
||||
const min = +$('#stockMeses').value || 0;
|
||||
STOCK = d.grupos.map(g => { const m = /^(\d{4})-(\d{2})/.exec(g.mes || ''); const meses = m ? ahoraM - ((+m[1]) * 12 + (+m[2]) - 1) : 0; return {...g, meses, sel: meses >= min, raiz: raizActual()}; });
|
||||
$('#stockResumen').textContent = `${d.n} archivos de stock · ${gb(d.bytes)} en total`;
|
||||
$('#stockCaja').style.display = ''; pintarStock();
|
||||
});
|
||||
function pintarStock(){
|
||||
$('#tablaStock tbody').innerHTML = STOCK.map((g,i) => `<tr><td><input type="checkbox" data-i="${i}" ${g.sel ? 'checked' : ''} aria-label="marcar"></td><td class="path">${esc(g.carpeta)}</td><td>${esc(g.mes)}</td><td class="num">${g.n}</td><td class="num">${gb(g.bytes)}</td><td class="cell-small">${g.ejemplos.map(esc).join('<br>')}</td></tr>`).join('') || '<tr><td colspan="6" class="mut">Nada encontrado.</td></tr>';
|
||||
$$('#tablaStock input').forEach(i => i.onchange = () => { STOCK[+i.dataset.i].sel = i.checked; resumenStock(); }); resumenStock();
|
||||
const tramoDe = g => TRAMOS.find(t => g.meses >= t[0]) || TRAMOS[TRAMOS.length - 1];
|
||||
let html = '';
|
||||
TRAMOS.forEach(t => {
|
||||
const gs = STOCK.map((g, i) => ({g, i})).filter(x => tramoDe(x.g) === t);
|
||||
if (!gs.length) return;
|
||||
const todos = gs.every(x => x.g.sel);
|
||||
html += `<tr class="stk-grp"><td><input type="checkbox" data-t="${t[0]}" ${todos ? 'checked' : ''} aria-label="marcar tramo"></td><td colspan="2">${t[1]}</td><td class="num">${num(gs.reduce((a, x) => a + x.g.n, 0))}</td><td class="num">${gb(gs.reduce((a, x) => a + x.g.bytes, 0))}</td><td class="mut" style="font-weight:500">${gs.length} carpeta${gs.length > 1 ? 's' : ''}</td></tr>`;
|
||||
html += gs.map(({g, i}) => `<tr><td><input type="checkbox" data-i="${i}" ${g.sel ? 'checked' : ''} aria-label="marcar"></td><td class="path">${esc(g.carpeta)}</td><td>${esc(g.mes)}</td><td class="num">${g.n}</td><td class="num">${gb(g.bytes)}</td><td class="cell-small">${g.ejemplos.map(esc).join('<br>')}</td></tr>`).join('');
|
||||
});
|
||||
$('#tablaStock tbody').innerHTML = html || '<tr><td colspan="6" class="mut">No hay stock de Envato en esta carpeta.</td></tr>';
|
||||
$$('#tablaStock input[data-i]').forEach(i => i.onchange = () => { STOCK[+i.dataset.i].sel = i.checked; pintarStock(); });
|
||||
$$('#tablaStock input[data-t]').forEach(i => i.onchange = () => { const t = TRAMOS.find(x => x[0] === +i.dataset.t); STOCK.forEach(g => { if (tramoDe(g) === t) g.sel = i.checked; }); pintarStock(); });
|
||||
resumenStock();
|
||||
}
|
||||
function resumenStock(){ const s = STOCK.filter(g => g.sel); bloquear('btnStockPapelera', STOCK.length ? (s.length ? '' : 'Marca alguna carpeta') : 'Busca primero'); }
|
||||
function resumenStock(){ const s = STOCK.filter(g => g.sel); bloquear('btnStockPapelera', STOCK.length ? (s.length ? '' : 'Marca alguna carpeta') : 'Busca primero'); $('#btnStockPapelera').textContent = s.length ? `Apartar ${s.length} carpeta${s.length > 1 ? 's' : ''} (${gb(s.reduce((a, g) => a + g.bytes, 0))})` : 'Apartar a _Para borrar/stock'; }
|
||||
$('#btnStockPapelera').onclick = () => { const s = STOCK.filter(g => g.sel && g.raiz === raizActual()); const rutas = s.flatMap(g => g.rutas); if (!rutas.length) return aviso('La lista es de otra carpeta. Busca de nuevo.', 'bad'); modal('Apartar stock de Envato', `<p><b>${rutas.length}</b> archivos (${gb(s.reduce((a,g)=>a+g.bytes,0))}) se mueven a <span class="path">_Para borrar/stock</span> dentro de la carpeta del cliente. Nada se borra; se deshace desde Historial.</p>`, () => accion($('#btnStockPapelera'), async () => { const r = await api('/api/stock/papelera', {raiz: raizActual(), rutas}); aviso(`${r.movimientos} archivos de stock apartados.`, 'ok', {texto: 'Ver historial', ir: () => irA('historial')}); STOCK = []; pintarStock(); $('#stockCaja').style.display = 'none'; MAPA = null; await cargarEstado(); })); $('#modalSi').textContent = 'Apartar'; };
|
||||
|
||||
// ---------------- convertir ----------------
|
||||
@@ -947,10 +1031,10 @@ function pintarRespaldo(){
|
||||
$('#btnRespCalc').onclick = () => accion($('#btnRespCalc'), async () => {
|
||||
const dest = $('#respDestino').value.trim(); if (!dest) throw new ApiError('Indica la carpeta de destino');
|
||||
$('#respEstado').innerHTML = '<span class="spin"></span> calculando…';
|
||||
RESP = await api(`/api/respaldo/plan?raiz=${encodeURIComponent(raizActual())}&destino=${encodeURIComponent(dest)}&por_tipo=${$('#respTipo').checked ? 1 : 0}&sin_stock=${$('#respSinStock').checked ? 1 : 0}&incluir_raw=${$('#respRaw').checked ? 1 : 0}`);
|
||||
RESP = await api(`/api/respaldo/plan?raiz=${encodeURIComponent(raizActual())}&destino=${encodeURIComponent(dest)}&por_tipo=${$('#respTipo').checked ? 1 : 0}&sin_stock=${$('#respSinStock').checked ? 1 : 0}&incluir_raw=${$('#respRaw').checked ? 1 : 0}&meses=${$('#respMeses').value}`);
|
||||
RESP.raiz = raizActual();
|
||||
$('#respEstado').textContent = `se copiaría a ${RESP.base}`;
|
||||
$('#respCards').innerHTML = [[num(RESP.n), 'archivos a copiar'], [gb(RESP.bytes), 'peso del respaldo'], [RESP.convertidos, 'clips ya en H.265 10 bits'], [gb(RESP.raw_omitidos.bytes), `originales de cámara NO copiados (${RESP.raw_omitidos.n})`], [gb(RESP.stock_omitido.bytes), `stock de Envato omitido (${RESP.stock_omitido.n})`]].map(([b,s]) => `<div class="card"><b>${b}</b><small>${s}</small></div>`).join('');
|
||||
$('#respCards').innerHTML = [[num(RESP.n), 'archivos a copiar'], [gb(RESP.bytes), 'peso del respaldo'], [RESP.convertidos, 'clips ya en H.265 10 bits'], [gb(RESP.raw_omitidos.bytes), `originales de cámara NO copiados (${RESP.raw_omitidos.n})`], [gb(RESP.stock_omitido.bytes), `stock de Envato omitido (${RESP.stock_omitido.n})`]].concat(RESP.reciente_omitido && RESP.reciente_omitido.meses ? [[gb(RESP.reciente_omitido.bytes), `de menos de ${RESP.reciente_omitido.meses} meses, no se copia aún (${num(RESP.reciente_omitido.n)})`]] : []).map(([b,s]) => `<div class="card"><b>${b}</b><small>${s}</small></div>`).join('');
|
||||
$('#respTipos').innerHTML = `<table>${RESP.por_tipo.map(t => `<tr><td>${esc(t.tipo)}</td><td class="num">${num(t.n)}</td><td class="num">${gb(t.bytes)}</td></tr>`).join('')}</table>`;
|
||||
$('#respRawLista').innerHTML = RESP.raw_omitidos.n ? `<ul class="ejemplos" style="margin:0;padding-left:16px">${RESP.raw_omitidos.ejemplos.map(x => `<li>${esc(x.rel)} · ${gb(x.bytes)}</li>`).join('')}${RESP.raw_omitidos.n > 15 ? `<li>… y ${RESP.raw_omitidos.n - 15} más</li>` : ''}</ul>` : '<span class="ok">Ninguno: todo lo de cámara ya tiene su versión 10 bits (o se incluye tal cual).</span>';
|
||||
$('#respMuestra').innerHTML = RESP.muestra.map(m => `${esc(m.de)} <span class="arrow">→</span> ${esc(m.a)}`).join('<br>');
|
||||
@@ -974,8 +1058,7 @@ async function cargarBit(){
|
||||
// Avisos de error recientes (los globos rojos), con su detalle: para saber qué pasó después de que desaparecieron.
|
||||
const av = avisosGuardados().filter(a => !solo || !raiz || (a.raiz || '').toLowerCase() === raiz);
|
||||
const avHtml = av.length ? `<div class="dia">Avisos recientes <button class="lnk" id="btnBorrarAvisos" style="margin-left:8px;font-size:12px">borrar</button></div>` + av.slice(0, 20).map(a => `<div class="ev"><span class="h">${hora(a.ts)}</span><span class="q"><b>${esc(a.texto)}</b>${!solo && a.raiz ? ` · <span class="mut">${esc(a.raiz.split(/[\\/]/).filter(Boolean).pop())}</span>` : ''}${a.detalle ? `<details><summary>detalle</summary><pre class="cell-small" style="white-space:pre-wrap">${esc(a.detalle)}</pre></details>` : ''}</span><span></span></div>`).join('') : '';
|
||||
$('#historial').innerHTML = avHtml + (Object.keys(dias).map(k => `<div class="dia">${esc(titulo(k))}</div>` + dias[k].map(l => `<div class="ev"><span class="h">${hora(l.ts)}</span><span class="q"><b>${esc(queSeHizo(l.lote))}</b> · ${l.movimientos} movimiento${l.movimientos !== 1 ? 's' : ''}${!solo ? ` · <span class="mut">${esc((l.raiz || '').split(/[\\/]/).filter(Boolean).pop() || l.raiz)}</span>` : ''}${l.deshecho ? ' <span class="tag">deshecho</span>' : ''}${l.errores ? ` <span class="tag baja">${l.errores} avisos</span>` : ''}${l.ejemplos.length ? `<details><summary>ejemplos</summary><div class="cell-small">${l.ejemplos.map(e => esc(e.de.split(/[\\/]/).slice(-2).join('/')) + ' → ' + esc(e.a.split(/[\\/]/).slice(-3).join('/'))).join('<br>')}</div></details>` : ''}</span>${l.deshecho || l.lote === 'optimizar' ? '<span></span>' : `<button data-l="${esc(l.lote)}">Deshacer</button>`}</div>`).join('')).join('') || '<div class="mut">Todavía no se ha movido nada.</div>');
|
||||
$$('#historial button[data-l]').forEach(b => b.onclick = () => deshacer(b.dataset.l));
|
||||
$('#historial').innerHTML = avHtml + (Object.keys(dias).map(k => `<div class="dia">${esc(titulo(k))}</div>` + dias[k].map(l => `<div class="ev"><span class="h">${hora(l.ts)}</span><span class="q"><b>${esc(queSeHizo(l.lote))}</b> · ${l.movimientos} movimiento${l.movimientos !== 1 ? 's' : ''}${!solo ? ` · <span class="mut">${esc((l.raiz || '').split(/[\\/]/).filter(Boolean).pop() || l.raiz)}</span>` : ''}${l.deshecho ? ' <span class="tag">deshecho</span>' : ''}${l.errores ? ` <span class="tag baja">${l.errores} avisos</span>` : ''}${l.ejemplos.length ? `<details><summary>ejemplos</summary><div class="cell-small">${l.ejemplos.map(e => esc(e.de.split(/[\\/]/).slice(-2).join('/')) + ' → ' + esc(e.a.split(/[\\/]/).slice(-3).join('/'))).join('<br>')}</div></details>` : ''}</span><span></span></div>`).join('')).join('') || '<div class="mut">Todavía no se ha movido nada.</div>');
|
||||
const ba = $('#btnBorrarAvisos'); if (ba) ba.onclick = () => { try { localStorage.removeItem(AVISOS_CLAVE); } catch (e) {} cargarBit().catch(e => aviso(e.message, 'bad')); };
|
||||
}
|
||||
$('#bitSoloRaiz').onchange = () => cargarBit().catch(e => aviso(e.message, 'bad'));
|
||||
@@ -1025,6 +1108,12 @@ document.addEventListener('drop', seguro(async e => {
|
||||
const texto = e.dataTransfer.getData('text');
|
||||
if (!nombre && texto && /^[A-Za-z]:\\/.test(texto.trim())) return proponerRaiz(texto.trim());
|
||||
if (!nombre) return aviso('Suelta una carpeta (no un archivo).', 'bad');
|
||||
// dentro de la ventana de OrganiLAB (WebView2) la ruta completa llega por el puente: no hay que buscar
|
||||
if (window.pywebview || (window.chrome && window.chrome.webview)) {
|
||||
modal('Leyendo la carpeta…', `<p><span class="spin"></span> «${esc(nombre)}»</p>`, null);
|
||||
const s = await api('/api/soltado?nombre=' + encodeURIComponent(nombre));
|
||||
if (s.ruta) return proponerRaiz(s.es_archivo ? s.ruta.replace(/[\/][^\/]*$/, '') : s.ruta);
|
||||
}
|
||||
modal('Buscando la carpeta…', `<p><span class="spin"></span> El navegador solo entrega el nombre («${esc(nombre)}»). Buscando en los discos…</p>`, null);
|
||||
const d = await api('/api/buscar-carpeta?nombre=' + encodeURIComponent(nombre));
|
||||
if (!d.coincidencias.length) return modal('No la encontré', `<p>No hay ninguna carpeta llamada <b>${esc(nombre)}</b> en los tres primeros niveles de los discos. Pega la ruta completa:</p><input type="text" id="rutaPegada" style="width:100%" placeholder="Z:\\Clientes\\${esc(nombre)}">`, () => proponerRaiz($('#rutaPegada').value.trim()));
|
||||
|
||||
Referencia en una nueva incidencia
Block a user