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:
parent
1c8856209a
commit
6fd5fe3e44
198
search_gui.py
198
search_gui.py
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user