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 = ` `; 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, "'"); } 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;