Implement date handling features in PhotoTagger. Add 'date_taken' and 'date_added' columns to the photos database, along with EXIF data extraction for photo dates. Enhance the GUI with date filter options for face identification, allowing users to filter by date taken and processed dates. Introduce a calendar dialog for easier date selection, improving user experience and data management.
This commit is contained in:
parent
34aba85fc6
commit
e1bed343b6
446
photo_tagger.py
446
photo_tagger.py
@ -10,6 +10,7 @@ import argparse
|
||||
import face_recognition
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from PIL.ExifTags import TAGS
|
||||
import pickle
|
||||
import numpy as np
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
@ -18,6 +19,7 @@ import tempfile
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
from contextlib import contextmanager
|
||||
|
||||
@ -153,6 +155,7 @@ class PhotoTagger:
|
||||
path TEXT UNIQUE NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
date_added DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
date_taken DATE,
|
||||
processed BOOLEAN DEFAULT 0
|
||||
)
|
||||
''')
|
||||
@ -219,11 +222,66 @@ class PhotoTagger:
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_faces_quality ON faces(quality_score)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_person_encodings_person_id ON person_encodings(person_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_person_encodings_quality ON person_encodings(quality_score)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_photos_date_taken ON photos(date_taken)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_photos_date_added ON photos(date_added)')
|
||||
|
||||
# Migration: Add date_taken column to existing photos table if it doesn't exist
|
||||
try:
|
||||
cursor.execute('ALTER TABLE photos ADD COLUMN date_taken DATE')
|
||||
if self.verbose >= 1:
|
||||
print("✅ Added date_taken column to photos table")
|
||||
except Exception:
|
||||
# Column already exists, ignore
|
||||
pass
|
||||
|
||||
# Migration: Add date_added column to existing photos table if it doesn't exist
|
||||
try:
|
||||
cursor.execute('ALTER TABLE photos ADD COLUMN date_added DATETIME DEFAULT CURRENT_TIMESTAMP')
|
||||
if self.verbose >= 1:
|
||||
print("✅ Added date_added column to photos table")
|
||||
except Exception:
|
||||
# Column already exists, ignore
|
||||
pass
|
||||
|
||||
|
||||
if self.verbose >= 1:
|
||||
print(f"✅ Database initialized: {self.db_path}")
|
||||
|
||||
def _extract_photo_date(self, photo_path: str) -> Optional[str]:
|
||||
"""Extract date taken from photo EXIF data"""
|
||||
try:
|
||||
with Image.open(photo_path) as image:
|
||||
exifdata = image.getexif()
|
||||
|
||||
# Look for date taken in EXIF tags
|
||||
date_tags = [
|
||||
306, # DateTime
|
||||
36867, # DateTimeOriginal
|
||||
36868, # DateTimeDigitized
|
||||
]
|
||||
|
||||
for tag_id in date_tags:
|
||||
if tag_id in exifdata:
|
||||
date_str = exifdata[tag_id]
|
||||
if date_str:
|
||||
# Parse EXIF date format (YYYY:MM:DD HH:MM:SS)
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y:%m:%d %H:%M:%S')
|
||||
return date_obj.strftime('%Y-%m-%d')
|
||||
except ValueError:
|
||||
# Try alternative format
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
|
||||
return date_obj.strftime('%Y-%m-%d')
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
if self.verbose >= 2:
|
||||
print(f" ⚠️ Could not extract date from {os.path.basename(photo_path)}: {e}")
|
||||
return None
|
||||
|
||||
def scan_folder(self, folder_path: str, recursive: bool = True) -> int:
|
||||
"""Scan folder for photos and add to database"""
|
||||
# BREAKPOINT: Set breakpoint here for debugging
|
||||
@ -266,14 +324,18 @@ class PhotoTagger:
|
||||
|
||||
for photo_path, filename in found_photos:
|
||||
try:
|
||||
# Extract date taken from EXIF data
|
||||
date_taken = self._extract_photo_date(photo_path)
|
||||
|
||||
cursor.execute(
|
||||
'INSERT OR IGNORE INTO photos (path, filename) VALUES (?, ?)',
|
||||
(photo_path, filename)
|
||||
'INSERT OR IGNORE INTO photos (path, filename, date_taken) VALUES (?, ?, ?)',
|
||||
(photo_path, filename, date_taken)
|
||||
)
|
||||
if cursor.rowcount > 0:
|
||||
added_count += 1
|
||||
if self.verbose >= 2:
|
||||
print(f" 📸 Added: {filename}")
|
||||
date_info = f" (taken: {date_taken})" if date_taken else " (no date)"
|
||||
print(f" 📸 Added: {filename}{date_info}")
|
||||
elif self.verbose >= 3:
|
||||
print(f" 📸 Already exists: {filename}")
|
||||
except Exception as e:
|
||||
@ -283,6 +345,7 @@ class PhotoTagger:
|
||||
print(f"📁 Found {len(found_photos)} photos, added {added_count} new photos")
|
||||
return added_count
|
||||
|
||||
|
||||
def process_faces(self, limit: int = 50, model: str = "hog") -> int:
|
||||
"""Process unprocessed photos for faces"""
|
||||
with self.get_db_connection() as conn:
|
||||
@ -355,18 +418,43 @@ class PhotoTagger:
|
||||
print(f"✅ Processed {processed_count} photos")
|
||||
return processed_count
|
||||
|
||||
def identify_faces(self, batch_size: int = 20, show_faces: bool = False, tolerance: float = 0.6) -> int:
|
||||
def identify_faces(self, batch_size: int = 20, show_faces: bool = False, tolerance: float = 0.6,
|
||||
date_from: str = None, date_to: str = None, date_processed_from: str = None, date_processed_to: str = None) -> int:
|
||||
"""Interactive face identification with optimized performance"""
|
||||
with self.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
# Build the SQL query with optional date filtering
|
||||
query = '''
|
||||
SELECT f.id, f.photo_id, p.path, p.filename, f.location
|
||||
FROM faces f
|
||||
JOIN photos p ON f.photo_id = p.id
|
||||
WHERE f.person_id IS NULL
|
||||
LIMIT ?
|
||||
''', (batch_size,))
|
||||
'''
|
||||
params = []
|
||||
|
||||
# Add date taken filtering if specified
|
||||
if date_from:
|
||||
query += ' AND p.date_taken >= ?'
|
||||
params.append(date_from)
|
||||
|
||||
if date_to:
|
||||
query += ' AND p.date_taken <= ?'
|
||||
params.append(date_to)
|
||||
|
||||
# Add date processed filtering if specified
|
||||
if date_processed_from:
|
||||
query += ' AND DATE(p.date_added) >= ?'
|
||||
params.append(date_processed_from)
|
||||
|
||||
if date_processed_to:
|
||||
query += ' AND DATE(p.date_added) <= ?'
|
||||
params.append(date_processed_to)
|
||||
|
||||
query += ' LIMIT ?'
|
||||
params.append(batch_size)
|
||||
|
||||
cursor.execute(query, params)
|
||||
|
||||
unidentified = cursor.fetchall()
|
||||
|
||||
@ -554,20 +642,354 @@ class PhotoTagger:
|
||||
root.rowconfigure(0, weight=1)
|
||||
main_frame.columnconfigure(0, weight=1) # Left panel
|
||||
main_frame.columnconfigure(1, weight=1) # Right panel for similar faces
|
||||
main_frame.rowconfigure(1, weight=1) # Main content row
|
||||
main_frame.rowconfigure(2, weight=1) # Main content row
|
||||
|
||||
# Photo info
|
||||
info_label = ttk.Label(main_frame, text="", font=("Arial", 10, "bold"))
|
||||
info_label.grid(row=0, column=0, columnspan=2, pady=(0, 10), sticky=tk.W)
|
||||
|
||||
# Calendar dialog function for date filter
|
||||
def open_date_calendar(date_var, title):
|
||||
"""Open a visual calendar dialog to select date"""
|
||||
from datetime import datetime, date, timedelta
|
||||
import calendar
|
||||
|
||||
# Create calendar window
|
||||
calendar_window = tk.Toplevel(root)
|
||||
calendar_window.title(title)
|
||||
calendar_window.resizable(False, False)
|
||||
calendar_window.transient(root)
|
||||
calendar_window.grab_set()
|
||||
|
||||
# Calculate center position before showing the window
|
||||
window_width = 400
|
||||
window_height = 400
|
||||
screen_width = calendar_window.winfo_screenwidth()
|
||||
screen_height = calendar_window.winfo_screenheight()
|
||||
x = (screen_width // 2) - (window_width // 2)
|
||||
y = (screen_height // 2) - (window_height // 2)
|
||||
|
||||
# Set geometry with center position before showing
|
||||
calendar_window.geometry(f"{window_width}x{window_height}+{x}+{y}")
|
||||
|
||||
# Calendar variables
|
||||
current_date = datetime.now()
|
||||
|
||||
# Check if there's already a date selected
|
||||
existing_date_str = date_var.get().strip()
|
||||
if existing_date_str:
|
||||
try:
|
||||
existing_date = datetime.strptime(existing_date_str, '%Y-%m-%d').date()
|
||||
display_year = existing_date.year
|
||||
display_month = existing_date.month
|
||||
selected_date = existing_date
|
||||
except ValueError:
|
||||
# If existing date is invalid, use current date
|
||||
display_year = current_date.year
|
||||
display_month = current_date.month
|
||||
selected_date = None
|
||||
else:
|
||||
# Default to current date
|
||||
display_year = current_date.year
|
||||
display_month = current_date.month
|
||||
selected_date = None
|
||||
|
||||
# Month names
|
||||
month_names = ["January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"]
|
||||
|
||||
# Create custom style for calendar buttons
|
||||
style = ttk.Style()
|
||||
style.configure("Calendar.TButton", padding=(2, 2))
|
||||
style.map("Calendar.TButton",
|
||||
background=[("active", "#e1e1e1")],
|
||||
relief=[("pressed", "sunken")])
|
||||
|
||||
# Main frame
|
||||
main_cal_frame = ttk.Frame(calendar_window, padding="10")
|
||||
main_cal_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Header frame with navigation
|
||||
header_frame = ttk.Frame(main_cal_frame)
|
||||
header_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
# Month/Year display and navigation
|
||||
nav_frame = ttk.Frame(header_frame)
|
||||
nav_frame.pack()
|
||||
|
||||
# Month/Year label (created once, updated later)
|
||||
month_year_label = ttk.Label(nav_frame, text="", font=("Arial", 12, "bold"))
|
||||
month_year_label.pack(side=tk.LEFT, padx=10)
|
||||
|
||||
def update_calendar():
|
||||
"""Update the calendar display"""
|
||||
# Update month/year label
|
||||
month_year_label.configure(text=f"{month_names[display_month-1]} {display_year}")
|
||||
|
||||
# Clear existing calendar
|
||||
for widget in calendar_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
# Get calendar data
|
||||
cal = calendar.monthcalendar(display_year, display_month)
|
||||
|
||||
# Day headers
|
||||
day_headers = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||
for i, day in enumerate(day_headers):
|
||||
header_label = ttk.Label(calendar_frame, text=day, font=("Arial", 10, "bold"))
|
||||
header_label.grid(row=0, column=i, padx=2, pady=2, sticky="nsew")
|
||||
|
||||
# Calendar days
|
||||
for week_num, week in enumerate(cal):
|
||||
for day_num, day in enumerate(week):
|
||||
if day == 0:
|
||||
# Empty cell
|
||||
empty_label = ttk.Label(calendar_frame, text="")
|
||||
empty_label.grid(row=week_num+1, column=day_num, padx=2, pady=2, sticky="nsew")
|
||||
else:
|
||||
# Day button
|
||||
day_date = date(display_year, display_month, day)
|
||||
is_selected = selected_date == day_date
|
||||
is_today = day_date == current_date.date()
|
||||
|
||||
# Button text and style
|
||||
button_text = str(day)
|
||||
if is_today:
|
||||
button_text = f"•{day}•" # Mark today
|
||||
|
||||
day_btn = ttk.Button(calendar_frame, text=button_text,
|
||||
style="Calendar.TButton" if not is_selected else "Calendar.TButton",
|
||||
command=lambda d=day_date: select_date(d))
|
||||
day_btn.grid(row=week_num+1, column=day_num, padx=2, pady=2, sticky="nsew")
|
||||
|
||||
# Highlight selected date
|
||||
if is_selected:
|
||||
day_btn.configure(style="Calendar.TButton")
|
||||
# Add visual indication of selection
|
||||
day_btn.configure(text=f"[{day}]")
|
||||
|
||||
def select_date(selected_day):
|
||||
"""Select a date and close calendar"""
|
||||
nonlocal selected_date
|
||||
selected_date = selected_day
|
||||
date_var.set(selected_day.strftime('%Y-%m-%d'))
|
||||
calendar_window.destroy()
|
||||
|
||||
def prev_month():
|
||||
nonlocal display_month, display_year
|
||||
display_month -= 1
|
||||
if display_month < 1:
|
||||
display_month = 12
|
||||
display_year -= 1
|
||||
update_calendar()
|
||||
|
||||
def next_month():
|
||||
nonlocal display_month, display_year
|
||||
display_month += 1
|
||||
if display_month > 12:
|
||||
display_month = 1
|
||||
display_year += 1
|
||||
update_calendar()
|
||||
|
||||
def prev_year():
|
||||
nonlocal display_year
|
||||
display_year -= 1
|
||||
update_calendar()
|
||||
|
||||
def next_year():
|
||||
nonlocal display_year
|
||||
display_year += 1
|
||||
update_calendar()
|
||||
|
||||
# Navigation buttons
|
||||
prev_year_btn = ttk.Button(nav_frame, text="<<", width=3, command=prev_year)
|
||||
prev_year_btn.pack(side=tk.LEFT)
|
||||
|
||||
prev_month_btn = ttk.Button(nav_frame, text="<", width=3, command=prev_month)
|
||||
prev_month_btn.pack(side=tk.LEFT, padx=(5, 0))
|
||||
|
||||
next_month_btn = ttk.Button(nav_frame, text=">", width=3, command=next_month)
|
||||
next_month_btn.pack(side=tk.LEFT, padx=(5, 0))
|
||||
|
||||
next_year_btn = ttk.Button(nav_frame, text=">>", width=3, command=next_year)
|
||||
next_year_btn.pack(side=tk.LEFT)
|
||||
|
||||
# Calendar grid frame
|
||||
calendar_frame = ttk.Frame(main_cal_frame)
|
||||
calendar_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||
|
||||
# Configure grid weights
|
||||
for i in range(7):
|
||||
calendar_frame.columnconfigure(i, weight=1)
|
||||
for i in range(7):
|
||||
calendar_frame.rowconfigure(i, weight=1)
|
||||
|
||||
# Buttons frame
|
||||
buttons_frame = ttk.Frame(main_cal_frame)
|
||||
buttons_frame.pack(fill=tk.X)
|
||||
|
||||
def clear_date():
|
||||
"""Clear the selected date"""
|
||||
date_var.set("")
|
||||
calendar_window.destroy()
|
||||
|
||||
# Clear button
|
||||
clear_btn = ttk.Button(buttons_frame, text="Clear", command=clear_date)
|
||||
clear_btn.pack(side=tk.LEFT)
|
||||
|
||||
# Cancel button
|
||||
cancel_btn = ttk.Button(buttons_frame, text="Cancel", command=calendar_window.destroy)
|
||||
cancel_btn.pack(side=tk.RIGHT)
|
||||
|
||||
# Initial calendar display
|
||||
update_calendar()
|
||||
|
||||
# Date filter controls
|
||||
date_filter_frame = ttk.LabelFrame(main_frame, text="Date Filters", padding="5")
|
||||
date_filter_frame.grid(row=1, column=0, columnspan=2, pady=(0, 10), sticky=(tk.W, tk.E))
|
||||
date_filter_frame.columnconfigure(1, weight=1)
|
||||
date_filter_frame.columnconfigure(4, weight=1)
|
||||
date_filter_frame.columnconfigure(7, weight=1)
|
||||
date_filter_frame.columnconfigure(10, weight=1)
|
||||
|
||||
# Date from
|
||||
ttk.Label(date_filter_frame, text="From:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))
|
||||
date_from_var = tk.StringVar(value=date_from or "")
|
||||
date_from_entry = ttk.Entry(date_filter_frame, textvariable=date_from_var, width=12, state='readonly')
|
||||
date_from_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 5))
|
||||
|
||||
# Calendar button for date from
|
||||
def open_calendar_from():
|
||||
open_date_calendar(date_from_var, "Select Start Date")
|
||||
|
||||
calendar_from_btn = ttk.Button(date_filter_frame, text="📅", width=3, command=open_calendar_from)
|
||||
calendar_from_btn.grid(row=0, column=2, padx=(0, 10))
|
||||
|
||||
# Date to
|
||||
ttk.Label(date_filter_frame, text="To:").grid(row=0, column=3, sticky=tk.W, padx=(0, 5))
|
||||
date_to_var = tk.StringVar(value=date_to or "")
|
||||
date_to_entry = ttk.Entry(date_filter_frame, textvariable=date_to_var, width=12, state='readonly')
|
||||
date_to_entry.grid(row=0, column=4, sticky=(tk.W, tk.E), padx=(0, 5))
|
||||
|
||||
# Calendar button for date to
|
||||
def open_calendar_to():
|
||||
open_date_calendar(date_to_var, "Select End Date")
|
||||
|
||||
calendar_to_btn = ttk.Button(date_filter_frame, text="📅", width=3, command=open_calendar_to)
|
||||
calendar_to_btn.grid(row=0, column=5, padx=(0, 10))
|
||||
|
||||
# Apply filter button
|
||||
def apply_date_filter():
|
||||
nonlocal date_from, date_to
|
||||
date_from = date_from_var.get().strip() or None
|
||||
date_to = date_to_var.get().strip() or None
|
||||
date_processed_from = date_processed_from_var.get().strip() or None
|
||||
date_processed_to = date_processed_to_var.get().strip() or None
|
||||
|
||||
# Reload faces with new date filter
|
||||
with self.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Build the SQL query with optional date filtering
|
||||
query = '''
|
||||
SELECT f.id, f.photo_id, p.path, p.filename, f.location
|
||||
FROM faces f
|
||||
JOIN photos p ON f.photo_id = p.id
|
||||
WHERE f.person_id IS NULL
|
||||
'''
|
||||
params = []
|
||||
|
||||
# Add date taken filtering if specified
|
||||
if date_from:
|
||||
query += ' AND p.date_taken >= ?'
|
||||
params.append(date_from)
|
||||
|
||||
if date_to:
|
||||
query += ' AND p.date_taken <= ?'
|
||||
params.append(date_to)
|
||||
|
||||
# Add date processed filtering if specified
|
||||
if date_processed_from:
|
||||
query += ' AND DATE(p.date_added) >= ?'
|
||||
params.append(date_processed_from)
|
||||
|
||||
if date_processed_to:
|
||||
query += ' AND DATE(p.date_added) <= ?'
|
||||
params.append(date_processed_to)
|
||||
|
||||
query += ' LIMIT ?'
|
||||
params.append(batch_size)
|
||||
|
||||
cursor.execute(query, params)
|
||||
unidentified = cursor.fetchall()
|
||||
|
||||
if not unidentified:
|
||||
messagebox.showinfo("No Faces Found", "No unidentified faces found with the current date filters.")
|
||||
return
|
||||
|
||||
# Update the global unidentified list and reset position
|
||||
nonlocal current_pos, total_unidentified
|
||||
current_pos = 0
|
||||
total_unidentified = len(unidentified)
|
||||
|
||||
# Reset to first face - display will update when user navigates
|
||||
if len(unidentified) > 0:
|
||||
# Reset to first face
|
||||
current_pos = 0
|
||||
# The display will be updated when the user navigates or when the window is shown
|
||||
|
||||
# Build filter description
|
||||
filters_applied = []
|
||||
if date_from or date_to:
|
||||
taken_filter = f"taken: {date_from or 'any'} to {date_to or 'any'}"
|
||||
filters_applied.append(taken_filter)
|
||||
if date_processed_from or date_processed_to:
|
||||
processed_filter = f"processed: {date_processed_from or 'any'} to {date_processed_to or 'any'}"
|
||||
filters_applied.append(processed_filter)
|
||||
|
||||
filter_desc = " | ".join(filters_applied) if filters_applied else "no filters"
|
||||
|
||||
print(f"📅 Applied filters: {filter_desc}")
|
||||
print(f"👤 Found {len(unidentified)} unidentified faces with date filters")
|
||||
print("💡 Navigate to refresh the display with filtered faces")
|
||||
|
||||
apply_filter_btn = ttk.Button(date_filter_frame, text="Apply Filter", command=apply_date_filter)
|
||||
apply_filter_btn.grid(row=0, column=6, padx=(10, 0))
|
||||
|
||||
# Date processed filter (second row)
|
||||
ttk.Label(date_filter_frame, text="Processed From:").grid(row=1, column=0, sticky=tk.W, padx=(0, 5), pady=(10, 0))
|
||||
date_processed_from_var = tk.StringVar()
|
||||
date_processed_from_entry = ttk.Entry(date_filter_frame, textvariable=date_processed_from_var, width=12, state='readonly')
|
||||
date_processed_from_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(0, 5), pady=(10, 0))
|
||||
|
||||
# Calendar button for date processed from
|
||||
def open_calendar_processed_from():
|
||||
open_date_calendar(date_processed_from_var, "Select Processing Start Date")
|
||||
|
||||
calendar_processed_from_btn = ttk.Button(date_filter_frame, text="📅", width=3, command=open_calendar_processed_from)
|
||||
calendar_processed_from_btn.grid(row=1, column=2, padx=(0, 10), pady=(10, 0))
|
||||
|
||||
# Date processed to
|
||||
ttk.Label(date_filter_frame, text="Processed To:").grid(row=1, column=3, sticky=tk.W, padx=(0, 5), pady=(10, 0))
|
||||
date_processed_to_var = tk.StringVar()
|
||||
date_processed_to_entry = ttk.Entry(date_filter_frame, textvariable=date_processed_to_var, width=12, state='readonly')
|
||||
date_processed_to_entry.grid(row=1, column=4, sticky=(tk.W, tk.E), padx=(0, 5), pady=(10, 0))
|
||||
|
||||
# Calendar button for date processed to
|
||||
def open_calendar_processed_to():
|
||||
open_date_calendar(date_processed_to_var, "Select Processing End Date")
|
||||
|
||||
calendar_processed_to_btn = ttk.Button(date_filter_frame, text="📅", width=3, command=open_calendar_processed_to)
|
||||
calendar_processed_to_btn.grid(row=1, column=5, padx=(0, 10), pady=(10, 0))
|
||||
|
||||
# Left panel for main face
|
||||
left_panel = ttk.Frame(main_frame)
|
||||
left_panel.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5))
|
||||
left_panel.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5))
|
||||
left_panel.columnconfigure(0, weight=1)
|
||||
|
||||
# Right panel for similar faces
|
||||
right_panel = ttk.LabelFrame(main_frame, text="Similar Faces", padding="5")
|
||||
right_panel.grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0))
|
||||
right_panel.grid(row=2, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0))
|
||||
right_panel.columnconfigure(0, weight=1)
|
||||
right_panel.rowconfigure(0, weight=1) # Make right panel expandable vertically
|
||||
|
||||
@ -1268,7 +1690,7 @@ class PhotoTagger:
|
||||
|
||||
# Bottom control panel
|
||||
control_frame = ttk.Frame(main_frame)
|
||||
control_frame.grid(row=2, column=0, columnspan=2, pady=(10, 0), sticky=tk.E)
|
||||
control_frame.grid(row=3, column=0, columnspan=2, pady=(10, 0), sticky=tk.E)
|
||||
|
||||
# Create button references for state management
|
||||
back_btn = ttk.Button(control_frame, text="⬅️ Back", command=on_back)
|
||||
@ -4279,7 +4701,7 @@ class PhotoTagger:
|
||||
|
||||
# Control buttons
|
||||
control_frame = ttk.Frame(main_frame)
|
||||
control_frame.grid(row=2, column=0, columnspan=2, pady=(10, 0), sticky=tk.E)
|
||||
control_frame.grid(row=3, column=0, columnspan=2, pady=(10, 0), sticky=tk.E)
|
||||
|
||||
def on_quit():
|
||||
nonlocal window_destroyed
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user