diff --git a/db.py b/db.py index e9088f9..85e60da 100644 --- a/db.py +++ b/db.py @@ -98,6 +98,24 @@ class ProjectDB: ).fetchone() return dict(row) if row else None + def rename_project(self, old_name: str, new_name: str) -> bool: + now = time.time() + cur = self.conn.execute( + "UPDATE projects SET name = ?, updated_at = ? WHERE name = ?", + (new_name, now, old_name), + ) + self.conn.commit() + return cur.rowcount > 0 + + def update_project_path(self, name: str, folder_path: str) -> bool: + now = time.time() + cur = self.conn.execute( + "UPDATE projects SET folder_path = ?, updated_at = ? WHERE name = ?", + (folder_path, now, name), + ) + self.conn.commit() + return cur.rowcount > 0 + def delete_project(self, name: str) -> bool: cur = self.conn.execute("DELETE FROM projects WHERE name = ?", (name,)) self.conn.commit() diff --git a/main.py b/main.py index 072fe1e..611adb6 100644 --- a/main.py +++ b/main.py @@ -156,14 +156,19 @@ def index(): background: rgba(255,255,255,0.2); } - /* Sub-sequence accent (teal) */ - .body--dark .subsegment-card > .q-expansion-item__container > .q-item { - border-left: 6px solid #06B6D4; - padding-left: 10px; - } - .body--dark .subsegment-card .q-expansion-item__toggle-icon { - color: #06B6D4 !important; - } + /* Sub-sequence accent colors (per sub-index, cycling) */ + .body--dark .subsegment-color-0 > .q-expansion-item__container > .q-item { border-left: 6px solid #06B6D4; padding-left: 10px; } + .body--dark .subsegment-color-0 .q-expansion-item__toggle-icon { color: #06B6D4 !important; } + .body--dark .subsegment-color-1 > .q-expansion-item__container > .q-item { border-left: 6px solid #A78BFA; padding-left: 10px; } + .body--dark .subsegment-color-1 .q-expansion-item__toggle-icon { color: #A78BFA !important; } + .body--dark .subsegment-color-2 > .q-expansion-item__container > .q-item { border-left: 6px solid #34D399; padding-left: 10px; } + .body--dark .subsegment-color-2 .q-expansion-item__toggle-icon { color: #34D399 !important; } + .body--dark .subsegment-color-3 > .q-expansion-item__container > .q-item { border-left: 6px solid #F472B6; padding-left: 10px; } + .body--dark .subsegment-color-3 .q-expansion-item__toggle-icon { color: #F472B6 !important; } + .body--dark .subsegment-color-4 > .q-expansion-item__container > .q-item { border-left: 6px solid #FBBF24; padding-left: 10px; } + .body--dark .subsegment-color-4 .q-expansion-item__toggle-icon { color: #FBBF24 !important; } + .body--dark .subsegment-color-5 > .q-expansion-item__container > .q-item { border-left: 6px solid #FB923C; padding-left: 10px; } + .body--dark .subsegment-color-5 .q-expansion-item__toggle-icon { color: #FB923C !important; } /* Secondary pane teal accent */ .pane-secondary .q-field--outlined.q-field--focused .q-field__control:after { diff --git a/tab_batch_ng.py b/tab_batch_ng.py index 3324169..dd685f8 100644 --- a/tab_batch_ng.py +++ b/tab_batch_ng.py @@ -1,4 +1,5 @@ import copy +import json import random from pathlib import Path @@ -13,6 +14,7 @@ from history_tree import HistoryTree IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.webp', '.bmp', '.gif'} SUB_SEGMENT_MULTIPLIER = 1000 +SUB_SEGMENT_NUM_COLORS = 6 FRAME_TO_SKIP_DEFAULT = DEFAULTS['frame_to_skip'] VACE_MODES = [ @@ -242,7 +244,7 @@ def render_batch_processor(state: AppState): lora_keys = ['lora 1 high', 'lora 1 low', 'lora 2 high', 'lora 2 low', 'lora 3 high', 'lora 3 low'] standard_keys = { - 'general_prompt', 'general_negative', 'current_prompt', 'negative', 'prompt', + 'name', 'general_prompt', 'general_negative', 'current_prompt', 'negative', 'prompt', 'seed', 'cfg', 'camera', 'flf', KEY_SEQUENCE_NUMBER, 'frame_to_skip', 'end_frame', 'transition', 'vace_length', 'input_a_frames', 'input_b_frames', 'reference switch', 'vace schedule', @@ -321,16 +323,36 @@ def _render_sequence_card(i, seq, batch_list, data, file_path, state, refresh_list.refresh() seq_num = seq.get(KEY_SEQUENCE_NUMBER, i + 1) + seq_name = seq.get('name', '') if is_subsegment(seq_num): label = f'Sub #{parent_of(seq_num)}.{sub_index_of(seq_num)} ({int(seq_num)})' else: label = f'Sequence #{seq_num}' + if seq_name: + label += f' — {seq_name}' - exp_classes = 'w-full subsegment-card' if is_subsegment(seq_num) else 'w-full' - with ui.expansion(label, icon='movie').classes(exp_classes): + if is_subsegment(seq_num): + color_idx = (sub_index_of(seq_num) - 1) % SUB_SEGMENT_NUM_COLORS + exp_classes = f'w-full subsegment-color-{color_idx}' + else: + exp_classes = 'w-full' + with ui.expansion(label, icon='movie').classes(exp_classes) as expansion: # --- Action row --- with ui.row().classes('w-full q-gutter-sm action-row'): + # Rename + def rename(idx=i, s=seq, exp=expansion): + async def do_rename(): + result = await ui.run_javascript( + f'prompt("Rename sequence:", {json.dumps(s.get("name", ""))})', + timeout=30.0, + ) + if result is not None: + s['name'] = result + commit('Renamed!') + await do_rename() + + ui.button('Rename', icon='edit', on_click=rename).props('outline') # Copy from source def copy_source(idx=i, sn=seq_num): item = copy.deepcopy(DEFAULTS) diff --git a/tab_projects_ng.py b/tab_projects_ng.py index 32494ac..8ad15dd 100644 --- a/tab_projects_ng.py +++ b/tab_projects_ng.py @@ -1,3 +1,4 @@ +import json import logging from pathlib import Path @@ -109,6 +110,47 @@ def render_projects_tab(state: AppState): ui.button('Deactivate', icon='cancel', on_click=deactivate).props('flat dense') + def rename_proj(name=proj['name']): + async def do_rename(): + new_name = await ui.run_javascript( + f'prompt("Rename project:", {json.dumps(name)})', + timeout=30.0, + ) + if new_name and new_name.strip() and new_name.strip() != name: + new_name = new_name.strip() + try: + state.db.rename_project(name, new_name) + if state.current_project == name: + state.current_project = new_name + state.config['current_project'] = new_name + save_config(state.current_dir, + state.config.get('favorites', []), + state.config) + ui.notify(f'Renamed to "{new_name}"', type='positive') + render_project_list.refresh() + except Exception as e: + ui.notify(f'Error: {e}', type='negative') + await do_rename() + + ui.button('Rename', icon='edit', + on_click=rename_proj).props('flat dense') + + def change_path(name=proj['name'], path=proj['folder_path']): + async def do_change(): + new_path = await ui.run_javascript( + f'prompt("New path for project:", {json.dumps(path)})', + timeout=30.0, + ) + if new_path and new_path.strip() and new_path.strip() != path: + new_path = new_path.strip() + state.db.update_project_path(name, new_path) + ui.notify(f'Path updated to "{new_path}"', type='positive') + render_project_list.refresh() + await do_change() + + ui.button('Path', icon='folder', + on_click=change_path).props('flat dense') + def import_folder(pid=proj['id'], pname=proj['name']): _import_folder(state, pid, pname, render_project_list)