feat(profiles): seed new profiles and refresh tabs on apply

Made-with: Cursor
This commit is contained in:
2026-03-21 23:15:19 +13:00
parent 3ee7b74152
commit 91de705647
3 changed files with 161 additions and 40 deletions

View File

@@ -81,11 +81,117 @@ async def apply_profile(request, session, id):
async def create_profile(request): async def create_profile(request):
"""Create a new profile.""" """Create a new profile."""
try: try:
data = request.json or {} data = dict(request.json or {})
name = data.get("name", "") name = data.get("name", "")
seed_raw = data.get("seed_dj_tab", False)
if isinstance(seed_raw, str):
seed_dj_tab = seed_raw.strip().lower() in ("1", "true", "yes", "on")
else:
seed_dj_tab = bool(seed_raw)
# Request-only flag: do not persist on profile records.
data.pop("seed_dj_tab", None)
profile_id = profiles.create(name) profile_id = profiles.create(name)
# Avoid persisting request-only fields.
data.pop("name", None)
if data: if data:
profiles.update(profile_id, data) profiles.update(profile_id, data)
# New profiles always start with a default tab pre-populated with starter presets.
default_preset_ids = []
default_preset_defs = [
{
"name": "on",
"pattern": "on",
"colors": ["#FFFFFF"],
"brightness": 255,
"delay": 100,
"auto": True,
},
{
"name": "off",
"pattern": "off",
"colors": [],
"brightness": 0,
"delay": 100,
"auto": True,
},
{
"name": "rainbow",
"pattern": "rainbow",
"colors": [],
"brightness": 255,
"delay": 100,
"auto": True,
"n1": 2,
},
{
"name": "transition",
"pattern": "transition",
"colors": ["#FF0000", "#00FF00", "#0000FF"],
"brightness": 255,
"delay": 500,
"auto": True,
},
]
for preset_data in default_preset_defs:
pid = presets.create(profile_id)
presets.update(pid, preset_data)
default_preset_ids.append(str(pid))
default_tab_id = tabs.create(name="default", names=["1"], presets=[default_preset_ids])
tabs.update(default_tab_id, {
"presets_flat": default_preset_ids,
"default_preset": default_preset_ids[0] if default_preset_ids else None,
})
profile = profiles.read(profile_id) or {}
profile_tabs = profile.get("tabs", []) if isinstance(profile.get("tabs", []), list) else []
profile_tabs.append(str(default_tab_id))
if seed_dj_tab:
# Seed a DJ-focused tab with three starter presets.
seeded_preset_ids = []
preset_defs = [
{
"name": "DJ Rainbow",
"pattern": "rainbow",
"colors": [],
"brightness": 220,
"delay": 60,
"n1": 12,
},
{
"name": "DJ Single Color",
"pattern": "on",
"colors": ["#ff00ff"],
"brightness": 220,
"delay": 100,
},
{
"name": "DJ Transition",
"pattern": "transition",
"colors": ["#ff0000", "#00ff00", "#0000ff"],
"brightness": 220,
"delay": 250,
},
]
for preset_data in preset_defs:
pid = presets.create(profile_id)
presets.update(pid, preset_data)
seeded_preset_ids.append(str(pid))
dj_tab_id = tabs.create(name="dj", names=["dj"], presets=[seeded_preset_ids])
tabs.update(dj_tab_id, {
"presets_flat": seeded_preset_ids,
"default_preset": seeded_preset_ids[0] if seeded_preset_ids else None,
})
profile_tabs.append(str(dj_tab_id))
profiles.update(profile_id, {"tabs": profile_tabs})
profile_data = profiles.read(profile_id) profile_data = profiles.read(profile_id)
return json.dumps({profile_id: profile_data}), 201, {'Content-Type': 'application/json'} return json.dumps({profile_id: profile_data}), 201, {'Content-Type': 'application/json'}
except Exception as e: except Exception as e:

View File

