Implement enhanced tag management features in PhotoTagger

This update introduces a comprehensive system for managing photo tags, including the ability to add and remove tags with pending changes tracked until saved. A new tag management dialog allows users to link tags intuitively, featuring a linkage icon for easy access. The interface now supports real-time updates and visual indicators for saved and pending tags, improving user experience. Additionally, the README has been updated to reflect these enhancements and provide clear documentation on the new functionalities.
This commit is contained in:
tanyar09 2025-10-02 13:07:08 -04:00
parent 639b283c0c
commit 31691d7c47
2 changed files with 236 additions and 28 deletions

View File

@ -195,6 +195,11 @@ This GUI provides a file explorer-like interface for managing photo tags with ad
- 🏷️ **Smart Tag Management** - Duplicate tag prevention with silent handling
- 🔄 **Accurate Change Tracking** - Only counts photos with actual new tags as "changed"
- 🎯 **Reliable Tag Operations** - Uses tag IDs internally for consistent, bug-free behavior
- 🔗 **Enhanced Tag Linking** - Linkage icon (🔗) for intuitive tag management
- 📋 **Comprehensive Tag Dialog** - Manage tags dialog similar to Manage Tags interface
- ✅ **Pending Tag System** - Add and remove tags with pending changes until saved
- 🎯 **Visual Status Indicators** - Clear distinction between saved and pending tags
- 🗑️ **Smart Tag Removal** - Remove both pending and saved tags with proper tracking
**📋 Available View Modes:**
@ -245,6 +250,18 @@ This GUI provides a file explorer-like interface for managing photo tags with ad
- 🖼️ **Photo Details** - Expanded folders show all photos with their metadata
- 🔄 **Persistent State** - Folder expansion state is maintained while browsing
**🏷️ Enhanced Tag Management System:**
- 🔗 **Linkage Icon** - Click the 🔗 button next to tags to open the tag management dialog
- 📋 **Comprehensive Dialog** - Similar interface to Manage Tags with dropdown selection and tag listing
- ✅ **Pending Changes** - Add and remove tags with changes tracked until "Save Tagging" is clicked
- 🎯 **Visual Status** - Tags show "(pending)" in blue or "(saved)" in black for clear status indication
- 🗑️ **Smart Removal** - Remove both pending and saved tags with proper database tracking
- 📊 **Batch Operations** - Select multiple tags for removal with checkboxes
- 🔄 **Real-time Updates** - Tag display updates immediately when changes are made
- 💾 **Save System** - All tag changes (additions and removals) saved atomically when "Save Tagging" is clicked
- 🎯 **ID-Based Architecture** - Uses tag IDs internally for efficient, reliable operations
- ⚡ **Performance Optimized** - Fast tag operations with minimal database queries
**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
@ -791,6 +808,21 @@ This is now a minimal, focused tool. Key principles:
- ✅ **Robust Tag Parsing** - Handles edge cases like empty tag strings and malformed data
- ✅ **Consistent Behavior** - All tag operations use the same reliable logic throughout the application
### 🔗 Enhanced Tag Management Interface (LATEST!)
- ✅ **Linkage Icon** - Replaced "+" button with intuitive 🔗 linkage icon for tag management
- ✅ **Comprehensive Tag Dialog** - Redesigned tag management dialog similar to Manage Tags interface
- ✅ **Dropdown Tag Selection** - Select from existing tags or create new ones via dropdown
- ✅ **Pending Tag System** - Add and remove tags with changes tracked until explicitly saved
- ✅ **Visual Status Indicators** - Clear distinction between saved tags (black) and pending tags (blue)
- ✅ **Smart Tag Removal** - Remove both pending and saved tags with proper database tracking
- ✅ **Batch Tag Operations** - Select multiple tags for removal with checkboxes
- ✅ **Real-time UI Updates** - Tag display updates immediately when changes are made
- ✅ **Atomic Save Operations** - All tag changes (additions and removals) saved in single transaction
- ✅ **Efficient ID-Based Operations** - Uses tag IDs internally for fast, reliable tag management
- ✅ **Scrollable Tag Lists** - Handle photos with many tags in scrollable interface
- ✅ **Immediate Visual Feedback** - Removed tags disappear from UI immediately
- ✅ **Database Integrity** - Proper cleanup of pending changes when tags are deleted
### 🎨 Enhanced Modify Identified Interface (NEW!)
- ✅ **Complete Person Information** - Shows full names with middle names, maiden names, and birth dates
- ✅ **Last Name Search** - Filter people by last name with case-insensitive search

