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:
parent
639b283c0c
commit
31691d7c47
32
README.md
32
README.md
@ -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
|
||||
|
||||
232
photo_tagger.py
232
photo_tagger.py
@ -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))
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user