punimtag/dashboard_gui.py
tanyar09 36aaadca1d Add folder browsing and path validation features to Dashboard GUI and photo management
This commit introduces a folder browsing button in the Dashboard GUI, allowing users to select a folder for photo scanning. It also implements path normalization and validation using new utility functions from the path_utils module, ensuring that folder paths are absolute and accessible before scanning. Additionally, the PhotoManager class has been updated to utilize these path utilities, enhancing the robustness of folder scanning operations. This improves user experience by preventing errors related to invalid paths and streamlining folder management across the application.
2025-10-09 12:43:28 -04:00

253 lines
11 KiB
Python

#!/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