Update tab_gallery_sorter.py
This commit is contained in:
@@ -3,7 +3,26 @@ import os
|
|||||||
import math
|
import math
|
||||||
from engine import SorterEngine
|
from engine import SorterEngine
|
||||||
|
|
||||||
# --- CALLBACKS ---
|
# ==========================================
|
||||||
|
# 1. CACHED DATA LOADER (The Fix)
|
||||||
|
# ==========================================
|
||||||
|
@st.cache_data(show_spinner=False)
|
||||||
|
def get_cached_images(path, mutation_id):
|
||||||
|
"""
|
||||||
|
Scans the folder ONLY when 'path' or 'mutation_id' changes.
|
||||||
|
Navigating pages does NOT change these, so it remains instant.
|
||||||
|
"""
|
||||||
|
return SorterEngine.get_images(path, recursive=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 2. CALLBACKS (Updated with Refresh Logic)
|
||||||
|
# ==========================================
|
||||||
|
def trigger_refresh():
|
||||||
|
"""Increments the mutation counter to force a file re-scan."""
|
||||||
|
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):
|
||||||
if selected_cat.startswith("---") or selected_cat == "":
|
if selected_cat.startswith("---") or selected_cat == "":
|
||||||
st.toast("⚠️ Select a valid category first!", icon="🚫")
|
st.toast("⚠️ Select a valid category first!", icon="🚫")
|
||||||
@@ -13,30 +32,37 @@ def cb_tag_image(img_path, selected_cat):
|
|||||||
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)
|
||||||
|
# Note: Tagging does NOT need a file re-scan, just a grid refresh.
|
||||||
|
|
||||||
def cb_untag_image(img_path):
|
def cb_untag_image(img_path):
|
||||||
SorterEngine.clear_staged_item(img_path)
|
SorterEngine.clear_staged_item(img_path)
|
||||||
|
|
||||||
def cb_delete_image(img_path):
|
def cb_delete_image(img_path):
|
||||||
SorterEngine.delete_to_trash(img_path)
|
SorterEngine.delete_to_trash(img_path)
|
||||||
|
trigger_refresh() # Force re-scan so the image disappears from the list
|
||||||
|
|
||||||
def cb_apply_batch(current_batch, path_o, cleanup_mode, operation):
|
def cb_apply_batch(current_batch, path_o, cleanup_mode, operation):
|
||||||
"""Applies ONLY the current page."""
|
|
||||||
SorterEngine.commit_batch(current_batch, path_o, cleanup_mode, operation)
|
SorterEngine.commit_batch(current_batch, path_o, cleanup_mode, operation)
|
||||||
|
trigger_refresh() # Force re-scan to remove moved files
|
||||||
|
|
||||||
def cb_apply_global(path_o, cleanup_mode, operation, path_s):
|
def cb_apply_global(path_o, cleanup_mode, operation, path_s):
|
||||||
"""Applies EVERYTHING in the staging area."""
|
|
||||||
SorterEngine.commit_global(path_o, cleanup_mode, operation, source_root=path_s)
|
SorterEngine.commit_global(path_o, cleanup_mode, operation, source_root=path_s)
|
||||||
|
trigger_refresh() # Force re-scan
|
||||||
|
|
||||||
def cb_change_page(delta):
|
def cb_change_page(delta):
|
||||||
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
|
||||||
st.session_state.t5_page += delta
|
st.session_state.t5_page += delta
|
||||||
|
# No trigger_refresh() here -> This is why page turning is now instant!
|
||||||
|
|
||||||
def cb_jump_page(k):
|
def cb_jump_page(k):
|
||||||
val = st.session_state[k]
|
val = st.session_state[k]
|
||||||
st.session_state.t5_page = val - 1
|
st.session_state.t5_page = val - 1
|
||||||
|
|
||||||
# --- SIDEBAR FRAGMENT (Manager) ---
|
|
||||||
|
# ==========================================
|
||||||
|
# 3. FRAGMENTS (Sidebar, Grid, Batch)
|
||||||
|
# ==========================================
|
||||||
|
# ... (Sidebar code remains exactly the same) ...
|
||||||
@st.fragment
|
@st.fragment
|
||||||
def render_sidebar_content():
|
def render_sidebar_content():
|
||||||
st.divider()
|
st.divider()
|
||||||
@@ -86,13 +112,12 @@ def render_sidebar_content():
|
|||||||
else:
|
else:
|
||||||
st.info("Select a valid category to edit.")
|
st.info("Select a valid category to edit.")
|
||||||
|
|
||||||
# --- GALLERY GRID FRAGMENT ---
|
|
||||||
|
# ... (Gallery Grid code remains exactly the same) ...
|
||||||
@st.fragment
|
@st.fragment
|
||||||
def render_gallery_grid(current_batch, quality, grid_cols):
|
def render_gallery_grid(current_batch, quality, grid_cols):
|
||||||
staged = SorterEngine.get_staged_data()
|
staged = SorterEngine.get_staged_data()
|
||||||
# NEW: Fetch processed history
|
|
||||||
history = SorterEngine.get_processed_log()
|
history = SorterEngine.get_processed_log()
|
||||||
|
|
||||||
selected_cat = st.session_state.get("t5_active_cat", "Default")
|
selected_cat = st.session_state.get("t5_active_cat", "Default")
|
||||||
tagging_disabled = selected_cat.startswith("---")
|
tagging_disabled = selected_cat.startswith("---")
|
||||||
|
|
||||||
@@ -108,11 +133,9 @@ def render_gallery_grid(current_batch, quality, grid_cols):
|
|||||||
c_head1.caption(os.path.basename(img_path)[:15])
|
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,))
|
c_head2.button("❌", key=f"del_{unique_key}", on_click=cb_delete_image, args=(img_path,))
|
||||||
|
|
||||||
# STATUS BANNERS
|
|
||||||
if is_staged:
|
if is_staged:
|
||||||
st.success(f"🏷️ {staged[img_path]['cat']}")
|
st.success(f"🏷️ {staged[img_path]['cat']}")
|
||||||
elif is_processed:
|
elif is_processed:
|
||||||
# NEW: Show history status
|
|
||||||
st.info(f"✅ {history[img_path]['action']} -> {history[img_path]['cat']}")
|
st.info(f"✅ {history[img_path]['action']} -> {history[img_path]['cat']}")
|
||||||
|
|
||||||
img_data = SorterEngine.compress_for_web(img_path, quality)
|
img_data = SorterEngine.compress_for_web(img_path, quality)
|
||||||
@@ -125,36 +148,38 @@ def render_gallery_grid(current_batch, quality, grid_cols):
|
|||||||
st.button("Untag", key=f"untag_{unique_key}", use_container_width=True,
|
st.button("Untag", key=f"untag_{unique_key}", use_container_width=True,
|
||||||
on_click=cb_untag_image, args=(img_path,))
|
on_click=cb_untag_image, args=(img_path,))
|
||||||
|
|
||||||
# --- BATCH ACTIONS FRAGMENT ---
|
|
||||||
|
# ... (Batch Actions code remains exactly the same) ...
|
||||||
@st.fragment
|
@st.fragment
|
||||||
def render_batch_actions(current_batch, path_o, page_num, path_s):
|
def render_batch_actions(current_batch, path_o, page_num, path_s):
|
||||||
st.write(f"### 🚀 Processing Actions")
|
st.write(f"### 🚀 Processing Actions")
|
||||||
st.caption("Settings apply to both Page and Global actions.")
|
st.caption("Settings apply to both Page and Global actions.")
|
||||||
|
|
||||||
c_set1, c_set2 = st.columns(2)
|
c_set1, c_set2 = st.columns(2)
|
||||||
op_mode = c_set1.radio("Tagged Files:", ["Move", "Copy"], horizontal=True, key="t5_op_mode")
|
op_mode = c_set1.radio("Tagged Files:", ["Move", "Copy"], horizontal=True, key="t5_op_mode")
|
||||||
cleanup = c_set2.radio("Untagged Files:", ["Keep", "Move to Unused", "Delete"], horizontal=True, key="t5_cleanup_mode")
|
cleanup = c_set2.radio("Untagged Files:", ["Keep", "Move to Unused", "Delete"], horizontal=True, key="t5_cleanup_mode")
|
||||||
|
|
||||||
st.divider()
|
st.divider()
|
||||||
|
|
||||||
c_btn1, c_btn2 = st.columns(2)
|
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,
|
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)):
|
on_click=cb_apply_batch, args=(current_batch, path_o, cleanup, op_mode)):
|
||||||
st.toast(f"Page {page_num} Applied!")
|
st.toast(f"Page {page_num} Applied!")
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
# BUTTON 2: APPLY GLOBAL
|
|
||||||
if c_btn2.button("APPLY ALL (GLOBAL)", type="primary", use_container_width=True,
|
if c_btn2.button("APPLY ALL (GLOBAL)", type="primary", use_container_width=True,
|
||||||
help="Process ALL tagged files across all pages.",
|
help="Process ALL tagged files across all pages.",
|
||||||
on_click=cb_apply_global, args=(path_o, cleanup, op_mode, path_s)):
|
on_click=cb_apply_global, args=(path_o, cleanup, op_mode, path_s)):
|
||||||
st.toast("Global Apply Complete!")
|
st.toast("Global Apply Complete!")
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
# --- MAIN RENDERER ---
|
|
||||||
|
# ==========================================
|
||||||
|
# 4. MAIN RENDERER
|
||||||
|
# ==========================================
|
||||||
def render(quality, profile_name):
|
def render(quality, profile_name):
|
||||||
st.subheader("🖼️ Gallery Staging Sorter")
|
st.subheader("🖼️ Gallery Staging Sorter")
|
||||||
|
|
||||||
|
# Init Mutation ID (This triggers the scanner cache refresh)
|
||||||
|
if 't5_file_id' not in st.session_state: st.session_state.t5_file_id = 0
|
||||||
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
|
||||||
|
|
||||||
profiles = SorterEngine.load_profiles()
|
profiles = SorterEngine.load_profiles()
|
||||||
@@ -166,6 +191,8 @@ def render(quality, profile_name):
|
|||||||
if path_s != p_data.get("tab5_source") or path_o != p_data.get("tab5_out"):
|
if path_s != p_data.get("tab5_source") or path_o != p_data.get("tab5_out"):
|
||||||
if st.button("💾 Save Settings"):
|
if st.button("💾 Save Settings"):
|
||||||
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)
|
||||||
|
# Saving settings might mean new folder, so we trigger refresh
|
||||||
|
trigger_refresh()
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
if not os.path.exists(path_s): return
|
if not os.path.exists(path_s): return
|
||||||
@@ -178,7 +205,10 @@ def render(quality, profile_name):
|
|||||||
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)
|
||||||
|
|
||||||
all_images = SorterEngine.get_images(path_s, recursive=True)
|
# --- USING CACHED LOADER ---
|
||||||
|
# We pass the mutation ID. If ID is same as last run, scan is SKIPPED.
|
||||||
|
all_images = get_cached_images(path_s, st.session_state.t5_file_id)
|
||||||
|
|
||||||
if not all_images:
|
if not all_images:
|
||||||
st.info("No images found.")
|
st.info("No images found.")
|
||||||
return
|
return
|
||||||
@@ -206,5 +236,4 @@ def render(quality, profile_name):
|
|||||||
nav_controls("bottom")
|
nav_controls("bottom")
|
||||||
st.divider()
|
st.divider()
|
||||||
|
|
||||||
# Pass 'path_s' so global cleanup works if enabled
|
|
||||||
render_batch_actions(current_batch, path_o, st.session_state.t5_page + 1, path_s)
|
render_batch_actions(current_batch, path_o, st.session_state.t5_page + 1, path_s)
|
||||||
Reference in New Issue
Block a user