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.
This commit is contained in:
tanyar09 2025-10-07 12:27:57 -04:00
parent d4504ee81a
commit 0883a47914
4 changed files with 263 additions and 2 deletions

View File

@ -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
```

234
dashboard_gui.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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()