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.
253 lines
11 KiB
Python
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
|
|
|
|
|