Update gallery_app.py
This commit is contained in:
129
gallery_app.py
129
gallery_app.py
@@ -71,6 +71,9 @@ class AppState:
|
||||
self.pair_adj_output = "/storage" # Output folder for adjacent images
|
||||
self.pair_index = 1 # Shared index for both sides
|
||||
|
||||
# Pairing mode index maps (index -> (main_path, adj_path))
|
||||
self.pair_index_map: Dict[int, Dict] = {} # {idx: {"main": path, "adj": path}}
|
||||
|
||||
def load_active_profile(self):
|
||||
"""Load paths from active profile."""
|
||||
p_data = self.profiles.get(self.profile_name, {})
|
||||
@@ -234,12 +237,20 @@ def get_file_timestamp(filepath: str) -> Optional[float]:
|
||||
return None
|
||||
|
||||
def load_adjacent_folder():
|
||||
"""Load images from adjacent folder for pairing."""
|
||||
"""Load images from adjacent folder for pairing, excluding main folder."""
|
||||
if not state.pair_adjacent_folder or not os.path.exists(state.pair_adjacent_folder):
|
||||
state.pair_adjacent_images = []
|
||||
ui.notify("Adjacent folder path is empty or doesn't exist", type='warning')
|
||||
return
|
||||
|
||||
state.pair_adjacent_images = SorterEngine.get_images(state.pair_adjacent_folder, recursive=True)
|
||||
# Exclude the main source folder to avoid duplicates
|
||||
exclude = [state.source_dir] if state.source_dir else []
|
||||
|
||||
state.pair_adjacent_images = SorterEngine.get_images(
|
||||
state.pair_adjacent_folder,
|
||||
recursive=True,
|
||||
exclude_paths=exclude
|
||||
)
|
||||
ui.notify(f"Loaded {len(state.pair_adjacent_images)} images from adjacent folder", type='info')
|
||||
|
||||
def find_time_matches(source_image: str) -> List[str]:
|
||||
@@ -459,7 +470,7 @@ def refresh_staged_info():
|
||||
if img_path in staged_keys:
|
||||
state.green_dots.add(idx // state.page_size)
|
||||
|
||||
# Build index map for active category
|
||||
# Build index map for active category (gallery mode)
|
||||
state.index_map.clear()
|
||||
|
||||
# Add staged images
|
||||
@@ -478,6 +489,23 @@ def refresh_staged_info():
|
||||
if idx is not None and idx not in state.index_map:
|
||||
state.index_map[idx] = os.path.join(cat_path, filename)
|
||||
|
||||
# Build pairing mode index map (both categories)
|
||||
state.pair_index_map.clear()
|
||||
|
||||
for orig_path, info in state.staged_data.items():
|
||||
idx = _extract_index(info['name'])
|
||||
if idx is None:
|
||||
continue
|
||||
|
||||
if idx not in state.pair_index_map:
|
||||
state.pair_index_map[idx] = {"main": None, "adj": None}
|
||||
|
||||
# Check if this is from main or adjacent category
|
||||
if info['cat'] == state.pair_main_category:
|
||||
state.pair_index_map[idx]["main"] = orig_path
|
||||
elif info['cat'] == state.pair_adj_category:
|
||||
state.pair_index_map[idx]["adj"] = orig_path
|
||||
|
||||
def _extract_index(filename: str) -> Optional[int]:
|
||||
"""Extract numeric index from filename (e.g., 'Cat_042.jpg' -> 42)."""
|
||||
try:
|
||||
@@ -663,6 +691,57 @@ def open_zoom_dialog(path: str, title: Optional[str] = None, show_untag: bool =
|
||||
ui.image(f"/full_res?path={path}").classes('w-full h-auto object-contain max-h-[85vh]')
|
||||
dialog.open()
|
||||
|
||||
def open_pair_preview_dialog(index: int, pair_info: Dict):
|
||||
"""Open dialog showing both main and adjacent images for a paired index."""
|
||||
main_path = pair_info.get("main")
|
||||
adj_path = pair_info.get("adj")
|
||||
|
||||
with ui.dialog() as dialog, ui.card().classes('w-full max-w-screen-xl p-4 bg-gray-900'):
|
||||
with ui.row().classes('w-full justify-between items-center mb-4'):
|
||||
ui.label(f"Pair #{index}").classes('text-xl font-bold text-white')
|
||||
ui.button(icon='close', on_click=dialog.close).props('flat round dense color=white')
|
||||
|
||||
with ui.row().classes('w-full gap-4'):
|
||||
# Main image
|
||||
with ui.card().classes('flex-1 p-4 bg-gray-800'):
|
||||
ui.label(f"📁 {state.pair_main_category}").classes('text-lg font-bold text-blue-400 mb-2')
|
||||
if main_path:
|
||||
ui.label(os.path.basename(main_path)).classes('text-xs text-gray-400 truncate mb-2')
|
||||
ui.image(f"/thumbnail?path={main_path}&size=600&q=80") \
|
||||
.classes('w-full h-64 bg-black rounded') \
|
||||
.props('fit=contain')
|
||||
|
||||
def untag_main():
|
||||
action_untag(main_path)
|
||||
dialog.close()
|
||||
render_sidebar()
|
||||
|
||||
ui.button("Untag", icon='label_off', on_click=untag_main) \
|
||||
.props('flat color=red').classes('mt-2')
|
||||
else:
|
||||
ui.label("No image").classes('text-gray-500 text-center py-20')
|
||||
|
||||
# Adjacent image
|
||||
with ui.card().classes('flex-1 p-4 bg-gray-800'):
|
||||
ui.label(f"📂 {state.pair_adj_category}").classes('text-lg font-bold text-orange-400 mb-2')
|
||||
if adj_path:
|
||||
ui.label(os.path.basename(adj_path)).classes('text-xs text-gray-400 truncate mb-2')
|
||||
ui.image(f"/thumbnail?path={adj_path}&size=600&q=80") \
|
||||
.classes('w-full h-64 bg-black rounded') \
|
||||
.props('fit=contain')
|
||||
|
||||
def untag_adj():
|
||||
action_untag(adj_path)
|
||||
dialog.close()
|
||||
render_sidebar()
|
||||
|
||||
ui.button("Untag", icon='label_off', on_click=untag_adj) \
|
||||
.props('flat color=red').classes('mt-2')
|
||||
else:
|
||||
ui.label("No image").classes('text-gray-500 text-center py-20')
|
||||
|
||||
dialog.open()
|
||||
|
||||
def open_hotkey_dialog(category: str):
|
||||
"""Open dialog to set/change hotkey for a category."""
|
||||
# Find current hotkey if any
|
||||
@@ -730,7 +809,49 @@ def render_sidebar():
|
||||
with state.sidebar_container:
|
||||
ui.label("🏷️ Category Manager").classes('text-xl font-bold mb-2 text-white')
|
||||
|
||||
# Number grid (1-25)
|
||||
# Number grid (1-25) - different view for pairing mode
|
||||
if state.current_mode == "pairing":
|
||||
# Pairing mode: show both main and adjacent in grid
|
||||
ui.label(f"Index Grid ({state.pair_main_category} + {state.pair_adj_category})").classes('text-xs text-gray-400 mb-1')
|
||||
with ui.grid(columns=5).classes('gap-1 mb-4 w-full'):
|
||||
for i in range(1, 26):
|
||||
pair_info = state.pair_index_map.get(i, {})
|
||||
has_main = pair_info.get("main") is not None
|
||||
has_adj = pair_info.get("adj") is not None
|
||||
|
||||
# Color coding: green=both, blue=main only, orange=adj only, grey=none
|
||||
if has_main and has_adj:
|
||||
color = 'green'
|
||||
elif has_main:
|
||||
color = 'blue'
|
||||
elif has_adj:
|
||||
color = 'orange'
|
||||
else:
|
||||
color = 'grey-9'
|
||||
|
||||
def make_pair_click_handler(num: int):
|
||||
def handler():
|
||||
pair_info = state.pair_index_map.get(num, {})
|
||||
if pair_info.get("main") or pair_info.get("adj"):
|
||||
# Show dialog with both images
|
||||
open_pair_preview_dialog(num, pair_info)
|
||||
else:
|
||||
# Number is free - set as next index
|
||||
state.pair_index = num
|
||||
render_sidebar()
|
||||
return handler
|
||||
|
||||
ui.button(str(i), on_click=make_pair_click_handler(i)) \
|
||||
.props(f'color={color} size=sm flat') \
|
||||
.classes('w-full border border-gray-800')
|
||||
|
||||
# Legend
|
||||
with ui.row().classes('w-full gap-2 text-xs mb-4'):
|
||||
ui.label("🟢 Both").classes('text-green-400')
|
||||
ui.label("🔵 Main").classes('text-blue-400')
|
||||
ui.label("🟠 Adj").classes('text-orange-400')
|
||||
else:
|
||||
# Gallery mode: show single category
|
||||
with ui.grid(columns=5).classes('gap-1 mb-4 w-full'):
|
||||
for i in range(1, 26):
|
||||
is_used = i in state.index_map
|
||||
|
||||
Reference in New Issue
Block a user