Implement folder grouping and collapsible sections in PhotoTagger GUI

Enhance the photo display functionality by introducing folder grouping, allowing users to view photos organized by their respective folders. Implement collapsible sections for each folder, enabling users to expand or collapse folder contents. This update improves the organization and accessibility of photos within the interface, enhancing overall user experience.
This commit is contained in:
tanyar09 2025-10-01 14:52:15 -04:00
parent b6e6b38a76
commit 0f599d3d16

View File

@ -4255,6 +4255,9 @@ class PhotoTagger:
temp_crops = []
photo_images = [] # Keep PhotoImage refs alive
# Track folder expand/collapse states
folder_states = {} # folder_path -> is_expanded
# Hide window initially to prevent flash at corner
root.withdraw()
@ -4519,6 +4522,66 @@ class PhotoTagger:
'tags': row[7] or ""
})
def prepare_folder_grouped_data():
"""Prepare photo data grouped by folders"""
import os
from collections import defaultdict
# Group photos by folder
folder_groups = defaultdict(list)
for photo in photos_data:
folder_path = os.path.dirname(photo['path'])
folder_name = os.path.basename(folder_path) if folder_path else "Root"
folder_groups[folder_path].append(photo)
# Sort folders by path and photos within each folder by date_taken
sorted_folders = []
for folder_path in sorted(folder_groups.keys()):
folder_name = os.path.basename(folder_path) if folder_path else "Root"
photos_in_folder = sorted(folder_groups[folder_path],
key=lambda x: x['date_taken'] or '', reverse=True)
# Initialize folder state if not exists (default to expanded)
if folder_path not in folder_states:
folder_states[folder_path] = True
sorted_folders.append({
'folder_path': folder_path,
'folder_name': folder_name,
'photos': photos_in_folder,
'photo_count': len(photos_in_folder)
})
return sorted_folders
def create_folder_header(parent, folder_info, current_row, col_count, view_mode):
"""Create a collapsible folder header with toggle button"""
# Create folder header frame
folder_header_frame = ttk.Frame(parent)
folder_header_frame.grid(row=current_row, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(10, 5))
folder_header_frame.configure(relief='raised', borderwidth=1)
# Create toggle button
is_expanded = folder_states.get(folder_info['folder_path'], True)
toggle_text = "" if is_expanded else ""
toggle_button = tk.Button(folder_header_frame, text=toggle_text, width=2, height=1,
command=lambda: toggle_folder(folder_info['folder_path'], view_mode),
font=("Arial", 8), relief='flat', bd=1)
toggle_button.pack(side=tk.LEFT, padx=(5, 2), pady=5)
# Create folder label
folder_label = ttk.Label(folder_header_frame,
text=f"📁 {folder_info['folder_name']} ({folder_info['photo_count']} photos)",
font=("Arial", 11, "bold"))
folder_label.pack(side=tk.LEFT, padx=(0, 10), pady=5)
return folder_header_frame
def toggle_folder(folder_path, view_mode):
"""Toggle folder expand/collapse state and refresh view"""
folder_states[folder_path] = not folder_states.get(folder_path, True)
switch_view_mode(view_mode)
def clear_content():
for widget in content_inner.winfo_children():
widget.destroy()
@ -4690,33 +4753,46 @@ class PhotoTagger:
# Add separator
ttk.Separator(content_inner, orient='horizontal').grid(row=1, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
# Add photo rows
for idx, photo in enumerate(photos_data):
row_frame = ttk.Frame(content_inner)
row_frame.grid(row=idx+2, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=2)
# Get folder-grouped data
folder_data = prepare_folder_grouped_data()
# Add folder sections and photo rows
current_row = 2
for folder_info in folder_data:
# Add collapsible folder header
create_folder_header(content_inner, folder_info, current_row, col_count, 'list')
current_row += 1
# Configure row frame columns (no separators in data rows)
for i, col in enumerate(current_visible_cols):
row_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
for i, col in enumerate(current_visible_cols):
key = col['key']
if key == 'id':
text = str(photo['id'])
elif key == 'filename':
text = photo['filename']
elif key == 'path':
text = photo['path']
elif key == 'processed':
text = "Yes" if photo['processed'] else "No"
elif key == 'date_taken':
text = photo['date_taken'] or "Unknown"
elif key == 'faces':
text = str(photo['face_count'])
elif key == 'tags':
text = photo['tags'] or "None"
ttk.Label(row_frame, text=text).grid(row=0, column=i, padx=5, sticky=tk.W)
# Add photos in this folder only if expanded
if folder_states.get(folder_info['folder_path'], True):
for photo in folder_info['photos']:
row_frame = ttk.Frame(content_inner)
row_frame.grid(row=current_row, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=1)
# Configure row frame columns (no separators in data rows)
for i, col in enumerate(current_visible_cols):
row_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
for i, col in enumerate(current_visible_cols):
key = col['key']
if key == 'id':
text = str(photo['id'])
elif key == 'filename':
text = photo['filename']
elif key == 'path':
text = photo['path']
elif key == 'processed':
text = "Yes" if photo['processed'] else "No"
elif key == 'date_taken':
text = photo['date_taken'] or "Unknown"
elif key == 'faces':
text = str(photo['face_count'])
elif key == 'tags':
text = photo['tags'] or "None"
ttk.Label(row_frame, text=text).grid(row=0, column=i, padx=5, sticky=tk.W)
current_row += 1
def show_icon_view():
clear_content()
@ -4754,62 +4830,75 @@ class PhotoTagger:
# Add separator
ttk.Separator(content_inner, orient='horizontal').grid(row=1, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
# Show all photos in structured rows
for idx, photo in enumerate(photos_data):
row_frame = ttk.Frame(content_inner)
row_frame.grid(row=idx+2, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=2)
# Get folder-grouped data
folder_data = prepare_folder_grouped_data()
# Show photos grouped by folders
current_row = 2
for folder_info in folder_data:
# Add collapsible folder header
create_folder_header(content_inner, folder_info, current_row, col_count, 'icons')
current_row += 1
for i, col in enumerate(visible_cols):
row_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
col_idx = 0
for col in visible_cols:
key = col['key']
if key == 'thumbnail':
# Thumbnail column
thumbnail_frame = ttk.Frame(row_frame)
thumbnail_frame.grid(row=0, column=col_idx, padx=5, sticky=tk.W)
# Add photos in this folder only if expanded
if folder_states.get(folder_info['folder_path'], True):
for photo in folder_info['photos']:
row_frame = ttk.Frame(content_inner)
row_frame.grid(row=current_row, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=2)
try:
if os.path.exists(photo['path']):
img = Image.open(photo['path'])
img.thumbnail((150, 150), Image.Resampling.LANCZOS)
photo_img = ImageTk.PhotoImage(img)
photo_images.append(photo_img)
for i, col in enumerate(visible_cols):
row_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
col_idx = 0
for col in visible_cols:
key = col['key']
if key == 'thumbnail':
# Thumbnail column
thumbnail_frame = ttk.Frame(row_frame)
thumbnail_frame.grid(row=0, column=col_idx, padx=5, sticky=tk.W)
# Create canvas for image
canvas = tk.Canvas(thumbnail_frame, width=150, height=150, bg=canvas_bg_color, highlightthickness=0)
canvas.pack()
canvas.create_image(75, 75, image=photo_img)
try:
if os.path.exists(photo['path']):
img = Image.open(photo['path'])
img.thumbnail((150, 150), Image.Resampling.LANCZOS)
photo_img = ImageTk.PhotoImage(img)
photo_images.append(photo_img)
# Create canvas for image
canvas = tk.Canvas(thumbnail_frame, width=150, height=150, bg=canvas_bg_color, highlightthickness=0)
canvas.pack()
canvas.create_image(75, 75, image=photo_img)
else:
# Placeholder for missing image
canvas = tk.Canvas(thumbnail_frame, width=150, height=150, bg=canvas_bg_color, highlightthickness=0)
canvas.pack()
canvas.create_text(75, 75, text="🖼️", fill="gray", font=("Arial", 24))
except Exception:
# Error loading image
canvas = tk.Canvas(thumbnail_frame, width=150, height=150, bg=canvas_bg_color, highlightthickness=0)
canvas.pack()
canvas.create_text(75, 75, text="", fill="red", font=("Arial", 24))
else:
# Placeholder for missing image
canvas = tk.Canvas(thumbnail_frame, width=150, height=150, bg=canvas_bg_color, highlightthickness=0)
canvas.pack()
canvas.create_text(75, 75, text="🖼️", fill="gray", font=("Arial", 24))
except Exception:
# Error loading image
canvas = tk.Canvas(thumbnail_frame, width=150, height=150, bg=canvas_bg_color, highlightthickness=0)
canvas.pack()
canvas.create_text(75, 75, text="", fill="red", font=("Arial", 24))
else:
# Data columns
if key == 'id':
text = str(photo['id'])
elif key == 'filename':
text = photo['filename']
elif key == 'processed':
text = "Yes" if photo['processed'] else "No"
elif key == 'date_taken':
text = photo['date_taken'] or "Unknown"
elif key == 'faces':
text = str(photo['face_count'])
elif key == 'tags':
text = photo['tags'] or "None"
# Data columns
if key == 'id':
text = str(photo['id'])
elif key == 'filename':
text = photo['filename']
elif key == 'processed':
text = "Yes" if photo['processed'] else "No"
elif key == 'date_taken':
text = photo['date_taken'] or "Unknown"
elif key == 'faces':
text = str(photo['face_count'])
elif key == 'tags':
text = photo['tags'] or "None"
ttk.Label(row_frame, text=text).grid(row=0, column=col_idx, padx=5, sticky=tk.W)
col_idx += 1
ttk.Label(row_frame, text=text).grid(row=0, column=col_idx, padx=5, sticky=tk.W)
col_idx += 1
current_row += 1
def show_compact_view():
clear_content()
@ -4847,26 +4936,39 @@ class PhotoTagger:
# Add separator
ttk.Separator(content_inner, orient='horizontal').grid(row=1, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5))
# Add photo rows
for idx, photo in enumerate(photos_data):
row_frame = ttk.Frame(content_inner)
row_frame.grid(row=idx+2, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=1)
# Get folder-grouped data
folder_data = prepare_folder_grouped_data()
# Add folder sections and photo rows
current_row = 2
for folder_info in folder_data:
# Add collapsible folder header
create_folder_header(content_inner, folder_info, current_row, col_count, 'compact')
current_row += 1
for i, col in enumerate(visible_cols):
row_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
col_idx = 0
for col in visible_cols:
key = col['key']
if key == 'filename':
text = photo['filename']
elif key == 'faces':
text = str(photo['face_count'])
elif key == 'tags':
text = photo['tags'] or "None"
ttk.Label(row_frame, text=text).grid(row=0, column=col_idx, padx=5, sticky=tk.W)
col_idx += 1
# Add photos in this folder only if expanded
if folder_states.get(folder_info['folder_path'], True):
for photo in folder_info['photos']:
row_frame = ttk.Frame(content_inner)
row_frame.grid(row=current_row, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=1)
for i, col in enumerate(visible_cols):
row_frame.columnconfigure(i, weight=col['weight'], minsize=col['width'])
col_idx = 0
for col in visible_cols:
key = col['key']
if key == 'filename':
text = photo['filename']
elif key == 'faces':
text = str(photo['face_count'])
elif key == 'tags':
text = photo['tags'] or "None"
ttk.Label(row_frame, text=text).grid(row=0, column=col_idx, padx=5, sticky=tk.W)
col_idx += 1
current_row += 1
def switch_view_mode(mode):
if mode == "list":