good progress

This commit is contained in:
TylerCG 2026-04-16 23:36:13 -04:00
parent af86f78ec3
commit 6e56807271
12 changed files with 792 additions and 86 deletions

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"chat.tools.terminal.autoApprove": {
"docker-compose": true
}
}

View File

@ -21,6 +21,9 @@ COPY control-service/public ./public
# Copy media files # Copy media files
COPY media ./media COPY media ./media
# Copy settings file
COPY settings.json ./settings.json
EXPOSE 3001 EXPOSE 3001
CMD ["node", "src/server.js"] CMD ["node", "src/server.js"]

View File

@ -19,6 +19,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 20px; padding: 20px;
overflow-x: hidden;
} }
.container { .container {
@ -187,6 +188,7 @@
gap: 20px; gap: 20px;
align-items: center; align-items: center;
margin-top: 15px; margin-top: 15px;
flex-wrap: wrap;
} }
.location-mode label { .location-mode label {
@ -197,6 +199,98 @@
margin-top: 10px; 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) { @media (max-width: 600px) {
.container { .container {
padding: 20px; padding: 20px;
@ -238,8 +332,8 @@
<div class="form-group"> <div class="form-group">
<label for="welcomeSuffix">Welcome Suffix</label> <label for="welcomeSuffix">Welcome Suffix</label>
<input type="text" id="welcomeSuffix" name="welcomeSuffix" placeholder="e.g., 's Family"> <input type="text" id="welcomeSuffix" name="welcomeSuffix" placeholder="e.g., Family">
<p class="info-text">Shown after the guest name on idle screen</p> <p class="info-text">Appended after guest name (e.g., " Family" displays as "Guest Family")</p>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -254,11 +348,7 @@
<p class="info-text">Displayed on hero page top bar</p> <p class="info-text">Displayed on hero page top bar</p>
</div> </div>
<div class="form-group">
<label for="brandColor">Brand Color</label>
<input type="color" id="brandColor" name="brand_color">
<p class="info-text">Primary accent color for UI</p>
</div>
<div class="form-group"> <div class="form-group">
<label for="idleTimeout">Idle Timeout (seconds)</label> <label for="idleTimeout">Idle Timeout (seconds)</label>
@ -288,66 +378,203 @@
<input type="text" id="heroGuestText" name="heroGuestText" placeholder="e.g., Guest" required> <input type="text" id="heroGuestText" name="heroGuestText" placeholder="e.g., Guest" required>
<p class="info-text">Guest name displayed below welcome text</p> <p class="info-text">Guest name displayed below welcome text</p>
</div> </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> </div>
<!-- Themes --> <!-- Themes -->
<div class="section"> <div class="section">
<h2>Color Themes</h2> <h2>Color Themes</h2>
<div class="form-group"> <div style="margin-bottom: 25px;">
<label>Hero Page Colors</label> <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>
<div class="form-group"> <div style="margin-bottom: 25px;">
<label for="heroStatusBarBg">Status Bar Background</label> <h3 style="font-size: 13px; color: #667eea; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.5px;">Hero Background Gradient</h3>
<input type="color" id="heroStatusBarBg" name="heroStatusBarBg"> <div class="color-grid">
<p class="info-text">Top bar background color</p> <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>
<div class="form-group"> <div style="margin-bottom: 25px;">
<label for="heroHeaderBg">Header Background</label> <h3 style="font-size: 13px; color: #667eea; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.5px;">Nav Bar Gradient</h3>
<input type="color" id="heroHeaderBg" name="heroHeaderBg"> <div class="color-grid">
<p class="info-text">Main header gradient base color</p> <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>
<div class="form-group"> <div style="margin-bottom: 25px;">
<label for="heroAccentLineColor">Accent Line Color</label> <h3 style="font-size: 13px; color: #667eea; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.5px;">Idle Page Status Bar</h3>
<input type="color" id="heroAccentLineColor" name="heroAccentLineColor"> <div class="color-grid">
<p class="info-text">Divider line between status and header</p> <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>
<div class="form-group"> <div style="margin-bottom: 25px;">
<label for="heroWelcomeTextColor">Welcome Text Color</label> <h3 style="font-size: 13px; color: #667eea; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.5px;">Idle Page Bottom Bar</h3>
<input type="color" id="heroWelcomeTextColor" name="heroWelcomeTextColor"> <div class="color-grid">
<p class="info-text">Color of "WELCOME" text</p> <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> </div>
<div class="form-group"> <!-- Theme Management -->
<label for="heroGuestTextColor">Guest Name Text Color</label> <div style="border-top: 2px solid #eee; padding-top: 20px; margin-top: 25px;">
<input type="color" id="heroGuestTextColor" name="heroGuestTextColor"> <h3 style="font-size: 13px; color: #667eea; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.5px;">💾 Saved Themes</h3>
<p class="info-text">Color of guest name text</p> <div class="theme-buttons">
</div> <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 class="form-group"> </div>
<label>Idle Page Colors</label> <input type="file" id="themeFileInput" accept=".json" onchange="importTheme(event)">
</div> <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 class="form-group">
<label for="idleBgColor">Background Color</label>
<input type="color" id="idleBgColor" name="idleBgColor">
<p class="info-text">Idle screen background</p>
</div>
<div class="form-group">
<label for="idleTitleColor">Title Color</label>
<input type="color" id="idleTitleColor" name="idleTitleColor">
<p class="info-text">Guest name color on idle screen</p>
</div>
<div class="form-group">
<label for="idleSuffixColor">Suffix Color</label>
<input type="color" id="idleSuffixColor" name="idleSuffixColor">
<p class="info-text">Family suffix color on idle screen</p>
</div> </div>
</div> </div>
@ -409,11 +636,52 @@
<script> <script>
const API_URL = '/api/settings'; const API_URL = '/api/settings';
const AUDIO_API_URL = '/api/audio-files';
const form = document.getElementById('settingsForm'); const form = document.getElementById('settingsForm');
const message = document.getElementById('message'); const message = document.getElementById('message');
// Load settings on page load // Load audio files first, then settings
loadSettings(); (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() { async function loadSettings() {
try { try {
@ -423,10 +691,10 @@
// Display Settings // Display Settings
document.getElementById('title').value = settings.title; document.getElementById('title').value = settings.title;
document.getElementById('welcomePrefix').value = settings.welcomePrefix || 'WELCOME'; document.getElementById('welcomePrefix').value = settings.welcomePrefix || 'WELCOME';
document.getElementById('welcomeSuffix').value = settings.welcomeSuffix || "'s Family"; document.getElementById('welcomeSuffix').value = settings.welcomeSuffix || " Family";
document.getElementById('resortName').value = settings.resortName || 'Mojo Dojo Casa House'; document.getElementById('resortName').value = settings.resortName || 'Mojo Dojo Casa House';
document.getElementById('roomNumber').value = settings.roomNumber || 'ROOM 201'; document.getElementById('roomNumber').value = settings.roomNumber || 'ROOM 201';
document.getElementById('brandColor').value = settings.brand_color;
document.getElementById('idleTimeout').value = settings.idle_timeout_seconds; document.getElementById('idleTimeout').value = settings.idle_timeout_seconds;
// Hero page // Hero page
@ -434,16 +702,43 @@
document.getElementById('heroWelcomeText').value = settings.heroWelcomeText || 'WELCOME'; document.getElementById('heroWelcomeText').value = settings.heroWelcomeText || 'WELCOME';
document.getElementById('heroGuestText').value = settings.heroGuestText || 'Guest'; 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 // Color themes
document.getElementById('heroStatusBarBg').value = settings.heroStatusBarBg || '#1B4965'; document.getElementById('heroStatusBarBg').value = settings.heroStatusBarBg || '#1B4965';
document.getElementById('heroHeaderBg').value = settings.heroHeaderBg || '#1E8E9F'; document.getElementById('heroHeaderBg').value = settings.heroHeaderBg || '#1E8E9F';
document.getElementById('heroAccentLineColor').value = settings.heroAccentLineColor || '#FF6F61'; document.getElementById('heroAccentLineColor').value = settings.heroAccentLineColor || '#FF6F61';
document.getElementById('heroWelcomePrefixColor').value = settings.heroWelcomePrefixColor || '#FF6F61';
document.getElementById('heroWelcomeTextColor').value = settings.heroWelcomeTextColor || '#FF6F61'; document.getElementById('heroWelcomeTextColor').value = settings.heroWelcomeTextColor || '#FF6F61';
document.getElementById('heroGuestTextColor').value = settings.heroGuestTextColor || '#ffffff'; 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('idleBgColor').value = settings.idleBgColor || '#1a1a1a';
document.getElementById('idleTitleColor').value = settings.idleTitleColor || '#ffffff'; document.getElementById('idleTitleColor').value = settings.idleTitleColor || '#ffffff';
document.getElementById('idleSuffixColor').value = settings.idleSuffixColor || '#d4af37'; 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('plexEnabled').checked = settings.plex_enabled;
document.getElementById('restaurantsEnabled').checked = settings.restaurants_enabled; document.getElementById('restaurantsEnabled').checked = settings.restaurants_enabled;
@ -455,6 +750,7 @@
} else { } else {
document.getElementById('locationManual').checked = true; document.getElementById('locationManual').checked = true;
} }
document.getElementById('manualLocation').value = settings.manual_location || 'Your Hotel Location';
} catch (error) { } catch (error) {
showMessage('Failed to load settings', 'error'); showMessage('Failed to load settings', 'error');
} }
@ -472,6 +768,9 @@
resortName: formData.get('resortName'), resortName: formData.get('resortName'),
roomNumber: formData.get('roomNumber'), roomNumber: formData.get('roomNumber'),
backgroundVideoPath: formData.get('backgroundVideoPath'), backgroundVideoPath: formData.get('backgroundVideoPath'),
backgroundAudioPath: formData.get('backgroundAudioPath'),
videoTopOffset: formData.get('videoTopOffset') || '0',
enableHeaderBarOpacity: formData.get('enableHeaderBarOpacity') === 'on',
heroWelcomeText: formData.get('heroWelcomeText'), heroWelcomeText: formData.get('heroWelcomeText'),
heroGuestText: formData.get('heroGuestText'), heroGuestText: formData.get('heroGuestText'),
use_ip_location: document.getElementById('locationIP').checked, use_ip_location: document.getElementById('locationIP').checked,
@ -480,15 +779,28 @@
plex_enabled: formData.get('plex_enabled') === 'on', plex_enabled: formData.get('plex_enabled') === 'on',
restaurants_enabled: formData.get('restaurants_enabled') === 'on', restaurants_enabled: formData.get('restaurants_enabled') === 'on',
attractions_enabled: formData.get('attractions_enabled') === 'on', attractions_enabled: formData.get('attractions_enabled') === 'on',
brand_color: formData.get('brand_color'),
heroStatusBarBg: formData.get('heroStatusBarBg'), heroStatusBarBg: formData.get('heroStatusBarBg'),
heroHeaderBg: formData.get('heroHeaderBg'), heroHeaderBg: formData.get('heroHeaderBg'),
heroAccentLineColor: formData.get('heroAccentLineColor'), heroAccentLineColor: formData.get('heroAccentLineColor'),
heroWelcomePrefixColor: formData.get('heroWelcomePrefixColor'),
heroWelcomeTextColor: formData.get('heroWelcomeTextColor'), heroWelcomeTextColor: formData.get('heroWelcomeTextColor'),
heroGuestTextColor: formData.get('heroGuestTextColor'), 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'), idleBgColor: formData.get('idleBgColor'),
idleTitleColor: formData.get('idleTitleColor'), idleTitleColor: formData.get('idleTitleColor'),
idleSuffixColor: formData.get('idleSuffixColor'), idleSuffixColor: formData.get('idleSuffixColor'),
idleStatusBarStart: formData.get('idleStatusBarStart'),
idleStatusBarEnd: formData.get('idleStatusBarEnd'),
idleBottomBarStart: formData.get('idleBottomBarStart'),
idleBottomBarEnd: formData.get('idleBottomBarEnd'),
}; };
try { try {
@ -515,6 +827,179 @@
message.className = 'message'; message.className = 'message';
}, 4000); }, 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> </script>
</body> </body>
</html> </html>

View File

@ -13,7 +13,7 @@ const PORT = parseInt(process.env.PORT || '3001', 10);
const CEC_DEVICE = process.env.CEC_DEVICE || '/dev/ttyAMA0'; const CEC_DEVICE = process.env.CEC_DEVICE || '/dev/ttyAMA0';
// Settings file path // Settings file path
const SETTINGS_FILE = path.join(__dirname, '../../settings.json'); const SETTINGS_FILE = path.join(__dirname, '../settings.json');
// Initialize handlers // Initialize handlers
const cec = new CECHandler(CEC_DEVICE); const cec = new CECHandler(CEC_DEVICE);
@ -54,7 +54,7 @@ function getDefaultSettings() {
plex_enabled: true, plex_enabled: true,
restaurants_enabled: true, restaurants_enabled: true,
attractions_enabled: true, attractions_enabled: true,
brand_color: '#1f2937',
}; };
} }
@ -100,7 +100,21 @@ const server = http.createServer((req, res) => {
if (fs.existsSync(mediaPath)) { if (fs.existsSync(mediaPath)) {
const stat = fs.statSync(mediaPath); const stat = fs.statSync(mediaPath);
const contentType = filename.endsWith('.mp4') ? 'video/mp4' : 'application/octet-stream'; let contentType = 'application/octet-stream';
// Determine proper MIME type
if (filename.endsWith('.mp4')) {
contentType = 'video/mp4';
} else if (filename.endsWith('.mp3')) {
contentType = 'audio/mpeg';
} else if (filename.endsWith('.wav')) {
contentType = 'audio/wav';
} else if (filename.endsWith('.m4a')) {
contentType = 'audio/mp4';
} else if (filename.endsWith('.ogg')) {
contentType = 'audio/ogg';
}
res.writeHead(200, { 'Content-Type': contentType, 'Content-Length': stat.size }); res.writeHead(200, { 'Content-Type': contentType, 'Content-Length': stat.size });
fs.createReadStream(mediaPath).pipe(res); fs.createReadStream(mediaPath).pipe(res);
} else { } else {
@ -150,6 +164,26 @@ const server = http.createServer((req, res) => {
return; return;
} }
// Audio Files API - List available audio files
if (req.url === '/api/audio-files' && req.method === 'GET') {
try {
const audioDir = path.join(__dirname, '../media/background_audio');
const files = fs.readdirSync(audioDir).filter(file => {
return file.endsWith('.mp3') || file.endsWith('.wav') || file.endsWith('.m4a') || file.endsWith('.ogg');
}).map(file => ({
name: file,
path: `/media/background_audio/${file}`
}));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(files));
} catch (error) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify([])); // Return empty array if directory doesn't exist
}
return;
}
if (req.url === '/health') { if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(executor.getHealth())); res.end(JSON.stringify(executor.getHealth()));

View File

@ -16,12 +16,16 @@
} from './lib/store.js'; } from './lib/store.js';
import { fetchRestaurants, fetchAttractions } from './lib/api.js'; import { fetchRestaurants, fetchAttractions } from './lib/api.js';
import WebSocketManager from './lib/websocket.js'; import WebSocketManager from './lib/websocket.js';
import { initConfig, welcomeTitle, idleTimeoutMs, backgroundVideoPath } from './lib/configStore.js'; import { initConfig, refreshConfig, welcomeTitle, idleTimeoutMs, backgroundVideoPath, backgroundAudioPath, videoTopOffset } from './lib/configStore.js';
let idleTimer; let idleTimer;
let ws; let ws;
let IDLE_TIMEOUT = 300000; // Default 5 minutes let IDLE_TIMEOUT = 300000; // Default 5 minutes
let welcomeName = 'Guest'; let welcomeName = 'Guest';
let audioElement;
const FADE_DURATION = 2000; // 2 second fades
let unsubscribeAudio;
let audioFading = false;
// Subscribe to config stores // Subscribe to config stores
const unsubscribeTitle = welcomeTitle.subscribe(val => { const unsubscribeTitle = welcomeTitle.subscribe(val => {
@ -41,6 +45,7 @@
function handleKeyboardInput(event) { function handleKeyboardInput(event) {
resetIdleTimer(); resetIdleTimer();
triggerAudioPlay(); // Enable audio on user input
const key = event.key.toLowerCase(); const key = event.key.toLowerCase();
@ -95,10 +100,111 @@
} }
} }
function triggerAudioPlay() {
// Called on user input to bypass browser autoplay restrictions
if (audioElement && !audioElement.paused) {
return; // Already playing
}
if (audioElement && $backgroundAudioPath) {
console.log('🎵 User input detected, attempting audio playback...');
audioElement.muted = false;
audioElement.play().then(() => {
console.log('✓ Audio playing (triggered by user input)');
}).catch(err => {
console.error('❌ Play failed on user input:', err.message);
});
}
}
function handleAudioLoop() {
if (!audioElement || audioElement.muted || audioFading) return;
console.log('🔄 Audio ending, starting fade out...');
audioFading = true;
const originalVolume = 0.5;
const fadeOutStart = Date.now();
const fadeOut = () => {
const elapsed = Date.now() - fadeOutStart;
const progress = Math.min(elapsed / FADE_DURATION, 1);
audioElement.volume = Math.max(0, originalVolume * (1 - progress));
if (progress < 1) {
requestAnimationFrame(fadeOut);
} else {
// Fade out complete, now reset and fade in
audioElement.currentTime = 0;
audioElement.volume = 0;
console.log('✓ Fade out complete, starting fade in...');
// Start fade in
const fadeInStart = Date.now();
const fadeIn = () => {
const elapsed = Date.now() - fadeInStart;
const progress = Math.min(elapsed / FADE_DURATION, 1);
audioElement.volume = originalVolume * progress;
if (progress < 1) {
requestAnimationFrame(fadeIn);
} else {
audioElement.volume = originalVolume;
audioFading = false;
console.log('✓ Fade in complete, restarting audio...');
// Restart the audio
audioElement.play().catch(e => console.error('Play failed:', e.message));
}
};
fadeIn();
}
};
fadeOut();
}
onMount(async () => { onMount(async () => {
// Initialize configuration from CMS // Initialize configuration from CMS
await initConfig(); await initConfig();
// Set up audio element with comprehensive event handling
if (audioElement) {
// Event listeners for debugging
audioElement.addEventListener('play', () => console.log('✓ Audio started playing'));
audioElement.addEventListener('pause', () => console.log('Audio paused'));
audioElement.addEventListener('ended', handleAudioLoop);
audioElement.addEventListener('error', (e) => {
console.error('❌ Audio error:', e);
if (audioElement.error) {
console.error('Error code:', audioElement.error.code);
}
});
audioElement.addEventListener('playing', () => console.log('✓ Audio is now playing (playing event)'));
audioElement.addEventListener('canplaythrough', () => {
console.log('✓ Audio ready to play through (waiting for user input to unmute)');
});
audioElement.addEventListener('loadstart', () => console.log('📂 Audio loading started...'));
audioElement.addEventListener('canplay', () => console.log('📂 Audio can play (but not through)'));
audioElement.addEventListener('loadedmetadata', () => console.log('📂 Audio metadata loaded'));
audioElement.volume = 0.5;
audioElement.preload = 'auto';
console.log('✓ Audio element fully initialized');
// NOW subscribe to audio path changes (after DOM is ready)
unsubscribeAudio = backgroundAudioPath.subscribe(path => {
if (audioElement && path) {
console.log('🎵 Audio path changed to:', path);
audioElement.src = path;
audioElement.load();
console.log('📂 Audio loaded and ready, waiting for user input to play');
}
});
}
// Initialize WebSocket // Initialize WebSocket
const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:3001'; const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:3001';
ws = new WebSocketManager(wsUrl); ws = new WebSocketManager(wsUrl);
@ -115,6 +221,7 @@
ws.on('input', (data) => { ws.on('input', (data) => {
resetIdleTimer(); resetIdleTimer();
triggerAudioPlay(); // Enable audio on any user input
if (data.type === 'up' || data.type === 'down') { if (data.type === 'up' || data.type === 'down') {
handleKeyboardInput({ key: `arrow${data.type}` }); handleKeyboardInput({ key: `arrow${data.type}` });
} else if (data.type === 'left' || data.type === 'right') { } else if (data.type === 'left' || data.type === 'right') {
@ -143,12 +250,17 @@
// Start idle timer // Start idle timer
resetIdleTimer(); resetIdleTimer();
// Refresh config every 2 seconds to pick up CMS changes
const configRefreshInterval = setInterval(refreshConfig, 2000);
return () => { return () => {
clearInterval(configRefreshInterval);
clearTimeout(idleTimer); clearTimeout(idleTimer);
window.removeEventListener('keydown', handleKeyboardInput); window.removeEventListener('keydown', handleKeyboardInput);
ws?.close(); ws?.close();
unsubscribeTitle(); unsubscribeTitle();
unsubscribeTimeout(); unsubscribeTimeout();
unsubscribeAudio();
}; };
}); });
</script> </script>
@ -163,8 +275,20 @@
loop loop
playsinline playsinline
preload="auto" preload="auto"
style="margin-top: {$videoTopOffset}px;"
/> />
<!-- Global background audio with fade in/out on loop -->
{#if $backgroundAudioPath}
<audio
bind:this={audioElement}
autoplay
muted
preload="auto"
on:ended={handleAudioLoop}
/>
{/if}
{#if $currentScreen === 'idle'} {#if $currentScreen === 'idle'}
<IdleScreen /> <IdleScreen />
{:else if $currentScreen === 'home'} {:else if $currentScreen === 'home'}
@ -174,12 +298,6 @@
{:else if $currentScreen === 'attractions'} {:else if $currentScreen === 'attractions'}
<AttractionsPage /> <AttractionsPage />
{/if} {/if}
{#if !$wsConnected}
<div class="connection-warning">
⚠ Control service disconnected
</div>
{/if}
</main> </main>
<style global> <style global>

View File

@ -3,13 +3,25 @@
import { import {
heroWelcomeText, heroWelcomeText,
heroGuestText, heroGuestText,
welcomePrefix,
welcomeSuffix,
resortName, resortName,
roomNumber, roomNumber,
heroStatusBarBg, heroStatusBarBg,
heroHeaderBg, heroHeaderBg,
heroAccentLineColor, heroAccentLineColor,
heroWelcomePrefixColor,
heroWelcomeTextColor, heroWelcomeTextColor,
heroGuestTextColor heroGuestTextColor,
enableHeaderBarOpacity,
borderTrimColor,
navElementsColor,
heroBgStart,
heroBgMid,
heroBgEnd,
navBarStart,
navBarMid,
navBarEnd,
} from '../lib/configStore.js'; } from '../lib/configStore.js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@ -195,9 +207,9 @@
<div class="accent-line" style="background-color: {$heroAccentLineColor};"></div> <div class="accent-line" style="background-color: {$heroAccentLineColor};"></div>
<!-- Header Bar --> <!-- Header Bar -->
<div class="header-bar" style="background: linear-gradient(135deg, {$heroHeaderBg} 0%, {$heroHeaderBg}dd 50%, {$heroHeaderBg} 100%);"> <div class="header-bar" style="background: linear-gradient(135deg, {$heroHeaderBg} 0%, {$enableHeaderBarOpacity ? $heroHeaderBg + 'dd' : $heroHeaderBg} 50%, {$heroHeaderBg} 100%);">
<div class="header-left"> <div class="header-left">
<div class="welcome" style="color: {$heroWelcomeTextColor};">{$heroWelcomeText}</div> <div class="welcome" style="color: {$heroWelcomePrefixColor};">{$welcomePrefix}</div>
<div class="guest-name" style="color: {$heroGuestTextColor};">{$heroGuestText}</div> <div class="guest-name" style="color: {$heroGuestTextColor};">{$heroGuestText}</div>
</div> </div>
<div class="header-right"> <div class="header-right">
@ -277,14 +289,14 @@
<!-- Hero Image Section --> <!-- Hero Image Section -->
<div class="hero-section"> <div class="hero-section">
<div class="hero-image"></div> <div class="hero-image" style="background: linear-gradient(135deg, {$heroBgStart} 0%, {$heroBgMid} 50%, {$heroBgEnd} 100%);"></div>
</div> </div>
<!-- Border Above Nav --> <!-- Border Above Nav -->
<div class="border-above-nav"></div> <div class="border-above-nav" style="background-color: {$borderTrimColor};"></div>
<!-- Bottom Navigation Bar --> <!-- Bottom Navigation Bar -->
<div class="nav-bar"> <div class="nav-bar" style="background: linear-gradient(to bottom, {$navBarStart} 0%, {$navBarMid} 50%, {$navBarEnd} 100%);">
{#each navItems as item, index (item.id)} {#each navItems as item, index (item.id)}
<button <button
class="nav-item" class="nav-item"
@ -297,8 +309,9 @@
on:keydown={(e) => { on:keydown={(e) => {
if (e.key === 'Enter') handleSelect(item); if (e.key === 'Enter') handleSelect(item);
}} }}
style="color: {$navElementsColor};"
> >
<div class="nav-icon"> <div class="nav-icon" style="color: {$navElementsColor};">
<svg viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.5"> <svg viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.5">
{#if item.icon === 'play'} {#if item.icon === 'play'}
<!-- Plex Logo --> <!-- Plex Logo -->
@ -325,7 +338,7 @@
</div> </div>
<!-- Bottom Trim --> <!-- Bottom Trim -->
<div class="bottom-trim"></div> <div class="bottom-trim" style="background-color: {$borderTrimColor};"></div>
<!-- Weather Icon Test Grid --> <!-- Weather Icon Test Grid -->
<div class="weather-test"> <div class="weather-test">

View File

@ -1,16 +1,16 @@
<script> <script>
import { config, welcomeTitle, welcomePrefix, welcomeSuffix, location, idleBgColor, idleTitleColor, idleSuffixColor, resortName } from '../lib/configStore.js'; import { config, welcomeTitle, welcomePrefix, welcomeSuffix, location, idleBgColor, idleTitleColor, idleSuffixColor, resortName, idleStatusBarStart, idleStatusBarEnd, idleBottomBarStart, idleBottomBarEnd } from '../lib/configStore.js';
</script> </script>
<div class="container" style="background: linear-gradient(135deg, {$idleBgColor} 0%, {$idleBgColor} 100%);"> <div class="container" style="background: linear-gradient(135deg, {$idleBgColor} 0%, {$idleBgColor} 100%);">
<div class="status-bar"> <div class="status-bar" style="background: linear-gradient(to bottom, {$idleStatusBarStart} 0%, {$idleStatusBarEnd} 100%);">
<div class="resort-name">{$resortName}</div> <div class="resort-name">{$resortName}</div>
</div> </div>
<div class="content"> <div class="content">
<h1 class="welcome" style="color: {$idleTitleColor};">{$welcomePrefix}</h1> <h1 class="welcome" style="color: rgb(0, 0, 0);">{$welcomePrefix}</h1>
<h2 class="guest-family" style="color: {$idleSuffixColor};">{$welcomeTitle}{$welcomeSuffix}</h2> <h2 class="guest-family" style="color: {$idleSuffixColor};">{$welcomeTitle}{$welcomeSuffix}</h2>
</div> </div>
<div class="bottom-bar"> <div class="bottom-bar" style="background: linear-gradient(to top, {$idleBottomBarStart} 0%, {$idleBottomBarEnd} 100%);">
<div class="bottom-text">Press a button on your remote to get started</div> <div class="bottom-text">Press a button on your remote to get started</div>
</div> </div>
</div> </div>
@ -85,7 +85,8 @@
font-size: 2.5rem; font-size: 2.5rem;
font-weight: 300; font-weight: 300;
letter-spacing: 0.1em; letter-spacing: 0.1em;
color: #333; position: relative;
z-index: 1;
} }
.guest-family { .guest-family {
@ -94,7 +95,8 @@
font-size: 7rem; font-size: 7rem;
font-weight: 700; font-weight: 700;
letter-spacing: -0.02em; letter-spacing: -0.02em;
color: #333; position: relative;
z-index: 1;
} }
.bottom-bar { .bottom-bar {

View File

@ -5,7 +5,7 @@ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
title: 'Guest', title: 'Guest',
welcomePrefix: 'WELCOME', welcomePrefix: 'WELCOME',
welcomeSuffix: 'Family', welcomeSuffix: " Family",
useCustomSuffix: false, useCustomSuffix: false,
use_ip_location: false, use_ip_location: false,
manual_location: '', manual_location: '',

View File

@ -13,7 +13,7 @@ export const welcomeTitle = derived(config, ($config) => $config?.title || 'Gues
// Derived store for welcome display // Derived store for welcome display
export const welcomePrefix = derived(config, ($config) => $config?.welcomePrefix || 'WELCOME'); export const welcomePrefix = derived(config, ($config) => $config?.welcomePrefix || 'WELCOME');
export const welcomeSuffix = derived(config, ($config) => $config?.welcomeSuffix || "'s Family"); export const welcomeSuffix = derived(config, ($config) => $config?.welcomeSuffix || " Family");
// Hero page text fields // Hero page text fields
export const heroWelcomeText = derived(config, ($config) => $config?.heroWelcomeText || 'WELCOME'); export const heroWelcomeText = derived(config, ($config) => $config?.heroWelcomeText || 'WELCOME');
@ -30,17 +30,48 @@ export const backgroundVideoPath = derived(config, ($config) => {
return path; return path;
}); });
export const backgroundAudioPath = derived(config, ($config) => {
const path = $config?.backgroundAudioPath || '';
// If path is relative, prepend the API URL
if (path && path.startsWith('/')) {
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3001';
return apiUrl + path;
}
return path;
});
// Hero page theme colors // Hero page theme colors
export const heroStatusBarBg = derived(config, ($config) => $config?.heroStatusBarBg || '#1B4965'); export const heroStatusBarBg = derived(config, ($config) => $config?.heroStatusBarBg || '#1B4965');
export const heroHeaderBg = derived(config, ($config) => $config?.heroHeaderBg || '#1E8E9F'); export const heroHeaderBg = derived(config, ($config) => $config?.heroHeaderBg || '#1E8E9F');
export const heroAccentLineColor = derived(config, ($config) => $config?.heroAccentLineColor || '#FF6F61'); export const heroAccentLineColor = derived(config, ($config) => $config?.heroAccentLineColor || '#FF6F61');
export const heroWelcomePrefixColor = derived(config, ($config) => $config?.heroWelcomePrefixColor || '#FF6F61');
export const heroWelcomeTextColor = derived(config, ($config) => $config?.heroWelcomeTextColor || '#FF6F61'); export const heroWelcomeTextColor = derived(config, ($config) => $config?.heroWelcomeTextColor || '#FF6F61');
export const heroGuestTextColor = derived(config, ($config) => $config?.heroGuestTextColor || '#ffffff'); export const heroGuestTextColor = derived(config, ($config) => $config?.heroGuestTextColor || '#ffffff');
export const borderTrimColor = derived(config, ($config) => $config?.borderTrimColor || '#1B4965');
export const navElementsColor = derived(config, ($config) => $config?.navElementsColor || '#1B4965');
// Idle page theme colors // Hero background gradient
export const heroBgStart = derived(config, ($config) => $config?.heroBgStart || '#1B6B7A');
export const heroBgMid = derived(config, ($config) => $config?.heroBgMid || '#2EC4B6');
export const heroBgEnd = derived(config, ($config) => $config?.heroBgEnd || '#0F4C5C');
// Nav bar gradient
export const navBarStart = derived(config, ($config) => $config?.navBarStart || '#F4E1C1');
export const navBarMid = derived(config, ($config) => $config?.navBarMid || '#EFDBAB');
export const navBarEnd = derived(config, ($config) => $config?.navBarEnd || '#EAD5A0');
// Video offset
export const videoTopOffset = derived(config, ($config) => $config?.videoTopOffset || '0');
// Header bar opacity
export const enableHeaderBarOpacity = derived(config, ($config) => $config?.enableHeaderBarOpacity !== false);
export const idleBgColor = derived(config, ($config) => $config?.idleBgColor || '#1a1a1a'); export const idleBgColor = derived(config, ($config) => $config?.idleBgColor || '#1a1a1a');
export const idleTitleColor = derived(config, ($config) => $config?.idleTitleColor || '#ffffff'); export const idleTitleColor = derived(config, ($config) => $config?.idleTitleColor || '#ffffff');
export const idleSuffixColor = derived(config, ($config) => $config?.idleSuffixColor || '#d4af37'); export const idleSuffixColor = derived(config, ($config) => $config?.idleSuffixColor || '#d4af37');
export const idleStatusBarStart = derived(config, ($config) => $config?.idleStatusBarStart || '#113CCF');
export const idleStatusBarEnd = derived(config, ($config) => $config?.idleStatusBarEnd || '#0f2fa8');
export const idleBottomBarStart = derived(config, ($config) => $config?.idleBottomBarStart || '#113CCF');
export const idleBottomBarEnd = derived(config, ($config) => $config?.idleBottomBarEnd || '#0f2fa8');
// Derived store for idle timeout in milliseconds // Derived store for idle timeout in milliseconds
export const idleTimeoutMs = derived( export const idleTimeoutMs = derived(

Binary file not shown.

Binary file not shown.

View File

@ -1,7 +1,7 @@
{ {
"title": "Guest", "title": "Guest",
"welcomePrefix": "WELCOME", "welcomePrefix": "WELCOME",
"welcomeSuffix": "'s Family", "welcomeSuffix": " Family",
"heroWelcomeText": "WELCOME", "heroWelcomeText": "WELCOME",
"heroGuestText": "Guest", "heroGuestText": "Guest",
"use_ip_location": false, "use_ip_location": false,
@ -10,16 +10,31 @@
"plex_enabled": true, "plex_enabled": true,
"restaurants_enabled": true, "restaurants_enabled": true,
"attractions_enabled": true, "attractions_enabled": true,
"brand_color": "#1f2937",
"resortName": "Mojo Dojo Casa House", "resortName": "Mojo Dojo Casa House",
"roomNumber": "ROOM 201", "roomNumber": "ROOM 201",
"backgroundVideoPath": "/media/background.mp4", "backgroundVideoPath": "/media/background.mp4",
"backgroundAudioPath": "/media/background_audio/pop_century.mp3",
"heroStatusBarBg": "#1B4965", "heroStatusBarBg": "#1B4965",
"heroHeaderBg": "#1E8E9F", "heroHeaderBg": "#1E8E9F",
"heroAccentLineColor": "#FF6F61", "heroAccentLineColor": "#FF6F61",
"heroWelcomePrefixColor": "#FF6F61",
"heroWelcomeTextColor": "#FF6F61", "heroWelcomeTextColor": "#FF6F61",
"heroGuestTextColor": "#ffffff", "heroGuestTextColor": "#ffffff",
"borderTrimColor": "#1B4965",
"navElementsColor": "#1B4965",
"heroBgStart": "#1B6B7A",
"heroBgMid": "#2EC4B6",
"heroBgEnd": "#0F4C5C",
"navBarStart": "#F4E1C1",
"navBarMid": "#EFDBAB",
"navBarEnd": "#EAD5A0",
"idleBgColor": "#1a1a1a", "idleBgColor": "#1a1a1a",
"idleTitleColor": "#ffffff", "idleTitleColor": "#ffffff",
"idleSuffixColor": "#d4af37" "idleSuffixColor": "#d4af37",
"idleStatusBarStart": "#113CCF",
"idleStatusBarEnd": "#0f2fa8",
"idleBottomBarStart": "#113CCF",
"idleBottomBarEnd": "#0f2fa8",
"enableHeaderBarOpacity": true,
"videoTopOffset": "0"
} }