import os import gradio as gr # Import robust business logic operations from logic import ( run_ingest, run_search, load_caption_browser, load_collections_view, update_collections_dropdown, update_search_dropdown, zip_selected_files ) from caption_store import entry_count, get_all_collections # Import modular UI Page builders from ui.sidebar import render_sidebar # from ui.home import render_home from ui.ingest import render_ingest from ui.search import render_search from ui.collections import render_collections from ui.captions import render_captions from ui.about import render_about # Load layout parameters from global stylesheet with open("style.css", "r") as f: custom_css = f.read() with gr.Blocks(css=custom_css, title="ShutterSearch — Photo Archive", fill_height=False) as demo: # 1. Render Navigation Sidebar nav_btns = render_sidebar() # 2. Render Main Content Container Pages with gr.Column(elem_classes="main-content"): # home_page, start_btn = render_home() ingest_page, upload, coll_dropdown, use_new_coll, new_coll_name, ingest_btn, ingest_status = render_ingest() # Search layout with selection returns ( search_page, search_query, search_col_filter, search_btn, search_status_msg, search_gallery, selected_search_paths, loaded_search_paths, search_selection_status, search_download_btn, search_download_file, search_clear_selection_btn, search_select_all_btn ) = render_search() # Collections layout with selection returns ( colls_page, view_coll_selector, refresh_coll_btn, coll_status, coll_gallery, selected_paths, loaded_original_paths, selection_status, download_btn, download_file, clear_selection_btn, select_all_btn ) = render_collections() caps_page, refresh_cap_btn, cap_status, caption_table = render_captions() about_page = render_about() # Define page indexes for visibility toggles # pages = [home_page, ingest_page, search_page, colls_page, caps_page, about_page] # page_names = ["home", "ingest", "search", "collections", "captions", "about"] pages = [ingest_page, search_page, colls_page, caps_page, about_page] page_names = ["ingest", "search", "collections", "captions", "about"] def switch_page(page_name): """Generates dynamic updates to manage current visible pages.""" return [gr.update(visible=(name == page_name)) for name in page_names] # --- Sidebar Navigation Interactions --- # nav_btns["home"].click(fn=lambda: switch_page("home"), outputs=pages) # nav_btns["ingest"].click(fn=lambda: switch_page("ingest"), outputs=pages) # Safe helpers are invoked here to avoid component value schema mismatch issues nav_btns["search"].click( fn=lambda: switch_page("search") + [update_search_dropdown()], outputs=pages + [search_col_filter] ) nav_btns["collections"].click( fn=lambda: switch_page("collections") + [update_collections_dropdown()], outputs=pages + [view_coll_selector] ) nav_btns["captions"].click(fn=lambda: switch_page("captions"), outputs=pages) nav_btns["about"].click(fn=lambda: switch_page("about"), outputs=pages) # start_btn.click(fn=lambda: switch_page("ingest"), outputs=pages) # --- Ingestion View Handlers --- def toggle_collection_inputs(is_new): """Toggles rendering states of manual and standard collections selectors.""" return ( gr.update(visible=not is_new), # Hide selector if creating new gr.update(visible=is_new) # Show text box if creating new ) use_new_coll.change( fn=toggle_collection_inputs, inputs=use_new_coll, outputs=[coll_dropdown, new_coll_name] ) ingest_btn.click( fn=run_ingest, inputs=[upload, coll_dropdown, use_new_coll, new_coll_name], outputs=[ingest_status, caption_table, coll_dropdown] ) # --- Shared Selection Utility Functions --- def format_selection_status(count, filenames): """Generates double-linebreak formatted selection statuses to divide output lines.""" if count == 0: return "**0** image(s) selected for download packaging.\n\n*No files currently selected.*" name_string = ", ".join(filenames) if len(name_string) > 150: name_string = name_string[:150] + "..." # Double linebreak forces a paragraph divide onto a separate line return f"**{count}** image(s) selected for download packaging.\n\nšŸ“‹ **Selected Files:** `{name_string}`" def handle_gallery_selection(evt: gr.SelectData, current_selections, raw_paths, gallery_data): """Toggles visual labels dynamically and outputs double-linebreak reports.""" clicked_file = raw_paths[evt.index] if clicked_file in current_selections: current_selections.remove(clicked_file) else: current_selections.append(clicked_file) updated_gallery = [] for idx, path in enumerate(raw_paths): thumb_path = gallery_data[idx][0] basename = os.path.basename(path) label = f"āœ… {basename}" if path in current_selections else basename updated_gallery.append((thumb_path, label)) filenames = [os.path.basename(f) for f in current_selections] summary = format_selection_status(len(current_selections), filenames) return current_selections, summary, updated_gallery def handle_select_all(raw_paths, gallery_data): """Packages all source paths inside current array list and displays checkmarks.""" if not raw_paths: return [], format_selection_status(0, []), [] current_selections = list(raw_paths) updated_gallery = [] for idx, path in enumerate(raw_paths): thumb_path = gallery_data[idx][0] basename = os.path.basename(path) updated_gallery.append((thumb_path, f"āœ… {basename}")) filenames = [os.path.basename(f) for f in current_selections] summary = format_selection_status(len(current_selections), filenames) return current_selections, summary, updated_gallery def clear_selection(raw_paths, gallery_data): """Flushes storage state lists and clears visual checkmarks from labels.""" updated_gallery = [] for idx, path in enumerate(raw_paths): thumb_path = gallery_data[idx][0] basename = os.path.basename(path) updated_gallery.append((thumb_path, basename)) return [], format_selection_status(0, []), updated_gallery, gr.update(visible=False) def handle_selected_download(selected_list): """Compiles specified absolute paths into a downloadable archive container.""" if not selected_list: return gr.update(visible=False), "āš ļø Download failure: No selections marked." file_path, status_msg = zip_selected_files(selected_list) if file_path: return gr.update(value=file_path, visible=True), status_msg return gr.update(visible=False), status_msg # --- Search View Bindings --- def trigger_search_load(query, col_filter): images, original_paths, status = run_search(query, col_filter) return images, original_paths, [], format_selection_status(0, []), status, gr.update(visible=False) search_btn.click( fn=trigger_search_load, inputs=[search_query, search_col_filter], outputs=[search_gallery, loaded_search_paths, selected_search_paths, search_selection_status, search_status_msg, search_download_file] ) search_query.submit( fn=trigger_search_load, inputs=[search_query, search_col_filter], outputs=[search_gallery, loaded_search_paths, selected_search_paths, search_selection_status, search_status_msg, search_download_file] ) # Search Interactive Select Hooks search_gallery.select( fn=handle_gallery_selection, inputs=[selected_search_paths, loaded_search_paths, search_gallery], outputs=[selected_search_paths, search_selection_status, search_gallery] ) search_select_all_btn.click( fn=handle_select_all, inputs=[loaded_search_paths, search_gallery], outputs=[selected_search_paths, search_selection_status, search_gallery] ) search_clear_selection_btn.click( fn=clear_selection, inputs=[loaded_search_paths, search_gallery], outputs=[selected_search_paths, search_selection_status, search_gallery, search_download_file] ) search_download_btn.click( fn=handle_selected_download, inputs=selected_search_paths, outputs=[search_download_file, search_selection_status] ) # --- Collections View Bindings --- def trigger_collection_load(selected_collection): images, original_paths, status = load_collections_view(selected_collection) return images, original_paths, [], format_selection_status(0, []), gr.update(visible=False) view_coll_selector.change( fn=trigger_collection_load, inputs=view_coll_selector, outputs=[coll_gallery, loaded_original_paths, selected_paths, selection_status, download_file] ) refresh_coll_btn.click( fn=trigger_collection_load, inputs=view_coll_selector, outputs=[coll_gallery, loaded_original_paths, selected_paths, selection_status, download_file] ) # Collections Interactive Select Hooks coll_gallery.select( fn=handle_gallery_selection, inputs=[selected_paths, loaded_original_paths, coll_gallery], outputs=[selected_paths, selection_status, coll_gallery] ) select_all_btn.click( fn=handle_select_all, inputs=[loaded_original_paths, coll_gallery], outputs=[selected_paths, selection_status, coll_gallery] ) clear_selection_btn.click( fn=clear_selection, inputs=[loaded_original_paths, coll_gallery], outputs=[selected_paths, selection_status, coll_gallery, download_file] ) download_btn.click( fn=handle_selected_download, inputs=selected_paths, outputs=[download_file, selection_status] ) # --- Caption Browser Refresh Bindings --- refresh_cap_btn.click(fn=load_caption_browser, outputs=[caption_table, cap_status]) # Initial startup payload loaders demo.load(fn=load_caption_browser, outputs=[caption_table, cap_status]) demo.load( fn=trigger_collection_load, inputs=view_coll_selector, outputs=[coll_gallery, loaded_original_paths, selected_paths, selection_status, download_file] ) if __name__ == "__main__": demo.launch()