From b909069174df3915fd5f139fff2b3e530357e8a6 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Mon, 19 Jan 2026 15:21:55 +0100 Subject: [PATCH] Update tab_gallery_sorter.py --- tab_gallery_sorter.py | 99 +++++++++++++++++++++++++++++++------------ 1 file changed, 71 insertions(+), 28 deletions(-) diff --git a/tab_gallery_sorter.py b/tab_gallery_sorter.py index d903f9d..9ba4042 100644 --- a/tab_gallery_sorter.py +++ b/tab_gallery_sorter.py @@ -23,16 +23,46 @@ def trigger_refresh(): if 't5_file_id' not in st.session_state: st.session_state.t5_file_id = 0 st.session_state.t5_file_id += 1 -def cb_tag_image(img_path, selected_cat): +def cb_tag_image(img_path, selected_cat, index_val, path_o): + """ + Tags image with manual number. + Handles collisions by creating variants (e.g. _001_1) and warning the user. + """ 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] - count = len([v for v in staged.values() if v['cat'] == selected_cat]) + 1 - new_name = f"{selected_cat}_{count:03d}{ext}" + base_name = f"{selected_cat}_{index_val:03d}" + new_name = f"{base_name}{ext}" + + # --- COLLISION DETECTION --- + # 1. Check Staging DB + staged = SorterEngine.get_staged_data() + # Get all names currently staged for this category + staged_names = {v['name'] for v in staged.values() if v['cat'] == selected_cat} + + # 2. Check Hard Drive + dest_path = os.path.join(path_o, selected_cat, new_name) + + collision = False + suffix = 1 + + # Loop until we find a free name + while new_name in staged_names or os.path.exists(dest_path): + collision = True + new_name = f"{base_name}_{suffix}{ext}" + dest_path = os.path.join(path_o, selected_cat, new_name) + suffix += 1 + + # --- SAVE --- SorterEngine.stage_image(img_path, selected_cat, new_name) - # Note: Tagging does NOT need a file re-scan, just a grid refresh. + + if collision: + st.toast(f"⚠️ Conflict! Saved as variant: {new_name}", icon="🔀") + + # REMOVED: st.session_state.t5_next_index += 1 + # The numbers in the input boxes will now stay static. def cb_untag_image(img_path): SorterEngine.clear_staged_item(img_path) @@ -138,19 +168,19 @@ def get_cached_thumbnail(path, quality, target_size, mtime): # --- UPDATED GALLERY FRAGMENT --- @st.fragment -def render_gallery_grid(current_batch, quality, grid_cols): +def render_gallery_grid(current_batch, quality, grid_cols, path_o): # <--- 1. Added path_o staged = SorterEngine.get_staged_data() history = SorterEngine.get_processed_log() selected_cat = st.session_state.get("t5_active_cat", "Default") tagging_disabled = selected_cat.startswith("---") + + # 2. Ensure global counter exists (default to 1) + if "t5_next_index" not in st.session_state: st.session_state.t5_next_index = 1 - # 1. SMART RESOLUTION CALCULATION - # We assume a wide screen (approx 2400px wide for the container). - # If you have 2 cols, you get 1200px images. If 8 cols, you get 300px. - # This ensures images are always crisp but never wasteful. + # 3. Smart Resolution (Wide screen assumption) target_size = int(2400 / grid_cols) - # 2. PARALLEL LOAD + # 4. Parallel Load (16 threads for WebP) import concurrent.futures batch_cache = {} @@ -161,15 +191,13 @@ def render_gallery_grid(current_batch, quality, grid_cols): except: return p, None - # We bump threads to 16 for WebP as it can be slightly more CPU intensive, - # but the smaller file size makes up for it in transfer speed. with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor: future_to_path = {executor.submit(fetch_one, p): p for p in current_batch} for future in concurrent.futures.as_completed(future_to_path): p, data = future.result() batch_cache[p] = data - # 3. RENDER GRID + # 5. Render Grid cols = st.columns(grid_cols) for idx, img_path in enumerate(current_batch): unique_key = f"frag_{os.path.basename(img_path)}" @@ -178,39 +206,54 @@ def render_gallery_grid(current_batch, quality, grid_cols): is_processed = img_path in history with st.container(border=True): - # HEADER LAYOUT: [Name (4)] [Zoom (1)] [Delete (1)] + # Header: [Name] [Zoom] [Delete] c_name, c_zoom, c_del = st.columns([4, 1, 1]) - c_name.caption(os.path.basename(img_path)[:10]) - # --- NEW ZOOM BUTTON --- - # When clicked, it calls the dialog function. - if c_zoom.button("🔍", key=f"zoom_{unique_key}", help="View Full Size"): + if c_zoom.button("🔍", key=f"zoom_{unique_key}"): view_high_res(img_path) - - # DELETE BUTTON + c_del.button("❌", key=f"del_{unique_key}", on_click=cb_delete_image, args=(img_path,)) - # STATUS BANNERS... + # Status Banners if is_staged: st.success(f"🏷️ {staged[img_path]['cat']}") elif is_processed: st.info(f"✅ {history[img_path]['action']}") - # THUMBNAIL IMAGE (Cached, Low Res) + # Image (Cached) img_data = batch_cache.get(img_path) if img_data: st.image(img_data, use_container_width=True) - # Buttons + # Action Area if not is_staged: - st.button("Tag", key=f"tag_{unique_key}", disabled=tagging_disabled, use_container_width=True, - on_click=cb_tag_image, args=(img_path, selected_cat)) + # 6. Split Row: [Idx Input] [Tag Button] + c_idx, c_tag = st.columns([1, 2], vertical_alignment="bottom") + + # Manual Override Box (Defaults to global session value) + card_index = c_idx.number_input( + "Idx", + min_value=1, step=1, + value=st.session_state.t5_next_index, + label_visibility="collapsed", + key=f"idx_{unique_key}" + ) + + # Tag Button (Passes path_o for conflict check) + c_tag.button( + "Tag", + key=f"tag_{unique_key}", + disabled=tagging_disabled, + use_container_width=True, + on_click=cb_tag_image, + # Passing card_index + path_o is vital here + args=(img_path, selected_cat, card_index, path_o) + ) else: st.button("Untag", key=f"untag_{unique_key}", use_container_width=True, on_click=cb_untag_image, args=(img_path,)) - # ... (Batch Actions code remains exactly the same) ... @st.fragment def render_batch_actions(current_batch, path_o, page_num, path_s): @@ -300,7 +343,7 @@ def render(quality, profile_name): st.divider() nav_controls("top") - render_gallery_grid(current_batch, quality, grid_cols) + render_gallery_grid(current_batch, quality, grid_cols, path_o) st.divider() nav_controls("bottom") st.divider()