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:
parent
b6e6b38a76
commit
0f599d3d16
294
photo_tagger.py
294
photo_tagger.py
@ -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":
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user