This commit transforms the Dashboard GUI into a unified interface designed for web migration, featuring a single window with a menu bar for easy access to all functionalities. Key enhancements include the addition of a content area for seamless panel switching, improved panel management, and real-time status updates. The README has also been updated to reflect these changes, providing a comprehensive overview of the new dashboard features and system requirements.
433 lines
17 KiB
Python
433 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Unified Dashboard GUI for PunimTag features
|
|
Designed with web migration in mind - single window with menu bar and content area
|
|
"""
|
|
|
|
import os
|
|
import threading
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox
|
|
from typing import Dict, Optional, Callable
|
|
|
|
from gui_core import GUICore
|
|
|
|
|
|
class DashboardGUI:
|
|
"""Unified Dashboard with menu bar and content area for all features.
|
|
|
|
Designed to be web-migration friendly with clear separation between
|
|
navigation (menu bar) and content (panels).
|
|
"""
|
|
|
|
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
|
|
|
|
# Panel management for future web migration
|
|
self.panels: Dict[str, ttk.Frame] = {}
|
|
self.current_panel: Optional[str] = None
|
|
self.root: Optional[tk.Tk] = None
|
|
|
|
def open(self) -> int:
|
|
"""Open the unified dashboard with menu bar and content area"""
|
|
self.root = tk.Tk()
|
|
self.root.title("PunimTag - Unified Dashboard")
|
|
self.root.resizable(True, True)
|
|
self.root.withdraw()
|
|
|
|
# Create main container
|
|
main_container = ttk.Frame(self.root)
|
|
main_container.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Create menu bar
|
|
self._create_menu_bar(main_container)
|
|
|
|
# Create content area
|
|
self._create_content_area(main_container)
|
|
|
|
# Initialize panels
|
|
self._initialize_panels()
|
|
|
|
# Show default panel
|
|
self.show_panel("home")
|
|
|
|
# Center and show window
|
|
self.root.update_idletasks()
|
|
self.gui_core.center_window(self.root, 1200, 800)
|
|
self.root.deiconify()
|
|
self.root.mainloop()
|
|
return 0
|
|
|
|
def _create_menu_bar(self, parent: ttk.Frame):
|
|
"""Create the top menu bar with all functionality buttons"""
|
|
menu_frame = ttk.Frame(parent)
|
|
menu_frame.pack(fill=tk.X, padx=10, pady=5)
|
|
|
|
# Title
|
|
title_label = ttk.Label(menu_frame, text="PunimTag", font=("Arial", 16, "bold"))
|
|
title_label.pack(side=tk.LEFT, padx=(0, 20))
|
|
|
|
# Menu buttons
|
|
menu_buttons = [
|
|
("📁 Scan", "scan", "Scan folders and add photos"),
|
|
("🔍 Process", "process", "Detect faces in photos"),
|
|
("👤 Identify", "identify", "Identify faces in photos"),
|
|
("🔗 Auto-Match", "auto_match", "Find and confirm matching faces"),
|
|
("🔎 Search", "search", "Search photos by person name"),
|
|
("✏️ Modify", "modify", "View and modify identified faces"),
|
|
("🏷️ Tags", "tags", "Manage photo tags"),
|
|
]
|
|
|
|
for text, panel_name, tooltip in menu_buttons:
|
|
btn = ttk.Button(
|
|
menu_frame,
|
|
text=text,
|
|
command=lambda p=panel_name: self.show_panel(p)
|
|
)
|
|
btn.pack(side=tk.LEFT, padx=2)
|
|
|
|
# Add tooltip functionality
|
|
self._add_tooltip(btn, tooltip)
|
|
|
|
# Separator
|
|
separator = ttk.Separator(menu_frame, orient='vertical')
|
|
separator.pack(side=tk.LEFT, padx=10, fill=tk.Y)
|
|
|
|
# Status/Info area (for future use)
|
|
self.status_label = ttk.Label(menu_frame, text="Ready", foreground="#666")
|
|
self.status_label.pack(side=tk.RIGHT)
|
|
|
|
def _create_content_area(self, parent: ttk.Frame):
|
|
"""Create the main content area where panels will be displayed"""
|
|
self.content_frame = ttk.Frame(parent)
|
|
self.content_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
|
|
|
# Add a subtle border
|
|
self.content_frame.configure(relief='sunken', borderwidth=1)
|
|
|
|
def _initialize_panels(self):
|
|
"""Initialize all panels (currently placeholders)"""
|
|
# Home panel (default)
|
|
self.panels["home"] = self._create_home_panel()
|
|
|
|
# Functional panels (placeholders for now)
|
|
self.panels["scan"] = self._create_scan_panel()
|
|
self.panels["process"] = self._create_process_panel()
|
|
self.panels["identify"] = self._create_identify_panel()
|
|
self.panels["auto_match"] = self._create_auto_match_panel()
|
|
self.panels["search"] = self._create_search_panel()
|
|
self.panels["modify"] = self._create_modify_panel()
|
|
self.panels["tags"] = self._create_tags_panel()
|
|
|
|
def show_panel(self, panel_name: str):
|
|
"""Show the specified panel in the content area"""
|
|
if panel_name not in self.panels:
|
|
messagebox.showerror("Error", f"Panel '{panel_name}' not found", parent=self.root)
|
|
return
|
|
|
|
# Hide current panel
|
|
if self.current_panel:
|
|
self.panels[self.current_panel].pack_forget()
|
|
|
|
# Show new panel
|
|
self.panels[panel_name].pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
self.current_panel = panel_name
|
|
|
|
# Update status
|
|
self.status_label.config(text=f"Viewing: {panel_name.replace('_', ' ').title()}")
|
|
|
|
def _add_tooltip(self, widget, text):
|
|
"""Add a simple tooltip to a widget"""
|
|
def show_tooltip(event):
|
|
tooltip = tk.Toplevel()
|
|
tooltip.wm_overrideredirect(True)
|
|
tooltip.wm_geometry(f"+{event.x_root+10}+{event.y_root+10}")
|
|
label = ttk.Label(tooltip, text=text, background="#ffffe0", relief="solid", borderwidth=1)
|
|
label.pack()
|
|
widget.tooltip = tooltip
|
|
|
|
def hide_tooltip(event):
|
|
if hasattr(widget, 'tooltip'):
|
|
widget.tooltip.destroy()
|
|
del widget.tooltip
|
|
|
|
widget.bind("<Enter>", show_tooltip)
|
|
widget.bind("<Leave>", hide_tooltip)
|
|
|
|
def _create_home_panel(self) -> ttk.Frame:
|
|
"""Create the home/welcome panel"""
|
|
panel = ttk.Frame(self.content_frame)
|
|
|
|
# Welcome content
|
|
welcome_frame = ttk.Frame(panel)
|
|
welcome_frame.pack(expand=True, fill=tk.BOTH)
|
|
|
|
# Center the content
|
|
center_frame = ttk.Frame(welcome_frame)
|
|
center_frame.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
|
|
|
|
# Title
|
|
title_label = ttk.Label(center_frame, text="Welcome to PunimTag", font=("Arial", 24, "bold"))
|
|
title_label.pack(pady=(0, 20))
|
|
|
|
# Description
|
|
desc_text = (
|
|
"PunimTag is a powerful photo face recognition and tagging system.\n\n"
|
|
"Use the menu above to access different features:\n\n"
|
|
"• 📁 Scan - Add photos to your collection\n"
|
|
"• 🔍 Process - Detect faces in photos\n"
|
|
"• 👤 Identify - Identify people in photos\n"
|
|
"• 🔗 Auto-Match - Find matching faces automatically\n"
|
|
"• 🔎 Search - Find photos by person name\n"
|
|
"• ✏️ Modify - Edit face identifications\n"
|
|
"• 🏷️ Tags - Manage photo tags\n\n"
|
|
"Select a feature from the menu to get started!"
|
|
)
|
|
|
|
desc_label = ttk.Label(center_frame, text=desc_text, font=("Arial", 12), justify=tk.LEFT)
|
|
desc_label.pack()
|
|
|
|
return panel
|
|
|
|
def _create_scan_panel(self) -> ttk.Frame:
|
|
"""Create the scan panel (migrated from original dashboard)"""
|
|
panel = ttk.Frame(self.content_frame)
|
|
|
|
# Title
|
|
title_label = ttk.Label(panel, text="📁 Scan Photos", font=("Arial", 18, "bold"))
|
|
title_label.pack(anchor="w", pady=(0, 20))
|
|
|
|
# Scan form
|
|
form_frame = ttk.LabelFrame(panel, text="Scan Configuration", padding="15")
|
|
form_frame.pack(fill=tk.X, pady=(0, 20))
|
|
|
|
# Folder selection
|
|
folder_frame = ttk.Frame(form_frame)
|
|
folder_frame.pack(fill=tk.X, pady=(0, 10))
|
|
|
|
ttk.Label(folder_frame, text="Folder to scan:").pack(anchor="w")
|
|
|
|
folder_input_frame = ttk.Frame(folder_frame)
|
|
folder_input_frame.pack(fill=tk.X, pady=(5, 0))
|
|
|
|
self.folder_var = tk.StringVar()
|
|
folder_entry = ttk.Entry(folder_input_frame, textvariable=self.folder_var)
|
|
folder_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
|
|
|
|
def browse_folder():
|
|
from tkinter import filedialog
|
|
folder_path = filedialog.askdirectory(title="Select folder to scan for photos")
|
|
if folder_path:
|
|
self.folder_var.set(folder_path)
|
|
|
|
browse_btn = ttk.Button(folder_input_frame, text="Browse", command=browse_folder)
|
|
browse_btn.pack(side=tk.LEFT)
|
|
|
|
# Recursive option
|
|
self.recursive_var = tk.BooleanVar(value=True)
|
|
recursive_check = ttk.Checkbutton(
|
|
form_frame,
|
|
text="Include photos in sub-folders",
|
|
variable=self.recursive_var
|
|
)
|
|
recursive_check.pack(anchor="w", pady=(10, 0))
|
|
|
|
# Action button
|
|
scan_btn = ttk.Button(form_frame, text="🔍 Start Scan", command=self._run_scan)
|
|
scan_btn.pack(anchor="w", pady=(20, 0))
|
|
|
|
return panel
|
|
|
|
def _create_process_panel(self) -> ttk.Frame:
|
|
"""Create the process panel (migrated from original dashboard)"""
|
|
panel = ttk.Frame(self.content_frame)
|
|
|
|
# Title
|
|
title_label = ttk.Label(panel, text="🔍 Process Faces", font=("Arial", 18, "bold"))
|
|
title_label.pack(anchor="w", pady=(0, 20))
|
|
|
|
# Process form
|
|
form_frame = ttk.LabelFrame(panel, text="Processing Configuration", padding="15")
|
|
form_frame.pack(fill=tk.X, pady=(0, 20))
|
|
|
|
# Limit option
|
|
limit_frame = ttk.Frame(form_frame)
|
|
limit_frame.pack(fill=tk.X, pady=(0, 10))
|
|
|
|
self.limit_enabled = tk.BooleanVar(value=False)
|
|
limit_check = ttk.Checkbutton(limit_frame, text="Limit processing to", variable=self.limit_enabled)
|
|
limit_check.pack(side=tk.LEFT)
|
|
|
|
self.limit_var = tk.StringVar(value="50")
|
|
limit_entry = ttk.Entry(limit_frame, textvariable=self.limit_var, width=8)
|
|
limit_entry.pack(side=tk.LEFT, padx=(10, 0))
|
|
|
|
ttk.Label(limit_frame, text="photos").pack(side=tk.LEFT, padx=(5, 0))
|
|
|
|
# Action button
|
|
process_btn = ttk.Button(form_frame, text="🚀 Start Processing", command=self._run_process)
|
|
process_btn.pack(anchor="w", pady=(20, 0))
|
|
|
|
return panel
|
|
|
|
def _create_identify_panel(self) -> ttk.Frame:
|
|
"""Create the identify panel (placeholder for now)"""
|
|
panel = ttk.Frame(self.content_frame)
|
|
|
|
# Title
|
|
title_label = ttk.Label(panel, text="👤 Identify Faces", font=("Arial", 18, "bold"))
|
|
title_label.pack(anchor="w", pady=(0, 20))
|
|
|
|
# Placeholder content
|
|
placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20")
|
|
placeholder_frame.pack(expand=True, fill=tk.BOTH)
|
|
|
|
placeholder_text = (
|
|
"The Identify panel will be integrated here.\n\n"
|
|
"This will contain the full face identification interface\n"
|
|
"currently available in the separate Identify window.\n\n"
|
|
"Features will include:\n"
|
|
"• Face browsing and identification\n"
|
|
"• Similar face matching\n"
|
|
"• Person management\n"
|
|
"• Batch processing options"
|
|
)
|
|
|
|
placeholder_label = ttk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 12), justify=tk.LEFT)
|
|
placeholder_label.pack(expand=True)
|
|
|
|
return panel
|
|
|
|
def _create_auto_match_panel(self) -> ttk.Frame:
|
|
"""Create the auto-match panel (placeholder)"""
|
|
panel = ttk.Frame(self.content_frame)
|
|
|
|
title_label = ttk.Label(panel, text="🔗 Auto-Match Faces", font=("Arial", 18, "bold"))
|
|
title_label.pack(anchor="w", pady=(0, 20))
|
|
|
|
placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20")
|
|
placeholder_frame.pack(expand=True, fill=tk.BOTH)
|
|
|
|
placeholder_text = "Auto-Match functionality will be integrated here."
|
|
placeholder_label = ttk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 12))
|
|
placeholder_label.pack(expand=True)
|
|
|
|
return panel
|
|
|
|
def _create_search_panel(self) -> ttk.Frame:
|
|
"""Create the search panel (placeholder)"""
|
|
panel = ttk.Frame(self.content_frame)
|
|
|
|
title_label = ttk.Label(panel, text="🔎 Search Photos", font=("Arial", 18, "bold"))
|
|
title_label.pack(anchor="w", pady=(0, 20))
|
|
|
|
placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20")
|
|
placeholder_frame.pack(expand=True, fill=tk.BOTH)
|
|
|
|
placeholder_text = "Search functionality will be integrated here."
|
|
placeholder_label = ttk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 12))
|
|
placeholder_label.pack(expand=True)
|
|
|
|
return panel
|
|
|
|
def _create_modify_panel(self) -> ttk.Frame:
|
|
"""Create the modify panel (placeholder)"""
|
|
panel = ttk.Frame(self.content_frame)
|
|
|
|
title_label = ttk.Label(panel, text="✏️ Modify Identified", font=("Arial", 18, "bold"))
|
|
title_label.pack(anchor="w", pady=(0, 20))
|
|
|
|
placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20")
|
|
placeholder_frame.pack(expand=True, fill=tk.BOTH)
|
|
|
|
placeholder_text = "Modify functionality will be integrated here."
|
|
placeholder_label = ttk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 12))
|
|
placeholder_label.pack(expand=True)
|
|
|
|
return panel
|
|
|
|
def _create_tags_panel(self) -> ttk.Frame:
|
|
"""Create the tags panel (placeholder)"""
|
|
panel = ttk.Frame(self.content_frame)
|
|
|
|
title_label = ttk.Label(panel, text="🏷️ Tag Manager", font=("Arial", 18, "bold"))
|
|
title_label.pack(anchor="w", pady=(0, 20))
|
|
|
|
placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20")
|
|
placeholder_frame.pack(expand=True, fill=tk.BOTH)
|
|
|
|
placeholder_text = "Tag management functionality will be integrated here."
|
|
placeholder_label = ttk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 12))
|
|
placeholder_label.pack(expand=True)
|
|
|
|
return panel
|
|
|
|
def _run_scan(self):
|
|
"""Run the scan operation (migrated from original dashboard)"""
|
|
folder = self.folder_var.get().strip()
|
|
recursive = bool(self.recursive_var.get())
|
|
|
|
if not folder:
|
|
messagebox.showwarning("Scan", "Please enter a folder path.", parent=self.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=self.root)
|
|
return
|
|
except ValueError as e:
|
|
messagebox.showerror("Scan", f"Invalid folder path: {e}", parent=self.root)
|
|
return
|
|
|
|
if not callable(self.on_scan):
|
|
messagebox.showinfo("Scan", "Scan functionality is not wired yet.", parent=self.root)
|
|
return
|
|
|
|
def worker():
|
|
try:
|
|
self.status_label.config(text="Scanning...")
|
|
result = self.on_scan(folder, recursive)
|
|
messagebox.showinfo("Scan", f"Scan completed. Result: {result}", parent=self.root)
|
|
self.status_label.config(text="Ready")
|
|
except Exception as e:
|
|
messagebox.showerror("Scan", f"Error during scan: {e}", parent=self.root)
|
|
self.status_label.config(text="Ready")
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
def _run_process(self):
|
|
"""Run the process operation (migrated from original dashboard)"""
|
|
if not callable(self.on_process):
|
|
messagebox.showinfo("Process", "Process functionality is not wired yet.", parent=self.root)
|
|
return
|
|
|
|
limit_value = None
|
|
if self.limit_enabled.get():
|
|
try:
|
|
limit_value = int(self.limit_var.get().strip())
|
|
if limit_value <= 0:
|
|
raise ValueError
|
|
except Exception:
|
|
messagebox.showerror("Process", "Please enter a valid positive integer for limit.", parent=self.root)
|
|
return
|
|
|
|
def worker():
|
|
try:
|
|
self.status_label.config(text="Processing...")
|
|
result = self.on_process(limit_value)
|
|
messagebox.showinfo("Process", f"Processing completed. Result: {result}", parent=self.root)
|
|
self.status_label.config(text="Ready")
|
|
except Exception as e:
|
|
messagebox.showerror("Process", f"Error during processing: {e}", parent=self.root)
|
|
self.status_label.config(text="Ready")
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
|