314 lines
12 KiB
JavaScript
314 lines
12 KiB
JavaScript
/**
|
|
* 🏷️ Widget UI Builder
|
|
* Builds all UI sections for the string multiline tag editor
|
|
* Extracted for modularity - preserves 100% of original logic
|
|
*/
|
|
|
|
import { TagUtilities } from "./tag-utilities.js";
|
|
import { loadSupportedLanguages } from "./language-constants.js";
|
|
|
|
// Load supported languages into memory on module load
|
|
loadSupportedLanguages();
|
|
|
|
export function buildHistorySection(state, storageKey) {
|
|
const historySection = document.createElement("div");
|
|
historySection.style.marginBottom = "8px";
|
|
historySection.style.paddingBottom = "8px";
|
|
historySection.style.borderBottom = "1px solid #444";
|
|
|
|
const historyLabel = document.createElement("div");
|
|
historyLabel.textContent = "History";
|
|
historyLabel.style.fontWeight = "bold";
|
|
historyLabel.style.marginBottom = "5px";
|
|
historyLabel.style.fontSize = "11px";
|
|
|
|
const historyControls = document.createElement("div");
|
|
historyControls.style.display = "flex";
|
|
historyControls.style.gap = "5px";
|
|
historyControls.style.marginBottom = "5px";
|
|
|
|
const undoBtn = document.createElement("button");
|
|
undoBtn.textContent = "↶";
|
|
undoBtn.title = "Undo (Alt+Z)";
|
|
undoBtn.style.flex = "1";
|
|
undoBtn.style.padding = "4px";
|
|
undoBtn.style.cursor = "pointer";
|
|
undoBtn.style.fontSize = "12px";
|
|
undoBtn.style.background = "#3a3a3a";
|
|
undoBtn.style.color = "#eee";
|
|
undoBtn.style.border = "1px solid #555";
|
|
undoBtn.style.borderRadius = "2px";
|
|
|
|
const redoBtn = document.createElement("button");
|
|
redoBtn.textContent = "↷";
|
|
redoBtn.title = "Redo (Alt+Shift+Z)";
|
|
redoBtn.style.flex = "1";
|
|
redoBtn.style.padding = "4px";
|
|
redoBtn.style.cursor = "pointer";
|
|
redoBtn.style.fontSize = "12px";
|
|
redoBtn.style.background = "#3a3a3a";
|
|
redoBtn.style.color = "#eee";
|
|
redoBtn.style.border = "1px solid #555";
|
|
redoBtn.style.borderRadius = "2px";
|
|
|
|
const historyStatus = document.createElement("div");
|
|
historyStatus.style.fontSize = "10px";
|
|
historyStatus.style.textAlign = "center";
|
|
historyStatus.style.color = "#999";
|
|
|
|
historyControls.appendChild(undoBtn);
|
|
historyControls.appendChild(redoBtn);
|
|
historySection.appendChild(historyLabel);
|
|
historySection.appendChild(historyControls);
|
|
historySection.appendChild(historyStatus);
|
|
|
|
return { historySection, undoBtn, redoBtn, historyStatus };
|
|
}
|
|
|
|
export function buildCharacterSection(state, storageKey, onManageAliases) {
|
|
const charSection = document.createElement("div");
|
|
charSection.style.marginBottom = "8px";
|
|
charSection.style.paddingBottom = "8px";
|
|
charSection.style.borderBottom = "1px solid #444";
|
|
|
|
const charLabel = document.createElement("div");
|
|
charLabel.textContent = "Character";
|
|
charLabel.style.fontWeight = "bold";
|
|
charLabel.style.marginBottom = "5px";
|
|
charLabel.style.fontSize = "11px";
|
|
charLabel.style.cursor = "pointer";
|
|
charLabel.style.outline = "none";
|
|
charLabel.style.transition = "color 120ms ease";
|
|
charLabel.tabIndex = 0;
|
|
charLabel.setAttribute("role", "button");
|
|
charLabel.setAttribute("aria-label", "Manage character aliases");
|
|
charLabel.title = "Manage character aliases";
|
|
const setLabelActive = active => {
|
|
charLabel.style.color = active ? "#38bdf8" : "";
|
|
};
|
|
charLabel.addEventListener("mouseenter", () => setLabelActive(true));
|
|
charLabel.addEventListener("mouseleave", () => {
|
|
if (document.activeElement !== charLabel) setLabelActive(false);
|
|
});
|
|
charLabel.addEventListener("focus", () => setLabelActive(true));
|
|
charLabel.addEventListener("blur", () => setLabelActive(false));
|
|
charLabel.addEventListener("click", () => onManageAliases?.());
|
|
charLabel.addEventListener("keydown", event => {
|
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
event.preventDefault();
|
|
onManageAliases?.();
|
|
});
|
|
|
|
const charSelect = document.createElement("select");
|
|
charSelect.style.width = "100%";
|
|
charSelect.style.marginBottom = "4px";
|
|
charSelect.style.padding = "3px";
|
|
charSelect.style.fontSize = "10px";
|
|
charSelect.style.background = "#2a2a2a";
|
|
charSelect.style.color = "#eee";
|
|
charSelect.style.border = "1px solid #444";
|
|
charSelect.innerHTML = "<option value=''>Select...</option>";
|
|
|
|
const populateCharacters = async (forceApi = false) => {
|
|
try {
|
|
const previousValue = charSelect.value;
|
|
let characters = [];
|
|
if (!forceApi && state.discoveredCharacters && typeof state.discoveredCharacters === 'object') {
|
|
if (!Array.isArray(state.discoveredCharacters)) {
|
|
characters = Object.keys(state.discoveredCharacters);
|
|
} else {
|
|
characters = state.discoveredCharacters;
|
|
}
|
|
}
|
|
|
|
if (characters.length === 0) {
|
|
try {
|
|
const response = await fetch("/api/tts-audio-suite/available-characters");
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.characters && Array.isArray(data.characters)) {
|
|
characters = data.characters;
|
|
}
|
|
}
|
|
} catch (err) {}
|
|
}
|
|
|
|
if (characters.length === 0) {
|
|
characters = ["Alice", "Bob", "Charlie", "Diana", "Emma", "Frank", "Grace", "Henry"];
|
|
}
|
|
|
|
charSelect.innerHTML = "<option value=''>Select...</option>";
|
|
characters.forEach(char => {
|
|
const option = document.createElement("option");
|
|
option.value = char;
|
|
option.textContent = char;
|
|
charSelect.appendChild(option);
|
|
});
|
|
if (characters.includes(previousValue)) charSelect.value = previousValue;
|
|
console.log(`✅ Loaded ${characters.length} character voices`);
|
|
} catch (err) {
|
|
console.warn("Could not populate characters:", err);
|
|
}
|
|
};
|
|
|
|
populateCharacters();
|
|
|
|
const charInput = document.createElement("input");
|
|
charInput.type = "text";
|
|
charInput.placeholder = "Custom";
|
|
charInput.style.width = "100%";
|
|
charInput.style.marginBottom = "4px";
|
|
charInput.style.padding = "3px";
|
|
charInput.style.fontSize = "10px";
|
|
charInput.style.background = "#2a2a2a";
|
|
charInput.style.color = "#eee";
|
|
charInput.style.border = "1px solid #444";
|
|
charInput.value = state.lastCharacter;
|
|
|
|
charInput.addEventListener("change", () => {
|
|
state.lastCharacter = charInput.value;
|
|
state.saveToLocalStorage(storageKey);
|
|
});
|
|
|
|
const addCharBtn = document.createElement("button");
|
|
addCharBtn.textContent = "Add Char";
|
|
addCharBtn.title = "Insert selected character tag at cursor position";
|
|
addCharBtn.style.width = "100%";
|
|
addCharBtn.style.padding = "4px";
|
|
addCharBtn.style.cursor = "pointer";
|
|
addCharBtn.style.fontSize = "10px";
|
|
addCharBtn.style.background = "#3a3a3a";
|
|
addCharBtn.style.color = "#eee";
|
|
addCharBtn.style.border = "1px solid #555";
|
|
addCharBtn.style.borderRadius = "2px";
|
|
|
|
charSection.appendChild(charLabel);
|
|
charSection.appendChild(charSelect);
|
|
charSection.appendChild(charInput);
|
|
charSection.appendChild(addCharBtn);
|
|
|
|
return {
|
|
charSection,
|
|
charSelect,
|
|
charInput,
|
|
addCharBtn,
|
|
refreshCharacters: () => populateCharacters(true),
|
|
};
|
|
}
|
|
|
|
export function buildLanguageSection(state, storageKey) {
|
|
const langSection = document.createElement("div");
|
|
langSection.style.marginBottom = "8px";
|
|
langSection.style.paddingBottom = "8px";
|
|
langSection.style.borderBottom = "1px solid #444";
|
|
|
|
const langLabel = document.createElement("div");
|
|
langLabel.textContent = "Language";
|
|
langLabel.style.fontWeight = "bold";
|
|
langLabel.style.marginBottom = "5px";
|
|
langLabel.style.fontSize = "11px";
|
|
|
|
const langSelect = document.createElement("select");
|
|
langSelect.style.width = "100%";
|
|
langSelect.style.marginBottom = "4px";
|
|
langSelect.style.padding = "3px";
|
|
langSelect.style.fontSize = "10px";
|
|
langSelect.style.background = "#2a2a2a";
|
|
langSelect.style.color = "#eee";
|
|
langSelect.style.border = "1px solid #444";
|
|
langSelect.innerHTML = "<option value=''>Select...</option>";
|
|
|
|
const populateLanguages = async () => {
|
|
try {
|
|
const response = await fetch("/api/tts-audio-suite/available-languages");
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.languages && Array.isArray(data.languages)) {
|
|
langSelect.innerHTML = "<option value=''>Select...</option>";
|
|
data.languages.forEach(lang => {
|
|
const option = document.createElement("option");
|
|
option.value = lang;
|
|
option.textContent = lang.toUpperCase();
|
|
langSelect.appendChild(option);
|
|
});
|
|
// Restore saved language AFTER options are populated
|
|
langSelect.value = state.lastLanguage;
|
|
console.log(`✅ Loaded ${data.languages.length} language codes`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn("Could not load languages from API, using fallback:", err);
|
|
// Fallback to hardcoded list
|
|
const fallbackLanguages = ["en", "de", "fr", "ja", "es", "it", "pt", "th", "no"];
|
|
langSelect.innerHTML = "<option value=''>Select...</option>";
|
|
fallbackLanguages.forEach(lang => {
|
|
const option = document.createElement("option");
|
|
option.value = lang;
|
|
option.textContent = lang.toUpperCase();
|
|
langSelect.appendChild(option);
|
|
});
|
|
// Restore saved language AFTER options are populated
|
|
langSelect.value = state.lastLanguage;
|
|
}
|
|
};
|
|
|
|
populateLanguages();
|
|
|
|
langSelect.addEventListener("change", () => {
|
|
state.lastLanguage = langSelect.value;
|
|
state.saveToLocalStorage(storageKey);
|
|
});
|
|
|
|
const addLangBtn = document.createElement("button");
|
|
addLangBtn.textContent = "Add Language Tag";
|
|
addLangBtn.title = "Insert language tag [lang:] or [lang:Character] at cursor";
|
|
addLangBtn.style.width = "100%";
|
|
addLangBtn.style.padding = "4px";
|
|
addLangBtn.style.cursor = "pointer";
|
|
addLangBtn.style.fontSize = "10px";
|
|
addLangBtn.style.background = "#3a3a3a";
|
|
addLangBtn.style.color = "#eee";
|
|
addLangBtn.style.border = "1px solid #555";
|
|
addLangBtn.style.borderRadius = "2px";
|
|
|
|
langSection.appendChild(langLabel);
|
|
langSection.appendChild(langSelect);
|
|
langSection.appendChild(addLangBtn);
|
|
|
|
return { langSection, langSelect, addLangBtn };
|
|
}
|
|
|
|
export function buildValidationSection() {
|
|
const validSection = document.createElement("div");
|
|
|
|
const formatBtn = document.createElement("button");
|
|
formatBtn.textContent = "Format";
|
|
formatBtn.title = "Normalize spacing and structure of tags and text";
|
|
formatBtn.style.width = "100%";
|
|
formatBtn.style.padding = "4px";
|
|
formatBtn.style.marginBottom = "4px";
|
|
formatBtn.style.cursor = "pointer";
|
|
formatBtn.style.fontSize = "10px";
|
|
formatBtn.style.background = "#3a3a3a";
|
|
formatBtn.style.color = "#eee";
|
|
formatBtn.style.border = "1px solid #555";
|
|
formatBtn.style.borderRadius = "2px";
|
|
|
|
const validateBtn = document.createElement("button");
|
|
validateBtn.textContent = "Validate";
|
|
validateBtn.title = "Check tag syntax for errors and issues";
|
|
validateBtn.style.width = "100%";
|
|
validateBtn.style.padding = "4px";
|
|
validateBtn.style.cursor = "pointer";
|
|
validateBtn.style.fontSize = "10px";
|
|
validateBtn.style.background = "#3a3a3a";
|
|
validateBtn.style.color = "#eee";
|
|
validateBtn.style.border = "1px solid #555";
|
|
validateBtn.style.borderRadius = "2px";
|
|
|
|
validSection.appendChild(formatBtn);
|
|
validSection.appendChild(validateBtn);
|
|
|
|
return { validSection, formatBtn, validateBtn };
|
|
}
|