2026-04-16 23:36:13 -04:00

1006 lines
43 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hotel Pi - Settings</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
overflow-x: hidden;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 600px;
width: 100%;
padding: 40px;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
color: #333;
font-weight: 500;
margin-bottom: 8px;
font-size: 14px;
}
input[type="text"],
input[type="number"],
input[type="color"],
select {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s;
}
input[type="text"]:focus,
input[type="number"]:focus,
input[type="color"]:focus,
select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
}
input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.checkbox-group label {
margin: 0;
cursor: pointer;
user-select: none;
}
.section {
margin-bottom: 30px;
padding-bottom: 30px;
border-bottom: 1px solid #eee;
}
.section:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.section h2 {
font-size: 16px;
color: #667eea;
margin-bottom: 20px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.buttons {
display: flex;
gap: 12px;
margin-top: 30px;
}
button {
flex: 1;
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-save {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-save:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.btn-save:active {
transform: translateY(0);
}
.btn-reset {
background: #f0f0f0;
color: #333;
}
.btn-reset:hover {
background: #e0e0e0;
}
.message {
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 20px;
display: none;
font-size: 14px;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
display: block;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
display: block;
}
.info-text {
font-size: 12px;
color: #888;
margin-top: 4px;
}
.location-mode {
display: flex;
gap: 20px;
align-items: center;
margin-top: 15px;
flex-wrap: wrap;
}
.location-mode label {
margin: 0;
}
#manualLocation {
margin-top: 10px;
}
.color-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 15px;
margin-top: 15px;
width: 100%;
}
.color-picker-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.color-picker-item label {
font-size: 12px;
font-weight: 600;
color: #555;
margin: 0;
word-break: break-word;
}
.color-preview {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
background: #f9f9f9;
border-radius: 6px;
border: 1px solid #eee;
}
.color-preview input[type="color"] {
width: 50px;
height: 50px;
border: none;
cursor: pointer;
padding: 0;
}
.color-hex {
font-size: 12px;
font-family: monospace;
color: #666;
flex: 1;
}
.theme-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
flex-wrap: wrap;
}
.btn-secondary {
padding: 10px 16px;
background: #4a90e2;
color: white;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover {
background: #3a7bc8;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3);
}
.btn-secondary.export {
background: #10b981;
}
.btn-secondary.export:hover {
background: #059669;
}
.btn-secondary.import {
background: #f59e0b;
}
.btn-secondary.import:hover {
background: #d97706;
}
#themeFileInput {
display: none;
}
@media (max-width: 600px) {
.container {
padding: 20px;
}
h1 {
font-size: 24px;
}
.buttons {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🎛️ Hotel Pi Settings</h1>
<p class="subtitle">Configure your kiosk display</p>
<div class="message" id="message"></div>
<form id="settingsForm">
<!-- Display Settings -->
<div class="section">
<h2>Display Settings</h2>
<div class="form-group">
<label for="title">Hero Name (Top Right)</label>
<input type="text" id="title" name="title" placeholder="e.g., Guest" required>
<p class="info-text">Shown as the guest/hero name (e.g., "Guest's Family")</p>
</div>
<div class="form-group">
<label for="welcomePrefix">Welcome Prefix</label>
<input type="text" id="welcomePrefix" name="welcomePrefix" placeholder="e.g., WELCOME" required>
<p class="info-text">First line above the hero name</p>
</div>
<div class="form-group">
<label for="welcomeSuffix">Welcome Suffix</label>
<input type="text" id="welcomeSuffix" name="welcomeSuffix" placeholder="e.g., Family">
<p class="info-text">Appended after guest name (e.g., " Family" displays as "Guest Family")</p>
</div>
<div class="form-group">
<label for="resortName">Resort Name</label>
<input type="text" id="resortName" name="resortName" placeholder="e.g., Mojo Dojo Casa House" required>
<p class="info-text">Displayed on both welcome and hero pages</p>
</div>
<div class="form-group">
<label for="roomNumber">Room Number</label>
<input type="text" id="roomNumber" name="roomNumber" placeholder="e.g., ROOM 201" required>
<p class="info-text">Displayed on hero page top bar</p>
</div>
<div class="form-group">
<label for="idleTimeout">Idle Timeout (seconds)</label>
<input type="number" id="idleTimeout" name="idle_timeout_seconds" min="30" max="600" required>
<p class="info-text">Time before returning to idle screen</p>
</div>
</div>
<!-- Hero Page Settings -->
<div class="section">
<h2>Hero Page</h2>
<div class="form-group">
<label for="backgroundVideoPath">Background Video Path</label>
<input type="text" id="backgroundVideoPath" name="backgroundVideoPath" placeholder="e.g., /media/background.mp4" required>
<p class="info-text">Path to video file (e.g., /media/background.mp4)</p>
</div>
<div class="form-group">
<label for="heroWelcomeText">Welcome Text</label>
<input type="text" id="heroWelcomeText" name="heroWelcomeText" placeholder="e.g., WELCOME" required>
<p class="info-text">Large text displayed on hero page (e.g., "WELCOME")</p>
</div>
<div class="form-group">
<label for="heroGuestText">Guest Name Text</label>
<input type="text" id="heroGuestText" name="heroGuestText" placeholder="e.g., Guest" required>
<p class="info-text">Guest name displayed below welcome text</p>
</div>
<div class="form-group">
<label for="backgroundAudioPath">Background Audio</label>
<select id="backgroundAudioPath" name="backgroundAudioPath" required>
<option value="">Loading audio files...</option>
</select>
<p class="info-text">Audio file to loop with 1 second crossfade on loop</p>
</div>
<div class="form-group">
<label for="videoTopOffset">Video Top Offset (px)</label>
<input type="number" id="videoTopOffset" name="videoTopOffset" min="0" max="500" value="0">
<p class="info-text">Space above the video in pixels (0-500)</p>
</div>
<div class="checkbox-group">
<input type="checkbox" id="enableHeaderBarOpacity" name="enableHeaderBarOpacity">
<label for="enableHeaderBarOpacity">Enable Header Bar Opacity (Fade)</label>
</div>
</div>
<!-- Themes -->
<div class="section">
<h2>Color Themes</h2>
<div style="margin-bottom: 25px;">
<h3 style="font-size: 13px; color: #667eea; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.5px;">Hero Page Colors</h3>
<div class="color-grid">
<div class="color-picker-item">
<label for="heroStatusBarBg">Status Bar</label>
<div class="color-preview">
<input type="color" id="heroStatusBarBg" name="heroStatusBarBg">
<div class="color-hex" id="heroStatusBarBg-hex">#1B4965</div>
</div>
</div>
<div class="color-picker-item">
<label for="heroHeaderBg">Header Bar</label>
<div class="color-preview">
<input type="color" id="heroHeaderBg" name="heroHeaderBg">
<div class="color-hex" id="heroHeaderBg-hex">#1E8E9F</div>
</div>
</div>
<div class="color-picker-item">
<label for="heroAccentLineColor">Accent Line</label>
<div class="color-preview">
<input type="color" id="heroAccentLineColor" name="heroAccentLineColor">
<div class="color-hex" id="heroAccentLineColor-hex">#FF6F61</div>
</div>
</div>
<div class="color-picker-item">
<label for="heroWelcomePrefixColor">Welcome Prefix</label>
<div class="color-preview">
<input type="color" id="heroWelcomePrefixColor" name="heroWelcomePrefixColor">
<div class="color-hex" id="heroWelcomePrefixColor-hex">#FF6F61</div>
</div>
</div>
<div class="color-picker-item">
<label for="heroWelcomeTextColor">Welcome Text</label>
<div class="color-preview">
<input type="color" id="heroWelcomeTextColor" name="heroWelcomeTextColor">
<div class="color-hex" id="heroWelcomeTextColor-hex">#FF6F61</div>
</div>
</div>
<div class="color-picker-item">
<label for="heroGuestTextColor">Guest Name</label>
<div class="color-preview">
<input type="color" id="heroGuestTextColor" name="heroGuestTextColor">
<div class="color-hex" id="heroGuestTextColor-hex">#ffffff</div>
</div>
</div>
<div class="color-picker-item">
<label for="borderTrimColor">Border Trim</label>
<div class="color-preview">
<input type="color" id="borderTrimColor" name="borderTrimColor">
<div class="color-hex" id="borderTrimColor-hex">#1B4965</div>
</div>
</div>
<div class="color-picker-item">
<label for="navElementsColor">Nav Elements</label>
<div class="color-preview">
<input type="color" id="navElementsColor" name="navElementsColor">
<div class="color-hex" id="navElementsColor-hex">#1B4965</div>
</div>
</div>
</div>
</div>
<div style="margin-bottom: 25px;">
<h3 style="font-size: 13px; color: #667eea; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.5px;">Hero Background Gradient</h3>
<div class="color-grid">
<div class="color-picker-item">
<label for="heroBgStart">Start</label>
<div class="color-preview">
<input type="color" id="heroBgStart" name="heroBgStart">
<div class="color-hex" id="heroBgStart-hex">#1B6B7A</div>
</div>
</div>
<div class="color-picker-item">
<label for="heroBgMid">Middle</label>
<div class="color-preview">
<input type="color" id="heroBgMid" name="heroBgMid">
<div class="color-hex" id="heroBgMid-hex">#2EC4B6</div>
</div>
</div>
<div class="color-picker-item">
<label for="heroBgEnd">End</label>
<div class="color-preview">
<input type="color" id="heroBgEnd" name="heroBgEnd">
<div class="color-hex" id="heroBgEnd-hex">#0F4C5C</div>
</div>
</div>
</div>
</div>
<div style="margin-bottom: 25px;">
<h3 style="font-size: 13px; color: #667eea; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.5px;">Nav Bar Gradient</h3>
<div class="color-grid">
<div class="color-picker-item">
<label for="navBarStart">Start</label>
<div class="color-preview">
<input type="color" id="navBarStart" name="navBarStart">
<div class="color-hex" id="navBarStart-hex">#F4E1C1</div>
</div>
</div>
<div class="color-picker-item">
<label for="navBarMid">Middle</label>
<div class="color-preview">
<input type="color" id="navBarMid" name="navBarMid">
<div class="color-hex" id="navBarMid-hex">#EFDBAB</div>
</div>
</div>
<div class="color-picker-item">
<label for="navBarEnd">End</label>
<div class="color-preview">
<input type="color" id="navBarEnd" name="navBarEnd">
<div class="color-hex" id="navBarEnd-hex">#EAD5A0</div>
</div>
</div>
</div>
</div>
<div style="margin-bottom: 25px;">
<h3 style="font-size: 13px; color: #667eea; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.5px;">Idle Page Status Bar</h3>
<div class="color-grid">
<div class="color-picker-item">
<label for="idleStatusBarStart">Start</label>
<div class="color-preview">
<input type="color" id="idleStatusBarStart" name="idleStatusBarStart">
<div class="color-hex" id="idleStatusBarStart-hex">#113CCF</div>
</div>
</div>
<div class="color-picker-item">
<label for="idleStatusBarEnd">End</label>
<div class="color-preview">
<input type="color" id="idleStatusBarEnd" name="idleStatusBarEnd">
<div class="color-hex" id="idleStatusBarEnd-hex">#0f2fa8</div>
</div>
</div>
</div>
</div>
<div style="margin-bottom: 25px;">
<h3 style="font-size: 13px; color: #667eea; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.5px;">Idle Page Bottom Bar</h3>
<div class="color-grid">
<div class="color-picker-item">
<label for="idleBottomBarStart">Start</label>
<div class="color-preview">
<input type="color" id="idleBottomBarStart" name="idleBottomBarStart">
<div class="color-hex" id="idleBottomBarStart-hex">#113CCF</div>
</div>
</div>
<div class="color-picker-item">
<label for="idleBottomBarEnd">End</label>
<div class="color-preview">
<input type="color" id="idleBottomBarEnd" name="idleBottomBarEnd">
<div class="color-hex" id="idleBottomBarEnd-hex">#0f2fa8</div>
</div>
</div>
</div>
</div>
<!-- Theme Management -->
<div style="border-top: 2px solid #eee; padding-top: 20px; margin-top: 25px;">
<h3 style="font-size: 13px; color: #667eea; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.5px;">💾 Saved Themes</h3>
<div class="theme-buttons">
<button type="button" class="btn-secondary export" onclick="exportTheme()">📥 Export Theme</button>
<button type="button" class="btn-secondary import" onclick="document.getElementById('themeFileInput').click()">📤 Import Theme</button>
</div>
<input type="file" id="themeFileInput" accept=".json" onchange="importTheme(event)">
<p class="info-text" style="margin-top: 15px;">Export your current theme as a JSON file, or import a previously saved theme file.</p>
</div>
</div>
<!-- Location Settings -->
<div class="section">
<h2>Location Settings</h2>
<div class="location-mode">
<label>
<input type="radio" name="location_mode" value="ip" id="locationIP">
Use IP Geolocation
</label>
<label>
<input type="radio" name="location_mode" value="manual" id="locationManual">
Manual Location
</label>
</div>
<div class="form-group">
<label for="manualLocation">Location String</label>
<input type="text" id="manualLocation" name="manual_location" placeholder="e.g., Disneyland, Anaheim, CA">
<p class="info-text">Used when manual location is selected</p>
</div>
</div>
<!-- Features -->
<div class="section">
<h2>Features</h2>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="plexEnabled" name="plex_enabled">
<label for="plexEnabled">Enable Plex Integration</label>
</div>
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="restaurantsEnabled" name="restaurants_enabled">
<label for="restaurantsEnabled">Show Restaurants Section</label>
</div>
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="attractionsEnabled" name="attractions_enabled">
<label for="attractionsEnabled">Show Attractions Section</label>
</div>
</div>
</div>
<!-- Buttons -->
<div class="buttons">
<button type="submit" class="btn-save">💾 Save Settings</button>
<button type="reset" class="btn-reset">↻ Reset</button>
</div>
</form>
</div>
<script>
const API_URL = '/api/settings';
const AUDIO_API_URL = '/api/audio-files';
const form = document.getElementById('settingsForm');
const message = document.getElementById('message');
// Load audio files first, then settings
(async () => {
await loadAudioFiles();
await loadSettings();
})();
async function loadAudioFiles() {
try {
const response = await fetch(AUDIO_API_URL);
const audioFiles = await response.json();
const select = document.getElementById('backgroundAudioPath');
// Clear the loading option
select.innerHTML = '';
// Add audio files
if (audioFiles.length > 0) {
audioFiles.forEach(file => {
const option = document.createElement('option');
option.value = file.path;
option.textContent = file.name;
select.appendChild(option);
});
// Auto-select first file
select.value = audioFiles[0].path;
console.log('✓ Auto-selected first audio file:', audioFiles[0].name);
} else {
const noFilesOption = document.createElement('option');
noFilesOption.textContent = 'No audio files found in /media/background_audio/';
noFilesOption.disabled = true;
select.appendChild(noFilesOption);
}
// Now that options are loaded, populate is ready
// (loadSettings will be called after this completes)
return true;
} catch (error) {
console.error('Failed to load audio files:', error);
const select = document.getElementById('backgroundAudioPath');
select.innerHTML = '<option>Error loading audio files</option>';
}
}
async function loadSettings() {
try {
const response = await fetch(API_URL);
const settings = await response.json();
// Display Settings
document.getElementById('title').value = settings.title;
document.getElementById('welcomePrefix').value = settings.welcomePrefix || 'WELCOME';
document.getElementById('welcomeSuffix').value = settings.welcomeSuffix || " Family";
document.getElementById('resortName').value = settings.resortName || 'Mojo Dojo Casa House';
document.getElementById('roomNumber').value = settings.roomNumber || 'ROOM 201';
document.getElementById('idleTimeout').value = settings.idle_timeout_seconds;
// Hero page
document.getElementById('backgroundVideoPath').value = settings.backgroundVideoPath || '/media/background.mp4';
document.getElementById('heroWelcomeText').value = settings.heroWelcomeText || 'WELCOME';
document.getElementById('heroGuestText').value = settings.heroGuestText || 'Guest';
// Set audio path if it exists in settings (or keep auto-selected first)
if (settings.backgroundAudioPath) {
document.getElementById('backgroundAudioPath').value = settings.backgroundAudioPath;
console.log('✓ Audio file loaded:', settings.backgroundAudioPath);
} else {
console.log(' No audio path in settings, using dropdown default (first file)');
}
// Video settings
document.getElementById('videoTopOffset').value = settings.videoTopOffset || '0';
document.getElementById('enableHeaderBarOpacity').checked = settings.enableHeaderBarOpacity !== false;
// Color themes
document.getElementById('heroStatusBarBg').value = settings.heroStatusBarBg || '#1B4965';
document.getElementById('heroHeaderBg').value = settings.heroHeaderBg || '#1E8E9F';
document.getElementById('heroAccentLineColor').value = settings.heroAccentLineColor || '#FF6F61';
document.getElementById('heroWelcomePrefixColor').value = settings.heroWelcomePrefixColor || '#FF6F61';
document.getElementById('heroWelcomeTextColor').value = settings.heroWelcomeTextColor || '#FF6F61';
document.getElementById('heroGuestTextColor').value = settings.heroGuestTextColor || '#ffffff';
document.getElementById('borderTrimColor').value = settings.borderTrimColor || '#1B4965';
document.getElementById('navElementsColor').value = settings.navElementsColor || '#1B4965';
// Gradient colors
document.getElementById('heroBgStart').value = settings.heroBgStart || '#1B6B7A';
document.getElementById('heroBgMid').value = settings.heroBgMid || '#2EC4B6';
document.getElementById('heroBgEnd').value = settings.heroBgEnd || '#0F4C5C';
document.getElementById('navBarStart').value = settings.navBarStart || '#F4E1C1';
document.getElementById('navBarMid').value = settings.navBarMid || '#EFDBAB';
document.getElementById('navBarEnd').value = settings.navBarEnd || '#EAD5A0';
document.getElementById('idleBgColor').value = settings.idleBgColor || '#1a1a1a';
document.getElementById('idleTitleColor').value = settings.idleTitleColor || '#ffffff';
document.getElementById('idleSuffixColor').value = settings.idleSuffixColor || '#d4af37';
document.getElementById('idleStatusBarStart').value = settings.idleStatusBarStart || '#113CCF';
document.getElementById('idleStatusBarEnd').value = settings.idleStatusBarEnd || '#0f2fa8';
document.getElementById('idleBottomBarStart').value = settings.idleBottomBarStart || '#113CCF';
document.getElementById('idleBottomBarEnd').value = settings.idleBottomBarEnd || '#0f2fa8';
document.getElementById('plexEnabled').checked = settings.plex_enabled;
document.getElementById('restaurantsEnabled').checked = settings.restaurants_enabled;
document.getElementById('attractionsEnabled').checked = settings.attractions_enabled;
// Set location mode
if (settings.use_ip_location) {
document.getElementById('locationIP').checked = true;
} else {
document.getElementById('locationManual').checked = true;
}
document.getElementById('manualLocation').value = settings.manual_location || 'Your Hotel Location';
} catch (error) {
showMessage('Failed to load settings', 'error');
}
}
// Handle form submission
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const settings = {
title: formData.get('title'),
welcomePrefix: formData.get('welcomePrefix'),
welcomeSuffix: formData.get('welcomeSuffix'),
resortName: formData.get('resortName'),
roomNumber: formData.get('roomNumber'),
backgroundVideoPath: formData.get('backgroundVideoPath'),
backgroundAudioPath: formData.get('backgroundAudioPath'),
videoTopOffset: formData.get('videoTopOffset') || '0',
enableHeaderBarOpacity: formData.get('enableHeaderBarOpacity') === 'on',
heroWelcomeText: formData.get('heroWelcomeText'),
heroGuestText: formData.get('heroGuestText'),
use_ip_location: document.getElementById('locationIP').checked,
manual_location: formData.get('manual_location'),
idle_timeout_seconds: parseInt(formData.get('idle_timeout_seconds')),
plex_enabled: formData.get('plex_enabled') === 'on',
restaurants_enabled: formData.get('restaurants_enabled') === 'on',
attractions_enabled: formData.get('attractions_enabled') === 'on',
heroStatusBarBg: formData.get('heroStatusBarBg'),
heroHeaderBg: formData.get('heroHeaderBg'),
heroAccentLineColor: formData.get('heroAccentLineColor'),
heroWelcomePrefixColor: formData.get('heroWelcomePrefixColor'),
heroWelcomeTextColor: formData.get('heroWelcomeTextColor'),
heroGuestTextColor: formData.get('heroGuestTextColor'),
borderTrimColor: formData.get('borderTrimColor'),
navElementsColor: formData.get('navElementsColor'),
heroBgStart: formData.get('heroBgStart'),
heroBgMid: formData.get('heroBgMid'),
heroBgEnd: formData.get('heroBgEnd'),
navBarStart: formData.get('navBarStart'),
navBarMid: formData.get('navBarMid'),
navBarEnd: formData.get('navBarEnd'),
idleBgColor: formData.get('idleBgColor'),
idleTitleColor: formData.get('idleTitleColor'),
idleSuffixColor: formData.get('idleSuffixColor'),
idleStatusBarStart: formData.get('idleStatusBarStart'),
idleStatusBarEnd: formData.get('idleStatusBarEnd'),
idleBottomBarStart: formData.get('idleBottomBarStart'),
idleBottomBarEnd: formData.get('idleBottomBarEnd'),
};
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings),
});
if (response.ok) {
showMessage('✓ Settings saved successfully!', 'success');
} else {
showMessage('Failed to save settings', 'error');
}
} catch (error) {
showMessage('Error saving settings: ' + error.message, 'error');
}
});
function showMessage(text, type) {
message.textContent = text;
message.className = `message ${type}`;
setTimeout(() => {
message.className = 'message';
}, 4000);
}
// Color picker hex display update
const colorPickerIds = [
'heroStatusBarBg', 'heroHeaderBg', 'heroAccentLineColor', 'heroWelcomePrefixColor', 'heroWelcomeTextColor', 'heroGuestTextColor',
'borderTrimColor', 'navElementsColor',
'heroBgStart', 'heroBgMid', 'heroBgEnd',
'navBarStart', 'navBarMid', 'navBarEnd',
'idleBgColor', 'idleTitleColor', 'idleSuffixColor',
'idleStatusBarStart', 'idleStatusBarEnd', 'idleBottomBarStart', 'idleBottomBarEnd'
];
colorPickerIds.forEach(id => {
const colorInput = document.getElementById(id);
if (colorInput) {
colorInput.addEventListener('input', (e) => {
const hexDisplay = document.getElementById(id + '-hex');
if (hexDisplay) {
hexDisplay.textContent = e.target.value.toUpperCase();
}
});
}
});
// Export theme as JSON file in themes.js format
function exportTheme() {
const formData = new FormData(form);
const themeKeyName = prompt('Enter theme key (camelCase, e.g. "coastalModern"):', 'customTheme');
if (themeKeyName === null) return;
const themeName = prompt('Enter display name (e.g. "Coastal Modern"):', 'Custom Theme');
if (themeName === null) return;
// Create theme object in themes.js format - include all color fields
const themeObject = {
[themeKeyName]: {
name: themeName,
statusBar: formData.get('heroStatusBarBg'),
accentLine: formData.get('heroAccentLineColor'),
headerBar: formData.get('heroHeaderBg'),
heroBg: {
start: formData.get('heroBgStart'),
mid: formData.get('heroBgMid'),
end: formData.get('heroBgEnd'),
},
navBar: {
start: formData.get('navBarStart'),
mid: formData.get('navBarMid'),
end: formData.get('navBarEnd'),
},
borderTrim: formData.get('borderTrimColor'),
navElements: formData.get('navElementsColor'),
welcomePrefix: formData.get('heroWelcomePrefixColor'),
welcomeText: formData.get('heroWelcomeTextColor'),
// Idle page colors
idleBg: formData.get('idleBgColor'),
idleTitle: formData.get('idleTitleColor'),
idleSuffix: formData.get('idleSuffixColor'),
idleStatusBarStart: formData.get('idleStatusBarStart'),
idleStatusBarEnd: formData.get('idleStatusBarEnd'),
idleBottomBarStart: formData.get('idleBottomBarStart'),
idleBottomBarEnd: formData.get('idleBottomBarEnd'),
}
};
// Format with each color on its own line for VS Code detection
const jsonStr = JSON.stringify(themeObject, null, 4);
const dataBlob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `${themeKeyName}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
showMessage('✓ Theme exported in themes.js format!', 'success');
}
// Import theme from JSON file (themes.js format)
function importTheme(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const themeData = JSON.parse(e.target.result);
// Get the first (and typically only) theme from the imported file
const themeName = Object.keys(themeData)[0];
if (!themeName) {
showMessage('❌ Invalid theme format', 'error');
return;
}
const theme = themeData[themeName];
// Single color mappings
const colorMapping = {
'statusBar': 'heroStatusBarBg',
'accentLine': 'heroAccentLineColor',
'headerBar': 'heroHeaderBg',
'welcomePrefix': 'heroWelcomePrefixColor',
'welcomeText': 'heroWelcomeTextColor',
'navElements': 'navElementsColor',
'borderTrim': 'borderTrimColor',
'idleBg': 'idleBgColor',
'idleTitle': 'idleTitleColor',
'idleSuffix': 'idleSuffixColor',
'idleStatusBarStart': 'idleStatusBarStart',
'idleStatusBarEnd': 'idleStatusBarEnd',
'idleBottomBarStart': 'idleBottomBarStart',
'idleBottomBarEnd': 'idleBottomBarEnd',
};
// Apply single colors
Object.keys(colorMapping).forEach(themeKey => {
const formKey = colorMapping[themeKey];
if (theme[themeKey] && typeof theme[themeKey] === 'string') {
const input = document.getElementById(formKey);
if (input) {
input.value = theme[themeKey];
const hexDisplay = document.getElementById(formKey + '-hex');
if (hexDisplay) {
hexDisplay.textContent = theme[themeKey].toUpperCase();
}
}
}
});
// Apply gradient colors (heroBg and navBar)
if (theme.heroBg && typeof theme.heroBg === 'object') {
['start', 'mid', 'end'].forEach(part => {
if (theme.heroBg[part]) {
const input = document.getElementById(`heroBg${part.charAt(0).toUpperCase() + part.slice(1)}`);
if (input) {
input.value = theme.heroBg[part];
const hexDisplay = document.getElementById(`heroBg${part.charAt(0).toUpperCase() + part.slice(1)}-hex`);
if (hexDisplay) {
hexDisplay.textContent = theme.heroBg[part].toUpperCase();
}
}
}
});
}
if (theme.navBar && typeof theme.navBar === 'object') {
['start', 'mid', 'end'].forEach(part => {
if (theme.navBar[part]) {
const input = document.getElementById(`navBar${part.charAt(0).toUpperCase() + part.slice(1)}`);
if (input) {
input.value = theme.navBar[part];
const hexDisplay = document.getElementById(`navBar${part.charAt(0).toUpperCase() + part.slice(1)}-hex`);
if (hexDisplay) {
hexDisplay.textContent = theme.navBar[part].toUpperCase();
}
}
}
});
}
showMessage(`✓ Theme "${theme.name}" imported successfully!`, 'success');
// Auto-save to backend
saveSettings();
} catch (error) {
console.error('Error importing theme:', error);
showMessage('❌ Failed to import theme: ' + error.message, 'error');
}
};
reader.readAsText(file);
// Reset file input
event.target.value = '';
}
</script>
</body>
</html>