# import gradio as gr # # Ensure the new dropdown helpers are imported # from logic import ( # run_ingest, run_search, load_caption_browser, load_collections_view, # update_collections_dropdown, update_search_dropdown # ) # from caption_store import entry_count, get_all_collections # # Import UI Views # 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 # # Define explicit Dark Mode theme variables # # dark_theme = gr.themes.Soft( # # primary_hue="green", # # secondary_hue="emerald" # # ).set( # # body_background_fill="#0b0f19", # # body_background_fill_dark="#0b0f19", # # block_background_fill="#1e293b", # # block_background_fill_dark="#1e293b", # # block_border_color="#334155", # # block_border_color_dark="#334155", # # input_background_fill="#0f172a", # # input_background_fill_dark="#0f172a", # # text_color="#f1f5f9", # # text_color_dark="#f1f5f9", # # ) # # Load Custom Stylesheet # with open("style.css", "r") as f: # custom_css = f.read() # with gr.Blocks(css=custom_css) as demo: # # 1. Render Navigation Sidebar # nav_btns, stats_label = 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_page, search_query, search_col_filter, search_btn, search_status_msg, search_gallery = render_search() # colls_page, view_coll_selector, refresh_coll_btn, coll_status, coll_gallery = render_collections() # caps_page, refresh_cap_btn, cap_status, caption_table = render_captions() # about_page = render_about() # # Routing definitions # pages = [home_page, ingest_page, search_page, colls_page, caps_page, about_page] # page_names = ["home", "ingest", "search", "collections", "captions", "about"] # def switch_page(page_name): # return [gr.update(visible=(name == page_name)) for name in page_names] # # --- Page Routing Event Handlers --- # nav_btns["home"].click(fn=lambda: switch_page("home"), outputs=pages) # nav_btns["ingest"].click(fn=lambda: switch_page("ingest"), outputs=pages) # # Use logic helpers to keep dropdown selections safe when changing tabs # 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) # # --- Component Value Interaction Bindings --- # # Toggle dropdown/text field depending on toggle state # def toggle_collection_inputs(is_new): # 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] # ).then( # fn=lambda: f"Total Photos: {entry_count()}", # outputs=stats_label # ) # # Search Logic Action triggers # search_btn.click(fn=run_search, inputs=[search_query, search_col_filter], outputs=[search_gallery, search_status_msg]) # search_query.submit(fn=run_search, inputs=[search_query, search_col_filter], outputs=[search_gallery, search_status_msg]) # # Gallery view loaders # view_coll_selector.change(fn=load_collections_view, inputs=view_coll_selector, outputs=[coll_gallery, coll_status]) # refresh_coll_btn.click(fn=load_collections_view, inputs=view_coll_selector, outputs=[coll_gallery, coll_status]) # # Captions refresh actions # refresh_cap_btn.click(fn=load_caption_browser, outputs=[caption_table, cap_status]) # # Initial startup data population # demo.load(fn=load_caption_browser, outputs=[caption_table, cap_status]) # demo.load( # fn=lambda: load_collections_view(get_all_collections()[0] if get_all_collections() else "General"), # outputs=[coll_gallery, coll_status] # ) # if __name__ == "__main__": # demo.launch() 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"] 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()