Scope presets to active profiles and support cloning.

This keeps data isolated per profile while letting users duplicate setups quickly.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-08 13:51:02 +13:00
parent 00514f0525
commit 6c6ed22dbe
8 changed files with 406 additions and 45 deletions

View File

@@ -1,10 +1,14 @@
from microdot import Microdot
from microdot.session import with_session
from models.profile import Profile
from models.tab import Tab
from models.preset import Preset
import json
controller = Microdot()
profiles = Profile()
tabs = Tab()
presets = Preset()
@controller.get('')
@with_session
@@ -12,6 +16,8 @@ async def list_profiles(request, session):
"""List all profiles with current profile info."""
profile_list = profiles.list()
current_id = session.get('current_profile')
if current_id and current_id not in profile_list:
current_id = None
# If no current profile in session, use first one
if not current_id and profile_list:
@@ -37,6 +43,8 @@ async def get_current_profile(request, session):
"""Get the current profile ID from session (or fallback)."""
profile_list = profiles.list()
current_id = session.get('current_profile')
if current_id and current_id not in profile_list:
current_id = None
if not current_id and profile_list:
current_id = profile_list[0]
session['current_profile'] = str(current_id)
@@ -83,6 +91,118 @@ async def create_profile(request):
except Exception as e:
return json.dumps({"error": str(e)}), 400
@controller.post('/<id>/clone')
async def clone_profile(request, id):
"""Clone an existing profile along with its tabs and palette."""
try:
source = profiles.read(id)
if not source:
return json.dumps({"error": "Profile not found"}), 404
data = request.json or {}
source_name = source.get("name") or f"Profile {id}"
new_name = data.get("name") or source_name
profile_type = source.get("type", "tabs")
def allocate_id(model, cache):
if "next" not in cache:
max_id = max((int(k) for k in model.keys() if str(k).isdigit()), default=0)
cache["next"] = max_id + 1
next_id = str(cache["next"])
cache["next"] += 1
return next_id
def map_preset_container(value, id_map, preset_cache, new_profile_id, new_presets):
if isinstance(value, list):
return [map_preset_container(v, id_map, preset_cache, new_profile_id, new_presets) for v in value]
if value is None:
return None
preset_id = str(value)
if preset_id in id_map:
return id_map[preset_id]
preset_data = presets.read(preset_id)
if not preset_data:
return None
new_preset_id = allocate_id(presets, preset_cache)
clone_data = dict(preset_data)
clone_data["profile_id"] = str(new_profile_id)
new_presets[new_preset_id] = clone_data
id_map[preset_id] = new_preset_id
return new_preset_id
# Prepare new IDs without writing until everything is ready.
profile_cache = {}
palette_cache = {}
tab_cache = {}
preset_cache = {}
new_profile_id = allocate_id(profiles, profile_cache)
new_palette_id = allocate_id(profiles._palette_model, palette_cache)
# Clone palette colors into the new profile's palette
src_palette_id = source.get("palette_id")
palette_colors = []
if src_palette_id:
try:
palette_colors = profiles._palette_model.read(src_palette_id)
except Exception:
palette_colors = []
# Clone tabs and presets used by those tabs
source_tabs = source.get("tabs")
if not isinstance(source_tabs, list) or len(source_tabs) == 0:
source_tabs = source.get("tab_order", [])
source_tabs = source_tabs or []
cloned_tab_ids = []
preset_id_map = {}
new_tabs = {}
new_presets = {}
for tab_id in source_tabs:
tab = tabs.read(tab_id)
if not tab:
continue
tab_name = tab.get("name") or f"Tab {tab_id}"
clone_name = tab_name
mapped_presets = map_preset_container(tab.get("presets"), preset_id_map, preset_cache, new_profile_id, new_presets)
clone_id = allocate_id(tabs, tab_cache)
clone_data = {
"name": clone_name,
"names": tab.get("names") or [],
"presets": mapped_presets if mapped_presets is not None else []
}
extra = {k: v for k, v in tab.items() if k not in ("name", "names", "presets")}
if "presets_flat" in extra:
extra["presets_flat"] = map_preset_container(extra.get("presets_flat"), preset_id_map, preset_cache, new_profile_id, new_presets)
if extra:
clone_data.update(extra)
new_tabs[clone_id] = clone_data
cloned_tab_ids.append(clone_id)
new_profile_data = {
"name": new_name,
"type": profile_type,
"tabs": cloned_tab_ids,
"scenes": list(source.get("scenes", [])) if isinstance(source.get("scenes", []), list) else [],
"palette_id": str(new_palette_id),
}
# Commit all changes and save once per model.
profiles._palette_model[str(new_palette_id)] = list(palette_colors) if palette_colors else []
for pid, pdata in new_presets.items():
presets[pid] = pdata
for tid, tdata in new_tabs.items():
tabs[tid] = tdata
profiles[str(new_profile_id)] = new_profile_data
profiles._palette_model.save()
presets.save()
tabs.save()
profiles.save()
return json.dumps({new_profile_id: new_profile_data}), 201, {'Content-Type': 'application/json'}
except Exception as e:
return json.dumps({"error": str(e)}), 400
@controller.put('/current')
@with_session
async def update_current_profile(request, session):