Implement date search functionality in Search GUI and SearchStats

This commit enhances the SearchGUI and SearchStats classes by introducing the ability to search for photos within a specified date range. The SearchGUI now includes input fields for users to enter 'From' and 'To' dates, along with calendar buttons for easier date selection. The SearchStats class has been updated to execute database queries that retrieve photos based on the provided date criteria, returning results that include the date taken. This addition improves the overall search capabilities and user experience in managing photo collections.
This commit is contained in:
tanyar09 2025-10-08 14:47:58 -04:00
parent 1c8856209a
commit 6fd5fe3e44
2 changed files with 243 additions and 22 deletions

View File

@ -20,7 +20,7 @@ class SearchGUI:
SEARCH_TYPES = [
"Search photos by name",
"Search photos by date (planned)",
"Search photos by date",
"Search photos by tags",
"Search photos by multiple people (planned)",
"Most common tags (planned)",
@ -96,6 +96,41 @@ class SearchGUI:
tag_mode_combo.pack(side=tk.LEFT, padx=(6, 0))
ttk.Label(tag_mode_frame, text="(ANY = photos with any tag, ALL = photos with all tags)").pack(side=tk.LEFT, padx=(6, 0))
# Date search inputs
date_frame = ttk.Frame(inputs)
ttk.Label(date_frame, text="From date:").pack(side=tk.LEFT)
date_from_var = tk.StringVar()
date_from_entry = ttk.Entry(date_frame, textvariable=date_from_var, width=12)
date_from_entry.pack(side=tk.LEFT, padx=(6, 0))
# Calendar button for date from
def open_calendar_from():
current_date = date_from_var.get()
selected_date = self.gui_core.create_calendar_dialog(root, "Select From Date", current_date)
if selected_date is not None:
date_from_var.set(selected_date)
date_from_btn = ttk.Button(date_frame, text="📅", width=3, command=open_calendar_from)
date_from_btn.pack(side=tk.LEFT, padx=(6, 0))
ttk.Label(date_frame, text="(YYYY-MM-DD)").pack(side=tk.LEFT, padx=(6, 0))
date_to_frame = ttk.Frame(inputs)
ttk.Label(date_to_frame, text="To date:").pack(side=tk.LEFT)
date_to_var = tk.StringVar()
date_to_entry = ttk.Entry(date_to_frame, textvariable=date_to_var, width=12)
date_to_entry.pack(side=tk.LEFT, padx=(6, 0))
# Calendar button for date to
def open_calendar_to():
current_date = date_to_var.get()
selected_date = self.gui_core.create_calendar_dialog(root, "Select To Date", current_date)
if selected_date is not None:
date_to_var.set(selected_date)
date_to_btn = ttk.Button(date_to_frame, text="📅", width=3, command=open_calendar_to)
date_to_btn.pack(side=tk.LEFT, padx=(6, 0))
ttk.Label(date_to_frame, text="(YYYY-MM-DD, optional)").pack(side=tk.LEFT, padx=(6, 0))
# Planned inputs (stubs)
planned_label = ttk.Label(inputs, text="This search type is planned and not yet implemented.", foreground="#888")
@ -104,7 +139,7 @@ class SearchGUI:
results_frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(results_frame, text="Results:", font=("Arial", 10, "bold")).pack(anchor="w")
columns = ("select", "person", "tags", "open_dir", "open_photo", "path")
columns = ("select", "person", "tags", "open_dir", "open_photo", "path", "date_taken")
tree = ttk.Treeview(results_frame, columns=columns, show="headings", selectmode="browse")
tree.heading("select", text="")
tree.heading("person", text="Person", command=lambda: sort_treeview("person"))
@ -112,12 +147,14 @@ class SearchGUI:
tree.heading("open_dir", text="📁")
tree.heading("open_photo", text="👤")
tree.heading("path", text="Photo path", command=lambda: sort_treeview("path"))
tree.heading("date_taken", text="Date Taken", command=lambda: sort_treeview("date_taken"))
tree.column("select", width=50, anchor="center")
tree.column("person", width=180, anchor="w")
tree.column("tags", width=200, anchor="w")
tree.column("open_dir", width=50, anchor="center")
tree.column("open_photo", width=50, anchor="center")
tree.column("path", width=400, anchor="w")
tree.column("date_taken", width=100, anchor="center")
tree.pack(fill=tk.BOTH, expand=True, pady=(4, 0))
# Buttons
@ -149,9 +186,18 @@ class SearchGUI:
# Sort the items
# For person, tags, and path columns, sort alphabetically
# For date_taken column, sort by date
# For icon columns, maintain original order
if col in ['person', 'tags', 'path']:
items.sort(key=lambda x: x[0].lower(), reverse=self.sort_reverse)
elif col == 'date_taken':
# Sort by date, handling "No date" entries
def date_sort_key(item):
date_str = item[0]
if date_str == "No date":
return "9999-12-31" # Put "No date" entries at the end
return date_str
items.sort(key=date_sort_key, reverse=self.sort_reverse)
else:
# For icon columns, just reverse if clicking same column
if self.sort_column == col and self.sort_reverse:
@ -170,6 +216,7 @@ class SearchGUI:
tree.heading("person", text="Person")
tree.heading("tags", text="Tags")
tree.heading("path", text="Photo path")
tree.heading("date_taken", text="Date Taken")
# Add sort indicator to current sort column
if self.sort_column == "person":
@ -181,6 +228,9 @@ class SearchGUI:
elif self.sort_column == "path":
indicator = "" if self.sort_reverse else ""
tree.heading("path", text="Photo path" + indicator)
elif self.sort_column == "date_taken":
indicator = "" if self.sort_reverse else ""
tree.heading("date_taken", text="Date Taken" + indicator)
# Behavior
def switch_inputs(*_):
@ -193,6 +243,12 @@ class SearchGUI:
if choice == self.SEARCH_TYPES[0]: # Search photos by name
name_frame.pack(fill=tk.X)
name_entry.configure(state="normal")
tag_entry.configure(state="disabled")
tag_mode_combo.configure(state="disabled")
date_from_entry.configure(state="disabled")
date_to_entry.configure(state="disabled")
date_from_btn.configure(state="disabled")
date_to_btn.configure(state="disabled")
search_btn.configure(state="normal")
# Show person column for name search
tree.column("person", width=180, minwidth=50, anchor="w")
@ -201,12 +257,36 @@ class SearchGUI:
tree.column("open_photo", width=50, minwidth=50, anchor="center")
tree.heading("open_photo", text="👤")
# Restore all columns to display
tree["displaycolumns"] = ("select", "person", "tags", "open_dir", "open_photo", "path")
tree["displaycolumns"] = ("select", "person", "tags", "open_dir", "open_photo", "path", "date_taken")
elif choice == self.SEARCH_TYPES[1]: # Search photos by date
date_frame.pack(fill=tk.X)
date_to_frame.pack(fill=tk.X, pady=(4, 0))
name_entry.configure(state="disabled")
tag_entry.configure(state="disabled")
tag_mode_combo.configure(state="disabled")
date_from_entry.configure(state="normal")
date_to_entry.configure(state="normal")
date_from_btn.configure(state="normal")
date_to_btn.configure(state="normal")
search_btn.configure(state="normal")
# Show person column for date search since photos might have people
tree.column("person", width=180, minwidth=50, anchor="w")
tree.heading("person", text="Person", command=lambda: sort_treeview("person"))
# Restore people icon column for date search
tree.column("open_photo", width=50, minwidth=50, anchor="center")
tree.heading("open_photo", text="👤")
# Show all columns for date search
tree["displaycolumns"] = ("select", "person", "tags", "open_dir", "open_photo", "path", "date_taken")
elif choice == self.SEARCH_TYPES[2]: # Search photos by tags
tag_frame.pack(fill=tk.X)
tag_mode_frame.pack(fill=tk.X, pady=(4, 0))
name_entry.configure(state="disabled")
tag_entry.configure(state="normal")
tag_mode_combo.configure(state="readonly")
date_from_entry.configure(state="disabled")
date_to_entry.configure(state="disabled")
date_from_btn.configure(state="disabled")
date_to_btn.configure(state="disabled")
search_btn.configure(state="normal")
# Hide person column completely for tag search
tree.column("person", width=0, minwidth=0, anchor="w")
@ -215,7 +295,7 @@ class SearchGUI:
tree.column("open_photo", width=50, minwidth=50, anchor="center")
tree.heading("open_photo", text="👤")
# Also hide the column from display
tree["displaycolumns"] = ("select", "tags", "open_dir", "open_photo", "path")
tree["displaycolumns"] = ("select", "tags", "open_dir", "open_photo", "path", "date_taken")
elif choice == self.SEARCH_TYPES[6]: # Photos without faces
# No input needed for this search type
search_btn.configure(state="normal")
@ -226,7 +306,7 @@ class SearchGUI:
tree.column("open_photo", width=0, minwidth=0, anchor="center")
tree.heading("open_photo", text="")
# Also hide the columns from display
tree["displaycolumns"] = ("select", "tags", "open_dir", "path")
tree["displaycolumns"] = ("select", "tags", "open_dir", "path", "date_taken")
# Auto-run search for photos without faces
do_search()
elif choice == self.SEARCH_TYPES[7]: # Photos without tags
@ -239,7 +319,7 @@ class SearchGUI:
tree.column("open_photo", width=50, minwidth=50, anchor="center")
tree.heading("open_photo", text="👤")
# Show all columns since we want to display person info and tags (which will be empty)
tree["displaycolumns"] = ("select", "person", "tags", "open_dir", "open_photo", "path")
tree["displaycolumns"] = ("select", "person", "tags", "open_dir", "open_photo", "path", "date_taken")
# Auto-run search for photos without tags
do_search()
else:
@ -247,6 +327,10 @@ class SearchGUI:
name_entry.configure(state="disabled")
tag_entry.configure(state="disabled")
tag_mode_combo.configure(state="disabled")
date_from_entry.configure(state="disabled")
date_to_entry.configure(state="disabled")
date_from_btn.configure(state="disabled")
date_to_btn.configure(state="disabled")
search_btn.configure(state="disabled")
# Show person column for other search types
tree.column("person", width=180, minwidth=50, anchor="w")
@ -255,7 +339,7 @@ class SearchGUI:
tree.column("open_photo", width=50, minwidth=50, anchor="center")
tree.heading("open_photo", text="👤")
# Restore all columns to display
tree["displaycolumns"] = ("select", "person", "tags", "open_dir", "open_photo", "path")
tree["displaycolumns"] = ("select", "person", "tags", "open_dir", "open_photo", "path", "date_taken")
def clear_results():
for i in tree.get_children():
@ -270,36 +354,49 @@ class SearchGUI:
update_header_display()
def add_results(rows: List[tuple]):
# rows expected: List[(full_name, path)] or List[(path, tag_info)] for tag search
# rows expected: List[(full_name, path)] or List[(path, tag_info)] for tag search or List[(path, date_taken)] for date search
for row in rows:
if len(row) == 2:
if search_type_var.get() == self.SEARCH_TYPES[2]: # Tag search
if search_type_var.get() == self.SEARCH_TYPES[1]: # Date search
# For date search: (path, date_taken) - show all columns
path, date_taken = row
person_name = get_person_name_for_photo(path)
photo_tags = get_photo_tags_for_display(path)
tree.insert("", tk.END, values=("", person_name, photo_tags, "📁", "👤", path, date_taken))
elif search_type_var.get() == self.SEARCH_TYPES[2]: # Tag search
# For tag search: (path, tag_info) - hide person column
# Show ALL tags for the photo, not just matching ones
path, tag_info = row
photo_tags = get_photo_tags_for_display(path)
tree.insert("", tk.END, values=("", "", photo_tags, "📁", "👤", path))
date_taken = get_photo_date_taken(path)
tree.insert("", tk.END, values=("", "", photo_tags, "📁", "👤", path, date_taken))
elif search_type_var.get() == self.SEARCH_TYPES[6]: # Photos without faces
# For photos without faces: (path, tag_info) - hide person and people icon columns
path, tag_info = row
photo_tags = get_photo_tags_for_display(path)
tree.insert("", tk.END, values=("", "", photo_tags, "📁", "", path))
date_taken = get_photo_date_taken(path)
tree.insert("", tk.END, values=("", "", photo_tags, "📁", "", path, date_taken))
elif search_type_var.get() == self.SEARCH_TYPES[7]: # Photos without tags
# For photos without tags: (path, filename) - show all columns but tags will be empty
path, filename = row
person_name = get_person_name_for_photo(path)
photo_tags = get_photo_tags_for_display(path) # Will be "No tags"
tree.insert("", tk.END, values=("", person_name, photo_tags, "📁", "👤", path))
date_taken = get_photo_date_taken(path)
tree.insert("", tk.END, values=("", person_name, photo_tags, "📁", "👤", path, date_taken))
else:
# For name search: (full_name, path) - show person column
full_name, p = row
# Get tags for this photo
photo_tags = get_photo_tags_for_display(p)
tree.insert("", tk.END, values=("", full_name, photo_tags, "📁", "👤", p))
date_taken = get_photo_date_taken(p)
tree.insert("", tk.END, values=("", full_name, photo_tags, "📁", "👤", p, date_taken))
# Sort by appropriate column by default when results are first loaded
if rows and self.sort_column is None:
if search_type_var.get() == self.SEARCH_TYPES[2]: # Tag search
if search_type_var.get() == self.SEARCH_TYPES[1]: # Date search
# Sort by date_taken column for date search
self.sort_column = "date_taken"
elif search_type_var.get() == self.SEARCH_TYPES[2]: # Tag search
# Sort by tags column for tag search
self.sort_column = "tags"
elif search_type_var.get() == self.SEARCH_TYPES[6]: # Photos without faces
@ -315,7 +412,16 @@ class SearchGUI:
self.sort_reverse = False
# Get all items and sort them directly
items = [(tree.set(child, self.sort_column), child) for child in tree.get_children('')]
items.sort(key=lambda x: x[0].lower(), reverse=False) # Ascending
if self.sort_column == 'date_taken':
# Sort by date, handling "No date" entries
def date_sort_key(item):
date_str = item[0]
if date_str == "No date":
return "9999-12-31" # Put "No date" entries at the end
return date_str
items.sort(key=date_sort_key, reverse=False) # Ascending
else:
items.sort(key=lambda x: x[0].lower(), reverse=False) # Ascending
# Reorder items in treeview
for index, (val, child) in enumerate(items):
tree.move(child, '', index)
@ -334,6 +440,47 @@ class SearchGUI:
if not rows:
messagebox.showinfo("Search", f"No photos found for '{query}'.", parent=root)
add_results(rows)
elif choice == self.SEARCH_TYPES[1]: # Search photos by date
date_from = date_from_var.get().strip()
date_to = date_to_var.get().strip()
# Validate date format if provided
if date_from:
try:
from datetime import datetime
datetime.strptime(date_from, '%Y-%m-%d')
except ValueError:
messagebox.showerror("Invalid Date", "From date must be in YYYY-MM-DD format.", parent=root)
return
if date_to:
try:
from datetime import datetime
datetime.strptime(date_to, '%Y-%m-%d')
except ValueError:
messagebox.showerror("Invalid Date", "To date must be in YYYY-MM-DD format.", parent=root)
return
# Check if at least one date is provided
if not date_from and not date_to:
messagebox.showinfo("Search", "Please enter at least one date (from date or to date).", parent=root)
return
rows = self.search_stats.search_photos_by_date(date_from if date_from else None,
date_to if date_to else None)
if not rows:
date_range_text = ""
if date_from and date_to:
date_range_text = f" between {date_from} and {date_to}"
elif date_from:
date_range_text = f" from {date_from}"
elif date_to:
date_range_text = f" up to {date_to}"
messagebox.showinfo("Search", f"No photos found{date_range_text}.", parent=root)
else:
# Convert to the format expected by add_results: (path, date_taken)
formatted_rows = [(path, date_taken) for path, date_taken in rows]
add_results(formatted_rows)
elif choice == self.SEARCH_TYPES[2]: # Search photos by tags
tag_query = tag_var.get().strip()
if not tag_query:
@ -574,6 +721,20 @@ class SearchGUI:
else:
return "No tags"
def get_photo_date_taken(photo_path):
"""Get date_taken for a photo to display in the date_taken column."""
try:
with self.db.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute('SELECT date_taken FROM photos WHERE path = ?', (photo_path,))
result = cursor.fetchone()
if result and result[0]:
return result[0] # Return the date as stored in database
else:
return "No date" # No date_taken available
except Exception:
return "No date"
def get_photo_people_tooltip(photo_path):
"""Get people information for a photo to display in tooltip."""
try:
@ -1085,11 +1246,14 @@ class SearchGUI:
name_entry.bind("<Return>", lambda e: do_search())
# Enter key in tag field triggers search
tag_entry.bind("<Return>", lambda e: do_search())
# Enter key in date fields triggers search
date_from_entry.bind("<Return>", lambda e: do_search())
date_to_entry.bind("<Return>", lambda e: do_search())
# Show and center
root.update_idletasks()
# Widened to ensure all columns are visible by default
self.gui_core.center_window(root, 1000, 520)
# Widened to ensure all columns are visible by default, including the new date taken column
self.gui_core.center_window(root, 1200, 520)
root.deiconify()
root.mainloop()
return 0

View File

@ -184,11 +184,68 @@ class SearchStats:
'tags_per_photo': stats['tags_per_photo']
}
def search_photos_by_date(self, date_from: str = None, date_to: str = None) -> List[Tuple]:
"""Search photos by date range"""
# This would need to be implemented in the database module
# For now, return empty list
return []
def search_photos_by_date(self, date_from: str = None, date_to: str = None) -> List[Tuple[str, str]]:
"""Search photos by date range.
Args:
date_from: Start date in YYYY-MM-DD format (inclusive)
date_to: End date in YYYY-MM-DD format (inclusive)
Returns:
List of tuples: (photo_path, date_taken)
"""
try:
with self.db.get_db_connection() as conn:
cursor = conn.cursor()
# Build the query based on provided date parameters
if date_from and date_to:
# Both dates provided - search within range
query = '''
SELECT path, date_taken
FROM photos
WHERE date_taken IS NOT NULL
AND date_taken >= ? AND date_taken <= ?
ORDER BY date_taken DESC, filename
'''
cursor.execute(query, (date_from, date_to))
elif date_from:
# Only start date provided - search from date onwards
query = '''
SELECT path, date_taken
FROM photos
WHERE date_taken IS NOT NULL
AND date_taken >= ?
ORDER BY date_taken DESC, filename
'''
cursor.execute(query, (date_from,))
elif date_to:
# Only end date provided - search up to date
query = '''
SELECT path, date_taken
FROM photos
WHERE date_taken IS NOT NULL
AND date_taken <= ?
ORDER BY date_taken DESC, filename
'''
cursor.execute(query, (date_to,))
else:
# No dates provided - return all photos with date_taken
query = '''
SELECT path, date_taken
FROM photos
WHERE date_taken IS NOT NULL
ORDER BY date_taken DESC, filename
'''
cursor.execute(query)
results = cursor.fetchall()
return [(row[0], row[1]) for row in results]
except Exception as e:
if self.verbose >= 1:
print(f"Error searching photos by date: {e}")
return []
def search_photos_by_tags(self, tags: List[str], match_all: bool = False) -> List[Tuple]:
"""Search photos by tags