diff --git a/photo_tagger.py b/photo_tagger.py index 0fbe0f8..8aefaa6 100644 --- a/photo_tagger.py +++ b/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":