From 07de323f2b82ccc55aa8260516c572ab9f3bb99f Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sun, 18 Jan 2026 22:48:54 +0100 Subject: [PATCH] Update tab_gallery_sorter.py --- tab_gallery_sorter.py | 111 +++++++++++++++++++++++++++++------------- 1 file changed, 78 insertions(+), 33 deletions(-) diff --git a/tab_gallery_sorter.py b/tab_gallery_sorter.py index 24e8d3f..d10f132 100644 --- a/tab_gallery_sorter.py +++ b/tab_gallery_sorter.py @@ -3,62 +3,113 @@ import os import math from engine import SorterEngine -# --- CALLBACKS (The Secret to No Refreshing) --- +# --- CALLBACKS --- def cb_tag_image(img_path, selected_cat): - """Callback: Tags image, then lets Streamlit update automatically.""" + # Guard against tagging with a separator + if selected_cat.startswith("---") or selected_cat == "": + st.toast("⚠️ Select a valid category first!", icon="🚫") + return + staged = SorterEngine.get_staged_data() ext = os.path.splitext(img_path)[1] - # Calculate suffix count = len([v for v in staged.values() if v['cat'] == selected_cat]) + 1 new_name = f"{selected_cat}_{count:03d}{ext}" SorterEngine.stage_image(img_path, selected_cat, new_name) def cb_untag_image(img_path): - """Callback: Untags image.""" SorterEngine.clear_staged_item(img_path) def cb_delete_image(img_path): - """Callback: Moves image to trash immediately.""" SorterEngine.delete_to_trash(img_path) def cb_apply_batch(current_batch, path_o, cleanup_mode): - """Callback: Applies changes to disk.""" SorterEngine.commit_batch(current_batch, path_o, cleanup_mode) -# --- FRAGMENT 1: SIDEBAR --- +# --- FRAGMENT 1: SIDEBAR (Manager) --- @st.fragment def render_sidebar_content(): st.divider() st.subheader("🏷️ Category Manager") - # Add Category - c_add1, c_add2 = st.columns([3, 1]) - new_cat = c_add1.text_input("New Category", label_visibility="collapsed", placeholder="New...", key="t5_new_cat_input") + # Tabs for different actions + tab_add, tab_edit = st.tabs(["➕ Add", "✏️ Rename"]) - # We use a callback here too for smoothness, or just simple rerun - if c_add2.button("➕", help="Add"): - if new_cat: - SorterEngine.add_category(new_cat) - st.rerun() + with tab_add: + c1, c2 = st.columns([3, 1]) + new_cat = c1.text_input("New Cat", label_visibility="collapsed", placeholder="New...", key="t5_new_cat") + if c2.button("Add", key="btn_add_cat"): + if new_cat: + SorterEngine.add_category(new_cat) + st.rerun() + # --- CATEGORY LIST LOGIC --- cats = SorterEngine.get_categories() if not cats: st.warning("No categories.") return - + + # 1. Insert Visual Separators + processed_cats = [] + last_char = "" + + for cat in cats: + current_char = cat[0].upper() + # If letter changes (and it's not the very first one), add separator + if last_char and current_char != last_char: + processed_cats.append(f"--- {current_char} ---") + elif not last_char: + # Optional: Add header for the first group + # processed_cats.append(f"--- {current_char} ---") + pass + + processed_cats.append(cat) + last_char = current_char + + # 2. State Management if "t5_active_cat" not in st.session_state: st.session_state.t5_active_cat = cats[0] + + # Ensure current selection is valid in the new list (handle edge cases) + if st.session_state.t5_active_cat not in processed_cats: + if cats[0] in processed_cats: + st.session_state.t5_active_cat = cats[0] + + # 3. The Radio List + selection = st.radio("Active Tag", processed_cats, key="t5_radio_select") + + # 4. Handle Separator Selection (Auto-revert) + if selection.startswith("---"): + # If user clicks separator, we revert to previous valid or just show warning + st.warning("Please select a category, not the divider.") + # We don't update the official session state 't5_active_cat' used by the grid + else: + st.session_state.t5_active_cat = selection + + # --- RENAME LOGIC (Inside Edit Tab) --- + with tab_edit: + # Defaults to currently selected valid category + target_cat = st.session_state.t5_active_cat if not st.session_state.t5_active_cat.startswith("---") else "" - st.radio("Active Tag", cats, key="t5_active_cat") + st.caption(f"Editing: **{target_cat}**") + rename_val = st.text_input("New Name", value=target_cat, key="t5_rename_input") + + if st.button("Update Name", key="btn_rename"): + if target_cat and rename_val and rename_val != target_cat: + SorterEngine.rename_category(target_cat, rename_val) + # Force update session state to new name so we don't lose selection + st.session_state.t5_active_cat = rename_val + st.rerun() # --- FRAGMENT 2: GALLERY GRID --- @st.fragment def render_gallery_grid(current_batch, quality, grid_cols): staged = SorterEngine.get_staged_data() + # Safely get category, falling back if it's a separator selected_cat = st.session_state.get("t5_active_cat", "Default") - + if selected_cat.startswith("---"): selected_cat = "" # Disable tagging if separator selected + cols = st.columns(grid_cols) for idx, img_path in enumerate(current_batch): @@ -68,26 +119,21 @@ def render_gallery_grid(current_batch, quality, grid_cols): is_staged = img_path in staged with st.container(border=True): - # Header c_head1, c_head2 = st.columns([5, 1]) c_head1.caption(os.path.basename(img_path)[:15]) - - # Delete X (Using Callback) - c_head2.button("❌", key=f"del_{unique_key}", - on_click=cb_delete_image, args=(img_path,)) + c_head2.button("❌", key=f"del_{unique_key}", on_click=cb_delete_image, args=(img_path,)) - # Status if is_staged: st.success(f"🏷️ {staged[img_path]['cat']}") - # Image img_data = SorterEngine.compress_for_web(img_path, quality) if img_data: st.image(img_data, use_container_width=True) - # Action Buttons (Using Callbacks) if not is_staged: - st.button("Tag", key=f"tag_{unique_key}", use_container_width=True, + # Disable button if separator is selected + btn_disabled = (selected_cat == "") + st.button("Tag", key=f"tag_{unique_key}", disabled=btn_disabled, use_container_width=True, on_click=cb_tag_image, args=(img_path, selected_cat)) else: st.button("Untag", key=f"untag_{unique_key}", use_container_width=True, @@ -114,7 +160,7 @@ def render(quality, profile_name): if not os.path.exists(path_s): return - # 1. Sidebar (Fragment) + # 1. Sidebar Fragment with st.sidebar: render_sidebar_content() @@ -124,7 +170,7 @@ def render(quality, profile_name): page_size = c_v1.slider("Images per Page", 12, 100, 24, 4) grid_cols = c_v2.slider("Grid Columns", 2, 8, 4) - # 3. Data & Pagination + # 3. Pagination Logic all_images = SorterEngine.get_images(path_s, recursive=True) if not all_images: st.info("No images found.") @@ -138,7 +184,7 @@ def render(quality, profile_name): end_idx = start_idx + page_size current_batch = all_images[start_idx:end_idx] - # Navigation Controls + # Nav Helpers def nav_controls(key): c1, c2, c3 = st.columns([1, 2, 1]) if c1.button("⬅️ Prev", disabled=(st.session_state.t5_page==0), key=f"p_{key}"): @@ -152,18 +198,17 @@ def render(quality, profile_name): nav_controls("top") st.divider() - # 4. Gallery (Fragment with Callbacks) + # 4. Gallery Fragment render_gallery_grid(current_batch, quality, grid_cols) st.divider() nav_controls("bottom") st.divider() - # 5. Batch Apply (NO FRAGMENT - Needs full refresh to show files are moved) + # 5. Batch Apply st.write(f"### 🚀 Batch Actions (Page {st.session_state.t5_page + 1})") c_act1, c_act2 = st.columns([3, 1]) cleanup = c_act1.radio("Untagged Action:", ["Keep", "Move to Unused", "Delete"], horizontal=True, key="t5_cleanup_mode") - # We use on_click here too for consistency, but the page WILL reload fully after this. c_act2.button("APPLY PAGE", type="primary", use_container_width=True, on_click=cb_apply_batch, args=(current_batch, path_o, cleanup)) \ No newline at end of file