@@ -4,6 +4,7 @@ document.addEventListener("DOMContentLoaded", () => {
const profilesCloseButton = document.getElementById("profiles-close-btn"); const profilesCloseButton = document.getElementById("profiles-close-btn");
const profilesList = document.getElementById("profiles-list"); const profilesList = document.getElementById("profiles-list");
const newProfileInput = document.getElementById("new-profile-name"); const newProfileInput = document.getElementById("new-profile-name");
const newProfileSeedDjInput = document.getElementById("new-profile-seed-dj");
const createProfileButton = document.getElementById("create-profile-btn"); const createProfileButton = document.getElementById("create-profile-btn");
if (!profilesButton || !profilesModal || !profilesList) { if (!profilesButton || !profilesModal || !profilesList) {
@@ -19,6 +20,18 @@ document.addEventListener("DOMContentLoaded", () => {
profilesModal.classList.remove("active"); profilesModal.classList.remove("active");
}; };
const refreshTabsForActiveProfile = async () => {
// Clear stale current tab so tab controller falls back to first tab of applied profile.
document.cookie = "current_tab=; path=/; max-age=0";
if (window.tabsManager && typeof window.tabsManager.loadTabs === "function") {
await window.tabsManager.loadTabs();
}
if (window.tabsManager && typeof window.tabsManager.loadTabsModal === "function") {
await window.tabsManager.loadTabsModal();
}
};
const renderProfiles = (profiles, currentProfileId) => { const renderProfiles = (profiles, currentProfileId) => {
profilesList.innerHTML = ""; profilesList.innerHTML = "";
let entries = []; let entries = [];
@@ -66,7 +79,7 @@ document.addEventListener("DOMContentLoaded", () => {
throw new Error("Failed to apply profile"); throw new Error("Failed to apply profile");
} }
await loadProfiles(); await loadProfiles();
document.body.dispatchEvent(new Event("tabs-updated")); await refreshTabsForActiveProfile();
} catch (error) { } catch (error) {
console.error("Apply profile failed:", error); console.error("Apply profile failed:", error);
alert("Failed to apply profile."); alert("Failed to apply profile.");
@@ -115,22 +128,8 @@ document.addEventListener("DOMContentLoaded", () => {
headers: { Accept: "application/json" }, headers: { Accept: "application/json" },
}); });
} }
document.cookie = "current_tab=; path=/; max-age=0";
await loadProfiles(); await loadProfiles();
if (typeof window.loadTabs === "function") { await refreshTabsForActiveProfile();
await window.loadTabs();
}
if (typeof window.loadTabsModal === "function") {
await window.loadTabsModal();
}
const tabContent = document.getElementById("tab-content");
if (tabContent) {
tabContent.innerHTML = `
<div class="tab-content-placeholder">
Select a tab to get started
</div>
`;
}
} catch (error) { } catch (error) {
console.error("Clone profile failed:", error); console.error("Clone profile failed:", error);
alert("Failed to clone profile."); alert("Failed to clone profile.");
@@ -210,7 +209,10 @@ document.addEventListener("DOMContentLoaded", () => {
const response = await fetch("/profiles", { const response = await fetch("/profiles", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }), body: JSON.stringify({
name,
seed_dj_tab: !!(newProfileSeedDjInput && newProfileSeedDjInput.checked),
}),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to create profile"); throw new Error("Failed to create profile");
@@ -236,23 +238,11 @@ document.addEventListener("DOMContentLoaded", () => {
} }
newProfileInput.value = ""; newProfileInput.value = "";
// Clear current tab and refresh the UI so the new profile starts empty. if (newProfileSeedDjInput) {
document.cookie = "current_tab=; path=/; max-age=0"; newProfileSeedDjInput.checked = false;
}
await loadProfiles(); await loadProfiles();
if (typeof window.loadTabs === "function") { await refreshTabsForActiveProfile();
await window.loadTabs();
}
if (typeof window.loadTabsModal === "function") {
await window.loadTabsModal();
}
const tabContent = document.getElementById("tab-content");
if (tabContent) {
tabContent.innerHTML = `
<div class="tab-content-placeholder">
Select a tab to get started
</div>
`;
}
} catch (error) { } catch (error) {
console.error("Create profile failed:", error); console.error("Create profile failed:", error);
alert("Failed to create profile."); alert("Failed to create profile.");

View File

@@ -1,6 +1,11 @@
// Tab management JavaScript // Tab management JavaScript
let currentTabId = null; let currentTabId = null;
const isEditModeActive = () => {
const toggle = document.querySelector('.ui-mode-toggle');
return !!(toggle && toggle.getAttribute('aria-pressed') === 'true');
};
// Get current tab from cookie // Get current tab from cookie
function getCurrentTabFromCookie() { function getCurrentTabFromCookie() {
const cookies = document.cookie.split(';'); const cookies = document.cookie.split(';');
@@ -38,10 +43,12 @@ async function loadTabs() {
// Load current tab content if available // Load current tab content if available
if (currentTabId) { if (currentTabId) {
loadTabContent(currentTabId); await loadTabContent(currentTabId);
} else if (data.tab_order && data.tab_order.length > 0) { } else if (data.tab_order && data.tab_order.length > 0) {
// Set first tab as current if none is set // Set first tab as current if none is set
await setCurrentTab(data.tab_order[0]); const firstTabId = data.tab_order[0];
await setCurrentTab(firstTabId);
await loadTabContent(firstTabId);
} }
} catch (error) { } catch (error) {
console.error('Failed to load tabs:', error); console.error('Failed to load tabs:', error);
@@ -62,6 +69,7 @@ function renderTabsList(tabs, tabOrder, currentTabId) {
return; return;
} }
const editMode = isEditModeActive();
let html = '<div class="tabs-list">'; let html = '<div class="tabs-list">';
for (const tabId of tabOrder) { for (const tabId of tabOrder) {
const tab = tabs[tabId]; const tab = tabs[tabId];
@@ -71,7 +79,7 @@ function renderTabsList(tabs, tabOrder, currentTabId) {
html += ` html += `
<button class="tab-button ${activeClass}" <button class="tab-button ${activeClass}"
data-tab-id="${tabId}" data-tab-id="${tabId}"
title="Click to select, right-click to edit" title="${editMode ? 'Click to select, right-click to edit' : 'Click to select'}"
onclick="selectTab('${tabId}')"> onclick="selectTab('${tabId}')">
${tabName} ${tabName}
</button> </button>
@@ -106,6 +114,7 @@ function renderTabsListModal(tabs, tabOrder, currentTabId) {
return; return;
} }
const editMode = isEditModeActive();
entries.forEach(([tabId, tab]) => { entries.forEach(([tabId, tab]) => {
const row = document.createElement("div"); const row = document.createElement("div");
row.className = "profiles-row"; row.className = "profiles-row";
@@ -224,10 +233,12 @@ function renderTabsListModal(tabs, tabOrder, currentTabId) {
row.appendChild(label); row.appendChild(label);
row.appendChild(applyButton); row.appendChild(applyButton);
row.appendChild(editButton);
row.appendChild(sendPresetsButton); row.appendChild(sendPresetsButton);
if (editMode) {
row.appendChild(editButton);
row.appendChild(cloneButton); row.appendChild(cloneButton);
row.appendChild(deleteButton); row.appendChild(deleteButton);
}
container.appendChild(row); container.appendChild(row);
}); });
} }
@@ -714,6 +725,9 @@ document.addEventListener('DOMContentLoaded', () => {
// Right-click on a tab button in the main header bar to edit that tab // Right-click on a tab button in the main header bar to edit that tab
document.addEventListener('contextmenu', async (event) => { document.addEventListener('contextmenu', async (event) => {
if (!isEditModeActive()) {
return;
}
const btn = event.target.closest('.tab-button'); const btn = event.target.closest('.tab-button');
if (!btn || !btn.dataset.tabId) { if (!btn || !btn.dataset.tabId) {
return; return;
@@ -796,11 +810,22 @@ document.addEventListener('DOMContentLoaded', () => {
await sendProfilePresets(); await sendProfilePresets();
}); });
} }
// When run/edit mode toggles, refresh tabs UI so edit actions show/hide immediately.
document.querySelectorAll('.ui-mode-toggle').forEach((btn) => {
btn.addEventListener('click', async () => {
await loadTabs();
if (tabsModal && tabsModal.classList.contains('active')) {
await loadTabsModal();
}
});
});
}); });
// Export for use in other scripts // Export for use in other scripts
window.tabsManager = { window.tabsManager = {
loadTabs, loadTabs,
loadTabsModal,
selectTab, selectTab,
createTab, createTab,
updateTab, updateTab,