Add rename for sequences and projects, per-sub color cycling, project path editing
- Sequences: add rename button with name shown in expansion header - Subsequences: cycle through 6 distinct border colors by sub-index - Projects: add rename and change path buttons with DB methods Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
18
db.py
18
db.py
@@ -98,6 +98,24 @@ class ProjectDB:
|
|||||||
).fetchone()
|
).fetchone()
|
||||||
return dict(row) if row else None
|
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:
|
def delete_project(self, name: str) -> bool:
|
||||||
cur = self.conn.execute("DELETE FROM projects WHERE name = ?", (name,))
|
cur = self.conn.execute("DELETE FROM projects WHERE name = ?", (name,))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|||||||
21
main.py
21
main.py
@@ -156,14 +156,19 @@ def index():
|
|||||||
background: rgba(255,255,255,0.2);
|
background: rgba(255,255,255,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sub-sequence accent (teal) */
|
/* Sub-sequence accent colors (per sub-index, cycling) */
|
||||||
.body--dark .subsegment-card > .q-expansion-item__container > .q-item {
|
.body--dark .subsegment-color-0 > .q-expansion-item__container > .q-item { border-left: 6px solid #06B6D4; padding-left: 10px; }
|
||||||
border-left: 6px solid #06B6D4;
|
.body--dark .subsegment-color-0 .q-expansion-item__toggle-icon { color: #06B6D4 !important; }
|
||||||
padding-left: 10px;
|
.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-card .q-expansion-item__toggle-icon {
|
.body--dark .subsegment-color-2 > .q-expansion-item__container > .q-item { border-left: 6px solid #34D399; padding-left: 10px; }
|
||||||
color: #06B6D4 !important;
|
.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 */
|
/* Secondary pane teal accent */
|
||||||
.pane-secondary .q-field--outlined.q-field--focused .q-field__control:after {
|
.pane-secondary .q-field--outlined.q-field--focused .q-field__control:after {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import copy
|
import copy
|
||||||
|
import json
|
||||||
import random
|
import random
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ from history_tree import HistoryTree
|
|||||||
|
|
||||||
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.webp', '.bmp', '.gif'}
|
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.webp', '.bmp', '.gif'}
|
||||||
SUB_SEGMENT_MULTIPLIER = 1000
|
SUB_SEGMENT_MULTIPLIER = 1000
|
||||||
|
SUB_SEGMENT_NUM_COLORS = 6
|
||||||
FRAME_TO_SKIP_DEFAULT = DEFAULTS['frame_to_skip']
|
FRAME_TO_SKIP_DEFAULT = DEFAULTS['frame_to_skip']
|
||||||
|
|
||||||
VACE_MODES = [
|
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_keys = ['lora 1 high', 'lora 1 low', 'lora 2 high', 'lora 2 low',
|
||||||
'lora 3 high', 'lora 3 low']
|
'lora 3 high', 'lora 3 low']
|
||||||
standard_keys = {
|
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,
|
'seed', 'cfg', 'camera', 'flf', KEY_SEQUENCE_NUMBER,
|
||||||
'frame_to_skip', 'end_frame', 'transition', 'vace_length',
|
'frame_to_skip', 'end_frame', 'transition', 'vace_length',
|
||||||
'input_a_frames', 'input_b_frames', 'reference switch', 'vace schedule',
|
'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()
|
refresh_list.refresh()
|
||||||
|
|
||||||
seq_num = seq.get(KEY_SEQUENCE_NUMBER, i + 1)
|
seq_num = seq.get(KEY_SEQUENCE_NUMBER, i + 1)
|
||||||
|
seq_name = seq.get('name', '')
|
||||||
|
|
||||||
if is_subsegment(seq_num):
|
if is_subsegment(seq_num):
|
||||||
label = f'Sub #{parent_of(seq_num)}.{sub_index_of(seq_num)} ({int(seq_num)})'
|
label = f'Sub #{parent_of(seq_num)}.{sub_index_of(seq_num)} ({int(seq_num)})'
|
||||||
else:
|
else:
|
||||||
label = f'Sequence #{seq_num}'
|
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'
|
if is_subsegment(seq_num):
|
||||||
with ui.expansion(label, icon='movie').classes(exp_classes):
|
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 ---
|
# --- Action row ---
|
||||||
with ui.row().classes('w-full q-gutter-sm 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
|
# Copy from source
|
||||||
def copy_source(idx=i, sn=seq_num):
|
def copy_source(idx=i, sn=seq_num):
|
||||||
item = copy.deepcopy(DEFAULTS)
|
item = copy.deepcopy(DEFAULTS)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -109,6 +110,47 @@ def render_projects_tab(state: AppState):
|
|||||||
ui.button('Deactivate', icon='cancel',
|
ui.button('Deactivate', icon='cancel',
|
||||||
on_click=deactivate).props('flat dense')
|
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']):
|
def import_folder(pid=proj['id'], pname=proj['name']):
|
||||||
_import_folder(state, pid, pname, render_project_list)
|
_import_folder(state, pid, pname, render_project_list)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user