diff --git a/README.md b/README.md index f723bf5..763bed6 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,8 @@ python3 photo_tagger.py tag-manager This GUI provides a file explorer-like interface for managing photo tags with advanced column resizing and multiple view modes. **🎯 Tag Manager Features:** -- 📊 **Multiple View Modes** - List view, icon view, and compact view for different needs +- 📊 **Multiple View Modes** - List view, icon view, compact view, and folder view for different needs +- 📁 **Folder Grouping** - Group photos by directory with expandable/collapsible folders - 🔧 **Resizable Columns** - Drag column separators to resize both headers and data rows - 👁️ **Column Visibility** - Right-click to show/hide columns in each view mode - 🖼️ **Thumbnail Display** - Icon view shows photo thumbnails with metadata @@ -211,6 +212,14 @@ This GUI provides a file explorer-like interface for managing photo tags with ad - ⚡ **Fast Loading** - Minimal data for quick browsing - 🎯 **Focused Display** - Perfect for quick tag management +**Folder View:** +- 📁 **Directory Grouping** - Photos grouped by their directory path +- 🔽 **Expandable Folders** - Click folder headers to expand/collapse +- 📊 **Photo Counts** - Shows number of photos in each folder +- 🎯 **File Explorer Style** - Familiar tree-like interface +- 📄 **Photo Details** - Shows filename, processed status, date taken, face count, and tags +- 🖱️ **Easy Navigation** - Click anywhere on folder header to toggle + **🔧 Column Resizing:** - 🖱️ **Drag to Resize** - Click and drag red separators between columns - 📏 **Minimum Width** - Columns maintain minimum 50px width @@ -224,6 +233,15 @@ This GUI provides a file explorer-like interface for managing photo tags with ad - 🎯 **View-Specific** - Column settings saved per view mode - 🔄 **Instant Updates** - Changes apply immediately +**📁 Folder View Usage:** +- 🖱️ **Click Folder Headers** - Click anywhere on a folder row to expand/collapse +- 🔽 **Expand/Collapse Icons** - ▶ indicates collapsed, ▼ indicates expanded +- 📊 **Photo Counts** - Each folder shows "(X photos)" in the header +- 🎯 **Root Directory** - Photos without a directory path are grouped under "Root" +- 📁 **Alphabetical Sorting** - Folders are sorted alphabetically by directory name +- 🖼️ **Photo Details** - Expanded folders show all photos with their metadata +- 🔄 **Persistent State** - Folder expansion state is maintained while browsing + **Left Panel (People):** - 🔍 **Last Name Search** - Search box to filter people by last name (case-insensitive) - 🔎 **Search Button** - Apply filter to show only matching people @@ -365,7 +383,8 @@ The tool uses SQLite database (`data/photos.db` by default) with these tables: - **photos** - Photo file paths and processing status - **people** - Known people with separate first_name, last_name, and date_of_birth fields - **faces** - Face encodings, locations, and quality scores -- **tags** - Custom tags for photos +- **tags** - Tag definitions (unique tag names) +- **phototaglinkage** - Links between photos and tags (many-to-many relationship) - **person_encodings** - Face encodings for each person (for matching) ### Database Schema Improvements @@ -374,6 +393,8 @@ The tool uses SQLite database (`data/photos.db` by default) with these tables: - **Unique Constraint** - Prevents duplicate people with same name and birth date combination - **No Comma Issues** - Names are stored without commas, displayed as "Last, First" format - **Quality Scoring** - Faces table includes quality scores for better matching +- **Normalized Tag Structure** - Separate `tags` table for tag definitions and `phototaglinkage` table for photo-tag relationships +- **No Duplicate Tags** - Unique constraint prevents duplicate tag-photo combinations - **Optimized Queries** - Efficient indexing and query patterns for fast performance - **Data Integrity** - Proper foreign key relationships and constraints diff --git a/photo_tagger.py b/photo_tagger.py index 8aefaa6..ab475ad 100644 --- a/photo_tagger.py +++ b/photo_tagger.py @@ -204,14 +204,25 @@ class PhotoTagger: ) ''') - # Tags table + # Tags table - holds only tag information cursor.execute(''' CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, + tag_name TEXT UNIQUE NOT NULL, + created_date DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Photo-Tag linkage table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS phototaglinkage ( + linkage_id INTEGER PRIMARY KEY AUTOINCREMENT, photo_id INTEGER NOT NULL, - tag_name TEXT NOT NULL, + tag_id INTEGER NOT NULL, created_date DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (photo_id) REFERENCES photos (id) + FOREIGN KEY (photo_id) REFERENCES photos (id), + FOREIGN KEY (tag_id) REFERENCES tags (id), + UNIQUE(photo_id, tag_id) ) ''') @@ -3150,10 +3161,22 @@ class PhotoTagger: if tags_input: tags = [tag.strip() for tag in tags_input.split(',') if tag.strip()] - for tag in tags: + for tag_name in tags: + # First, insert or get the tag_id from tags table cursor.execute( - 'INSERT INTO tags (photo_id, tag_name) VALUES (?, ?)', - (photo_id, tag) + 'INSERT OR IGNORE INTO tags (tag_name) VALUES (?)', + (tag_name,) + ) + cursor.execute( + 'SELECT id FROM tags WHERE tag_name = ?', + (tag_name,) + ) + tag_id = cursor.fetchone()[0] + + # Then, insert the linkage (ignore if already exists due to UNIQUE constraint) + cursor.execute( + 'INSERT OR IGNORE INTO phototaglinkage (photo_id, tag_id) VALUES (?, ?)', + (photo_id, tag_id) ) print(f" ✅ Added {len(tags)} tags") tagged_count += 1 @@ -3189,7 +3212,7 @@ class PhotoTagger: result = cursor.fetchone() stats['total_people'] = result[0] if result else 0 - cursor.execute('SELECT COUNT(DISTINCT tag_name) FROM tags') + cursor.execute('SELECT COUNT(*) FROM tags') result = cursor.fetchone() stats['unique_tags'] = result[0] if result else 0 @@ -4505,7 +4528,8 @@ class PhotoTagger: GROUP_CONCAT(DISTINCT t.tag_name) as tags FROM photos p LEFT JOIN faces f ON f.photo_id = p.id - LEFT JOIN tags t ON t.photo_id = p.id + LEFT JOIN phototaglinkage ptl ON ptl.photo_id = p.id + LEFT JOIN tags t ON t.id = ptl.tag_id GROUP BY p.id, p.filename, p.path, p.processed, p.date_taken, p.date_added ORDER BY p.date_taken DESC, p.filename ''')