diff --git a/README_UNIFIED_DASHBOARD.md b/README_UNIFIED_DASHBOARD.md new file mode 100644 index 0000000..e48114a --- /dev/null +++ b/README_UNIFIED_DASHBOARD.md @@ -0,0 +1,376 @@ +# PunimTag - Unified Photo Face Tagger + +A powerful photo face recognition and tagging system with a modern unified dashboard interface. Designed for easy web migration with clean separation between navigation and functionality. + +## 🎯 What's New: Unified Dashboard + +**PunimTag now features a unified dashboard interface** that brings all functionality into a single, professional window: + +- **πŸ“± Single Window Interface** - No more managing multiple windows +- **πŸŽ›οΈ Menu Bar Navigation** - All features accessible from the top menu +- **πŸ”„ Panel Switching** - Seamless transitions between different functions +- **🌐 Web-Ready Architecture** - Designed for easy migration to web application +- **πŸ“Š Status Updates** - Real-time feedback on current operations + +## πŸ“‹ System Requirements + +### Minimum Requirements +- **Python**: 3.7 or higher +- **Operating System**: Linux, macOS, or Windows +- **RAM**: 2GB+ (4GB+ recommended for large photo collections) +- **Storage**: 100MB for application + space for photos and database +- **Display**: X11 display server (Linux) or equivalent for GUI interface + +### Supported Platforms +- βœ… **Ubuntu/Debian** (fully supported with automatic dependency installation) +- βœ… **macOS** (manual dependency installation required) +- βœ… **Windows** (with WSL or manual setup) +- ⚠️ **Other Linux distributions** (manual dependency installation required) + +### What Gets Installed Automatically (Ubuntu/Debian) +The setup script automatically installs these system packages: +- **Build tools**: `cmake`, `build-essential` +- **Math libraries**: `libopenblas-dev`, `liblapack-dev` (for face recognition) +- **GUI libraries**: `libx11-dev`, `libgtk-3-dev`, `libboost-python-dev` +- **Image viewer**: `feh` (for face identification interface) + +## πŸš€ Quick Start + +```bash +# 1. Setup (one time only) +git clone +cd PunimTag +python3 -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +python3 setup.py # Installs system deps + Python packages + +# 2. Launch Unified Dashboard +python3 photo_tagger.py dashboard + +# 3. Use the menu bar to access all features: +# πŸ“ Scan - Add photos to your collection +# πŸ” Process - Detect faces in photos +# πŸ‘€ Identify - Identify people in photos +# πŸ”— Auto-Match - Find matching faces automatically +# πŸ”Ž Search - Find photos by person name +# ✏️ Modify - Edit face identifications +# 🏷️ Tags - Manage photo tags +``` + +## πŸ“¦ Installation + +### Automatic Setup (Recommended) +```bash +# Clone and setup +git clone +cd PunimTag + +# Create virtual environment (IMPORTANT!) +python3 -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Run setup script +python3 setup.py +``` + +**⚠️ IMPORTANT**: Always activate the virtual environment before running any commands: +```bash +source venv/bin/activate # Run this every time you open a new terminal +``` + +### Manual Setup (Alternative) +```bash +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +python3 photo_tagger.py stats # Creates database +``` + +## πŸŽ›οΈ Unified Dashboard Interface + +### Launch the Dashboard +```bash +# Open the unified dashboard (RECOMMENDED) +python3 photo_tagger.py dashboard +``` + +### Dashboard Features + +#### **🏠 Home Panel** +- Welcome screen with feature overview +- Quick access guide to all functionality +- Professional, modern interface + +#### **πŸ“ Scan Panel** +- **Folder Selection**: Browse and select photo directories +- **Recursive Scanning**: Include photos in subfolders +- **Path Validation**: Automatic validation and error handling +- **Real-time Status**: Live updates during scanning process + +#### **πŸ” Process Panel** +- **Batch Processing**: Process photos in configurable batches +- **Quality Scoring**: Automatic face quality assessment +- **Model Selection**: Choose between HOG (fast) and CNN (accurate) models +- **Progress Tracking**: Real-time processing status + +#### **πŸ‘€ Identify Panel** *(Coming Soon)* +- **Visual Face Display**: See individual face crops +- **Smart Identification**: Separate fields for first name, last name, middle name, maiden name +- **Similar Face Matching**: Compare with other unidentified faces +- **Batch Processing**: Handle multiple faces efficiently + +#### **πŸ”— Auto-Match Panel** *(Coming Soon)* +- **Person-Centric View**: Show matched person with potential matches +- **Confidence Scoring**: Color-coded match confidence levels +- **Bulk Selection**: Select multiple faces for identification +- **Smart Navigation**: Efficient browsing through matches + +#### **πŸ”Ž Search Panel** *(Coming Soon)* +- **Advanced Search**: Find photos by person name +- **Multiple Search Types**: Name-based, tag-based, date-based searches +- **Results Display**: Grid view with face thumbnails +- **Export Options**: Save search results + +#### **✏️ Modify Panel** *(Coming Soon)* +- **Review Identifications**: View all identified people +- **Edit Names**: Rename people across all photos +- **Unmatch Faces**: Temporarily remove face associations +- **Bulk Operations**: Handle multiple changes efficiently + +#### **🏷️ Tags Panel** *(Coming Soon)* +- **File Explorer Interface**: Browse photos like a file manager +- **Tag Management**: Add, remove, and organize tags +- **Multiple View Modes**: List, icon, compact, and folder views +- **Column Customization**: Resizable columns and visibility controls + +## 🎯 Command Line Interface (Legacy) + +While the unified dashboard is the recommended interface, the command line interface is still available: + +### Scan for Photos +```bash +# Scan a folder (absolute path recommended) +python3 photo_tagger.py scan /path/to/photos + +# Scan with relative path (auto-converted to absolute) +python3 photo_tagger.py scan demo_photos + +# Scan recursively (recommended) +python3 photo_tagger.py scan /path/to/photos --recursive +``` + +### Process Photos for Faces +```bash +# Process 50 photos (default) +python3 photo_tagger.py process + +# Process 20 photos with CNN model (more accurate) +python3 photo_tagger.py process --limit 20 --model cnn + +# Process with HOG model (faster) +python3 photo_tagger.py process --limit 100 --model hog +``` + +### Individual GUI Windows (Legacy) +```bash +# Open individual GUI windows (legacy mode) +python3 photo_tagger.py identify --show-faces --batch 10 +python3 photo_tagger.py auto-match --show-faces +python3 photo_tagger.py search-gui +python3 photo_tagger.py modifyidentified +python3 photo_tagger.py tag-manager +``` + +## πŸ—οΈ Architecture: Web Migration Ready + +### Current Desktop Architecture +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Unified Dashboard β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ +β”‚ β”‚ Menu Bar β”‚β”‚ +β”‚ β”‚ [Scan] [Process] [Identify] [Search] [Tags] [Modify] β”‚β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ +β”‚ β”‚ Content Area β”‚β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚β”‚ +β”‚ β”‚ β”‚Scan Panel β”‚ β”‚Identify β”‚ β”‚Search Panel β”‚ β”‚β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚Panel β”‚ β”‚ β”‚ β”‚β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ PhotoTagger β”‚ + β”‚ (Business β”‚ + β”‚ Logic) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Future Web Architecture +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Web Browser β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ +β”‚ β”‚ Navigation Bar β”‚β”‚ +β”‚ β”‚ [Scan] [Process] [Identify] [Search] [Tags] [Modify] β”‚β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ +β”‚ β”‚ Main Content Area β”‚β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚β”‚ +β”‚ β”‚ β”‚Scan Page β”‚ β”‚Identify β”‚ β”‚Search Page β”‚ β”‚β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚Page β”‚ β”‚ β”‚ β”‚β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Web API β”‚ + β”‚ (Flask/FastAPI)β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ PhotoTagger β”‚ + β”‚ (Business β”‚ + β”‚ Logic) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Migration Benefits +- **Clean Separation**: Navigation (menu bar) and content (panels) are clearly separated +- **Panel-Based Design**: Each panel can become a web page/route +- **Service Layer**: Business logic is already separated from GUI components +- **State Management**: Panel switching system mirrors web routing concepts +- **API-Ready**: Panel methods can easily become API endpoints + +## πŸ”§ Advanced Features + +### Face Recognition Technology +- **Quality Scoring**: Automatic assessment of face quality (0.0-1.0) +- **Smart Filtering**: Only high-quality faces used for matching +- **Multiple Models**: HOG (fast) and CNN (accurate) detection models +- **Encoding Caching**: Optimized performance with face encoding caching + +### Database Management +- **SQLite Database**: Lightweight, portable database +- **Optimized Queries**: Efficient database operations +- **Connection Pooling**: Thread-safe database access +- **Automatic Schema**: Self-initializing database structure + +### Performance Optimizations +- **Pre-fetching**: Data loaded in advance for faster UI response +- **Background Processing**: Long operations run in separate threads +- **Memory Management**: Efficient cleanup of temporary files and caches +- **Batch Operations**: Process multiple items efficiently + +## πŸ“Š Statistics and Monitoring + +```bash +# View database statistics +python3 photo_tagger.py stats +``` + +**Statistics Include:** +- Total photos in database +- Total faces detected +- Identified vs unidentified faces +- People count +- Tag statistics +- Processing status + +## πŸ”„ Common Commands Cheat Sheet + +```bash +# Setup (one time) +python3 -m venv venv && source venv/bin/activate && python3 setup.py + +# Daily usage - Unified Dashboard (RECOMMENDED) +source venv/bin/activate +python3 photo_tagger.py dashboard + +# Then use the menu bar in the dashboard: +# πŸ“ Scan - Add photos +# πŸ” Process - Detect faces +# πŸ‘€ Identify - Identify people +# πŸ”— Auto-Match - Find matches +# πŸ”Ž Search - Find photos +# ✏️ Modify - Edit identifications +# 🏷️ Tags - Manage tags + +# Legacy command line usage +python3 photo_tagger.py scan ~/Pictures --recursive +python3 photo_tagger.py process --limit 50 +python3 photo_tagger.py identify --show-faces --batch 10 +python3 photo_tagger.py auto-match --show-faces +python3 photo_tagger.py search-gui +python3 photo_tagger.py modifyidentified +python3 photo_tagger.py tag-manager +python3 photo_tagger.py stats +``` + +## πŸš€ Development Roadmap + +### Phase 1: Core Panel Integration βœ… +- [x] Unified dashboard structure +- [x] Menu bar navigation +- [x] Panel switching system +- [x] Scan panel (fully functional) +- [x] Process panel (fully functional) +- [x] Home panel with welcome screen + +### Phase 2: GUI Panel Integration (In Progress) +- [ ] Identify panel integration +- [ ] Auto-Match panel integration +- [ ] Search panel integration +- [ ] Modify panel integration +- [ ] Tags panel integration + +### Phase 3: Web Migration Preparation +- [ ] Service layer extraction +- [ ] API endpoint design +- [ ] State management refactoring +- [ ] File handling abstraction + +### Phase 4: Web Application +- [ ] Web API implementation +- [ ] Frontend development +- [ ] Authentication system +- [ ] Deployment configuration + +## πŸŽ‰ Key Benefits + +### User Experience +- **Single Window**: No more managing multiple windows +- **Consistent Interface**: All features follow the same design patterns +- **Professional Look**: Modern, clean interface design +- **Intuitive Navigation**: Menu bar makes all features easily accessible + +### Developer Experience +- **Modular Design**: Each panel is independent and maintainable +- **Web-Ready**: Architecture designed for easy web migration +- **Clean Code**: Clear separation of concerns +- **Extensible**: Easy to add new panels and features + +### Performance +- **Optimized Loading**: Panels load only when needed +- **Background Processing**: Long operations don't block the UI +- **Memory Efficient**: Proper cleanup and resource management +- **Responsive**: Fast panel switching and updates + +--- + +**Total project size**: ~4,000+ lines of Python code +**Dependencies**: 6 essential packages +**Setup time**: ~5 minutes +**Perfect for**: Professional photo management with modern unified interface + +## πŸ“ž Support + +For issues, questions, or contributions: +- **GitHub Issues**: Report bugs and request features +- **Documentation**: Check this README for detailed usage instructions +- **Community**: Join discussions about photo management and face recognition + +--- + +*PunimTag - Making photo face recognition simple, powerful, and web-ready.* diff --git a/dashboard_gui.py b/dashboard_gui.py index 959ef69..0c72e50 100644 --- a/dashboard_gui.py +++ b/dashboard_gui.py @@ -1,20 +1,23 @@ #!/usr/bin/env python3 """ -Dashboard GUI for PunimTag features +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: - """Dashboard with launchers for core features. - - on_scan: callable taking (folder_path: str, recursive: bool) -> int + """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): @@ -22,231 +25,408 @@ class DashboardGUI: 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: - root = tk.Tk() - root.title("PunimTag Dashboard") - root.resizable(True, True) - root.withdraw() + """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() - main = ttk.Frame(root, padding="16") - main.pack(fill=tk.BOTH, expand=True) + # Create main container + main_container = ttk.Frame(self.root) + main_container.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) + # 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("", show_tooltip) + widget.bind("", 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)) - # 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) + 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)) - 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") + scan_btn = ttk.Button(form_frame, text="πŸ” Start Scan", command=self._run_scan) + scan_btn.pack(anchor="w", pady=(20, 0)) + + return panel - 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) + 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 - - # Validate folder path using path utilities - from path_utils import validate_path_exists, normalize_path + 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: - 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) + 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: - 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 + 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() - - 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 + threading.Thread(target=worker, daemon=True).start()