import os import threading import tkinter as tk from core.config import ConfigManager, Profile from core.robocopy_engine import RobocopySyncEngine from core.sync_engine import SyncEngine from ui import styles from ui.path_panel import PathPanel from ui.profile_bar import ProfileBar from ui.status_bar import StatusBar from ui.summary_panel import SummaryPanel from ui.system_list import SystemList class PocketSyncApp: def __init__(self, root: tk.Tk, config_manager: ConfigManager): self.root = root self.cm = config_manager self.engine: SyncEngine = RobocopySyncEngine() self._build_ui() self._load_profile(self.cm.active_profile) # ------------------------------------------------------------------ # Construcción de la UI # ------------------------------------------------------------------ def _build_ui(self) -> None: self.root.title("PocketSync") self.root.geometry(f"{styles.WINDOW_WIDTH}x{styles.WINDOW_HEIGHT}") # --- Barra de perfiles --- self.profile_bar = ProfileBar(self.root, on_change=self._on_profile_change) self.profile_bar.pack(fill="x", padx=styles.PAD_X, pady=(8, 2)) self.profile_bar.set_callbacks( on_new=self._on_new_profile, on_rename=self._on_rename_profile, on_delete=self._on_delete_profile, ) self._refresh_profile_bar() # --- Rutas de origen --- self.src_panel = PathPanel(self.root, title="Rutas de origen") self.src_panel.pack(fill="x", padx=styles.PAD_X, pady=styles.PAD_Y) self.src_panel.set_roms_callback(self._on_roms_src_change) # --- Lista de sistemas --- self.system_list = SystemList(self.root) self.system_list.pack(fill="both", expand=True, padx=styles.PAD_X) # --- Rutas de destino --- self.dst_panel = PathPanel(self.root, title="Rutas de destino") self.dst_panel.pack(fill="x", padx=styles.PAD_X, pady=styles.PAD_Y) # --- Botón Sync --- tk.Button( self.root, text="Sync Now", font=styles.FONT_BUTTON, command=self._run_sync, ).pack(pady=6) # --- Barra de estado --- self.status_bar = StatusBar(self.root) self.status_bar.pack(fill="x", padx=styles.PAD_X, pady=styles.PAD_Y) # --- Panel de resumen --- self.summary = SummaryPanel(self.root) self.summary.pack(fill="x", padx=styles.PAD_X, pady=styles.PAD_Y) self.root.protocol("WM_DELETE_WINDOW", self._on_close) # ------------------------------------------------------------------ # Gestión de perfiles # ------------------------------------------------------------------ def _refresh_profile_bar(self) -> None: self.profile_bar.refresh(self.cm.profile_names(), self.cm.active_profile_name) def _collect_ui_into_profile(self) -> None: """Escribe el estado actual de la UI en el perfil activo.""" p = self.cm.active_profile p.esde_src = self.src_panel.get_esde() p.roms_src = self.src_panel.get_roms() p.esde_dst = self.dst_panel.get_esde() p.roms_dst = self.dst_panel.get_roms() p.selected = self.system_list.get_selected() def _load_profile(self, profile: Profile) -> None: self.src_panel.set_esde(profile.esde_src) self.src_panel.set_roms(profile.roms_src) self.dst_panel.set_esde(profile.esde_dst) self.dst_panel.set_roms(profile.roms_dst) if os.path.isdir(profile.roms_src): self.system_list.populate(profile.roms_src) else: self.system_list.populate("") self.system_list.set_selected(profile.selected) def _on_profile_change(self, name: str) -> None: self._collect_ui_into_profile() self.cm.active_profile_name = name self._load_profile(self.cm.active_profile) self._refresh_profile_bar() def _on_new_profile(self, name: str) -> bool: if self.cm.get_profile(name) is not None: return False self._collect_ui_into_profile() self.cm.add_profile(name) self.cm.active_profile_name = name self._load_profile(self.cm.active_profile) self._refresh_profile_bar() return True def _on_rename_profile(self, old: str, new: str) -> bool: ok = self.cm.rename_profile(old, new) if ok: self._refresh_profile_bar() return ok def _on_delete_profile(self, name: str) -> bool: ok = self.cm.delete_profile(name) if ok: self._load_profile(self.cm.active_profile) self._refresh_profile_bar() return ok # ------------------------------------------------------------------ # Callbacks de widgets # ------------------------------------------------------------------ def _on_roms_src_change(self, path: str) -> None: self.system_list.populate(path) # ------------------------------------------------------------------ # Loop de sincronización # ------------------------------------------------------------------ def _run_sync(self) -> None: thread = threading.Thread(target=self._sync_thread) thread.daemon = True thread.start() def _sync_thread(self) -> None: selected = self.system_list.get_selected() if not selected: self.summary.append("No hay sistemas seleccionados.") return esde_src = self.src_panel.get_esde() roms_src = self.src_panel.get_roms() esde_dst = self.dst_panel.get_esde() roms_dst = self.dst_panel.get_roms() if not all([esde_src, roms_src, esde_dst, roms_dst]): self.summary.append("ERROR: Debes configurar todas las rutas antes de continuar.") return self.summary.clear() self.summary.append("=" * 60) self.summary.append("INICIANDO PROCESO DE COPIA") self.summary.append(f"Total de sistemas a procesar: {len(selected)}") self.summary.append("=" * 60) total = len(selected) for idx, system in enumerate(selected, 1): self.status_bar.set_system(f"Sistema: {idx}/{total} - {system.upper()}") self.summary.append(f"\nSISTEMA [{idx}/{total}]: {system.upper()}") # Fase 1: ROMs self.status_bar.set_phase("Fase: [1/3] Copiando ROMs...") self.summary.append(" [1/3] Copiando ROMs...") self.engine.sync_folder( os.path.join(roms_src, system), os.path.join(roms_dst, system), on_file=self.status_bar.set_file, on_summary=self.summary.append, ) # Fase 2: gamelists self.status_bar.set_phase("Fase: [2/3] Copiando gamelists...") self.summary.append(" [2/3] Copiando gamelists...") self.engine.sync_folder( os.path.join(esde_src, "gamelists", system), os.path.join(esde_dst, "gamelists", system), on_file=self.status_bar.set_file, on_summary=self.summary.append, ) # Fase 3: media self.status_bar.set_phase("Fase: [3/3] Copiando media...") self.summary.append(" [3/3] Copiando media...") self.engine.sync_folder( os.path.join(esde_src, "downloaded_media", system), os.path.join(esde_dst, "downloaded_media", system), on_file=self.status_bar.set_file, on_summary=self.summary.append, ) self.summary.append(f" Sistema '{system}' completado\n") self.status_bar.reset() self.summary.append("=" * 60) self.summary.append("PROCESO COMPLETADO EXITOSAMENTE") self.summary.append("=" * 60) # ------------------------------------------------------------------ # Cierre # ------------------------------------------------------------------ def _on_close(self) -> None: self._collect_ui_into_profile() self.cm.save() self.root.destroy()