diff --git a/tab_gallery_sorter.py b/tab_gallery_sorter.py index d10f132..65c0cfa 100644 --- a/tab_gallery_sorter.py +++ b/tab_gallery_sorter.py @@ -32,183 +32,78 @@ def render_sidebar_content(): st.divider() st.subheader("🏷️ Category Manager") - # Tabs for different actions - tab_add, tab_edit = st.tabs(["➕ Add", "✏️ Rename"]) + # 1. GET CATEGORIES & PROCESS SEPARATORS + cats = SorterEngine.get_categories() + # Setup List with Separators + processed_cats = [] + last_char = "" + if cats: + for cat in cats: + current_char = cat[0].upper() + if last_char and current_char != last_char: + processed_cats.append(f"--- {current_char} ---") + processed_cats.append(cat) + last_char = current_char + + # 2. RADIO SELECTION + # We default to the first real category if nothing is selected + if "t5_active_cat" not in st.session_state: + st.session_state.t5_active_cat = cats[0] if cats else "Default" + + # Handle case where previously selected cat was deleted + if st.session_state.t5_active_cat not in processed_cats: + st.session_state.t5_active_cat = cats[0] if cats else "Default" + + selection = st.radio("Active Tag", processed_cats, key="t5_radio_select") + + # Update global state (but ignore separators) + if not selection.startswith("---"): + st.session_state.t5_active_cat = selection + + st.divider() + + # 3. TABS: ADD / EDIT + tab_add, tab_edit = st.tabs(["➕ Add", "✏️ Edit"]) + + # --- ADD TAB --- 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") + new_cat = c1.text_input("New Name", 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) --- + # --- EDIT TAB (Rename & Delete) --- 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.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 + # Determine target (block separators) + target_cat = st.session_state.t5_active_cat + is_valid = target_cat and not target_cat.startswith("---") and target_cat in cats + + if is_valid: + st.caption(f"Editing: **{target_cat}**") + + # RENAME SECTION + # TRICK: We use the category name in the key. + # This forces Streamlit to reset the input box when you switch categories. + rename_val = st.text_input("Rename to:", value=target_cat, key=f"ren_{target_cat}") + + if st.button("💾 Save Name", key=f"save_{target_cat}", use_container_width=True): + if rename_val and rename_val != target_cat: + SorterEngine.rename_category(target_cat, rename_val) + st.session_state.t5_active_cat = rename_val # Update selection + st.rerun() + + st.markdown("---") + + # DELETE SECTION + if st.button("🗑️ Delete Category", key=f"del_cat_{target_cat}", type="primary", use_container_width=True): + SorterEngine.delete_category(target_cat) + # Fallback to first available category after delete + remaining = SorterEngine.get_categories() + st.session_state.t5_active_cat = remaining[0] if remaining else "Default" 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): - unique_key = f"frag_{os.path.basename(img_path)}" - - with cols[idx % grid_cols]: - is_staged = img_path in staged - - with st.container(border=True): - c_head1, c_head2 = st.columns([5, 1]) - c_head1.caption(os.path.basename(img_path)[:15]) - c_head2.button("❌", key=f"del_{unique_key}", on_click=cb_delete_image, args=(img_path,)) - - if is_staged: - st.success(f"🏷️ {staged[img_path]['cat']}") - - img_data = SorterEngine.compress_for_web(img_path, quality) - if img_data: - st.image(img_data, use_container_width=True) - - if not is_staged: - # 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, - on_click=cb_untag_image, args=(img_path,)) - - -# --- MAIN PAGE RENDERER --- -def render(quality, profile_name): - st.subheader("🖼️ Gallery Staging Sorter") - - if 't5_page' not in st.session_state: st.session_state.t5_page = 0 - - profiles = SorterEngine.load_profiles() - p_data = profiles.get(profile_name, {}) - - c1, c2 = st.columns(2) - path_s = c1.text_input("Source Folder", value=p_data.get("tab5_source", "/storage"), key="t5_s") - path_o = c2.text_input("Output Folder", value=p_data.get("tab5_out", "/storage"), key="t5_o") - - if path_s != p_data.get("tab5_source") or path_o != p_data.get("tab5_out"): - if st.button("💾 Save Settings"): - SorterEngine.save_tab_paths(profile_name, t5_s=path_s, t5_o=path_o) - st.rerun() - - if not os.path.exists(path_s): return - - # 1. Sidebar Fragment - with st.sidebar: - render_sidebar_content() - - # 2. View Settings - with st.expander("👀 View Settings"): - c_v1, c_v2 = st.columns(2) - page_size = c_v1.slider("Images per Page", 12, 100, 24, 4) - grid_cols = c_v2.slider("Grid Columns", 2, 8, 4) - - # 3. Pagination Logic - all_images = SorterEngine.get_images(path_s, recursive=True) - if not all_images: - st.info("No images found.") - return - - total_items = len(all_images) - total_pages = math.ceil(total_items / page_size) - if st.session_state.t5_page >= total_pages: st.session_state.t5_page = max(0, total_pages - 1) - - start_idx = st.session_state.t5_page * page_size - end_idx = start_idx + page_size - current_batch = all_images[start_idx:end_idx] - - # 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}"): - st.session_state.t5_page -= 1 - st.rerun() - c2.markdown(f"
Page {st.session_state.t5_page+1} / {total_pages}
", unsafe_allow_html=True) - if c3.button("Next ➡️", disabled=(st.session_state.t5_page>=total_pages-1), key=f"n_{key}"): - st.session_state.t5_page += 1 - st.rerun() - - nav_controls("top") - st.divider() - - # 4. Gallery Fragment - render_gallery_grid(current_batch, quality, grid_cols) - - st.divider() - nav_controls("bottom") - st.divider() - - # 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") - - 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 + else: + st.info("Select a valid category above to edit.") \ No newline at end of file