From 6363ea459035c86515187ae76a5c9e94cfa179bf Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Mon, 19 Jan 2026 14:22:24 +0100 Subject: [PATCH 1/9] Update engine.py --- engine.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/engine.py b/engine.py index 078cf5f..8abad8a 100644 --- a/engine.py +++ b/engine.py @@ -354,14 +354,25 @@ class SorterEngine: return t_dst, c_dst @staticmethod - def compress_for_web(path, quality): - """Compresses images for UI performance.""" + def compress_for_web(path, quality, target_size=400): + """ + Loads image, RESIZES it to a thumbnail, and returns bytes. + """ try: with Image.open(path) as img: + # 1. Convert to RGB (fixes PNG/Transparency issues) + img = img.convert("RGB") + + # 2. HUGE SPEEDUP: Resize before saving + # We use 'thumbnail' which maintains aspect ratio + img.thumbnail((target_size, target_size), Image.Resampling.LANCZOS) + + # 3. Save to buffer buf = BytesIO() - img.convert("RGB").save(buf, format="JPEG", quality=quality) - return buf - except: return None + img.save(buf, format="JPEG", quality=quality, optimize=True) + return buf.getvalue() # Return bytes directly for caching + except Exception: + return None @staticmethod def revert_action(action): -- 2.49.1 From 8328e4d3b4b5d2df0b2788e71d260db62f6bc82d Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Mon, 19 Jan 2026 14:24:25 +0100 Subject: [PATCH 2/9] Update tab_gallery_sorter.py --- tab_gallery_sorter.py | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/tab_gallery_sorter.py b/tab_gallery_sorter.py index ebd0761..7e20244 100644 --- a/tab_gallery_sorter.py +++ b/tab_gallery_sorter.py @@ -114,6 +114,13 @@ def render_sidebar_content(): # ... (Gallery Grid code remains exactly the same) ... +# --- NEW CACHED FUNCTION --- +# This saves the thumbnail in RAM. +# We include 'mtime' so if the file changes on disk, the cache invalidates. +@st.cache_data(show_spinner=False, max_entries=2000) +def get_cached_thumbnail(path, quality, target_size, mtime): + return SorterEngine.compress_for_web(path, quality, target_size) + @st.fragment def render_gallery_grid(current_batch, quality, grid_cols): staged = SorterEngine.get_staged_data() @@ -121,10 +128,32 @@ def render_gallery_grid(current_batch, quality, grid_cols): selected_cat = st.session_state.get("t5_active_cat", "Default") tagging_disabled = selected_cat.startswith("---") - # --- NEW: LOAD ALL IMAGES IN PARALLEL --- - # This runs multithreaded and is much faster than the old loop - batch_cache = SorterEngine.load_batch_parallel(current_batch, quality) + # 1. CALCULATE OPTIMAL SIZE + # If 8 columns, we need small images (200px). If 2 cols, big images (800px). + # This keeps it crisp but fast. + target_size = int(1600 / grid_cols) + # 2. PARALLEL LOAD WITH CACHING + # We use a ThreadPool to fill the cache quickly + import concurrent.futures + + batch_cache = {} + + def fetch_one(p): + try: + # We pass mtime to ensure cache freshness + mtime = os.path.getmtime(p) + return p, get_cached_thumbnail(p, quality, target_size, mtime) + except: + return p, None + + with concurrent.futures.ThreadPoolExecutor(max_workers=8) 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 cols = st.columns(grid_cols) for idx, img_path in enumerate(current_batch): unique_key = f"frag_{os.path.basename(img_path)}" @@ -133,6 +162,7 @@ def render_gallery_grid(current_batch, quality, grid_cols): is_processed = img_path in history with st.container(border=True): + # ... (Header/Delete code same as before) ... 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,)) @@ -140,13 +170,14 @@ def render_gallery_grid(current_batch, quality, grid_cols): if is_staged: st.success(f"🏷️ {staged[img_path]['cat']}") elif is_processed: - st.info(f"✅ {history[img_path]['action']} -> {history[img_path]['cat']}") + st.info(f"✅ {history[img_path]['action']}") - # --- CHANGED: USE PRE-LOADED DATA --- + # DISPLAY FROM CACHE img_data = batch_cache.get(img_path) if img_data: st.image(img_data, use_container_width=True) + # ... (Buttons code same as before) ... 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)) -- 2.49.1 From e7144eb6cfac9debe022fb8f670f2a267e620669 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Mon, 19 Jan 2026 14:27:25 +0100 Subject: [PATCH 3/9] webp --- engine.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/engine.py b/engine.py index 8abad8a..1a7dd40 100644 --- a/engine.py +++ b/engine.py @@ -354,23 +354,27 @@ class SorterEngine: return t_dst, c_dst @staticmethod - def compress_for_web(path, quality, target_size=400): + def compress_for_web(path, quality, target_size=None): """ - Loads image, RESIZES it to a thumbnail, and returns bytes. + Loads image, resizes smart, and saves as WebP. """ try: with Image.open(path) as img: - # 1. Convert to RGB (fixes PNG/Transparency issues) - img = img.convert("RGB") + # 1. Convert to RGB (WebP handles RGBA, but RGB is safer for consistency) + if img.mode not in ('RGB', 'RGBA'): + img = img.convert("RGB") - # 2. HUGE SPEEDUP: Resize before saving - # We use 'thumbnail' which maintains aspect ratio - img.thumbnail((target_size, target_size), Image.Resampling.LANCZOS) + # 2. Smart Resize (Only if target_size is provided) + if target_size: + # Only resize if the original is actually bigger + if img.width > target_size or img.height > target_size: + img.thumbnail((target_size, target_size), Image.Resampling.LANCZOS) - # 3. Save to buffer + # 3. Save as WebP buf = BytesIO() - img.save(buf, format="JPEG", quality=quality, optimize=True) - return buf.getvalue() # Return bytes directly for caching + # WebP is faster to decode in browser and smaller on disk + img.save(buf, format="WEBP", quality=quality) + return buf.getvalue() except Exception: return None -- 2.49.1 From 758125a60babc6ceaef121c48016cf10502f6027 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Mon, 19 Jan 2026 14:28:22 +0100 Subject: [PATCH 4/9] webp --- tab_gallery_sorter.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/tab_gallery_sorter.py b/tab_gallery_sorter.py index 7e20244..97f0b20 100644 --- a/tab_gallery_sorter.py +++ b/tab_gallery_sorter.py @@ -114,13 +114,13 @@ def render_sidebar_content(): # ... (Gallery Grid code remains exactly the same) ... -# --- NEW CACHED FUNCTION --- -# This saves the thumbnail in RAM. -# We include 'mtime' so if the file changes on disk, the cache invalidates. +# --- UPDATED CACHE FUNCTION --- @st.cache_data(show_spinner=False, max_entries=2000) def get_cached_thumbnail(path, quality, target_size, mtime): + # We pass the dynamic target_size here return SorterEngine.compress_for_web(path, quality, target_size) +# --- UPDATED GALLERY FRAGMENT --- @st.fragment def render_gallery_grid(current_batch, quality, grid_cols): staged = SorterEngine.get_staged_data() @@ -128,26 +128,26 @@ def render_gallery_grid(current_batch, quality, grid_cols): selected_cat = st.session_state.get("t5_active_cat", "Default") tagging_disabled = selected_cat.startswith("---") - # 1. CALCULATE OPTIMAL SIZE - # If 8 columns, we need small images (200px). If 2 cols, big images (800px). - # This keeps it crisp but fast. - target_size = int(1600 / grid_cols) + # 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. + target_size = int(2400 / grid_cols) - # 2. PARALLEL LOAD WITH CACHING - # We use a ThreadPool to fill the cache quickly + # 2. PARALLEL LOAD import concurrent.futures - batch_cache = {} def fetch_one(p): try: - # We pass mtime to ensure cache freshness mtime = os.path.getmtime(p) return p, get_cached_thumbnail(p, quality, target_size, mtime) except: return p, None - with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + # 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() @@ -162,22 +162,23 @@ def render_gallery_grid(current_batch, quality, grid_cols): is_processed = img_path in history with st.container(border=True): - # ... (Header/Delete code same as before) ... + # Header 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,)) + # Status if is_staged: st.success(f"🏷️ {staged[img_path]['cat']}") elif is_processed: st.info(f"✅ {history[img_path]['action']}") - # DISPLAY FROM CACHE + # Image img_data = batch_cache.get(img_path) if img_data: st.image(img_data, use_container_width=True) - # ... (Buttons code same as before) ... + # Buttons 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)) -- 2.49.1 From 7eb71cab56960ea82c11ded647d332f746c8a2cf Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Mon, 19 Jan 2026 14:32:56 +0100 Subject: [PATCH 5/9] Update tab_gallery_sorter.py --- tab_gallery_sorter.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/tab_gallery_sorter.py b/tab_gallery_sorter.py index 97f0b20..04f481a 100644 --- a/tab_gallery_sorter.py +++ b/tab_gallery_sorter.py @@ -112,6 +112,22 @@ def render_sidebar_content(): else: st.info("Select a valid category to edit.") +@st.dialog("🔍 High-Res Inspection", width="large") +def view_high_res(img_path): + """ + Opens a modal and loads the ORIGINAL size image on demand. + We still compress to WebP (q=90) to ensure it sends fast, + but we do NOT resize the dimensions. + """ + # Load with target_size=None to keep original dimensions + # Quality=90 for high fidelity + img_data = SorterEngine.compress_for_web(img_path, quality=90, target_size=None) + + if img_data: + st.image(img_data, use_container_width=True) + st.caption(f"Filename: {os.path.basename(img_path)}") + else: + st.error("Could not load full resolution image.") # ... (Gallery Grid code remains exactly the same) ... # --- UPDATED CACHE FUNCTION --- @@ -162,18 +178,26 @@ def render_gallery_grid(current_batch, quality, grid_cols): is_processed = img_path in history with st.container(border=True): - # Header - 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,)) + # HEADER LAYOUT: [Name (4)] [Zoom (1)] [Delete (1)] + 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"): + view_high_res(img_path) + + # DELETE BUTTON + c_del.button("❌", key=f"del_{unique_key}", on_click=cb_delete_image, args=(img_path,)) - # Status + # STATUS BANNERS... if is_staged: st.success(f"🏷️ {staged[img_path]['cat']}") elif is_processed: st.info(f"✅ {history[img_path]['action']}") - # Image + # THUMBNAIL IMAGE (Cached, Low Res) img_data = batch_cache.get(img_path) if img_data: st.image(img_data, use_container_width=True) -- 2.49.1 From ff27a3bc8379461a171cd72ccd3983f7dd509f63 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Mon, 19 Jan 2026 14:50:00 +0100 Subject: [PATCH 6/9] Update engine.py --- engine.py | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/engine.py b/engine.py index 1a7dd40..3fd8a93 100644 --- a/engine.py +++ b/engine.py @@ -254,7 +254,7 @@ class SorterEngine: @staticmethod def commit_global(output_root, cleanup_mode, operation="Move", source_root=None): - """Commits ALL staged files (Global Apply).""" + """Commits ALL staged files and fixes permissions.""" data = SorterEngine.get_staged_data() conn = sqlite3.connect(SorterEngine.DB_PATH) cursor = conn.cursor() @@ -278,20 +278,26 @@ class SorterEngine: else: shutil.move(old_p, final_dst) + # --- FIX PERMISSIONS --- + SorterEngine.fix_permissions(final_dst) + # Log History cursor.execute("INSERT OR REPLACE INTO processed_log VALUES (?, ?, ?)", (old_p, info['cat'], operation)) - # 2. Global Cleanup (Optional) - # Only run cleanup if explicitly asked, as global cleanup is risky + # 2. Global Cleanup if cleanup_mode != "Keep" and source_root: all_imgs = SorterEngine.get_images(source_root, recursive=True) for img_p in all_imgs: - if img_p not in data: # Not currently staged + if img_p not in data: if cleanup_mode == "Move to Unused": unused_dir = os.path.join(source_root, "unused") os.makedirs(unused_dir, exist_ok=True) - shutil.move(img_p, os.path.join(unused_dir, os.path.basename(img_p))) + dest_unused = os.path.join(unused_dir, os.path.basename(img_p)) + + shutil.move(img_p, dest_unused) + SorterEngine.fix_permissions(dest_unused) + elif cleanup_mode == "Delete": os.remove(img_p) @@ -398,9 +404,18 @@ class SorterEngine: conn.close() return {r[0]: {"cat": r[1], "action": r[2]} for r in rows} + @staticmethod + def fix_permissions(path): + """Forces file to be fully accessible (rwxrwxrwx).""" + try: + # 0o777 gives Read, Write, and Execute access to Owner, Group, and Others. + os.chmod(path, 0o777) + except Exception: + pass # Ignore errors if OS doesn't support chmod (e.g. some Windows setups) + @staticmethod def commit_batch(file_list, output_root, cleanup_mode, operation="Move"): - """Commits specified files and LOGS them to history.""" + """Commits files and fixes permissions.""" data = SorterEngine.get_staged_data() conn = sqlite3.connect(SorterEngine.DB_PATH) cursor = conn.cursor() @@ -423,13 +438,16 @@ class SorterEngine: final_dst = os.path.join(output_root, f"{root}_{c}{ext}") c += 1 - # Action + # Perform Action if operation == "Copy": shutil.copy2(file_path, final_dst) else: shutil.move(file_path, final_dst) - # Update DB: Remove from Staging, Add to History + # --- FIX PERMISSIONS --- + SorterEngine.fix_permissions(final_dst) + + # Update DB cursor.execute("DELETE FROM staging_area WHERE original_path = ?", (file_path,)) cursor.execute("INSERT OR REPLACE INTO processed_log VALUES (?, ?, ?)", (file_path, info['cat'], operation)) @@ -439,7 +457,11 @@ class SorterEngine: if cleanup_mode == "Move to Unused": unused_dir = os.path.join(os.path.dirname(file_path), "unused") os.makedirs(unused_dir, exist_ok=True) - shutil.move(file_path, os.path.join(unused_dir, os.path.basename(file_path))) + dest_unused = os.path.join(unused_dir, os.path.basename(file_path)) + + shutil.move(file_path, dest_unused) + SorterEngine.fix_permissions(dest_unused) # Fix here too + elif cleanup_mode == "Delete": os.remove(file_path) -- 2.49.1 From 4636a79ada69dc47a6f0ad9f0bfb8f1b6cb83380 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Mon, 19 Jan 2026 14:58:52 +0100 Subject: [PATCH 7/9] Update tab_gallery_sorter.py --- tab_gallery_sorter.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tab_gallery_sorter.py b/tab_gallery_sorter.py index 04f481a..d903f9d 100644 --- a/tab_gallery_sorter.py +++ b/tab_gallery_sorter.py @@ -216,24 +216,31 @@ def render_gallery_grid(current_batch, quality, grid_cols): def render_batch_actions(current_batch, path_o, page_num, path_s): st.write(f"### 🚀 Processing Actions") st.caption("Settings apply to both Page and Global actions.") + c_set1, c_set2 = st.columns(2) - op_mode = c_set1.radio("Tagged Files:", ["Move", "Copy"], horizontal=True, key="t5_op_mode") + + # CHANGED: "Copy" is now first, making it the default + op_mode = c_set1.radio("Tagged Files:", ["Copy", "Move"], horizontal=True, key="t5_op_mode") + cleanup = c_set2.radio("Untagged Files:", ["Keep", "Move to Unused", "Delete"], horizontal=True, key="t5_cleanup_mode") + st.divider() + c_btn1, c_btn2 = st.columns(2) + # BUTTON 1: APPLY PAGE if c_btn1.button(f"APPLY PAGE {page_num}", type="secondary", use_container_width=True, on_click=cb_apply_batch, args=(current_batch, path_o, cleanup, op_mode)): st.toast(f"Page {page_num} Applied!") st.rerun() + # BUTTON 2: APPLY GLOBAL if c_btn2.button("APPLY ALL (GLOBAL)", type="primary", use_container_width=True, help="Process ALL tagged files across all pages.", on_click=cb_apply_global, args=(path_o, cleanup, op_mode, path_s)): st.toast("Global Apply Complete!") st.rerun() - # ========================================== # 4. MAIN RENDERER # ========================================== -- 2.49.1 From 9c86eb4b72943d911e11403520f323ff326c7825 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Mon, 19 Jan 2026 14:59:43 +0100 Subject: [PATCH 8/9] Update engine.py --- engine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine.py b/engine.py index 3fd8a93..72a10dc 100644 --- a/engine.py +++ b/engine.py @@ -253,7 +253,7 @@ class SorterEngine: return {r[0]: {"cat": r[1], "name": r[2], "marked": r[3]} for r in rows} @staticmethod - def commit_global(output_root, cleanup_mode, operation="Move", source_root=None): + def commit_global(output_root, cleanup_mode, operation="Copy", source_root=None): """Commits ALL staged files and fixes permissions.""" data = SorterEngine.get_staged_data() conn = sqlite3.connect(SorterEngine.DB_PATH) @@ -414,7 +414,7 @@ class SorterEngine: pass # Ignore errors if OS doesn't support chmod (e.g. some Windows setups) @staticmethod - def commit_batch(file_list, output_root, cleanup_mode, operation="Move"): + def commit_batch(file_list, output_root, cleanup_mode, operation="Copy"): """Commits files and fixes permissions.""" data = SorterEngine.get_staged_data() conn = sqlite3.connect(SorterEngine.DB_PATH) -- 2.49.1 From b909069174df3915fd5f139fff2b3e530357e8a6 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Mon, 19 Jan 2026 15:21:55 +0100 Subject: [PATCH 9/9] 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() -- 2.49.1