Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
638 lines
24 KiB
JavaScript
638 lines
24 KiB
JavaScript
// ========== THEME PICKER + THEME BUILDER ==========
|
|
(function () {
|
|
var themeBeforeBuilder = null;
|
|
var editingSlug = null;
|
|
var advancedOverrides = {}; // tracks which advanced fields user manually changed
|
|
|
|
// Display names for built-in themes
|
|
var THEME_LABELS = {
|
|
dark: 'Dark',
|
|
light: 'Light',
|
|
blue: 'Blue',
|
|
black: 'Black',
|
|
nord: 'Nord',
|
|
dracula: 'Dracula',
|
|
'solarized-dark': 'Solarized Dark',
|
|
'solarized-light': 'Solarized Light',
|
|
taxi: 'Taxi',
|
|
ocean: 'Ocean',
|
|
};
|
|
|
|
// Color picker field definitions: [css-prop, label, section]
|
|
var FIELDS = [
|
|
['bg', 'Background', 'base'],
|
|
['card-base', 'Card', 'base'],
|
|
['fg', 'Text', 'base'],
|
|
['muted', 'Muted Text', 'base'],
|
|
['border', 'Border', 'base'],
|
|
['accent', 'Accent', 'accent'],
|
|
['accent-strong', 'Accent Strong', 'accent'],
|
|
['ok-bg', 'OK Background', 'status'],
|
|
['ok-fg', 'OK Text', 'status'],
|
|
['bad-bg', 'Error Bg', 'status'],
|
|
['bad-fg', 'Error Text', 'status'],
|
|
['dot-ok', 'Dot OK', 'status'],
|
|
['dot-bad', 'Dot Error', 'status'],
|
|
['uptime', 'Uptime Bar', 'status'],
|
|
['hover', 'Hover', 'advanced'],
|
|
['card-hover', 'Card Hover', 'advanced'],
|
|
['base', 'Tags/Badges', 'advanced'],
|
|
['fg-muted', 'Dim Text', 'advanced'],
|
|
['success', 'Success', 'advanced'],
|
|
['error', 'Error', 'advanced'],
|
|
['warning', 'Warning', 'advanced'],
|
|
];
|
|
|
|
// ─── Theme Cycle Button ───────────────────────────────
|
|
|
|
var themeBtn = document.getElementById('theme');
|
|
if (!themeBtn) return;
|
|
var themeLabel = document.getElementById('theme-label');
|
|
|
|
function getLabelFor(t) {
|
|
if (THEME_LABELS[t]) return THEME_LABELS[t];
|
|
var userThemes = safeGetJSON(window.USER_THEMES_KEY, {});
|
|
if (userThemes[t]) return userThemes[t].name || t;
|
|
return t;
|
|
}
|
|
|
|
function updateLabel() {
|
|
if (themeLabel) themeLabel.textContent = getLabelFor(window.getActiveTheme());
|
|
}
|
|
|
|
themeBtn.addEventListener('click', function () {
|
|
var list = window.THEMES.slice();
|
|
var current = window.getActiveTheme();
|
|
var idx = list.indexOf(current);
|
|
var next = list[(idx + 1) % list.length];
|
|
window.applyTheme(next);
|
|
updateLabel();
|
|
});
|
|
|
|
updateLabel();
|
|
|
|
// ─── Theme Builder Modal ───────────────────────────────
|
|
|
|
function buildFieldsHTML() {
|
|
var sections = { base: 'Base Colors', accent: 'Accent', status: 'Status', advanced: 'Advanced (auto-derived)' };
|
|
var grouped = {};
|
|
FIELDS.forEach(function (f) {
|
|
if (!grouped[f[2]]) grouped[f[2]] = [];
|
|
grouped[f[2]].push(f);
|
|
});
|
|
|
|
var html = '';
|
|
Object.keys(sections).forEach(function (key) {
|
|
if (key === 'advanced') {
|
|
html += '<div id="theme-builder-advanced-toggle" class="theme-builder-advanced-toggle" style="margin:12px 0 4px;cursor:pointer;color:var(--accent);font-size:.85rem;user-select:none;">Show advanced colors ▼</div>';
|
|
html += '<div id="theme-builder-advanced" class="theme-builder-section" style="display:none;">';
|
|
} else {
|
|
html += '<div class="theme-builder-section">';
|
|
}
|
|
html += '<div class="theme-builder-section-title">' + sections[key] + '</div>';
|
|
(grouped[key] || []).forEach(function (f) {
|
|
html += '<div class="theme-builder-row">' +
|
|
'<span class="theme-builder-label">' + f[1] + '</span>' +
|
|
'<input type="color" class="theme-builder-color" data-prop="' + f[0] + '"' + (key === 'advanced' ? ' data-advanced="1"' : '') + ' value="#000000" />' +
|
|
'<span class="theme-builder-hex" data-hex="' + f[0] + '">#000000</span>' +
|
|
'</div>';
|
|
});
|
|
html += '</div>';
|
|
});
|
|
return html;
|
|
}
|
|
|
|
function buildStartFromOptions() {
|
|
return window.THEMES.map(function (t) {
|
|
return '<option value="' + t + '">' + getLabelFor(t) + '</option>';
|
|
}).join('');
|
|
}
|
|
|
|
var modalHTML = '<div id="theme-builder-modal" class="weather-modal">' +
|
|
'<div class="weather-modal-content" style="min-width:420px;max-width:560px;">' +
|
|
'<h3>Theme Builder</h3>' +
|
|
|
|
// Edit existing user themes dropdown
|
|
'<div id="theme-builder-existing" style="margin-bottom:16px;display:none;">' +
|
|
'<label style="font-size:.85rem;color:var(--muted);margin-right:8px;">Edit:</label>' +
|
|
'<select id="theme-builder-edit-select" style="padding:6px 10px;background:var(--card-bg);color:var(--fg);border:1px solid var(--border);border-radius:6px;font-size:.85rem;margin-right:8px;">' +
|
|
'<option value="">— New Theme —</option>' +
|
|
'</select>' +
|
|
'<button id="theme-builder-delete" style="padding:4px 10px;background:color-mix(in srgb,var(--bad-fg) 15%,transparent);border:1px solid var(--bad-fg);color:var(--bad-fg);border-radius:6px;font-size:.8rem;cursor:pointer;">Delete</button>' +
|
|
'</div>' +
|
|
|
|
'<div style="margin-bottom:16px;">' +
|
|
'<label style="font-size:.85rem;color:var(--muted);margin-right:8px;">Name:</label>' +
|
|
'<input type="text" id="theme-builder-name" maxlength="20" placeholder="My Theme" ' +
|
|
'style="padding:6px 10px;background:var(--card-bg);color:var(--fg);border:1px solid var(--border);border-radius:6px;font-size:.85rem;width:140px;" />' +
|
|
'</div>' +
|
|
|
|
'<div style="margin-bottom:16px;display:flex;align-items:center;">' +
|
|
'<label style="font-size:.85rem;color:var(--muted);margin-right:8px;cursor:pointer;" for="theme-builder-lightbg">' +
|
|
'<input type="checkbox" id="theme-builder-lightbg" style="margin-right:6px;cursor:pointer;vertical-align:middle;" />' +
|
|
'Light background (use dark logo)</label>' +
|
|
'</div>' +
|
|
|
|
'<div style="margin-bottom:16px;">' +
|
|
'<label style="font-size:.85rem;color:var(--muted);margin-right:8px;">Start from:</label>' +
|
|
'<select id="theme-builder-start" style="padding:6px 10px;background:var(--card-bg);color:var(--fg);border:1px solid var(--border);border-radius:6px;font-size:.85rem;">' +
|
|
buildStartFromOptions() +
|
|
'</select>' +
|
|
'</div>' +
|
|
|
|
'<div class="theme-builder-preview" id="theme-builder-preview">' +
|
|
'<div class="theme-builder-card" id="theme-preview-card">' +
|
|
'<div class="preview-title">Sample Card</div>' +
|
|
'<div class="preview-muted">Secondary text preview</div>' +
|
|
'<div class="preview-badges">' +
|
|
'<span class="preview-badge" id="preview-badge-ok">ON</span>' +
|
|
'<span class="preview-badge" id="preview-badge-bad">OFF</span>' +
|
|
'</div>' +
|
|
'<div class="preview-dots">' +
|
|
'<span><span class="preview-dot" id="preview-dot-ok"></span> Online</span>' +
|
|
'<span><span class="preview-dot" id="preview-dot-bad"></span> Offline</span>' +
|
|
'</div>' +
|
|
'<button class="preview-btn" id="preview-accent-btn">Accent Button</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
buildFieldsHTML() +
|
|
|
|
'<div class="weather-modal-buttons">' +
|
|
'<button id="theme-builder-cancel">Cancel</button>' +
|
|
'<button id="theme-builder-import" style="margin-left:auto;" title="Import theme from JSON file">Import</button>' +
|
|
'<button id="theme-builder-export" title="Export theme as JSON file">Export</button>' +
|
|
'<button id="theme-builder-save" class="btn-accent">Save Theme</button>' +
|
|
'</div>' +
|
|
'</div></div>';
|
|
|
|
injectModal('theme-builder-modal', modalHTML);
|
|
|
|
var modal = document.getElementById('theme-builder-modal');
|
|
var startSelect = document.getElementById('theme-builder-start');
|
|
var nameInput = document.getElementById('theme-builder-name');
|
|
var editSelect = document.getElementById('theme-builder-edit-select');
|
|
var existingSection = document.getElementById('theme-builder-existing');
|
|
var lightBgCheck = document.getElementById('theme-builder-lightbg');
|
|
var pickers = modal.querySelectorAll('.theme-builder-color');
|
|
var advancedSection = document.getElementById('theme-builder-advanced');
|
|
var advancedToggle = document.getElementById('theme-builder-advanced-toggle');
|
|
var exportBtn = document.getElementById('theme-builder-export');
|
|
|
|
// ─── Advanced toggle ───────────────────────────────
|
|
|
|
var advancedVisible = false;
|
|
advancedToggle.addEventListener('click', function () {
|
|
advancedVisible = !advancedVisible;
|
|
advancedSection.style.display = advancedVisible ? '' : 'none';
|
|
advancedToggle.innerHTML = advancedVisible
|
|
? 'Hide advanced colors ▲'
|
|
: 'Show advanced colors ▼';
|
|
});
|
|
|
|
// ─── Color picker helpers ───────────────────────────────
|
|
|
|
function getCurrentColors() {
|
|
var colors = {};
|
|
pickers.forEach(function (p) {
|
|
colors[p.dataset.prop] = p.value;
|
|
});
|
|
colors['card-bg'] = colors['card-base'];
|
|
return colors;
|
|
}
|
|
|
|
function getBaseColors() {
|
|
var colors = {};
|
|
pickers.forEach(function (p) {
|
|
if (!p.dataset.advanced) colors[p.dataset.prop] = p.value;
|
|
});
|
|
colors['card-bg'] = colors['card-base'];
|
|
if (lightBgCheck.checked) colors.lightBg = true;
|
|
return colors;
|
|
}
|
|
|
|
function updateAdvancedFromDerived() {
|
|
var base = getBaseColors();
|
|
var derived = window.deriveExtendedColors ? window.deriveExtendedColors(base) : {};
|
|
pickers.forEach(function (p) {
|
|
if (p.dataset.advanced && !advancedOverrides[p.dataset.prop]) {
|
|
var val = derived[p.dataset.prop] || '#333333';
|
|
p.value = val;
|
|
var hex = modal.querySelector('[data-hex="' + p.dataset.prop + '"]');
|
|
if (hex) hex.textContent = val.toUpperCase();
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadThemeIntoPickers(themeName) {
|
|
var colors = window.THEME_COLORS[themeName];
|
|
if (!colors) return;
|
|
advancedOverrides = {};
|
|
pickers.forEach(function (p) {
|
|
var val = colors[p.dataset.prop] || '#000000';
|
|
if (val.startsWith('rgba') || val.startsWith('color-mix')) val = '#333333';
|
|
p.value = val;
|
|
var hex = modal.querySelector('[data-hex="' + p.dataset.prop + '"]');
|
|
if (hex) hex.textContent = val.toUpperCase();
|
|
});
|
|
lightBgCheck.checked = !!colors.lightBg;
|
|
updatePreview();
|
|
}
|
|
|
|
function loadUserThemeIntoPickers(slug) {
|
|
var userThemes = safeGetJSON(window.USER_THEMES_KEY, {});
|
|
var theme = userThemes[slug];
|
|
if (!theme) return;
|
|
advancedOverrides = {};
|
|
pickers.forEach(function (p) {
|
|
var val = theme[p.dataset.prop] || '#000000';
|
|
p.value = val;
|
|
var hex = modal.querySelector('[data-hex="' + p.dataset.prop + '"]');
|
|
if (hex) hex.textContent = val.toUpperCase();
|
|
// If theme has explicit advanced values, mark as overridden
|
|
if (p.dataset.advanced && theme[p.dataset.prop]) {
|
|
advancedOverrides[p.dataset.prop] = true;
|
|
}
|
|
});
|
|
nameInput.value = theme.name || '';
|
|
lightBgCheck.checked = !!theme.lightBg;
|
|
// Fill any missing advanced fields from derivation
|
|
updateAdvancedFromDerived();
|
|
updatePreview();
|
|
}
|
|
|
|
function updatePreview() {
|
|
var c = getCurrentColors();
|
|
var preview = document.getElementById('theme-builder-preview');
|
|
var card = document.getElementById('theme-preview-card');
|
|
var title = card.querySelector('.preview-title');
|
|
var muted = card.querySelector('.preview-muted');
|
|
var badgeOk = document.getElementById('preview-badge-ok');
|
|
var badgeBad = document.getElementById('preview-badge-bad');
|
|
var dotOk = document.getElementById('preview-dot-ok');
|
|
var dotBad = document.getElementById('preview-dot-bad');
|
|
var btn = document.getElementById('preview-accent-btn');
|
|
var dots = card.querySelector('.preview-dots');
|
|
|
|
preview.style.background = c['bg'];
|
|
card.style.background = c['card-base'];
|
|
card.style.borderColor = c['border'];
|
|
title.style.color = c['fg'];
|
|
muted.style.color = c['muted'];
|
|
badgeOk.style.background = c['ok-bg'];
|
|
badgeOk.style.color = c['ok-fg'];
|
|
badgeBad.style.background = c['bad-bg'];
|
|
badgeBad.style.color = c['bad-fg'];
|
|
dotOk.style.background = c['dot-ok'];
|
|
dotBad.style.background = c['dot-bad'];
|
|
dots.style.color = c['fg'];
|
|
btn.style.background = c['accent'];
|
|
btn.style.color = c['bg'];
|
|
}
|
|
|
|
function applyColorsLive(colors) {
|
|
// Include derived colors for full live preview
|
|
var derived = window.deriveExtendedColors ? window.deriveExtendedColors(colors) : {};
|
|
window.THEME_PROPS.forEach(function (prop) {
|
|
var val = colors[prop] || derived[prop];
|
|
if (val) {
|
|
document.documentElement.style.setProperty('--' + prop, val);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wire up color pickers
|
|
pickers.forEach(function (p) {
|
|
p.addEventListener('input', function () {
|
|
var hex = modal.querySelector('[data-hex="' + p.dataset.prop + '"]');
|
|
if (hex) hex.textContent = p.value.toUpperCase();
|
|
|
|
// Track manual advanced overrides
|
|
if (p.dataset.advanced) {
|
|
advancedOverrides[p.dataset.prop] = true;
|
|
} else {
|
|
// Base color changed — re-derive advanced values (unless overridden)
|
|
updateAdvancedFromDerived();
|
|
}
|
|
|
|
updatePreview();
|
|
applyColorsLive(getCurrentColors());
|
|
});
|
|
});
|
|
|
|
// Light-bg checkbox should re-derive advanced colors
|
|
lightBgCheck.addEventListener('change', function () {
|
|
updateAdvancedFromDerived();
|
|
updatePreview();
|
|
applyColorsLive(getCurrentColors());
|
|
});
|
|
|
|
startSelect.addEventListener('change', function () {
|
|
advancedOverrides = {};
|
|
loadThemeIntoPickers(startSelect.value);
|
|
applyColorsLive(getCurrentColors());
|
|
});
|
|
|
|
// Populate and refresh the edit-existing dropdown
|
|
function refreshEditDropdown() {
|
|
var userThemes = safeGetJSON(window.USER_THEMES_KEY, {});
|
|
var slugs = Object.keys(userThemes);
|
|
editSelect.innerHTML = '<option value="">— New Theme —</option>';
|
|
slugs.forEach(function (slug) {
|
|
var opt = document.createElement('option');
|
|
opt.value = slug;
|
|
opt.textContent = userThemes[slug].name || slug;
|
|
editSelect.appendChild(opt);
|
|
});
|
|
existingSection.style.display = slugs.length ? '' : 'none';
|
|
}
|
|
|
|
// Also refresh "Start from" options
|
|
function refreshStartFrom() {
|
|
startSelect.innerHTML = buildStartFromOptions();
|
|
}
|
|
|
|
// Edit dropdown change
|
|
editSelect.addEventListener('change', function () {
|
|
var slug = this.value;
|
|
if (slug) {
|
|
editingSlug = slug;
|
|
loadUserThemeIntoPickers(slug);
|
|
applyColorsLive(getCurrentColors());
|
|
} else {
|
|
editingSlug = null;
|
|
advancedOverrides = {};
|
|
nameInput.value = '';
|
|
startSelect.value = window.getActiveTheme();
|
|
loadThemeIntoPickers(startSelect.value);
|
|
}
|
|
exportBtn.style.display = editingSlug ? '' : 'none';
|
|
});
|
|
|
|
function openBuilder() {
|
|
themeBeforeBuilder = window.getActiveTheme();
|
|
editingSlug = null;
|
|
advancedOverrides = {};
|
|
|
|
// Reset advanced section to collapsed
|
|
advancedVisible = false;
|
|
advancedSection.style.display = 'none';
|
|
advancedToggle.innerHTML = 'Show advanced colors ▼';
|
|
|
|
refreshEditDropdown();
|
|
refreshStartFrom();
|
|
|
|
var userThemes = safeGetJSON(window.USER_THEMES_KEY, {});
|
|
if (userThemes[themeBeforeBuilder]) {
|
|
// Active theme is a user theme — open it for editing
|
|
editSelect.value = themeBeforeBuilder;
|
|
editingSlug = themeBeforeBuilder;
|
|
loadUserThemeIntoPickers(themeBeforeBuilder);
|
|
} else {
|
|
editSelect.value = '';
|
|
nameInput.value = '';
|
|
startSelect.value = themeBeforeBuilder;
|
|
loadThemeIntoPickers(themeBeforeBuilder);
|
|
}
|
|
|
|
exportBtn.style.display = editingSlug ? '' : 'none';
|
|
modal.classList.add('show');
|
|
}
|
|
|
|
var customizeBtn = document.getElementById('theme-customize-btn');
|
|
if (customizeBtn) {
|
|
customizeBtn.addEventListener('click', function () { openBuilder(); });
|
|
}
|
|
|
|
// ─── Save ───────────────────────────────
|
|
|
|
document.getElementById('theme-builder-save').addEventListener('click', function () {
|
|
var colors = getCurrentColors();
|
|
var name = nameInput.value.trim();
|
|
if (!name) {
|
|
showNotification('Please enter a theme name', 'warning', 3000);
|
|
nameInput.focus();
|
|
return;
|
|
}
|
|
|
|
var userThemes = safeGetJSON(window.USER_THEMES_KEY, {});
|
|
var slug;
|
|
var oldSlug = null;
|
|
|
|
if (editingSlug) {
|
|
slug = editingSlug;
|
|
// If name changed, re-slugify and potentially rename
|
|
var newSlug = window.slugifyThemeName(name, editingSlug);
|
|
if (newSlug !== editingSlug) {
|
|
oldSlug = editingSlug;
|
|
delete userThemes[editingSlug];
|
|
var idx = window.THEMES.indexOf(editingSlug);
|
|
if (idx !== -1) window.THEMES.splice(idx, 1);
|
|
delete window.THEME_COLORS[editingSlug];
|
|
slug = newSlug;
|
|
}
|
|
} else {
|
|
slug = window.slugifyThemeName(name);
|
|
}
|
|
|
|
// Build theme data with ALL color properties
|
|
var themeData = { name: name };
|
|
if (lightBgCheck.checked) themeData.lightBg = true;
|
|
window.THEME_PROPS.forEach(function (p) {
|
|
if (colors[p]) themeData[p] = colors[p];
|
|
});
|
|
|
|
// Update localStorage cache immediately for instant UI
|
|
userThemes[slug] = themeData;
|
|
safeSet(window.USER_THEMES_KEY, JSON.stringify(userThemes));
|
|
|
|
// Clear inline preview properties
|
|
window.clearCustomProperties();
|
|
|
|
// Re-inject CSS and apply
|
|
window.injectUserThemeStyles();
|
|
window.applyTheme(slug);
|
|
modal.classList.remove('show');
|
|
updateLabel();
|
|
|
|
// Save to server (source of truth)
|
|
var apiColors = {};
|
|
window.THEME_PROPS.forEach(function (p) { if (colors[p]) apiColors[p] = colors[p]; });
|
|
|
|
// If slug changed, delete the old one from server
|
|
if (oldSlug) {
|
|
secureFetch('/api/v1/themes/' + oldSlug, { method: 'DELETE' }).catch(function () {});
|
|
}
|
|
|
|
secureFetch('/api/v1/themes/' + slug, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: name, colors: apiColors, lightBg: lightBgCheck.checked })
|
|
}).then(function () {
|
|
showNotification(name + ' theme saved', 'success', 3000);
|
|
}).catch(function () {
|
|
showNotification(name + ' theme saved locally (server sync failed)', 'warning', 3000);
|
|
});
|
|
});
|
|
|
|
// ─── Cancel ───────────────────────────────
|
|
|
|
document.getElementById('theme-builder-cancel').addEventListener('click', function () {
|
|
modal.classList.remove('show');
|
|
window.clearCustomProperties();
|
|
if (themeBeforeBuilder) {
|
|
window.applyTheme(themeBeforeBuilder);
|
|
}
|
|
});
|
|
|
|
// ─── Delete ───────────────────────────────
|
|
|
|
document.getElementById('theme-builder-delete').addEventListener('click', function () {
|
|
if (!editingSlug) return;
|
|
|
|
var userThemes = safeGetJSON(window.USER_THEMES_KEY, {});
|
|
var name = userThemes[editingSlug] ? userThemes[editingSlug].name : editingSlug;
|
|
|
|
if (!confirm('Delete "' + name + '" theme?')) return;
|
|
|
|
var slugToDelete = editingSlug;
|
|
|
|
// Update localStorage cache immediately
|
|
delete userThemes[slugToDelete];
|
|
safeSet(window.USER_THEMES_KEY, JSON.stringify(userThemes));
|
|
|
|
// Remove from runtime
|
|
var idx = window.THEMES.indexOf(slugToDelete);
|
|
if (idx !== -1) window.THEMES.splice(idx, 1);
|
|
delete window.THEME_COLORS[slugToDelete];
|
|
|
|
window.clearCustomProperties();
|
|
window.injectUserThemeStyles();
|
|
|
|
var fallback = themeBeforeBuilder && themeBeforeBuilder !== slugToDelete ? themeBeforeBuilder : 'dark';
|
|
window.applyTheme(fallback);
|
|
|
|
editingSlug = null;
|
|
modal.classList.remove('show');
|
|
updateLabel();
|
|
|
|
// Delete from server
|
|
secureFetch('/api/v1/themes/' + slugToDelete, { method: 'DELETE' }).then(function () {
|
|
showNotification(name + ' theme deleted', 'success', 3000);
|
|
}).catch(function () {
|
|
showNotification(name + ' theme deleted locally (server sync failed)', 'warning', 3000);
|
|
});
|
|
});
|
|
|
|
// ─── Export ───────────────────────────────
|
|
|
|
document.getElementById('theme-builder-export').addEventListener('click', function () {
|
|
if (!editingSlug) {
|
|
showNotification('Save the theme first, then export', 'warning', 3000);
|
|
return;
|
|
}
|
|
var userThemes = safeGetJSON(window.USER_THEMES_KEY, {});
|
|
var theme = userThemes[editingSlug];
|
|
if (!theme) return;
|
|
|
|
var exportData = {
|
|
_dashcaddy_theme: true,
|
|
version: '1.0',
|
|
exportDate: new Date().toISOString(),
|
|
slug: editingSlug,
|
|
name: theme.name,
|
|
lightBg: theme.lightBg || false,
|
|
colors: {}
|
|
};
|
|
window.THEME_PROPS.forEach(function (p) {
|
|
if (theme[p]) exportData.colors[p] = theme[p];
|
|
});
|
|
|
|
var blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
|
var url = URL.createObjectURL(blob);
|
|
var a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = editingSlug + '-theme.json';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
showNotification('Theme exported as ' + editingSlug + '-theme.json', 'success', 3000);
|
|
});
|
|
|
|
// ─── Import ───────────────────────────────
|
|
|
|
document.getElementById('theme-builder-import').addEventListener('click', function () {
|
|
var input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = '.json';
|
|
input.onchange = function (e) {
|
|
var file = e.target.files[0];
|
|
if (!file) return;
|
|
var reader = new FileReader();
|
|
reader.onload = function (ev) {
|
|
try {
|
|
var data = JSON.parse(ev.target.result);
|
|
|
|
// Support both wrapped format and raw theme format
|
|
var themeName, themeColors, themeLightBg;
|
|
if (data._dashcaddy_theme && data.colors) {
|
|
// Wrapped export format
|
|
themeName = data.name || 'Imported';
|
|
themeColors = data.colors;
|
|
themeLightBg = data.lightBg || false;
|
|
} else if (data.name && (data.bg || data['card-base'])) {
|
|
// Raw theme JSON (same format as server files)
|
|
themeName = data.name;
|
|
themeColors = {};
|
|
window.THEME_PROPS.forEach(function (p) { if (data[p]) themeColors[p] = data[p]; });
|
|
themeLightBg = data.lightBg || false;
|
|
} else {
|
|
throw new Error('Not a valid DashCaddy theme file');
|
|
}
|
|
|
|
// Load into builder
|
|
nameInput.value = themeName;
|
|
lightBgCheck.checked = !!themeLightBg;
|
|
editingSlug = null;
|
|
editSelect.value = '';
|
|
advancedOverrides = {};
|
|
|
|
// Apply colors to pickers
|
|
pickers.forEach(function (p) {
|
|
var val = themeColors[p.dataset.prop] || '#000000';
|
|
p.value = val;
|
|
var hex = modal.querySelector('[data-hex="' + p.dataset.prop + '"]');
|
|
if (hex) hex.textContent = val.toUpperCase();
|
|
if (p.dataset.advanced && themeColors[p.dataset.prop]) {
|
|
advancedOverrides[p.dataset.prop] = true;
|
|
}
|
|
});
|
|
|
|
// Fill any missing advanced fields
|
|
updateAdvancedFromDerived();
|
|
updatePreview();
|
|
applyColorsLive(getCurrentColors());
|
|
exportBtn.style.display = 'none';
|
|
showNotification('"' + themeName + '" loaded into builder. Click Save to keep it.', 'success', 5000);
|
|
} catch (err) {
|
|
showNotification('Import failed: ' + err.message, 'error', 5000);
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
};
|
|
input.click();
|
|
});
|
|
|
|
// ─── Modal close handling ───────────────────────────────
|
|
|
|
wireModal(modal);
|
|
|
|
modal.addEventListener('click', function (e) {
|
|
if (e.target === modal) {
|
|
window.clearCustomProperties();
|
|
if (themeBeforeBuilder) window.applyTheme(themeBeforeBuilder);
|
|
}
|
|
});
|
|
})();
|