Files
led-controller/src/static/profiles.js
jimmy d907ca37ad Refresh tabs/presets UI and add a mobile menu.
This improves navigation and profile workflows on smaller screens.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 13:51:09 +13:00

283 lines
9.3 KiB
JavaScript

document.addEventListener("DOMContentLoaded", () => {
const profilesButton = document.getElementById("profiles-btn");
const profilesModal = document.getElementById("profiles-modal");
const profilesCloseButton = document.getElementById("profiles-close-btn");
const profilesList = document.getElementById("profiles-list");
const newProfileInput = document.getElementById("new-profile-name");
const createProfileButton = document.getElementById("create-profile-btn");
if (!profilesButton || !profilesModal || !profilesList) {
return;
}
const openModal = () => {
profilesModal.classList.add("active");
loadProfiles();
};
const closeModal = () => {
profilesModal.classList.remove("active");
};
const renderProfiles = (profiles, currentProfileId) => {
profilesList.innerHTML = "";
let entries = [];
if (Array.isArray(profiles)) {
entries = profiles.map((profileId) => [profileId, {}]);
} else if (profiles && typeof profiles === "object") {
// Make sure we're iterating over profile entries, not metadata
entries = Object.entries(profiles).filter(([key]) => {
// Skip metadata keys like 'current_profile_id' if they exist
return key !== 'current_profile_id' && key !== 'profiles';
});
}
if (entries.length === 0) {
const empty = document.createElement("p");
empty.className = "muted-text";
empty.textContent = "No profiles found.";
profilesList.appendChild(empty);
return;
}
entries.forEach(([profileId, profile]) => {
const row = document.createElement("div");
row.className = "profiles-row";
const label = document.createElement("span");
label.textContent = (profile && profile.name) || profileId;
if (String(profileId) === String(currentProfileId)) {
label.textContent = `${label.textContent}`;
label.style.fontWeight = "bold";
label.style.color = "#FFD700";
}
const applyButton = document.createElement("button");
applyButton.className = "btn btn-secondary btn-small profiles-apply-btn";
applyButton.textContent = "Apply";
applyButton.addEventListener("click", async () => {
try {
const response = await fetch(`/profiles/${profileId}/apply`, {
method: "POST",
headers: { Accept: "application/json" },
});
if (!response.ok) {
throw new Error("Failed to apply profile");
}
await loadProfiles();
document.body.dispatchEvent(new Event("tabs-updated"));
} catch (error) {
console.error("Apply profile failed:", error);
alert("Failed to apply profile.");
}
});
const cloneButton = document.createElement("button");
cloneButton.className = "btn btn-secondary btn-small";
cloneButton.textContent = "Clone";
cloneButton.addEventListener("click", async () => {
const baseName = (profile && profile.name) || profileId;
const suggested = `${baseName}`;
const name = prompt("New profile name:", suggested);
if (name === null) {
return;
}
const trimmed = String(name).trim();
if (!trimmed) {
alert("Profile name cannot be empty.");
return;
}
try {
const response = await fetch(`/profiles/${profileId}/clone`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ name: trimmed }),
});
if (!response.ok) {
throw new Error("Failed to clone profile");
}
const data = await response.json().catch(() => null);
let newProfileId = null;
if (data && typeof data === "object") {
if (data.id) {
newProfileId = String(data.id);
} else {
const ids = Object.keys(data);
if (ids.length > 0) {
newProfileId = String(ids[0]);
}
}
}
if (newProfileId) {
await fetch(`/profiles/${newProfileId}/apply`, {
method: "POST",
headers: { Accept: "application/json" },
});
}
document.cookie = "current_tab=; path=/; max-age=0";
await loadProfiles();
if (typeof window.loadTabs === "function") {
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) {
console.error("Clone profile failed:", error);
alert("Failed to clone profile.");
}
});
const deleteButton = document.createElement("button");
deleteButton.className = "btn btn-danger btn-small";
deleteButton.textContent = "Delete";
deleteButton.addEventListener("click", async () => {
const confirmed = confirm(`Delete profile "${label.textContent}"?`);
if (!confirmed) {
return;
}
try {
const response = await fetch(`/profiles/${profileId}`, {
method: "DELETE",
headers: { Accept: "application/json" },
});
if (!response.ok) {
throw new Error("Failed to delete profile");
}
await loadProfiles();
} catch (error) {
console.error("Delete profile failed:", error);
alert("Failed to delete profile.");
}
});
row.appendChild(label);
row.appendChild(applyButton);
row.appendChild(cloneButton);
row.appendChild(deleteButton);
profilesList.appendChild(row);
});
};
const loadProfiles = async () => {
profilesList.innerHTML = "";
const loading = document.createElement("p");
loading.className = "muted-text";
loading.textContent = "Loading profiles...";
profilesList.appendChild(loading);
try {
const response = await fetch("/profiles", {
headers: { Accept: "application/json" },
});
if (!response.ok) {
throw new Error("Failed to load profiles");
}
const data = await response.json();
// Handle both old format (just profiles object) and new format (with current_profile_id)
const profiles = data.profiles || data;
const currentProfileId = data.current_profile_id || null;
renderProfiles(profiles, currentProfileId);
} catch (error) {
console.error("Load profiles failed:", error);
profilesList.innerHTML = "";
const errorMessage = document.createElement("p");
errorMessage.className = "muted-text";
errorMessage.textContent = "Failed to load profiles.";
profilesList.appendChild(errorMessage);
}
};
const createProfile = async () => {
if (!newProfileInput) {
return;
}
const name = newProfileInput.value.trim();
if (!name) {
alert("Profile name cannot be empty.");
return;
}
try {
const response = await fetch("/profiles", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
});
if (!response.ok) {
throw new Error("Failed to create profile");
}
const data = await response.json().catch(() => null);
let newProfileId = null;
if (data && typeof data === "object") {
if (data.id) {
newProfileId = String(data.id);
} else {
const ids = Object.keys(data);
if (ids.length > 0) {
newProfileId = String(ids[0]);
}
}
}
if (newProfileId) {
await fetch(`/profiles/${newProfileId}/apply`, {
method: "POST",
headers: { Accept: "application/json" },
});
}
newProfileInput.value = "";
// Clear current tab and refresh the UI so the new profile starts empty.
document.cookie = "current_tab=; path=/; max-age=0";
await loadProfiles();
if (typeof window.loadTabs === "function") {
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) {
console.error("Create profile failed:", error);
alert("Failed to create profile.");
}
};
profilesButton.addEventListener("click", openModal);
if (profilesCloseButton) {
profilesCloseButton.addEventListener("click", closeModal);
}
if (createProfileButton) {
createProfileButton.addEventListener("click", createProfile);
}
if (newProfileInput) {
newProfileInput.addEventListener("keypress", (event) => {
if (event.key === "Enter") {
createProfile();
}
});
}
profilesModal.addEventListener("click", (event) => {
if (event.target === profilesModal) {
closeModal();
}
});
});