From 0883a47914d239fd9dcbbf08ea1c617ae49990e0 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Tue, 7 Oct 2025 12:27:57 -0400 Subject: [PATCH] Add Dashboard GUI for core feature management This commit introduces the DashboardGUI class, providing a user-friendly interface for launching core features of the PunimTag application. The dashboard includes placeholder buttons for scanning, processing, and identifying faces, along with options for folder input and batch processing. The PhotoTagger class is updated to integrate the new dashboard functionality, and the README is revised to include usage instructions for the new dashboard command. This enhancement aims to streamline user interactions and improve overall accessibility to key features. --- README.md | 1 + dashboard_gui.py | 234 +++++++++++++++++++++++++++++++++++++++++++++++ database.py | 2 +- photo_tagger.py | 28 +++++- 4 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 dashboard_gui.py diff --git a/README.md b/README.md index 1a9b776..5d29458 100644 --- a/README.md +++ b/README.md @@ -926,6 +926,7 @@ python3 photo_tagger.py identify --show-faces --batch 10 # Opens GUI python3 photo_tagger.py auto-match --show-faces # Opens GUI python3 photo_tagger.py search-gui # Opens Search GUI python3 photo_tagger.py modifyidentified # Opens GUI to view/modify +python3 photo_tagger.py dashboard # Opens Dashboard (placeholders) python3 photo_tagger.py tag-manager # Opens GUI for tag management python3 photo_tagger.py stats ``` \ No newline at end of file diff --git a/dashboard_gui.py b/dashboard_gui.py new file mode 100644 index 0000000..4772b8b --- /dev/null +++ b/dashboard_gui.py @@ -0,0 +1,234 @@ +#!/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) + # 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 + if not os.path.isdir(folder): + messagebox.showerror("Scan", "Folder does not exist.", 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 + + diff --git a/database.py b/database.py index f58cc71..ff3e400 100644 --- a/database.py +++ b/database.py @@ -26,7 +26,7 @@ class DatabaseManager: """Context manager for database connections with connection pooling""" with self._db_lock: if self._db_connection is None: - self._db_connection = sqlite3.connect(self.db_path, timeout=DB_TIMEOUT) + self._db_connection = sqlite3.connect(self.db_path, timeout=DB_TIMEOUT, check_same_thread=False) self._db_connection.row_factory = sqlite3.Row try: yield self._db_connection diff --git a/photo_tagger.py b/photo_tagger.py index 37336c5..4d0ca3c 100644 --- a/photo_tagger.py +++ b/photo_tagger.py @@ -26,6 +26,7 @@ from auto_match_gui import AutoMatchGUI from modify_identified_gui import ModifyIdentifiedGUI from tag_manager_gui import TagManagerGUI from search_gui import SearchGUI +from dashboard_gui import DashboardGUI class PhotoTagger: @@ -49,6 +50,7 @@ class PhotoTagger: self.modify_identified_gui = ModifyIdentifiedGUI(self.db, self.face_processor, verbose) self.tag_manager_gui = TagManagerGUI(self.db, self.gui_core, self.tag_manager, self.face_processor, verbose) self.search_gui = SearchGUI(self.db, self.search_stats, self.gui_core, verbose) + self.dashboard_gui = DashboardGUI(self.gui_core, on_scan=self._dashboard_scan, on_process=self._dashboard_process, on_identify=self._dashboard_identify) # Legacy compatibility - expose some methods directly self._db_connection = None @@ -193,6 +195,27 @@ class PhotoTagger: def searchgui(self) -> int: """Open the Search GUI.""" return self.search_gui.search_gui() + + def dashboard(self) -> int: + """Open the Dashboard GUI (placeholders only).""" + return self.dashboard_gui.open() + + # Dashboard callbacks + def _dashboard_scan(self, folder_path: str, recursive: bool) -> int: + """Callback to scan a folder from the dashboard.""" + return self.scan_folder(folder_path, recursive) + + def _dashboard_process(self, limit_value: Optional[int]) -> int: + """Callback to process faces from the dashboard with optional limit.""" + if limit_value is None: + return self.process_faces() + return self.process_faces(limit=limit_value) + + def _dashboard_identify(self, batch_value: Optional[int], show_faces: bool) -> int: + """Callback to identify faces from the dashboard with optional batch and show_faces.""" + if batch_value is None: + return self.identify_faces(show_faces=show_faces) + return self.identify_faces(batch_size=batch_value, show_faces=show_faces) def _setup_window_size_saving(self, root, config_file="gui_config.json"): """Set up window size saving functionality (legacy compatibility)""" @@ -291,7 +314,7 @@ Examples: ) parser.add_argument('command', - choices=['scan', 'process', 'identify', 'tag', 'search', 'search-gui', 'stats', 'match', 'auto-match', 'modifyidentified', 'tag-manager'], + choices=['scan', 'process', 'identify', 'tag', 'search', 'search-gui', 'dashboard', 'stats', 'match', 'auto-match', 'modifyidentified', 'tag-manager'], help='Command to execute') parser.add_argument('target', nargs='?', @@ -378,6 +401,9 @@ Examples: elif args.command == 'search-gui': tagger.searchgui() + elif args.command == 'dashboard': + tagger.dashboard() + elif args.command == 'stats': tagger.stats()