View File

@ -4337,6 +4337,8 @@ class PhotoTagger:
# Track pending tag changes (photo_id -> list of tag IDs)
pending_tag_changes = {}
# Track pending tag removals (photo_id -> list of tag IDs to remove)
pending_tag_removals = {}
existing_tags = [] # Cache of existing tag names from database (for UI display)
tag_id_to_name = {} # Cache of tag ID to name mapping
tag_name_to_id = {} # Cache of tag name to ID mapping
@ -4554,6 +4556,12 @@ class PhotoTagger:
if not pending_tag_changes[photo_id]:
del pending_tag_changes[photo_id]
# Clean up pending tag removals for deleted tags
for photo_id in list(pending_tag_removals.keys()):
pending_tag_removals[photo_id] = [tid for tid in pending_tag_removals[photo_id] if tid not in ids_to_delete]
if not pending_tag_removals[photo_id]:
del pending_tag_removals[photo_id]
refresh_tag_list()
load_existing_tags()
load_photos() # Refresh photo data to reflect deleted tags
@ -4948,7 +4956,7 @@ class PhotoTagger:
def save_tagging_changes():
"""Save all pending tag changes to database"""
if not pending_tag_changes:
if not pending_tag_changes and not pending_tag_removals:
messagebox.showinfo("Info", "No tag changes to save.")
return
@ -4956,6 +4964,7 @@ class PhotoTagger:
with self.get_db_connection() as conn:
cursor = conn.cursor()
# Handle tag additions
for photo_id, tag_ids in pending_tag_changes.items():
for tag_id in tag_ids:
# Insert linkage (ignore if already exists)
@ -4964,27 +4973,45 @@ class PhotoTagger:
(photo_id, tag_id)
)
# Handle tag removals
for photo_id, tag_ids in pending_tag_removals.items():
for tag_id in tag_ids:
# Remove linkage
cursor.execute(
'DELETE FROM phototaglinkage WHERE photo_id = ? AND tag_id = ?',
(photo_id, tag_id)
)
conn.commit()
# Store count before clearing
saved_count = len(pending_tag_changes)
# Store counts before clearing
saved_additions = len(pending_tag_changes)
saved_removals = len(pending_tag_removals)
# Clear pending changes and reload data
pending_tag_changes.clear()
pending_tag_removals.clear()
load_existing_tags()
load_photos()
switch_view_mode(view_mode_var.get())
update_save_button_text()
messagebox.showinfo("Success", f"Saved tags for {saved_count} photos.")
message = f"Saved {saved_additions} tag additions"
if saved_removals > 0:
message += f" and {saved_removals} tag removals"
message += "."
messagebox.showinfo("Success", message)
except Exception as e:
messagebox.showerror("Error", f"Failed to save tags: {str(e)}")
def update_save_button_text():
"""Update save button text to show pending changes count"""
if pending_tag_changes:
total_changes = sum(len(tags) for tags in pending_tag_changes.values())
total_additions = sum(len(tags) for tags in pending_tag_changes.values())
total_removals = sum(len(tags) for tags in pending_tag_removals.values())
total_changes = total_additions + total_removals
if total_changes > 0:
save_button.configure(text=f"Save Tagging ({total_changes} pending)")
else:
save_button.configure(text="Save Tagging")
@ -5104,29 +5131,35 @@ class PhotoTagger:
def create_add_tag_handler(photo_id, label_widget, photo_tags, available_tags):
"""Create a handler function for adding tags to a photo"""
def handler():
# Create popup window for tag selection
# Create popup window for tag management
popup = tk.Toplevel(root)
popup.title("Add Tag")
popup.title("Manage Photo Tags")
popup.transient(root)
popup.grab_set()
popup.geometry("300x150")
popup.resizable(False, False)
popup.geometry("500x400")
popup.resizable(True, True)
# Create main frame
main_frame = ttk.Frame(popup, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# Layout frames
top_frame = ttk.Frame(popup, padding="8")
top_frame.grid(row=0, column=0, sticky=(tk.W, tk.E))
list_frame = ttk.Frame(popup, padding="8")
list_frame.grid(row=1, column=0, sticky=(tk.N, tk.S, tk.W, tk.E))
bottom_frame = ttk.Frame(popup, padding="8")
bottom_frame.grid(row=2, column=0, sticky=(tk.W, tk.E))
ttk.Label(main_frame, text="Select a tag:").pack(pady=(0, 5))
popup.columnconfigure(0, weight=1)
popup.rowconfigure(1, weight=1)
# Top frame - dropdown to select tag to add
ttk.Label(top_frame, text="Add tag:").grid(row=0, column=0, padx=(0, 8), sticky=tk.W)
tag_var = tk.StringVar()
combo = ttk.Combobox(main_frame, textvariable=tag_var, values=available_tags, width=30)
combo.pack(pady=(0, 10), fill=tk.X)
combo = ttk.Combobox(top_frame, textvariable=tag_var, values=available_tags, width=30)
combo.grid(row=0, column=1, padx=(0, 8), sticky=(tk.W, tk.E))
combo.focus_set()
def confirm():
def add_selected_tag():
tag_name = tag_var.get().strip()
if not tag_name:
popup.destroy()
return
# Get or create tag ID
@ -5156,18 +5189,152 @@ class PhotoTagger:
if photo_id not in pending_tag_changes:
pending_tag_changes[photo_id] = []
pending_tag_changes[photo_id].append(tag_id)
# Update the display immediately - combine existing and pending, removing duplicates
existing_tags_list = self._parse_tags_string(photo_tags)
pending_tag_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_changes[photo_id]]
all_tags = existing_tags_list + pending_tag_names
unique_tags = self._deduplicate_tags(all_tags)
current_tags = ", ".join(unique_tags)
label_widget.configure(text=current_tags)
refresh_tag_list()
update_save_button_text()
tag_var.set("") # Clear the dropdown
add_btn = ttk.Button(top_frame, text="Add", command=add_selected_tag)
add_btn.grid(row=0, column=2, padx=(0, 8))
# List frame - show all linked tags (existing + pending) with checkboxes
ttk.Label(list_frame, text="Linked tags (check to remove):", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(0, 5))
# Create scrollable frame for tags
canvas = tk.Canvas(list_frame, height=200)
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Variables to track selected tags for removal
selected_tag_vars = {}
def refresh_tag_list():
# Clear existing widgets
for widget in scrollable_frame.winfo_children():
widget.destroy()
selected_tag_vars.clear()
# Get existing tags for this photo
existing_tag_ids = self._get_existing_tag_ids_for_photo(photo_id)
# Get pending tags for this photo
pending_tag_ids = pending_tag_changes.get(photo_id, [])
# Get pending removals for this photo
pending_removal_ids = pending_tag_removals.get(photo_id, [])
# Combine and deduplicate tag IDs, but exclude tags marked for removal
all_tag_ids = existing_tag_ids + pending_tag_ids
unique_tag_ids = list(set(all_tag_ids)) # Remove duplicates
# Remove tags that are marked for removal
unique_tag_ids = [tid for tid in unique_tag_ids if tid not in pending_removal_ids]
# Convert to names for display
unique_tag_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in unique_tag_ids]
if not unique_tag_names:
ttk.Label(scrollable_frame, text="No tags linked to this photo",
foreground="gray").pack(anchor=tk.W, pady=5)
return
# Create checkboxes for each tag
for i, tag_id in enumerate(unique_tag_ids):
tag_name = tag_id_to_name.get(tag_id, f"Unknown {tag_id}")
var = tk.BooleanVar()
selected_tag_vars[tag_name] = var
# Determine if this is a pending tag
is_pending = tag_id in pending_tag_ids
status_text = " (pending)" if is_pending else " (saved)"
status_color = "blue" if is_pending else "black"
frame = ttk.Frame(scrollable_frame)
frame.pack(fill=tk.X, pady=1)
checkbox = ttk.Checkbutton(frame, variable=var)
checkbox.pack(side=tk.LEFT, padx=(0, 5))
label = ttk.Label(frame, text=tag_name + status_text, foreground=status_color)
label.pack(side=tk.LEFT)
def remove_selected_tags():
# Get tag IDs to remove (convert names to IDs)
tag_ids_to_remove = []
for tag_name, var in selected_tag_vars.items():
if var.get() and tag_name in tag_name_to_id:
tag_ids_to_remove.append(tag_name_to_id[tag_name])
if not tag_ids_to_remove:
return
# Remove from pending changes (using IDs)
if photo_id in pending_tag_changes:
pending_tag_changes[photo_id] = [
tid for tid in pending_tag_changes[photo_id]
if tid not in tag_ids_to_remove
]
if not pending_tag_changes[photo_id]:
del pending_tag_changes[photo_id]
# Track removals for saved tags (using IDs)
existing_tag_ids = self._get_existing_tag_ids_for_photo(photo_id)
for tag_id in tag_ids_to_remove:
if tag_id in existing_tag_ids:
# This is a saved tag, add to pending removals
if photo_id not in pending_tag_removals:
pending_tag_removals[photo_id] = []
if tag_id not in pending_tag_removals[photo_id]:
pending_tag_removals[photo_id].append(tag_id)
refresh_tag_list()
update_save_button_text()
# Bottom frame - buttons
remove_btn = ttk.Button(bottom_frame, text="Remove selected tags", command=remove_selected_tags)
remove_btn.pack(side=tk.LEFT, padx=(0, 8))
close_btn = ttk.Button(bottom_frame, text="Close", command=popup.destroy)
close_btn.pack(side=tk.RIGHT)
# Initial load
refresh_tag_list()
# Update main display when dialog closes
def on_close():
# Update the main display
existing_tags_list = self._parse_tags_string(photo_tags)
# Get pending tag IDs and convert to names
pending_tag_names = []
if photo_id in pending_tag_changes:
pending_tag_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_changes[photo_id]]
# Get pending removal IDs and convert to names
pending_removal_names = []
if photo_id in pending_tag_removals:
pending_removal_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_removals[photo_id]]
all_tags = existing_tags_list + pending_tag_names
unique_tags = self._deduplicate_tags(all_tags)
# Remove tags marked for removal
unique_tags = [tag for tag in unique_tags if tag not in pending_removal_names]
current_tags = ", ".join(unique_tags) if unique_tags else "None"
label_widget.configure(text=current_tags)
popup.destroy()
ttk.Button(main_frame, text="Add", command=confirm).pack(pady=(0, 5))
popup.protocol("WM_DELETE_WINDOW", on_close)
return handler
@ -5177,19 +5344,28 @@ class PhotoTagger:
# Display current tags
existing_tags_list = self._parse_tags_string(photo_tags)
# Get pending tag names
pending_tag_names = []
if photo_id in pending_tag_changes:
pending_tag_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_changes[photo_id]]
# Get pending removal names
pending_removal_names = []
if photo_id in pending_tag_removals:
pending_removal_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_removals[photo_id]]
all_tags = existing_tags_list + pending_tag_names
unique_tags = self._deduplicate_tags(all_tags)
# Remove tags marked for removal
unique_tags = [tag for tag in unique_tags if tag not in pending_removal_names]
current_display = ", ".join(unique_tags) if unique_tags else "None"
tags_text = ttk.Label(tags_frame, text=current_display)
tags_text.pack(side=tk.LEFT)
# Add button
add_btn = tk.Button(tags_frame, text="+", width=2,
# Add button with linkage icon
add_btn = tk.Button(tags_frame, text="🔗", width=2,
command=create_add_tag_handler(photo_id, tags_text, photo_tags, existing_tags))
add_btn.pack(side=tk.LEFT, padx=(6, 0))