diff --git a/search_gui.py b/search_gui.py index 7149437..e380c8d 100644 --- a/search_gui.py +++ b/search_gui.py @@ -25,7 +25,7 @@ class SearchGUI: "Search photos by multiple people (planned)", "Most common tags (planned)", "Most photographed people (planned)", - "Photos without faces (planned)", + "Photos without faces", "Photos without tags (planned)", "Duplicate faces (planned)", "Face quality distribution (planned)", @@ -197,6 +197,9 @@ class SearchGUI: # Show person column for name search tree.column("person", width=180, minwidth=50, anchor="w") tree.heading("person", text="Person", command=lambda: sort_treeview("person")) + # Restore people icon column for name search + 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") elif choice == self.SEARCH_TYPES[2]: # Search photos by tags @@ -208,8 +211,22 @@ class SearchGUI: # Hide person column completely for tag search tree.column("person", width=0, minwidth=0, anchor="w") tree.heading("person", text="") + # Restore people icon column for tag search + 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") + elif choice == self.SEARCH_TYPES[6]: # Photos without faces + # No input needed for this search type + search_btn.configure(state="normal") + # Hide person column since photos without faces won't have person info + tree.column("person", width=0, minwidth=0, anchor="w") + tree.heading("person", text="") + # Hide the people icon column since there are no faces/people + 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") else: planned_label.pack(anchor="w") name_entry.configure(state="disabled") @@ -219,6 +236,9 @@ class SearchGUI: # Show person column for other search types tree.column("person", width=180, minwidth=50, anchor="w") tree.heading("person", text="Person", command=lambda: sort_treeview("person")) + # Restore people icon column for other search types + 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") @@ -244,6 +264,11 @@ class SearchGUI: path, tag_info = row photo_tags = get_photo_tags_for_display(path) tree.insert("", tk.END, values=("☐", "", photo_tags, "📁", "👤", path)) + 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)) else: # For name search: (full_name, path) - show person column full_name, p = row @@ -256,6 +281,9 @@ class SearchGUI: if 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 + # Sort by path column for photos without faces + self.sort_column = "path" else: # Sort by person column for name search self.sort_column = "person" @@ -302,6 +330,15 @@ class SearchGUI: mode_text = "all" if match_all else "any" messagebox.showinfo("Search", f"No photos found with {mode_text} of the tags: {', '.join(tags)}", parent=root) add_results(rows) + elif choice == self.SEARCH_TYPES[6]: # Photos without faces + rows = self.search_stats.get_photos_without_faces() + if not rows: + messagebox.showinfo("Search", "No photos without faces found.", parent=root) + else: + # Convert to the format expected by add_results: (path, tag_info) + # For photos without faces, we don't have person info, so we use empty string + formatted_rows = [(path, "") for path, filename in rows] + add_results(formatted_rows) def open_dir(path: str): try: @@ -861,6 +898,7 @@ class SearchGUI: # Determine column offsets based on search type is_tag_search = (search_type_var.get() == self.SEARCH_TYPES[2]) + is_photos_without_faces = (search_type_var.get() == self.SEARCH_TYPES[6]) if is_tag_search: # Tag search: person column is hidden, select is first select_col = "#1" # select is now column 1 @@ -868,6 +906,13 @@ class SearchGUI: face_col = "#4" # open_photo is now column 4 path_col = "#5" # path is now column 5 path_index = 5 # path is at index 5 in values array + elif is_photos_without_faces: + # Photos without faces: person and people icon columns are hidden + select_col = "#1" # select is now column 1 + open_dir_col = "#3" # open_dir is now column 3 + face_col = "#4" # open_photo is now column 4 (but hidden) + path_col = "#4" # path is now column 4 (since people icon is hidden) + path_index = 5 # path is at index 5 in values array else: # Name search: all columns visible, select is first select_col = "#1" # select is now column 1 @@ -931,6 +976,7 @@ class SearchGUI: # Determine column offsets based on search type is_tag_search = (search_type_var.get() == self.SEARCH_TYPES[2]) + is_photos_without_faces = (search_type_var.get() == self.SEARCH_TYPES[6]) if is_tag_search: # Tag search: person column is hidden, select is first tags_col = "#2" # tags is now column 2 @@ -938,6 +984,13 @@ class SearchGUI: face_col = "#4" # open_photo is now column 4 path_col = "#5" # path is now column 5 path_index = 5 # path is at index 5 in values array + elif is_photos_without_faces: + # Photos without faces: person and people icon columns are hidden + tags_col = "#2" # tags is now column 2 + open_dir_col = "#3" # open_dir is now column 3 + face_col = "#4" # open_photo is now column 4 (but hidden) + path_col = "#4" # path is now column 4 (since people icon is hidden) + path_index = 5 # path is at index 5 in values array else: # Name search: all columns visible, select is first tags_col = "#3" # tags is column 3 diff --git a/search_stats.py b/search_stats.py index 8af0662..4ecb863 100644 --- a/search_stats.py +++ b/search_stats.py @@ -276,10 +276,31 @@ class SearchStats: return [] def get_photos_without_faces(self) -> List[Tuple]: - """Get photos that have no detected faces""" - # This would need to be implemented in the database module - # For now, return empty list - return [] + """Get photos that have no detected faces + + Returns: + List of tuples: (photo_path, filename) + """ + results = [] + try: + with self.db.get_db_connection() as conn: + cursor = conn.cursor() + # Find photos that have no faces associated with them + cursor.execute(''' + SELECT p.path, p.filename + FROM photos p + LEFT JOIN faces f ON p.id = f.photo_id + WHERE f.photo_id IS NULL + ORDER BY p.filename + ''') + for row in cursor.fetchall(): + if row and row[0]: + results.append((row[0], row[1])) + except Exception as e: + if self.verbose > 0: + print(f"Error searching photos without faces: {e}") + + return results def get_photos_without_tags(self) -> List[Tuple]: """Get photos that have no tags"""