#!/usr/bin/env python3 """ Dashboard GUI for PunimTag features """ import os import threading import tkinter as tk from tkinter import ttk, messagebox from gui_core import GUICore class DashboardGUI: """Dashboard with launchers for core features. on_scan: callable taking (folder_path: str, recursive: bool) -> int """ def __init__(self, gui_core: GUICore, on_scan=None, on_process=None, on_identify=None): self.gui_core = gui_core self.on_scan = on_scan self.on_process = on_process self.on_identify = on_identify def open(self) -> int: root = tk.Tk() root.title("PunimTag Dashboard") root.resizable(True, True) root.withdraw() main = ttk.Frame(root, padding="16") main.pack(fill=tk.BOTH, expand=True) title = ttk.Label(main, text="PunimTag Dashboard", font=("Arial", 16, "bold")) title.pack(anchor="w", pady=(0, 12)) desc = ttk.Label( main, text=( "Launch core features. Buttons are placeholders for now; " "no actions are executed yet." ), foreground="#555", ) desc.pack(anchor="w", pady=(0, 16)) grid = ttk.Frame(main) grid.pack(fill=tk.BOTH, expand=True) # Helper to create a feature card def add_card(row: int, col: int, title_text: str, subtitle: str, button_text: str): card = ttk.Frame(grid, padding="12", relief=tk.RIDGE) card.grid(row=row, column=col, padx=8, pady=8, sticky="nsew") ttk.Label(card, text=title_text, font=("Arial", 12, "bold")).pack(anchor="w") ttk.Label(card, text=subtitle, foreground="#666").pack(anchor="w", pady=(2, 8)) # Placeholder button (no-op) ttk.Button(card, text=button_text, command=lambda: None).pack(anchor="w") # Layout configuration for responsive grid for i in range(3): grid.columnconfigure(i, weight=1) for i in range(3): grid.rowconfigure(i, weight=1) # Cards # Custom Scan card with folder entry and recursive checkbox scan_card = ttk.Frame(grid, padding="12", relief=tk.RIDGE) scan_card.grid(row=0, column=0, padx=8, pady=8, sticky="nsew") ttk.Label(scan_card, text="Scan", font=("Arial", 12, "bold")).pack(anchor="w") ttk.Label(scan_card, text="Scan folders and add photos", foreground="#666").pack(anchor="w", pady=(2, 8)) # Folder input folder_row = ttk.Frame(scan_card) folder_row.pack(fill=tk.X, pady=(0, 6)) ttk.Label(folder_row, text="Folder:").pack(side=tk.LEFT) folder_var = tk.StringVar() folder_entry = ttk.Entry(folder_row, textvariable=folder_var) folder_entry.pack(side=tk.LEFT, padx=(6, 0), fill=tk.X, expand=True) # Browse button for folder selection def browse_folder(): from tkinter import filedialog folder_path = filedialog.askdirectory(title="Select folder to scan for photos") if folder_path: folder_var.set(folder_path) browse_btn = ttk.Button(folder_row, text="Browse", command=browse_folder) browse_btn.pack(side=tk.LEFT, padx=(6, 0)) # Recursive checkbox recursive_var = tk.BooleanVar(value=True) ttk.Checkbutton( scan_card, text="include photos in sub-folders", variable=recursive_var ).pack(anchor="w", pady=(0, 8)) # Action button scan_btn = ttk.Button(scan_card, text="Add new photos") scan_btn.pack(anchor="w") def run_scan(): folder = folder_var.get().strip() recursive = bool(recursive_var.get()) if not folder: messagebox.showwarning("Scan", "Please enter a folder path.", parent=root) return # Validate folder path using path utilities from path_utils import validate_path_exists, normalize_path try: folder = normalize_path(folder) if not validate_path_exists(folder): messagebox.showerror("Scan", f"Folder does not exist or is not accessible: {folder}", parent=root) return except ValueError as e: messagebox.showerror("Scan", f"Invalid folder path: {e}", parent=root) return if not callable(self.on_scan): messagebox.showinfo("Scan", "Scan is not wired yet.", parent=root) return def worker(): try: scan_btn.config(state=tk.DISABLED) result = self.on_scan(folder, recursive) messagebox.showinfo("Scan", f"Scan completed. Result: {result}", parent=root) except Exception as e: messagebox.showerror("Scan", f"Error during scan: {e}", parent=root) finally: try: scan_btn.config(state=tk.NORMAL) except Exception: pass threading.Thread(target=worker, daemon=True).start() scan_btn.config(command=run_scan) # Other cards remain generic # Custom Process card with optional limit process_card = ttk.Frame(grid, padding="12", relief=tk.RIDGE) process_card.grid(row=0, column=1, padx=8, pady=8, sticky="nsew") ttk.Label(process_card, text="Process", font=("Arial", 12, "bold")).pack(anchor="w") ttk.Label(process_card, text="Detect faces in photos", foreground="#666").pack(anchor="w", pady=(2, 8)) # Limit controls limit_row = ttk.Frame(process_card) limit_row.pack(fill=tk.X, pady=(0, 8)) limit_enabled = tk.BooleanVar(value=False) ttk.Checkbutton(limit_row, text="limit to", variable=limit_enabled).pack(side=tk.LEFT) limit_var = tk.StringVar(value="50") limit_entry = ttk.Entry(limit_row, textvariable=limit_var, width=8) limit_entry.pack(side=tk.LEFT, padx=(6, 0)) # Action button process_btn = ttk.Button(process_card, text="Process photos") process_btn.pack(anchor="w") def run_process(): if not callable(self.on_process): messagebox.showinfo("Process", "Process is not wired yet.", parent=root) return limit_value = None if limit_enabled.get(): try: limit_value = int(limit_var.get().strip()) if limit_value <= 0: raise ValueError except Exception: messagebox.showerror("Process", "Please enter a valid positive integer for limit.", parent=root) return def worker(): try: process_btn.config(state=tk.DISABLED) result = self.on_process(limit_value) messagebox.showinfo("Process", f"Processing completed. Result: {result}", parent=root) except Exception as e: messagebox.showerror("Process", f"Error during processing: {e}", parent=root) finally: try: process_btn.config(state=tk.NORMAL) except Exception: pass threading.Thread(target=worker, daemon=True).start() process_btn.config(command=run_process) # Custom Identify card with show faces and batch options identify_card = ttk.Frame(grid, padding="12", relief=tk.RIDGE) identify_card.grid(row=0, column=2, padx=8, pady=8, sticky="nsew") ttk.Label(identify_card, text="Identify", font=("Arial", 12, "bold")).pack(anchor="w") ttk.Label(identify_card, text="Identify faces (GUI)", foreground="#666").pack(anchor="w", pady=(2, 8)) # Show faces checkbox show_faces_var = tk.BooleanVar(value=False) ttk.Checkbutton(identify_card, text="show faces", variable=show_faces_var).pack(anchor="w", pady=(0, 4)) # Batch controls batch_row = ttk.Frame(identify_card) batch_row.pack(fill=tk.X, pady=(0, 8)) batch_enabled = tk.BooleanVar(value=True) ttk.Checkbutton(batch_row, text="batch", variable=batch_enabled).pack(side=tk.LEFT) batch_var = tk.StringVar(value="10") batch_entry = ttk.Entry(batch_row, textvariable=batch_var, width=8) batch_entry.pack(side=tk.LEFT, padx=(6, 0)) # Action button identify_btn = ttk.Button(identify_card, text="Identify faces") identify_btn.pack(anchor="w") def run_identify(): if not callable(self.on_identify): messagebox.showinfo("Identify", "Identify is not wired yet.", parent=root) return batch_value = None if batch_enabled.get(): try: batch_value = int(batch_var.get().strip()) if batch_value <= 0: raise ValueError except Exception: messagebox.showerror("Identify", "Please enter a valid positive integer for batch.", parent=root) return def worker(): try: identify_btn.config(state=tk.DISABLED) result = self.on_identify(batch_value, show_faces_var.get()) messagebox.showinfo("Identify", f"Identification completed. Result: {result}", parent=root) except Exception as e: messagebox.showerror("Identify", f"Error during identification: {e}", parent=root) finally: try: identify_btn.config(state=tk.NORMAL) except Exception: pass threading.Thread(target=worker, daemon=True).start() identify_btn.config(command=run_identify) add_card(1, 0, "Auto-Match", "Find & confirm matches (GUI)", "Open Auto-Match") add_card(1, 1, "Search", "Find photos by name (GUI)", "Open Search") add_card(1, 2, "Modify Identified", "View/modify identified faces (GUI)", "Open Modify") add_card(2, 0, "Tag Manager", "Manage tags and photos (GUI)", "Open Tag Manager") # Close button footer = ttk.Frame(main) footer.pack(fill=tk.X, pady=(12, 0)) ttk.Button(footer, text="Close", command=lambda: root.destroy()).pack(side=tk.RIGHT) root.update_idletasks() self.gui_core.center_window(root, 900, 600) root.deiconify() root.mainloop() return 0