1158 lines
43 KiB
JavaScript
1158 lines
43 KiB
JavaScript
|
|
class ThemeManager {
|
|
constructor(preferencesManager, cssPath = '/core/css/themes.css') {
|
|
this.preferencesManager = preferencesManager;
|
|
this.themes = [];
|
|
this.customThemes = [];
|
|
this.externalCustomThemes = [];
|
|
this.targetElementId = 'theme-selector';
|
|
this.cssPath = cssPath;
|
|
this.customThemeModalId = 'custom-theme-modal';
|
|
this.isDragging = false;
|
|
this.dragOffset = { x: 0, y: 0 };
|
|
this.animationFrame = null;
|
|
this.currentEditingThemeId = null;
|
|
this.themeCache = new Map();
|
|
this.themeSets = [];
|
|
this.customLabels = {
|
|
'--color-background': 'Background',
|
|
'--color-primary-text': 'Text',
|
|
'--color-background-pattern': 'Background Pattern',
|
|
'--color-border': 'Border',
|
|
'--color-header-background': 'Header Background',
|
|
'--color-header-text': 'Header Text',
|
|
'--color-background-secondary': 'Widget Background',
|
|
'--color-button-primary': 'Button',
|
|
'--color-button-primary-text': 'Button Text',
|
|
'--color-button-primary-hover': 'Button Hover',
|
|
'--color-button-primary-text-hover': 'Button Text Hover',
|
|
'--color-button-primary-active': 'Button Active',
|
|
'--color-button-primary-text-active': 'Button Text Active',
|
|
'--color-button-secondary': 'Widget Button',
|
|
'--color-button-secondary-text': 'Widget Button Text',
|
|
'--color-button-secondary-hover': 'Widget Button Hover',
|
|
'--color-button-secondary-text-hover': 'Widget Button Text Hover',
|
|
'--color-button-secondary-active': 'Widget Button Active',
|
|
'--color-button-secondary-text-active': 'Widget Button Text Active',
|
|
'--color-progress-background': 'Progress Background',
|
|
'--color-progress-value': 'Progress Value',
|
|
'--color-scrollbar-track': 'Scrollbar Track',
|
|
'--color-scrollbar-thumb': 'Scrollbar Thumb',
|
|
'--color-input-range-thumb': 'Widget Range Thumb',
|
|
'--color-input-range-background': 'Widget Range Background',
|
|
'--color-spinner': 'Spinner',
|
|
'--color-spinner-highlight': 'Spinner Highlight',
|
|
};
|
|
this.cssVariables = Object.keys(this.customLabels);
|
|
this.onMouseMove = this.onMouseMove.bind(this);
|
|
this.onMouseUp = this.onMouseUp.bind(this);
|
|
}
|
|
|
|
static applyInitialTheme(preferencesManager) {
|
|
let themeToApply = preferencesManager.get('selectedTheme');
|
|
if (!themeToApply) {
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
themeToApply = prefersDark ? 'flow-dark' : 'flow-light';
|
|
}
|
|
document.documentElement.setAttribute('data-theme', themeToApply);
|
|
}
|
|
|
|
async init() {
|
|
try {
|
|
document.documentElement.classList.add('css-loading');
|
|
this.loadCustomThemesFromStorage();
|
|
this.applySavedTheme();
|
|
await this.loadThemesFromCSS();
|
|
await this.loadExternalCustomThemes();
|
|
this.loadThemeSetsFromStorage();
|
|
this.addMenu();
|
|
this.setupCustomThemeModal();
|
|
} catch (error) {
|
|
console.error('Failed to initialize ThemeManager:', error);
|
|
} finally {
|
|
document.documentElement.classList.remove('css-loading');
|
|
|
|
const overlay = document.getElementById('css-loading-overlay');
|
|
if (overlay) {
|
|
overlay.addEventListener('transitionend', () => {
|
|
if (overlay.parentNode) {
|
|
overlay.parentNode.removeChild(overlay);
|
|
}
|
|
});
|
|
setTimeout(() => {
|
|
if (overlay.parentNode) {
|
|
overlay.parentNode.removeChild(overlay);
|
|
}
|
|
}, 50);
|
|
}
|
|
}}
|
|
|
|
getTheme(themeValue) {
|
|
if (!this.themeCache.has(themeValue)) {
|
|
const theme = this.themes.find(t => t.value === themeValue) ||
|
|
this.customThemes.find(t => t.value === themeValue) ||
|
|
this.externalCustomThemes.flatMap(s => s.themes).find(t => t.value === themeValue);
|
|
if (theme) {
|
|
this.themeCache.set(themeValue, theme);
|
|
}
|
|
}
|
|
return this.themeCache.get(themeValue);
|
|
}
|
|
|
|
async loadThemesFromCSS() {
|
|
try {
|
|
const response = await fetch(this.cssPath);
|
|
const cssText = await response.text();
|
|
this.parseThemesFromCSS(cssText);
|
|
} catch (error) {
|
|
console.error('Failed to load themes:', error);
|
|
}
|
|
}
|
|
|
|
parseThemesFromCSS(cssText) {
|
|
const themeRegex = /\[data-theme="([^"]+)"\]\s*\{([^}]+)\}/g;
|
|
let match;
|
|
|
|
while ((match = themeRegex.exec(cssText)) !== null) {
|
|
const themeValue = match[1];
|
|
const themeName = this.formatThemeName(themeValue);
|
|
const variablesBlock = match[2];
|
|
const variables = this.parseCSSVariables(variablesBlock);
|
|
|
|
if (!this.themes.some(theme => theme.value === themeValue)) {
|
|
this.themes.push({ name: themeName, value: themeValue, variables: variables });
|
|
}
|
|
}
|
|
|
|
if (this.themes.length === 0) {
|
|
console.warn('No themes found in the CSS file.');
|
|
} else {
|
|
// console.info(`Loaded ${this.themes.length} themes from CSS.`);
|
|
}
|
|
|
|
if (!this.themes.some(t => t.value === 'create-custom')) {
|
|
this.themes.push({ name: 'Create Custom Theme', value: 'create-custom', variables: {} });
|
|
}
|
|
}
|
|
|
|
parseCSSVariables(cssBlock) {
|
|
const variableRegex = /(--[\w-]+)\s*:\s*([^;]+);/g;
|
|
let match;
|
|
const variables = {};
|
|
|
|
while ((match = variableRegex.exec(cssBlock)) !== null) {
|
|
const variable = match[1];
|
|
const value = match[2].trim();
|
|
variables[variable] = value;
|
|
}
|
|
|
|
return variables;
|
|
}
|
|
|
|
async loadExternalCustomThemes() {
|
|
try {
|
|
const response = await fetch('/core/css/themes/list');
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch the list of custom theme CSS files.');
|
|
}
|
|
const cssFiles = await response.json();
|
|
|
|
for (const fileName of cssFiles) {
|
|
if (!fileName.endsWith('.css')) continue;
|
|
const styleName = this.formatStyleName(fileName);
|
|
|
|
try {
|
|
const cssResponse = await fetch(`/core/css/themes/${fileName}`);
|
|
if (!cssResponse.ok) {
|
|
console.warn(`Failed to load CSS file: ${fileName}`);
|
|
continue;
|
|
}
|
|
const cssText = await cssResponse.text();
|
|
|
|
const themes = this.extractThemesFromCSS(cssText).map(theme => ({
|
|
...theme,
|
|
themesSetName: styleName,
|
|
}));
|
|
if (themes.length === 0) {
|
|
console.warn(`No valid themes found in CSS file: ${fileName}`);
|
|
continue;
|
|
}
|
|
|
|
this.externalCustomThemes.push({
|
|
styleName: styleName,
|
|
themes: themes
|
|
});
|
|
|
|
this.appendExternalCSS(fileName, cssText);
|
|
} catch (error) {
|
|
console.error(`Error loading custom theme from file ${fileName}:`, error);
|
|
}
|
|
}
|
|
|
|
if (this.externalCustomThemes.length === 0) {
|
|
// console.info('No external custom themes loaded.');
|
|
} else {
|
|
// console.info(`Loaded ${this.externalCustomThemes.length} external custom theme styles.`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading external custom themes:', error);
|
|
}
|
|
}
|
|
|
|
extractThemesFromCSS(cssText) {
|
|
const themeRegex = /\[data-theme="([^"]+)"\]\s*\{([^}]+)\}/g;
|
|
let match;
|
|
const themes = [];
|
|
|
|
while ((match = themeRegex.exec(cssText)) !== null) {
|
|
const themeValue = match[1];
|
|
const themeName = this.formatThemeName(themeValue);
|
|
const variablesBlock = match[2];
|
|
const variables = this.parseCSSVariables(variablesBlock);
|
|
|
|
if (!themes.some(theme => theme.value === themeValue)) {
|
|
themes.push({ name: themeName, value: themeValue, variables: variables });
|
|
}
|
|
}
|
|
|
|
return themes;
|
|
}
|
|
|
|
formatStyleName(fileName) {
|
|
const nameWithoutExt = fileName.replace('.css', '');
|
|
const nameWithSpaces = nameWithoutExt.replace(/[_-]+/g, ' ');
|
|
return nameWithSpaces
|
|
.split(' ')
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ');
|
|
}
|
|
|
|
formatThemeName(themeValue) {
|
|
if (this.customLabels[themeValue]) {
|
|
return this.customLabels[themeValue];
|
|
}
|
|
return themeValue
|
|
.split('-')
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ');
|
|
}
|
|
|
|
applySavedTheme() {
|
|
const savedTheme = this.preferencesManager.get('selectedTheme');
|
|
if (savedTheme) {
|
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
}
|
|
}
|
|
|
|
applyTheme(theme) {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
this.preferencesManager.set('selectedTheme', theme);
|
|
}
|
|
|
|
addMenu() {
|
|
const targetElement = document.getElementById(this.targetElementId);
|
|
console.debug(`Attempting to add/populate theme selector in element: ${this.targetElementId}`);
|
|
|
|
if (!targetElement) {
|
|
console.error(`Element with ID "${this.targetElementId}" not found. Theme selector will not be injected.`);
|
|
return;
|
|
}
|
|
|
|
let selector = targetElement.querySelector('#theme-selector-dropdown');
|
|
|
|
if (!selector) {
|
|
selector = document.createElement('select');
|
|
selector.id = 'theme-selector-dropdown';
|
|
selector.style.cursor = 'pointer';
|
|
selector.setAttribute('aria-label', 'Select Theme');
|
|
|
|
targetElement.appendChild(selector);
|
|
// console.info('Theme selector dropdown created and injected.');
|
|
} else {
|
|
// console.info('Theme selector dropdown already exists. Populating with themes.');
|
|
selector.innerHTML = '';
|
|
}
|
|
|
|
const defaultOption = document.createElement('option');
|
|
defaultOption.value = '';
|
|
defaultOption.textContent = 'Set Theme';
|
|
defaultOption.disabled = true;
|
|
defaultOption.selected = true;
|
|
selector.appendChild(defaultOption);
|
|
|
|
this.themes.forEach(theme => {
|
|
if (theme.value !== 'create-custom') {
|
|
const option = document.createElement('option');
|
|
option.value = theme.value;
|
|
option.textContent = theme.name;
|
|
selector.appendChild(option);
|
|
}
|
|
});
|
|
|
|
const createCustomOption = document.createElement('option');
|
|
createCustomOption.value = 'create-custom';
|
|
createCustomOption.textContent = 'Create Custom Theme';
|
|
createCustomOption.id = `theme-option-create-custom`;
|
|
|
|
selector.appendChild(createCustomOption);
|
|
|
|
if (this.externalCustomThemes.length + this.customThemes.length > 0) {
|
|
const customThemesHeader = document.createElement('option');
|
|
customThemesHeader.textContent = 'Custom Themes';
|
|
customThemesHeader.disabled = true;
|
|
selector.appendChild(customThemesHeader);
|
|
|
|
const allCustomThemes = [
|
|
...this.externalCustomThemes.flatMap(style => style.themes),
|
|
...this.customThemes
|
|
];
|
|
|
|
const groupedThemes = this.groupThemesBySet(allCustomThemes);
|
|
|
|
for (const [setName, themes] of groupedThemes) {
|
|
const optgroup = document.createElement('optgroup');
|
|
optgroup.label = setName;
|
|
|
|
themes.forEach(theme => {
|
|
const option = document.createElement('option');
|
|
option.value = theme.value;
|
|
option.textContent = theme.name;
|
|
optgroup.appendChild(option);
|
|
});
|
|
|
|
selector.appendChild(optgroup);
|
|
}
|
|
}
|
|
|
|
const currentTheme = this.preferencesManager.get('selectedTheme') || this.getDefaultTheme();
|
|
selector.value = currentTheme;
|
|
|
|
selector.removeEventListener('change', this.handleThemeChange);
|
|
selector.addEventListener('change', this.handleThemeChange.bind(this));
|
|
|
|
// console.info('Theme selector dropdown populated with themes.');
|
|
}
|
|
|
|
groupThemesBySet(themes) {
|
|
const map = new Map();
|
|
themes.forEach(theme => {
|
|
const setName = theme.themesSetName || 'Default Set';
|
|
if (!map.has(setName)) {
|
|
map.set(setName, []);
|
|
}
|
|
map.get(setName).push(theme);
|
|
});
|
|
return map;
|
|
}
|
|
|
|
getDefaultTheme() {
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
const preferredTheme = this.themes.find(theme =>
|
|
theme.value.toLowerCase().includes(prefersDark ? 'dark' : 'light')
|
|
);
|
|
return preferredTheme ? preferredTheme.value : this.themes[0]?.value || 'flow-dark';
|
|
}
|
|
|
|
handleThemeChange(e) {
|
|
const selectedTheme = e.target.value;
|
|
if (selectedTheme === 'create-custom') {
|
|
this.openCustomThemeModal();
|
|
} else {
|
|
this.applyTheme(selectedTheme);
|
|
}
|
|
}
|
|
|
|
setupCustomThemeModal() {
|
|
if (document.getElementById(this.customThemeModalId)) {
|
|
console.warn('Custom Theme Modal already exists.');
|
|
return;
|
|
}
|
|
|
|
const modalHTML = `
|
|
<div id="${this.customThemeModalId}" class="custom-theme-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
|
<div class="modal-content">
|
|
<header class="modal-header">
|
|
<h2 id="modal-title">Create Custom Theme</h2>
|
|
<div id="theme-notification" class="notification" aria-live="polite" hidden></div>
|
|
|
|
<button type="button" class="close-button" aria-label="Close">×</button>
|
|
</header>
|
|
|
|
<div class="modal-body">
|
|
|
|
<form id="custom-theme-form" class="theme-form">
|
|
<div id="custom-themes-listing">
|
|
|
|
<div id="custom-themes-input">
|
|
|
|
<!-- Theme Set Section -->
|
|
<section class="form-section">
|
|
<fieldset>
|
|
|
|
<div class="form-group">
|
|
<label for="theme-set-select">Select Theme Set</label>
|
|
<select id="theme-set-select" name="themeSet" required>
|
|
${this.themeSets.length > 0 ?
|
|
this.themeSets.map(set => `<option value="${this.escapeHTML(set)}">${this.escapeHTML(set)}</option>`).join('') :
|
|
`<option value="" disabled selected>No Theme Sets Available</option>`
|
|
}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group new-theme-set">
|
|
<input type="text" id="new-theme-set-name" name="newThemeSetName" placeholder="New Set Name">
|
|
<button type="button" id="save-theme-set-button" class="btn secondary">Add Theme Set</button>
|
|
</div>
|
|
|
|
</fieldset>
|
|
</section>
|
|
|
|
<!-- Theme Name Section -->
|
|
<section class="form-section">
|
|
<div class="form-group">
|
|
<input type="text" id="theme-name" name="themeName" required placeholder="Enter theme name">
|
|
</div>
|
|
</section>
|
|
<data value=""></data>
|
|
</div>
|
|
<!-- Custom Themes List -->
|
|
<aside class="custom-themes-section">
|
|
<h3>Custom Themes</h3>
|
|
<div id="custom-themes-list-container">
|
|
<ul id="custom-themes-list" class="custom-themes-list">
|
|
<li>No custom themes created yet.</li>
|
|
</ul>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
|
|
<!-- CSS Variables Section -->
|
|
<section class="form-section css-variables">
|
|
<fieldset>
|
|
<legend>Customize</legend>
|
|
<div class="variables-grid">
|
|
${this.cssVariables.map(variable => `
|
|
<div class="form-group color-field">
|
|
<label for="${this.escapeHTML(variable)}">${this.escapeHTML(this.formatLabel(variable))}</label>
|
|
<input type="color" id="${this.escapeHTML(variable)}" name="${this.escapeHTML(variable)}" value="${this.getCurrentCSSVariable(variable) || '#000000'}">
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</fieldset>
|
|
</section>
|
|
|
|
<!-- Actions -->
|
|
<div class="form-actions">
|
|
<button type="submit" id="save-theme-button" class="btn primary">Save Theme & Close</button>
|
|
<button type="button" id="save-and-continue-theme-button" class="btn secondary">Save Theme</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
CSS
|
|
`;
|
|
|
|
// document.head.appendChild(style);
|
|
this.attachModalEventListeners();
|
|
}
|
|
|
|
attachModalEventListeners() {
|
|
const modal = document.getElementById(this.customThemeModalId);
|
|
if (!modal) return;
|
|
|
|
const closeButton = modal.querySelector('.close-button');
|
|
if (closeButton) {
|
|
closeButton.addEventListener('click', this.closeCustomThemeModal.bind(this));
|
|
}
|
|
|
|
const modalHeader = modal.querySelector('.modal-header');
|
|
if (modalHeader) {
|
|
modalHeader.addEventListener('mousedown', this.onMouseDown.bind(this));
|
|
}
|
|
|
|
const saveThemeSetButton = modal.querySelector('#save-theme-set-button');
|
|
if (saveThemeSetButton) {
|
|
saveThemeSetButton.addEventListener('click', () => this.addThemeSet());
|
|
}
|
|
|
|
const form = modal.querySelector('#custom-theme-form');
|
|
if (form) {
|
|
form.addEventListener('submit', (event) => {
|
|
event.preventDefault();
|
|
this.saveCustomTheme(true);
|
|
});
|
|
|
|
const saveAndContinueButton = modal.querySelector('#save-and-continue-theme-button');
|
|
if (saveAndContinueButton) {
|
|
saveAndContinueButton.addEventListener('click', () => this.saveCustomTheme(false));
|
|
}
|
|
}
|
|
|
|
this.cssVariables.forEach(variable => {
|
|
const colorInput = modal.querySelector(`#${this.escapeHTML(variable)}`);
|
|
if (colorInput) {
|
|
colorInput.addEventListener('input', (e) => {
|
|
const colorValue = e.target.value;
|
|
this.applyInlineCustomCSS(variable, colorValue);
|
|
});
|
|
}
|
|
});
|
|
|
|
this.populateCustomThemesList();
|
|
}
|
|
|
|
escapeHTML(str) {
|
|
return str.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
addThemeSet() {
|
|
const input = document.getElementById('new-theme-set-name');
|
|
if (!input) {
|
|
this.displayNotification('Theme Set input field not found.', 'error');
|
|
return;
|
|
}
|
|
|
|
const setName = input.value.trim();
|
|
if (!setName) {
|
|
this.displayNotification('Theme Set name cannot be empty.', 'error');
|
|
return;
|
|
}
|
|
|
|
if (this.themeSets.includes(setName)) {
|
|
this.displayNotification('A Theme Set with this name already exists.', 'error');
|
|
return;
|
|
}
|
|
|
|
this.themeSets.push(setName);
|
|
this.saveThemeSetsToStorage();
|
|
this.updateThemeSetDropdown();
|
|
this.updateCustomThemesListUI();
|
|
input.value = '';
|
|
this.displayNotification(`Theme Set "${setName}" has been added.`, 'success');
|
|
}
|
|
|
|
deleteThemeSet(setName) {
|
|
const confirmDelete = confirm(`Are you sure you want to delete the Theme Set "${setName}" and all its themes?`);
|
|
if (!confirmDelete) return;
|
|
|
|
this.themeSets = this.themeSets.filter(set => set !== setName);
|
|
this.saveThemeSetsToStorage();
|
|
|
|
const themesToRemove = this.customThemes.filter(theme => theme.themesSetName === setName);
|
|
themesToRemove.forEach(theme => this.deleteCustomTheme(theme.id, false)); // false to prevent confirmation
|
|
|
|
const externalThemesToRemove = this.externalCustomThemes.filter(style => style.styleName === setName);
|
|
externalThemesToRemove.forEach(style => {
|
|
const styleElement = document.getElementById(`external-${style.styleName}.css`);
|
|
if (styleElement) {
|
|
styleElement.parentNode.removeChild(styleElement);
|
|
}
|
|
this.externalCustomThemes = this.externalCustomThemes.filter(s => s.styleName !== setName);
|
|
});
|
|
|
|
this.addMenu();
|
|
this.populateCustomThemesList();
|
|
|
|
this.displayNotification(`Theme Set "${setName}" and all its themes have been deleted.`, 'success');
|
|
}
|
|
|
|
updateThemeSetDropdown() {
|
|
const select = document.getElementById('theme-set-select');
|
|
if (!select) return;
|
|
|
|
select.innerHTML = '';
|
|
|
|
if (this.themeSets.length === 0) {
|
|
const noSetOption = document.createElement('option');
|
|
noSetOption.value = '';
|
|
noSetOption.textContent = 'No Theme Sets Available';
|
|
noSetOption.disabled = true;
|
|
noSetOption.selected = true;
|
|
select.appendChild(noSetOption);
|
|
select.disabled = true;
|
|
return;
|
|
}
|
|
|
|
this.themeSets.forEach(setName => {
|
|
const option = document.createElement('option');
|
|
option.value = setName;
|
|
option.textContent = setName;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
select.disabled = false;
|
|
}
|
|
|
|
formatLabel(variable) {
|
|
return this.customLabels[variable] || this.formatVariableName(variable);
|
|
}
|
|
|
|
formatVariableName(variable) {
|
|
return variable
|
|
.replace('--', '')
|
|
.replace(/-/g, ' ')
|
|
.replace(/\b\w/g, char => char.toUpperCase());
|
|
}
|
|
|
|
getCurrentCSSVariable(variable) {
|
|
return getComputedStyle(document.documentElement).getPropertyValue(variable).trim() || '#000000';
|
|
}
|
|
|
|
applyInlineCustomCSS(variable, value) {
|
|
document.documentElement.style.setProperty(variable, value);
|
|
}
|
|
|
|
openCustomThemeModal() {
|
|
const modal = document.getElementById(this.customThemeModalId);
|
|
if (modal) {
|
|
const currentTheme = this.preferencesManager.get('selectedTheme');
|
|
const activeThemeSet = this.findThemeSetByThemeValue(currentTheme);
|
|
const themeSetSelect = modal.querySelector('#theme-set-select');
|
|
if (themeSetSelect && activeThemeSet) {
|
|
themeSetSelect.value = activeThemeSet;
|
|
} else if (themeSetSelect && this.themeSets.length > 0) {
|
|
themeSetSelect.selectedIndex = 0;
|
|
}
|
|
|
|
modal.style.display = 'block';
|
|
this.resetCustomThemeForm();
|
|
this.populateCustomThemesList();
|
|
} else {
|
|
console.error('Custom Theme Modal not found.');
|
|
}
|
|
}
|
|
|
|
findThemeSetByThemeValue(themeValue) {
|
|
for (const theme of this.themes) {
|
|
if (theme.value === themeValue) {
|
|
return theme.themesSetName || 'Default Set';
|
|
}
|
|
}
|
|
|
|
for (const style of this.externalCustomThemes) {
|
|
if (style.themes.some(t => t.value === themeValue)) {
|
|
return style.styleName;
|
|
}
|
|
}
|
|
|
|
const customTheme = this.customThemes.find(t => t.value === themeValue);
|
|
if (customTheme) {
|
|
return customTheme.themesSetName;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
closeCustomThemeModal() {
|
|
const modal = document.getElementById(this.customThemeModalId);
|
|
if (modal) {
|
|
modal.style.display = 'none';
|
|
const currentTheme = this.preferencesManager.get('selectedTheme') || this.getDefaultTheme();
|
|
document.documentElement.removeAttribute('style');
|
|
this.applyTheme(currentTheme);
|
|
const selector = document.getElementById('theme-selector-dropdown');
|
|
if (selector) {
|
|
selector.value = currentTheme;
|
|
}
|
|
this.clearNotification();
|
|
}
|
|
}
|
|
|
|
resetCustomThemeForm() {
|
|
const form = document.getElementById('custom-theme-form');
|
|
if (form) {
|
|
form.reset();
|
|
const modal = document.getElementById(this.customThemeModalId);
|
|
const themeSetSelect = modal.querySelector('#theme-set-select');
|
|
if (themeSetSelect && this.themeSets.length > 0) {
|
|
|
|
const activeTheme = this.preferencesManager.get('selectedTheme');
|
|
const activeSet = this.findThemeSetByThemeValue(activeTheme);
|
|
if (activeSet) {
|
|
themeSetSelect.value = activeSet;
|
|
}
|
|
}
|
|
this.cssVariables.forEach(variable => {
|
|
const input = document.getElementById(variable);
|
|
if (input) {
|
|
input.value = this.getCurrentCSSVariable(variable) || '#000000';
|
|
this.applyInlineCustomCSS(variable, input.value);
|
|
}
|
|
});
|
|
this.currentEditingThemeId = null;
|
|
const modalTitle = modal.querySelector('h2');
|
|
if (modalTitle) {
|
|
modalTitle.textContent = 'Create Custom Theme';
|
|
}
|
|
const saveThemeButton = modal.querySelector('#save-theme-button');
|
|
if (saveThemeButton) {
|
|
saveThemeButton.textContent = 'Save Theme & Close';
|
|
}
|
|
const saveAndContinueButton = modal.querySelector('#save-and-continue-theme-button');
|
|
if (saveAndContinueButton) {
|
|
saveAndContinueButton.textContent = 'Save Theme';
|
|
}
|
|
this.clearNotification();
|
|
}
|
|
}
|
|
|
|
populateCustomThemesList() {
|
|
const customThemesList = document.getElementById('custom-themes-list');
|
|
if (!customThemesList) return;
|
|
|
|
customThemesList.innerHTML = '';
|
|
|
|
const allCustomThemes = [
|
|
...this.externalCustomThemes.flatMap(style => style.themes),
|
|
...this.customThemes
|
|
];
|
|
|
|
if (allCustomThemes.length === 0) {
|
|
const noThemesItem = document.createElement('li');
|
|
noThemesItem.textContent = 'No custom themes created yet.';
|
|
customThemesList.appendChild(noThemesItem);
|
|
return;
|
|
}
|
|
|
|
const groupedThemes = this.groupThemesBySet(allCustomThemes);
|
|
|
|
for (const [setName, themes] of groupedThemes) {
|
|
const themesSetItem = document.createElement('li');
|
|
themesSetItem.style.display = 'flex';
|
|
themesSetItem.style.justifyContent = 'space-between';
|
|
themesSetItem.style.alignItems = 'center';
|
|
themesSetItem.style.padding = '5px 0';
|
|
themesSetItem.style.marginTop = '10px';
|
|
themesSetItem.style.fontWeight = 'bold';
|
|
|
|
const setNameSpan = document.createElement('span');
|
|
setNameSpan.textContent = setName;
|
|
|
|
const buttonsContainer = document.createElement('div');
|
|
|
|
const deleteSetButton = document.createElement('button');
|
|
deleteSetButton.type = 'button';
|
|
deleteSetButton.textContent = 'Delete Set';
|
|
deleteSetButton.style.padding = '4px 8px';
|
|
deleteSetButton.style.cursor = 'pointer';
|
|
deleteSetButton.style.marginRight = '5px';
|
|
deleteSetButton.addEventListener('click', () => this.deleteThemeSet(setName));
|
|
|
|
const downloadSetButton = document.createElement('button');
|
|
downloadSetButton.type = 'button';
|
|
downloadSetButton.textContent = 'Download Set';
|
|
downloadSetButton.style.padding = '4px 8px';
|
|
downloadSetButton.style.cursor = 'pointer';
|
|
downloadSetButton.addEventListener('click', () => this.downloadThemeSet(setName));
|
|
|
|
buttonsContainer.appendChild(downloadSetButton);
|
|
buttonsContainer.appendChild(deleteSetButton);
|
|
|
|
themesSetItem.appendChild(setNameSpan);
|
|
themesSetItem.appendChild(buttonsContainer);
|
|
customThemesList.appendChild(themesSetItem);
|
|
|
|
themes.forEach(theme => {
|
|
const listItem = document.createElement('li');
|
|
listItem.style.display = 'flex';
|
|
listItem.style.justifyContent = 'space-between';
|
|
listItem.style.alignItems = 'center';
|
|
listItem.style.padding = '5px 0';
|
|
listItem.style.marginLeft = '20px';
|
|
|
|
const themeNameSpan = document.createElement('span');
|
|
themeNameSpan.textContent = `- ${theme.name}`;
|
|
|
|
const themeButtonsContainer = document.createElement('div');
|
|
|
|
if (this.customThemes.some(t => t.id === theme.id)) {
|
|
const editButton = document.createElement('button');
|
|
editButton.type = 'button';
|
|
editButton.textContent = 'Edit';
|
|
editButton.style.marginRight = '10px';
|
|
editButton.style.padding = '4px 8px';
|
|
editButton.style.cursor = 'pointer';
|
|
editButton.addEventListener('click', () => this.editCustomTheme(theme.id));
|
|
themeButtonsContainer.appendChild(editButton);
|
|
}
|
|
|
|
if (this.customThemes.some(t => t.id === theme.id)) {
|
|
const deleteButton = document.createElement('button');
|
|
deleteButton.type = 'button';
|
|
deleteButton.textContent = 'Delete';
|
|
deleteButton.style.padding = '4px 8px';
|
|
deleteButton.style.cursor = 'pointer';
|
|
deleteButton.addEventListener('click', () => this.deleteCustomTheme(theme.id));
|
|
themeButtonsContainer.appendChild(deleteButton);
|
|
}
|
|
|
|
listItem.appendChild(themeNameSpan);
|
|
listItem.appendChild(themeButtonsContainer);
|
|
|
|
customThemesList.appendChild(listItem);
|
|
});
|
|
}
|
|
}
|
|
|
|
downloadThemeSet(setName) {
|
|
const externalThemes = this.externalCustomThemes
|
|
.filter(style => style.styleName === setName)
|
|
.flatMap(style => style.themes);
|
|
|
|
const customThemes = this.customThemes
|
|
.filter(theme => theme.themesSetName === setName);
|
|
|
|
const allThemes = [...externalThemes, ...customThemes];
|
|
|
|
if (allThemes.length === 0) {
|
|
this.displayNotification(`No themes found in the set "${setName}".`, 'error');
|
|
return;
|
|
}
|
|
|
|
let cssContent = '';
|
|
allThemes.forEach(theme => {
|
|
cssContent += `[data-theme="${theme.value}"] {\n`;
|
|
if (this.customThemes.some(t => t.id === theme.id)) {
|
|
for (const [key, value] of Object.entries(theme.variables)) {
|
|
cssContent += ` ${key}: ${value};\n`;
|
|
}
|
|
} else {
|
|
for (const [key, value] of Object.entries(theme.variables)) {
|
|
cssContent += ` ${key}: ${value};\n`;
|
|
}
|
|
}
|
|
cssContent += '}\n\n';
|
|
});
|
|
|
|
const blob = new Blob([cssContent], { type: 'text/css' });
|
|
const fileName = this.generateFileName(setName);
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(blob);
|
|
link.download = fileName;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(link.href);
|
|
|
|
this.displayNotification(`Theme Set "${setName}" has been downloaded as "${fileName}".`, 'success');
|
|
}
|
|
|
|
generateFileName(setName) {
|
|
const fileName = setName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'custom-theme';
|
|
return `${fileName}.css`;
|
|
}
|
|
|
|
editCustomTheme(themeId) {
|
|
const theme = this.customThemes.find(t => t.id === themeId);
|
|
if (!theme) {
|
|
this.displayNotification(`Theme with ID "${themeId}" not found or cannot be edited (external themes).`, 'error');
|
|
return;
|
|
}
|
|
|
|
const form = document.getElementById('custom-theme-form');
|
|
if (form) {
|
|
const modal = document.getElementById(this.customThemeModalId);
|
|
const nameInput = modal.querySelector('#theme-name');
|
|
const themeSetSelect = modal.querySelector('#theme-set-select');
|
|
|
|
if (nameInput) {
|
|
nameInput.value = theme.name;
|
|
}
|
|
if (themeSetSelect) {
|
|
themeSetSelect.value = theme.themesSetName;
|
|
}
|
|
this.cssVariables.forEach(variable => {
|
|
const input = document.getElementById(variable);
|
|
if (input) {
|
|
input.value = theme.variables[variable] || '#000000';
|
|
this.applyInlineCustomCSS(variable, input.value);
|
|
}
|
|
});
|
|
this.currentEditingThemeId = themeId;
|
|
const modalTitle = modal.querySelector('h2');
|
|
if (modalTitle) {
|
|
modalTitle.textContent = 'Edit Custom Theme';
|
|
}
|
|
const saveThemeButton = modal.querySelector('#save-theme-button');
|
|
if (saveThemeButton) {
|
|
saveThemeButton.textContent = 'Update Theme & Close';
|
|
}
|
|
const saveAndContinueButton = modal.querySelector('#save-and-continue-theme-button');
|
|
if (saveAndContinueButton) {
|
|
saveAndContinueButton.textContent = 'Update Theme';
|
|
}
|
|
this.displayNotification(`Editing theme "${theme.name}". Make your changes and click "Update Theme".`, 'info');
|
|
}
|
|
}
|
|
|
|
deleteCustomTheme(themeId, confirmDelete = true) {
|
|
const theme = this.customThemes.find(t => t.id === themeId);
|
|
if (!theme) {
|
|
this.displayNotification(`Theme with ID "${themeId}" not found or cannot be deleted (external themes).`, 'error');
|
|
return;
|
|
}
|
|
|
|
if (confirmDelete) {
|
|
const userConfirmed = confirm(`Are you sure you want to delete the theme "${theme.name}"?`);
|
|
if (!userConfirmed) return;
|
|
}
|
|
|
|
const styleId = `custom-theme-${theme.value}`;
|
|
const styleElement = document.getElementById(styleId);
|
|
if (styleElement) {
|
|
styleElement.parentNode.removeChild(styleElement);
|
|
}
|
|
|
|
this.customThemes = this.customThemes.filter(t => t.id !== themeId);
|
|
|
|
this.saveCustomThemesToStorage();
|
|
|
|
this.addMenu();
|
|
this.populateCustomThemesList();
|
|
|
|
if (confirmDelete) {
|
|
this.displayNotification('Theme deleted successfully.', 'success');
|
|
}
|
|
}
|
|
|
|
saveCustomTheme(closeAfterSave = true) {
|
|
const form = document.getElementById('custom-theme-form');
|
|
if (!form) return;
|
|
|
|
const formData = new FormData(form);
|
|
const themeName = formData.get('themeName')?.trim() || `Theme ${Date.now()}`;
|
|
const themesSetName = formData.get('themeSet') || 'Default Set';
|
|
const themeValue = this.generateThemeValue(themeName);
|
|
|
|
const themeVariables = {};
|
|
this.cssVariables.forEach(variable => {
|
|
themeVariables[variable] = formData.get(variable) || '#000000';
|
|
});
|
|
|
|
if (this.currentEditingThemeId) {
|
|
const index = this.customThemes.findIndex(t => t.id === this.currentEditingThemeId);
|
|
if (index !== -1) {
|
|
this.customThemes[index] = {
|
|
...this.customThemes[index],
|
|
name: themeName,
|
|
value: themeValue,
|
|
themesSetName,
|
|
variables: themeVariables
|
|
};
|
|
}
|
|
} else {
|
|
this.customThemes.push({
|
|
id: `custom-${Date.now()}`,
|
|
name: themeName,
|
|
value: themeValue,
|
|
themesSetName,
|
|
variables: themeVariables
|
|
});
|
|
}
|
|
|
|
this.appendCustomCSSToDocument(themeValue, themeVariables);
|
|
localStorage.setItem('customThemes', JSON.stringify(this.customThemes));
|
|
this.addMenu();
|
|
this.applyTheme(themeValue);
|
|
|
|
if (closeAfterSave) {
|
|
this.closeCustomThemeModal();
|
|
}
|
|
}
|
|
|
|
generateThemeValue(themeName) {
|
|
return themeName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || `custom-theme-${Date.now()}`;
|
|
}
|
|
|
|
loadCustomThemesFromStorage() {
|
|
const storedThemes = localStorage.getItem('customThemes');
|
|
if (storedThemes) {
|
|
this.customThemes = JSON.parse(storedThemes);
|
|
this.customThemes.forEach(theme => {
|
|
this.appendCustomCSSToDocument(theme.value, theme.variables);
|
|
});
|
|
}
|
|
}
|
|
|
|
saveCustomThemesToStorage() {
|
|
localStorage.setItem('customThemes', JSON.stringify(this.customThemes));
|
|
}
|
|
|
|
loadThemeSetsFromStorage() {
|
|
const storedSets = localStorage.getItem('themeSets');
|
|
if (storedSets) {
|
|
this.themeSets = JSON.parse(storedSets);
|
|
} else {
|
|
this.themeSets = [];
|
|
this.saveThemeSetsToStorage();
|
|
}
|
|
}
|
|
|
|
saveThemeSetsToStorage() {
|
|
localStorage.setItem('themeSets', JSON.stringify(this.themeSets));
|
|
}
|
|
|
|
generateCustomCSS(themeValue, variables) {
|
|
let css = `[data-theme="${themeValue}"] {\n`;
|
|
for (const [key, value] of Object.entries(variables)) {
|
|
css += ` ${key}: ${value};\n`;
|
|
}
|
|
css += '}';
|
|
return css;
|
|
}
|
|
|
|
appendCustomCSSToDocument(themeValue, variables) {
|
|
const styleId = `custom-theme-${themeValue}`;
|
|
let styleElement = document.getElementById(styleId);
|
|
|
|
if (styleElement) {
|
|
styleElement.parentNode.removeChild(styleElement);
|
|
}
|
|
|
|
styleElement = document.createElement('style');
|
|
styleElement.type = 'text/css';
|
|
styleElement.id = styleId;
|
|
let css = `[data-theme="${themeValue}"] {\n`;
|
|
for (const [key, value] of Object.entries(variables)) {
|
|
css += ` ${key}: ${value};\n`;
|
|
}
|
|
css += '}';
|
|
styleElement.appendChild(document.createTextNode(css));
|
|
document.head.appendChild(styleElement);
|
|
}
|
|
|
|
updateCustomCSSInDocument(themeValue, variables) {
|
|
this.appendCustomCSSToDocument(themeValue, variables);
|
|
}
|
|
|
|
appendExternalCSS(fileName, cssText) {
|
|
const style = document.createElement('style');
|
|
style.id = `external-${fileName}`;
|
|
style.appendChild(document.createTextNode(cssText));
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
static applyTheme(theme, preferencesManager) {
|
|
if (!theme || !preferencesManager) return;
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
preferencesManager.set('selectedTheme', theme);
|
|
}
|
|
|
|
onMouseDown(e) {
|
|
e.preventDefault();
|
|
|
|
const modal = document.getElementById(this.customThemeModalId);
|
|
if (!modal) return;
|
|
|
|
this.isDragging = true;
|
|
|
|
const rect = modal.getBoundingClientRect();
|
|
this.dragOffset.x = e.clientX - rect.left;
|
|
this.dragOffset.y = e.clientY - rect.top;
|
|
|
|
document.addEventListener('mousemove', this.onMouseMove);
|
|
document.addEventListener('mouseup', this.onMouseUp);
|
|
}
|
|
|
|
onMouseMove(e) {
|
|
if (!this.isDragging) return;
|
|
|
|
const newLeft = e.clientX - this.dragOffset.x;
|
|
const newTop = e.clientY - this.dragOffset.y;
|
|
|
|
const modal = document.getElementById(this.customThemeModalId);
|
|
if (!modal) return;
|
|
|
|
const modalWidth = modal.offsetWidth;
|
|
const modalHeight = modal.offsetHeight;
|
|
const windowWidth = window.innerWidth;
|
|
const windowHeight = window.innerHeight;
|
|
|
|
const clampedLeft = Math.max(0, Math.min(newLeft, windowWidth - modalWidth));
|
|
const clampedTop = Math.max(0, Math.min(newTop, windowHeight - modalHeight));
|
|
|
|
if (this.animationFrame) {
|
|
cancelAnimationFrame(this.animationFrame);
|
|
}
|
|
|
|
this.animationFrame = requestAnimationFrame(() => {
|
|
modal.style.left = `${clampedLeft}px`;
|
|
modal.style.top = `${clampedTop}px`;
|
|
modal.style.transform = `translate(0, 0)`;
|
|
});
|
|
}
|
|
|
|
onMouseUp() {
|
|
if (!this.isDragging) return;
|
|
this.isDragging = false;
|
|
|
|
document.removeEventListener('mousemove', this.onMouseMove);
|
|
document.removeEventListener('mouseup', this.onMouseUp);
|
|
|
|
if (this.animationFrame) {
|
|
cancelAnimationFrame(this.animationFrame);
|
|
this.animationFrame = null;
|
|
}
|
|
}
|
|
|
|
displayNotification(message, type) {
|
|
const notification = document.getElementById('theme-notification');
|
|
if (!notification) return;
|
|
|
|
notification.textContent = message;
|
|
notification.style.display = 'block';
|
|
notification.hidden = false;
|
|
|
|
switch (type) {
|
|
case 'success':
|
|
// notification.style.backgroundColor = '#d4edda';
|
|
notification.style.color = '#50B452';
|
|
// notification.style.border = '1px solid #c3e6cb';
|
|
break;
|
|
case 'error':
|
|
// notification.style.backgroundColor = '#f8d7da';
|
|
notification.style.color = '#D24141';
|
|
// notification.style.border = '1px solid #f5c6cb';
|
|
break;
|
|
case 'info':
|
|
// notification.style.backgroundColor = '#1e1e1f';
|
|
notification.style.color = '#628BC0';
|
|
// notification.style.border = '1px solid #bee5eb';
|
|
break;
|
|
default:
|
|
notification.style.backgroundColor = '#1e1e1f';
|
|
notification.style.color = '#bababa';
|
|
}
|
|
|
|
setTimeout(() => {
|
|
this.clearNotification();
|
|
}, 5000);
|
|
}
|
|
|
|
|
|
clearNotification() {
|
|
const notification = document.getElementById('theme-notification');
|
|
if (!notification) return;
|
|
|
|
notification.textContent = '';
|
|
notification.style.display = 'none';
|
|
notification.hidden = true;
|
|
}
|
|
|
|
updateCustomThemesListUI() {
|
|
this.populateCustomThemesList();
|
|
}
|
|
}
|
|
|
|
export default ThemeManager;
|