| import os
|
| import gradio as gr
|
|
|
|
|
| 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
|
|
|
|
|
| from ui.sidebar import render_sidebar
|
|
|
| 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
|
|
|
|
|
| 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:
|
|
|
| nav_btns = render_sidebar()
|
|
|
|
|
| with gr.Column(elem_classes="main-content"):
|
|
|
| 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,
|
| 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()
|
|
|
|
|
| (
|
| 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()
|
|
|
|
|
|
|
|
|
| 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]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
| def toggle_collection_inputs(is_new):
|
| """Toggles rendering states of manual and standard collections selectors."""
|
| return (
|
| gr.update(visible=not is_new),
|
| gr.update(visible=is_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]
|
| )
|
|
|
|
|
| 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] + "..."
|
|
|
| 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
|
|
|
|
|
|
|
| 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_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]
|
| )
|
|
|
|
|
|
|
| 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]
|
| )
|
|
|
|
|
| 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]
|
| )
|
|
|
|
|
|
|
| refresh_cap_btn.click(fn=load_caption_browser, outputs=[caption_table, cap_status])
|
|
|
|
|
| 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() |