Compare commits
59 Commits
af2c148747
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 216ca1e460 | |||
| 0db2ee4587 | |||
| d43813cc2a | |||
| 97424ea0af | |||
| eafc5de6f2 | |||
| fa710e914e | |||
| e3e337af88 | |||
| 15ca74ad4b | |||
| a11d76fd5f | |||
| cf1238bbff | |||
| d3b7f31730 | |||
| 52c06c4db7 | |||
| 3a320f3187 | |||
| c37e2bd5e0 | |||
| 9418661be9 | |||
| 7349015177 | |||
| 918a6e9414 | |||
| 5909c0ec99 | |||
| 286b0410ff | |||
| 0c18f570d4 | |||
| 3f2160405a | |||
| f3f57f7c53 | |||
| 957aab0656 | |||
| 0a94548f5e | |||
| 124fbacd2a | |||
| 0f0aeed2f1 | |||
| fe6e55de16 | |||
| dd454ebf6f | |||
| 2854907359 | |||
| 48417b6d73 | |||
| ce7abd8a29 | |||
| df12413c5d | |||
| c56b07f999 | |||
| c89cecd43f | |||
| 37f6166b37 | |||
| dc31b0bebb | |||
| f0b0114fc5 | |||
| 0c9446b3f8 | |||
| 4c49635018 | |||
| 826ae384df | |||
| 54ba10d4e5 | |||
| 0e6de4ae0b | |||
| b919c52255 | |||
| 8fc8372a9b | |||
| 246b78719e | |||
| 0d5f393aff | |||
| 4fb038eda1 | |||
| 690aaafacf | |||
| 3e9ff43bc9 | |||
| 91a0cc5138 | |||
| 588822f856 | |||
| 1cbad1a3ed | |||
| b5794e9db5 | |||
| b938dc68fa | |||
| dde0e90442 | |||
| 0b5e9377e4 | |||
| 091936069a | |||
| 0d1eca4ef3 | |||
| 39153d3493 |
219
engine.py
219
engine.py
@@ -27,10 +27,19 @@ class SorterEngine:
|
||||
cursor.execute('''CREATE TABLE IF NOT EXISTS processed_log
|
||||
(source_path TEXT PRIMARY KEY, category TEXT, action_type TEXT)''')
|
||||
|
||||
# Seed categories if empty
|
||||
# --- NEW: FOLDER TAGS TABLE (persists tags by folder) ---
|
||||
cursor.execute('''CREATE TABLE IF NOT EXISTS folder_tags
|
||||
(folder_path TEXT, filename TEXT, category TEXT, tag_index INTEGER,
|
||||
PRIMARY KEY (folder_path, filename))''')
|
||||
|
||||
# --- NEW: PROFILE CATEGORIES TABLE (each profile has its own categories) ---
|
||||
cursor.execute('''CREATE TABLE IF NOT EXISTS profile_categories
|
||||
(profile TEXT, category TEXT, PRIMARY KEY (profile, category))''')
|
||||
|
||||
# Seed categories if empty (legacy table)
|
||||
cursor.execute("SELECT COUNT(*) FROM categories")
|
||||
if cursor.fetchone()[0] == 0:
|
||||
for cat in ["_TRASH", "Default", "Action", "Solo"]:
|
||||
for cat in ["_TRASH", "control", "Default", "Action", "Solo"]:
|
||||
cursor.execute("INSERT OR IGNORE INTO categories VALUES (?)", (cat,))
|
||||
|
||||
conn.commit()
|
||||
@@ -103,21 +112,45 @@ class SorterEngine:
|
||||
"tab5_source": r[7], "tab5_out": r[8]
|
||||
} for r in rows}
|
||||
|
||||
# --- 3. CATEGORY MANAGEMENT (Sorted A-Z) ---
|
||||
# --- 3. CATEGORY MANAGEMENT (Profile-based) ---
|
||||
@staticmethod
|
||||
def get_categories():
|
||||
def get_categories(profile=None):
|
||||
conn = sqlite3.connect(SorterEngine.DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT name FROM categories ORDER BY name COLLATE NOCASE ASC")
|
||||
cats = [r[0] for r in cursor.fetchall()]
|
||||
|
||||
# Ensure table exists
|
||||
cursor.execute('''CREATE TABLE IF NOT EXISTS profile_categories
|
||||
(profile TEXT, category TEXT, PRIMARY KEY (profile, category))''')
|
||||
|
||||
if profile:
|
||||
cursor.execute("SELECT category FROM profile_categories WHERE profile = ? ORDER BY category COLLATE NOCASE ASC", (profile,))
|
||||
cats = [r[0] for r in cursor.fetchall()]
|
||||
# If no categories for this profile, seed with defaults
|
||||
if not cats:
|
||||
for cat in ["_TRASH", "control"]:
|
||||
cursor.execute("INSERT OR IGNORE INTO profile_categories VALUES (?, ?)", (profile, cat))
|
||||
conn.commit()
|
||||
cats = ["_TRASH", "control"]
|
||||
else:
|
||||
# Fallback to legacy table
|
||||
cursor.execute("SELECT name FROM categories ORDER BY name COLLATE NOCASE ASC")
|
||||
cats = [r[0] for r in cursor.fetchall()]
|
||||
|
||||
conn.close()
|
||||
return cats
|
||||
|
||||
@staticmethod
|
||||
def add_category(name):
|
||||
def add_category(name, profile=None):
|
||||
conn = sqlite3.connect(SorterEngine.DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("INSERT OR IGNORE INTO categories VALUES (?)", (name,))
|
||||
|
||||
if profile:
|
||||
cursor.execute('''CREATE TABLE IF NOT EXISTS profile_categories
|
||||
(profile TEXT, category TEXT, PRIMARY KEY (profile, category))''')
|
||||
cursor.execute("INSERT OR IGNORE INTO profile_categories VALUES (?, ?)", (profile, name))
|
||||
else:
|
||||
cursor.execute("INSERT OR IGNORE INTO categories VALUES (?)", (name,))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -241,6 +274,15 @@ class SorterEngine:
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@staticmethod
|
||||
def clear_staging_area():
|
||||
"""Clears all items from the staging area."""
|
||||
conn = sqlite3.connect(SorterEngine.DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM staging_area")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@staticmethod
|
||||
def get_staged_data():
|
||||
"""Retrieves current tagged/staged images."""
|
||||
@@ -253,9 +295,14 @@ 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="Copy", source_root=None):
|
||||
def commit_global(output_root, cleanup_mode, operation="Copy", source_root=None, profile=None):
|
||||
"""Commits ALL staged files and fixes permissions."""
|
||||
data = SorterEngine.get_staged_data()
|
||||
|
||||
# Save folder tags BEFORE processing (so we can restore them later)
|
||||
if source_root:
|
||||
SorterEngine.save_folder_tags(source_root, profile)
|
||||
|
||||
conn = sqlite3.connect(SorterEngine.DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
@@ -494,11 +541,16 @@ class SorterEngine:
|
||||
conn.close()
|
||||
|
||||
@staticmethod
|
||||
def delete_category(name):
|
||||
def delete_category(name, profile=None):
|
||||
"""Deletes a category and clears any staged tags associated with it."""
|
||||
conn = sqlite3.connect(SorterEngine.DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM categories WHERE name = ?", (name,))
|
||||
|
||||
if profile:
|
||||
cursor.execute("DELETE FROM profile_categories WHERE profile = ? AND category = ?", (profile, name))
|
||||
else:
|
||||
cursor.execute("DELETE FROM categories WHERE name = ?", (name,))
|
||||
|
||||
cursor.execute("DELETE FROM staging_area WHERE target_category = ?", (name,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -513,4 +565,147 @@ class SorterEngine:
|
||||
for idx, img_path in enumerate(all_images):
|
||||
if img_path in staged_keys:
|
||||
tagged_pages.add(idx // page_size)
|
||||
return tagged_pages
|
||||
return tagged_pages
|
||||
|
||||
# --- 7. FOLDER TAG PERSISTENCE ---
|
||||
@staticmethod
|
||||
def save_folder_tags(folder_path, profile=None):
|
||||
"""
|
||||
Saves current staging data associated with a folder for later restoration.
|
||||
Call this BEFORE clearing the staging area.
|
||||
"""
|
||||
import re
|
||||
staged = SorterEngine.get_staged_data()
|
||||
if not staged:
|
||||
return 0
|
||||
|
||||
conn = sqlite3.connect(SorterEngine.DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Ensure table exists with profile column
|
||||
cursor.execute('''CREATE TABLE IF NOT EXISTS folder_tags
|
||||
(profile TEXT, folder_path TEXT, filename TEXT, category TEXT, tag_index INTEGER,
|
||||
PRIMARY KEY (profile, folder_path, filename))''')
|
||||
|
||||
# Check if old schema (without profile) - migrate if needed
|
||||
cursor.execute("PRAGMA table_info(folder_tags)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
if 'profile' not in columns:
|
||||
cursor.execute("DROP TABLE folder_tags")
|
||||
cursor.execute('''CREATE TABLE folder_tags
|
||||
(profile TEXT, folder_path TEXT, filename TEXT, category TEXT, tag_index INTEGER,
|
||||
PRIMARY KEY (profile, folder_path, filename))''')
|
||||
conn.commit()
|
||||
|
||||
profile = profile or "Default"
|
||||
saved_count = 0
|
||||
for orig_path, info in staged.items():
|
||||
# Only save tags for files that are in this folder (or subfolders)
|
||||
if orig_path.startswith(folder_path):
|
||||
filename = os.path.basename(orig_path)
|
||||
category = info['cat']
|
||||
|
||||
# Extract index from the new_name (e.g., "Action_042.jpg" -> 42)
|
||||
new_name = info['name']
|
||||
match = re.search(r'_(\d+)', new_name)
|
||||
tag_index = int(match.group(1)) if match else 0
|
||||
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO folder_tags VALUES (?, ?, ?, ?, ?)",
|
||||
(profile, folder_path, filename, category, tag_index)
|
||||
)
|
||||
saved_count += 1
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return saved_count
|
||||
|
||||
@staticmethod
|
||||
def restore_folder_tags(folder_path, all_images, profile=None):
|
||||
"""
|
||||
Restores previously saved tags for a folder back into the staging area.
|
||||
Call this when loading/reloading a folder.
|
||||
Returns the number of tags restored.
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(SorterEngine.DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Ensure table exists with profile column
|
||||
cursor.execute('''CREATE TABLE IF NOT EXISTS folder_tags
|
||||
(profile TEXT, folder_path TEXT, filename TEXT, category TEXT, tag_index INTEGER,
|
||||
PRIMARY KEY (profile, folder_path, filename))''')
|
||||
|
||||
# Check if old schema (without profile) - migrate if needed
|
||||
cursor.execute("PRAGMA table_info(folder_tags)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
if 'profile' not in columns:
|
||||
cursor.execute("DROP TABLE folder_tags")
|
||||
cursor.execute('''CREATE TABLE folder_tags
|
||||
(profile TEXT, folder_path TEXT, filename TEXT, category TEXT, tag_index INTEGER,
|
||||
PRIMARY KEY (profile, folder_path, filename))''')
|
||||
conn.commit()
|
||||
|
||||
profile = profile or "Default"
|
||||
|
||||
# Get saved tags for this folder and profile
|
||||
cursor.execute(
|
||||
"SELECT filename, category, tag_index FROM folder_tags WHERE profile = ? AND folder_path = ?",
|
||||
(profile, folder_path)
|
||||
)
|
||||
saved_tags = {row[0]: {"cat": row[1], "index": row[2]} for row in cursor.fetchall()}
|
||||
|
||||
if not saved_tags:
|
||||
conn.close()
|
||||
return 0
|
||||
|
||||
# Build a map of filename -> full path from current images
|
||||
filename_to_path = {}
|
||||
for img_path in all_images:
|
||||
fname = os.path.basename(img_path)
|
||||
if fname not in filename_to_path:
|
||||
filename_to_path[fname] = img_path
|
||||
|
||||
# Restore tags to staging area
|
||||
restored = 0
|
||||
for filename, tag_info in saved_tags.items():
|
||||
if filename in filename_to_path:
|
||||
full_path = filename_to_path[filename]
|
||||
cursor.execute("SELECT 1 FROM staging_area WHERE original_path = ?", (full_path,))
|
||||
if not cursor.fetchone():
|
||||
ext = os.path.splitext(filename)[1]
|
||||
new_name = f"{tag_info['cat']}_{tag_info['index']:03d}{ext}"
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO staging_area VALUES (?, ?, ?, 1)",
|
||||
(full_path, tag_info['cat'], new_name)
|
||||
)
|
||||
restored += 1
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return restored
|
||||
except Exception as e:
|
||||
print(f"Error restoring folder tags: {e}")
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def clear_folder_tags(folder_path):
|
||||
"""Clears saved tags for a specific folder."""
|
||||
conn = sqlite3.connect(SorterEngine.DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM folder_tags WHERE folder_path = ?", (folder_path,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@staticmethod
|
||||
def get_saved_folder_tags(folder_path):
|
||||
"""Returns saved tags for a folder (for debugging/display)."""
|
||||
conn = sqlite3.connect(SorterEngine.DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT filename, category, tag_index FROM folder_tags WHERE folder_path = ?",
|
||||
(folder_path,)
|
||||
)
|
||||
result = {row[0]: {"cat": row[1], "index": row[2]} for row in cursor.fetchall()}
|
||||
conn.close()
|
||||
return result
|
||||
1016
gallery_app.py
Normal file
1016
gallery_app.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,3 @@
|
||||
streamlit
|
||||
Pillow
|
||||
Pillow
|
||||
nicegui
|
||||
18
start.sh
Normal file
18
start.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 1. Navigate to app directory
|
||||
cd /app
|
||||
|
||||
# 2. Install dependencies (Including NiceGUI if missing)
|
||||
# This checks your requirements.txt AND ensures nicegui is present
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 3. Start NiceGUI in the Background (&)
|
||||
# This runs silently while the script continues
|
||||
echo "🚀 Starting NiceGUI on Port 8080..."
|
||||
python3 gallery_app.py &
|
||||
|
||||
# 4. Start Streamlit in the Foreground
|
||||
# This keeps the container running
|
||||
echo "🚀 Starting Streamlit on Port 8501..."
|
||||
streamlit run app.py --server.port=8501 --server.address=0.0.0.0
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user