-
🌐 async-utils.jsWeb, 3.69 kB⬇
-
🌐 confirm-dialog.jsWeb, 3.04 kB⬇
-
🌐 conversation-format-utils.jsWeb, 17.28 kB⬇
-
🌐 dom-utils.jsWeb, 2.92 kB⬇
-
🌐 format-utils.jsWeb, 20.91 kB⬇
-
🌐 overlay-utils.jsWeb, 1.99 kB⬇
-
🌐 selection-manager.jsWeb, 3.76 kB⬇
-
🌐 settings-utils.jsWeb, 22.87 kB⬇
-
🌐 theme-utils.jsWeb, 6.78 kB⬇
/**
* Theme loading, dynamic stylesheet injection, theme indexing, and page loader transitions.
*/
import { queryEl, queryAll } from './dom-utils.js';
export const DEFAULT_THEME = 'theme_dark';
export const DEFAULT_THEME_FAMILY = 'DARK';
let cachedThemeIndex = null;
/**
* Fetches and caches the themes index from themes/theme_index.json.
* @returns {Promise<object>} The theme index mapping families to lists of themes.
*/
export async function fetchThemeIndex() {
if (cachedThemeIndex) {
return cachedThemeIndex;
}
try {
const response = await fetch('themes/theme_index.json');
if (response.ok) {
cachedThemeIndex = await response.json();
return cachedThemeIndex;
}
} catch (e) {
console.warn('Failed to fetch theme_index.json, using fallback', e);
}
// Fallback structure
cachedThemeIndex = {
DARK: {
name: 'Dark',
themes: [{ key: 'theme_dark', name: 'Dark' }]
},
LIGHT: {
name: 'Light',
themes: [{ key: 'theme_light', name: 'Light' }]
}
};
return cachedThemeIndex;
}
/**
* Returns a flat array of all valid theme keys across all families.
* @param {object} [themeIndex=null] - Optional theme index object.
* @returns {string[]} Array of theme keys.
*/
export function getAllThemeKeys(themeIndex = null) {
const index = themeIndex || cachedThemeIndex;
if (!index || typeof index !== 'object') return [DEFAULT_THEME];
const keys = [];
for (const familyKey in index) {
if (Object.prototype.hasOwnProperty.call(index, familyKey)) {
const family = index[familyKey];
if (Array.isArray(family.themes)) {
family.themes.forEach(t => {
if (t && t.key) keys.push(t.key);
});
}
}
}
return keys;
}
/**
* Finds the family key (e.g. 'DARK', 'GRAY', 'LIGHT') that contains the given theme key.
* @param {string} themeKey - The theme key to search for.
* @param {object} [themeIndex=null] - Optional theme index.
* @returns {string} The family key, or DEFAULT_THEME_FAMILY if not found.
*/
export function getThemeFamilyForTheme(themeKey, themeIndex = null) {
const index = themeIndex || cachedThemeIndex;
if (!index || typeof index !== 'object') return DEFAULT_THEME_FAMILY;
for (const familyKey in index) {
if (Object.prototype.hasOwnProperty.call(index, familyKey)) {
const family = index[familyKey];
if (Array.isArray(family.themes)) {
const match = family.themes.find(t => t.key === themeKey);
if (match) return familyKey;
}
}
}
return DEFAULT_THEME_FAMILY;
}
/**
* Populates a select dropdown with theme families.
* @param {HTMLSelectElement|null} selectEl - Target select element for families.
* @param {object} themeIndex - Theme index map.
* @param {string} [selectedFamily='DARK'] - Selected family key.
*/
export function populateThemeFamilyDropdown(selectEl, themeIndex, selectedFamily = DEFAULT_THEME_FAMILY) {
if (!selectEl) return;
selectEl.innerHTML = '';
if (!themeIndex || typeof themeIndex !== 'object') return;
for (const key in themeIndex) {
if (Object.prototype.hasOwnProperty.call(themeIndex, key)) {
const family = themeIndex[key];
const option = document.createElement('option');
option.value = key;
option.textContent = family.name || key;
if (key === selectedFamily) {
option.selected = true;
}
selectEl.appendChild(option);
}
}
}
/**
* Populates a select dropdown with themes for a specific theme family.
* @param {HTMLSelectElement|null} selectEl - Target select element for themes.
* @param {object} themeIndex - Theme index map.
* @param {string} familyKey - Family key ('DARK' | 'GRAY' | 'LIGHT').
* @param {string} [selectedTheme=''] - Selected theme key.
*/
export function populateThemeDropdown(selectEl, themeIndex, familyKey, selectedTheme = '') {
if (!selectEl) return;
selectEl.innerHTML = '';
if (!themeIndex || typeof themeIndex !== 'object') return;
const family = themeIndex[familyKey];
if (!family || !Array.isArray(family.themes)) return;
family.themes.forEach(t => {
const option = document.createElement('option');
option.value = t.key;
option.textContent = t.name || t.key;
if (t.key === selectedTheme) {
option.selected = true;
}
selectEl.appendChild(option);
});
}
/**
* Fades out and hides the page loader element.
* @param {HTMLElement|null} [loaderEl=null] - Optional loader element reference.
*/
export function fadeOutLoader(loaderEl = null) {
const loader = loaderEl || queryEl('.page-loader');
if (loader) {
loader.style.opacity = '0';
loader.addEventListener('transitionend', () => {
loader.style.display = 'none';
}, { once: true });
}
}
/**
* Applies a theme by fetching root variable declarations from dynamic.php,
* updating the #theme-variables style element, and transitioning smoothly via View Transitions API.
* @param {string} theme - The theme identifier.
* @param {boolean} [includeHljs=true] - Whether to also swap the Highlight.js theme stylesheet.
* @param {Function} [onComplete] - Optional callback once theme stylesheet finishes applying.
*/
export async function applyTheme(theme, includeHljs = true, onComplete = null) {
const activeTheme = theme || DEFAULT_THEME;
const loader = queryEl('.page-loader');
try {
const response = await fetch(`dynamic.php?t=${encodeURIComponent(activeTheme)}`);
if (!response.ok) {
throw new Error(`Failed to fetch theme variables: ${response.status}`);
}
const cssText = await response.text();
const updateDom = () => {
let styleEl = document.getElementById('theme-variables');
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = 'theme-variables';
document.head.appendChild(styleEl);
}
styleEl.textContent = cssText;
void document.documentElement.offsetHeight;
if (includeHljs) {
const hljsTheme = activeTheme.includes('light')
? 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css'
: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/dark.min.css';
const existingHljs = queryEl('link[href*="highlight.js"]');
if (existingHljs && existingHljs.getAttribute('href') !== hljsTheme) {
existingHljs.setAttribute('href', hljsTheme);
}
}
};
const isLoaderVisible = loader && getComputedStyle(loader).display !== 'none' && getComputedStyle(loader).opacity !== '0';
if (document.startViewTransition && !isLoaderVisible) {
document.documentElement.classList.add('theme-transitioning');
const transition = document.startViewTransition(() => {
updateDom();
});
transition.finished.finally(() => {
document.documentElement.classList.remove('theme-transitioning');
});
} else {
updateDom();
}
fadeOutLoader(loader);
if (typeof onComplete === 'function') onComplete(activeTheme);
} catch (e) {
console.warn('Failed to apply theme dynamically', e);
fadeOutLoader(loader);
if (typeof onComplete === 'function') onComplete(activeTheme);
}
}
theme-utils.js
×
Type: Web, text/x-java
6.78 Kilobytes
Last Modified 2026-09-23 02:24:05
⬇ Download File
Type: Web, text/x-java
6.78 Kilobytes
Last Modified 2026-09-23 02:24:05
⬇ Download File