Update tab_gallery_sorter.py

This commit is contained in:
2026-01-18 22:33:43 +01:00
parent 0d3ff69f8d
commit 293cfd14f8

View File

@@ -3,21 +3,57 @@ import os
import math import math
from engine import SorterEngine from engine import SorterEngine
# --- FRAGMENT: This section updates independently --- # --- FRAGMENT 1: SIDEBAR (Category Manager) ---
@st.fragment @st.fragment
def render_gallery_grid(current_batch, quality, selected_cat, grid_cols): def render_sidebar_fragment():
""" """
This function isolates the grid updates. Isolates the category selection.
Clicking buttons here will NOT reload the whole page. Changing the active tag here updates Session State instantly
but DOES NOT reload the image grid.
""" """
# CRITICAL: Re-fetch staged status inside the fragment so the UI updates instantly with st.sidebar:
staged = SorterEngine.get_staged_data() st.divider()
st.subheader("🏷️ Category Manager")
# 1. ADD CATEGORY
c_add1, c_add2 = st.columns([3, 1])
new_cat = c_add1.text_input("New Category", label_visibility="collapsed", placeholder="New Category...", key="t5_new_cat_input")
if c_add2.button("", help="Add Category"):
if new_cat:
SorterEngine.add_category(new_cat)
st.rerun() # Refresh only this sidebar fragment
# 2. SELECT CATEGORY
cats = SorterEngine.get_categories()
if not cats:
st.warning("No categories.")
return None
# We use session state to ensure the grid can see the selection
# Default to first category if not set
if "t5_active_cat" not in st.session_state:
st.session_state.t5_active_cat = cats[0]
# The Radio Button
# on_change is not needed because the key automatically syncs with session_state
current = st.radio("Active Tag", cats, key="t5_active_cat")
return current
# --- FRAGMENT 2: GALLERY GRID ---
@st.fragment
def render_gallery_grid(current_batch, quality, grid_cols):
"""
Isolates the image grid.
Reads the 'Active Tag' directly from Session State when a button is clicked.
"""
# 1. Fetch latest data (DB + Session State)
staged = SorterEngine.get_staged_data()
selected_cat = st.session_state.get("t5_active_cat", "Default") # Read latest tag
# Dynamic Grid
cols = st.columns(grid_cols) cols = st.columns(grid_cols)
for idx, img_path in enumerate(current_batch): for idx, img_path in enumerate(current_batch):
# Unique key ensures buttons don't conflict
unique_key = f"frag_{os.path.basename(img_path)}" unique_key = f"frag_{os.path.basename(img_path)}"
with cols[idx % grid_cols]: with cols[idx % grid_cols]:
@@ -28,10 +64,10 @@ def render_gallery_grid(current_batch, quality, selected_cat, grid_cols):
c_head1, c_head2 = st.columns([5, 1]) c_head1, c_head2 = st.columns([5, 1])
c_head1.caption(os.path.basename(img_path)[:15]) c_head1.caption(os.path.basename(img_path)[:15])
# Delete Button # Delete X
if c_head2.button("", key=f"del_{unique_key}"): if c_head2.button("", key=f"del_{unique_key}"):
SorterEngine.delete_to_trash(img_path) SorterEngine.delete_to_trash(img_path)
st.rerun() # Reruns ONLY this fragment st.rerun()
# Status Banner # Status Banner
if is_staged: if is_staged:
@@ -42,31 +78,32 @@ def render_gallery_grid(current_batch, quality, selected_cat, grid_cols):
if img_data: if img_data:
st.image(img_data, use_container_width=True) st.image(img_data, use_container_width=True)
# Action Buttons # Buttons
if not is_staged: if not is_staged:
# Note: Label is static "Tag", but logic uses 'selected_cat'
if st.button("Tag", key=f"tag_{unique_key}", use_container_width=True): if st.button("Tag", key=f"tag_{unique_key}", use_container_width=True):
ext = os.path.splitext(img_path)[1] ext = os.path.splitext(img_path)[1]
# Calculate suffix
count = len([v for v in staged.values() if v['cat'] == selected_cat]) + 1 count = len([v for v in staged.values() if v['cat'] == selected_cat]) + 1
new_name = f"{selected_cat}_{count:03d}{ext}" new_name = f"{selected_cat}_{count:03d}{ext}"
SorterEngine.stage_image(img_path, selected_cat, new_name) SorterEngine.stage_image(img_path, selected_cat, new_name)
st.rerun() # Reruns ONLY this fragment st.rerun()
else: else:
if st.button("Untag", key=f"untag_{unique_key}", use_container_width=True): if st.button("Untag", key=f"untag_{unique_key}", use_container_width=True):
SorterEngine.clear_staged_item(img_path) SorterEngine.clear_staged_item(img_path)
st.rerun() # Reruns ONLY this fragment st.rerun()
# --- MAIN RENDER --- # --- MAIN PAGE RENDERER ---
def render(quality, profile_name): def render(quality, profile_name):
st.subheader("🖼️ Gallery Staging Sorter") st.subheader("🖼️ Gallery Staging Sorter")
# 1. Setup Session & Profile # 1. Init Session State
if 't5_page' not in st.session_state: st.session_state.t5_page = 0 if 't5_page' not in st.session_state: st.session_state.t5_page = 0
# 2. Profile & Paths
profiles = SorterEngine.load_profiles() profiles = SorterEngine.load_profiles()
p_data = profiles.get(profile_name, {}) p_data = profiles.get(profile_name, {})
# 2. Paths
c1, c2 = st.columns(2) c1, c2 = st.columns(2)
path_s = c1.text_input("Source Folder", value=p_data.get("tab5_source", "/storage"), key="t5_s") 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") path_o = c2.text_input("Output Folder", value=p_data.get("tab5_out", "/storage"), key="t5_o")
@@ -76,36 +113,24 @@ def render(quality, profile_name):
SorterEngine.save_tab_paths(profile_name, t5_s=path_s, t5_o=path_o) SorterEngine.save_tab_paths(profile_name, t5_s=path_s, t5_o=path_o)
st.rerun() st.rerun()
# 3. Sidebar (Categories)
with st.sidebar:
st.divider()
st.subheader("🏷️ Category Manager")
new_cat = st.text_input("New Category", key="t5_new_cat")
if st.button(" Add", key="t5_add_btn"):
if new_cat:
SorterEngine.add_category(new_cat)
st.rerun()
cats = SorterEngine.get_categories()
if not cats:
st.warning("No categories.")
return
selected_cat = st.radio("Active Tag", cats, key="t5_cat_radio")
if not os.path.exists(path_s): return if not os.path.exists(path_s): return
# 4. View Settings & Pagination # 3. CALL SIDEBAR FRAGMENT
with st.expander("👀 View Settings", expanded=False): # This draws the sidebar controls. Interactions here will NOT reload the main page.
render_sidebar_fragment()
# 4. View Settings (Main Body)
with st.expander("👀 View Settings"):
c_v1, c_v2 = st.columns(2) c_v1, c_v2 = st.columns(2)
page_size = c_v1.slider("Images per Page", 12, 100, 24, 4) page_size = c_v1.slider("Images per Page", 12, 100, 24, 4)
grid_cols = c_v2.slider("Grid Columns", 2, 8, 4) grid_cols = c_v2.slider("Grid Columns", 2, 8, 4)
# 5. Pagination Logic
all_images = SorterEngine.get_images(path_s, recursive=True) all_images = SorterEngine.get_images(path_s, recursive=True)
if not all_images: if not all_images:
st.info("No images found.") st.info("No images found.")
return return
# Pagination Logic
total_items = len(all_images) total_items = len(all_images)
total_pages = math.ceil(total_items / page_size) 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) if st.session_state.t5_page >= total_pages: st.session_state.t5_page = max(0, total_pages - 1)
@@ -128,15 +153,15 @@ def render(quality, profile_name):
nav_controls("top") nav_controls("top")
st.divider() st.divider()
# --- CALL THE FRAGMENT --- # 6. CALL GALLERY FRAGMENT
# This is the only part that will refresh when you tag/delete! # Interactions here (Tagging) will only reload this grid.
render_gallery_grid(current_batch, quality, selected_cat, grid_cols) render_gallery_grid(current_batch, quality, grid_cols)
st.divider() st.divider()
nav_controls("bottom") nav_controls("bottom")
st.divider() st.divider()
# 5. Apply Batch # 7. Batch Apply
st.write(f"### 🚀 Batch Actions (Page {st.session_state.t5_page + 1})") st.write(f"### 🚀 Batch Actions (Page {st.session_state.t5_page + 1})")
c_act1, c_act2 = st.columns([3, 1]) c_act1, c_act2 = st.columns([3, 1])
cleanup = c_act1.radio("Untagged Action:", ["Keep", "Move to Unused", "Delete"], horizontal=True) cleanup = c_act1.radio("Untagged Action:", ["Keep", "Move to Unused", "Delete"], horizontal=True)