good progress
This commit is contained in:
parent
af86f78ec3
commit
6e56807271
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"docker-compose": true
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,9 @@ COPY control-service/public ./public
|
||||
# Copy media files
|
||||
COPY media ./media
|
||||
|
||||
# Copy settings file
|
||||
COPY settings.json ./settings.json
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
CMD ["node", "src/server.js"]
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
@ -187,6 +188,7 @@
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.location-mode label {
|
||||
@ -197,6 +199,98 @@
|
||||
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;
|
||||
@ -238,8 +332,8 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="welcomeSuffix">Welcome Suffix</label>
|
||||
<input type="text" id="welcomeSuffix" name="welcomeSuffix" placeholder="e.g., 's Family">
|
||||
<p class="info-text">Shown after the guest name on idle screen</p>
|
||||
<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">
|
||||
@ -254,11 +348,7 @@
|
||||
<p class="info-text">Displayed on hero page top bar</p>
|
||||
</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">
|
||||
<label for="idleTimeout">Idle Timeout (seconds)</label>
|
||||
@ -288,66 +378,203 @@
|
||||
<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 class="form-group">
|
||||
<label>Hero Page Colors</label>
|
||||
<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 class="form-group">
|
||||
<label for="heroStatusBarBg">Status Bar Background</label>
|
||||
<input type="color" id="heroStatusBarBg" name="heroStatusBarBg">
|
||||
<p class="info-text">Top bar background color</p>
|
||||
<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 class="form-group">
|
||||
<label for="heroHeaderBg">Header Background</label>
|
||||
<input type="color" id="heroHeaderBg" name="heroHeaderBg">
|
||||
<p class="info-text">Main header gradient base color</p>
|
||||
<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 class="form-group">
|
||||
<label for="heroAccentLineColor">Accent Line Color</label>
|
||||
<input type="color" id="heroAccentLineColor" name="heroAccentLineColor">
|
||||
<p class="info-text">Divider line between status and header</p>
|
||||
<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 class="form-group">
|
||||
<label for="heroWelcomeTextColor">Welcome Text Color</label>
|
||||
<input type="color" id="heroWelcomeTextColor" name="heroWelcomeTextColor">
|
||||
<p class="info-text">Color of "WELCOME" text</p>
|
||||
<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>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="heroGuestTextColor">Guest Name Text Color</label>
|
||||
<input type="color" id="heroGuestTextColor" name="heroGuestTextColor">
|
||||
<p class="info-text">Color of guest name text</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Idle Page Colors</label>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<!-- 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>
|
||||
|
||||
@ -409,11 +636,52 @@
|
||||
|
||||
<script>
|
||||
const API_URL = '/api/settings';
|
||||
const AUDIO_API_URL = '/api/audio-files';
|
||||
const form = document.getElementById('settingsForm');
|
||||
const message = document.getElementById('message');
|
||||
|
||||
// Load settings on page load
|
||||
loadSettings();
|
||||
// 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 {
|
||||
@ -423,27 +691,54 @@
|
||||
// Display Settings
|
||||
document.getElementById('title').value = settings.title;
|
||||
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('roomNumber').value = settings.roomNumber || 'ROOM 201';
|
||||
document.getElementById('brandColor').value = settings.brand_color;
|
||||
|
||||
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;
|
||||
@ -455,6 +750,7 @@
|
||||
} else {
|
||||
document.getElementById('locationManual').checked = true;
|
||||
}
|
||||
document.getElementById('manualLocation').value = settings.manual_location || 'Your Hotel Location';
|
||||
} catch (error) {
|
||||
showMessage('Failed to load settings', 'error');
|
||||
}
|
||||
@ -472,6 +768,9 @@
|
||||
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,
|
||||
@ -480,15 +779,28 @@
|
||||
plex_enabled: formData.get('plex_enabled') === 'on',
|
||||
restaurants_enabled: formData.get('restaurants_enabled') === 'on',
|
||||
attractions_enabled: formData.get('attractions_enabled') === 'on',
|
||||
brand_color: formData.get('brand_color'),
|
||||
|
||||
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 {
|
||||
@ -515,6 +827,179 @@
|
||||
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>
|
||||
|
||||
@ -13,7 +13,7 @@ const PORT = parseInt(process.env.PORT || '3001', 10);
|
||||
const CEC_DEVICE = process.env.CEC_DEVICE || '/dev/ttyAMA0';
|
||||
|
||||
// Settings file path
|
||||
const SETTINGS_FILE = path.join(__dirname, '../../settings.json');
|
||||
const SETTINGS_FILE = path.join(__dirname, '../settings.json');
|
||||
|
||||
// Initialize handlers
|
||||
const cec = new CECHandler(CEC_DEVICE);
|
||||
@ -54,7 +54,7 @@ function getDefaultSettings() {
|
||||
plex_enabled: true,
|
||||
restaurants_enabled: true,
|
||||
attractions_enabled: true,
|
||||
brand_color: '#1f2937',
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@ -100,7 +100,21 @@ const server = http.createServer((req, res) => {
|
||||
|
||||
if (fs.existsSync(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 });
|
||||
fs.createReadStream(mediaPath).pipe(res);
|
||||
} else {
|
||||
@ -150,6 +164,26 @@ const server = http.createServer((req, res) => {
|
||||
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') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(executor.getHealth()));
|
||||
|
||||
@ -16,12 +16,16 @@
|
||||
} from './lib/store.js';
|
||||
import { fetchRestaurants, fetchAttractions } from './lib/api.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 ws;
|
||||
let IDLE_TIMEOUT = 300000; // Default 5 minutes
|
||||
let welcomeName = 'Guest';
|
||||
let audioElement;
|
||||
const FADE_DURATION = 2000; // 2 second fades
|
||||
let unsubscribeAudio;
|
||||
let audioFading = false;
|
||||
|
||||
// Subscribe to config stores
|
||||
const unsubscribeTitle = welcomeTitle.subscribe(val => {
|
||||
@ -41,6 +45,7 @@
|
||||
|
||||
function handleKeyboardInput(event) {
|
||||
resetIdleTimer();
|
||||
triggerAudioPlay(); // Enable audio on user input
|
||||
|
||||
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 () => {
|
||||
// Initialize configuration from CMS
|
||||
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
|
||||
const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:3001';
|
||||
ws = new WebSocketManager(wsUrl);
|
||||
@ -115,6 +221,7 @@
|
||||
|
||||
ws.on('input', (data) => {
|
||||
resetIdleTimer();
|
||||
triggerAudioPlay(); // Enable audio on any user input
|
||||
if (data.type === 'up' || data.type === 'down') {
|
||||
handleKeyboardInput({ key: `arrow${data.type}` });
|
||||
} else if (data.type === 'left' || data.type === 'right') {
|
||||
@ -143,12 +250,17 @@
|
||||
// Start idle timer
|
||||
resetIdleTimer();
|
||||
|
||||
// Refresh config every 2 seconds to pick up CMS changes
|
||||
const configRefreshInterval = setInterval(refreshConfig, 2000);
|
||||
|
||||
return () => {
|
||||
clearInterval(configRefreshInterval);
|
||||
clearTimeout(idleTimer);
|
||||
window.removeEventListener('keydown', handleKeyboardInput);
|
||||
ws?.close();
|
||||
unsubscribeTitle();
|
||||
unsubscribeTimeout();
|
||||
unsubscribeAudio();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@ -163,8 +275,20 @@
|
||||
loop
|
||||
playsinline
|
||||
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'}
|
||||
<IdleScreen />
|
||||
{:else if $currentScreen === 'home'}
|
||||
@ -174,12 +298,6 @@
|
||||
{:else if $currentScreen === 'attractions'}
|
||||
<AttractionsPage />
|
||||
{/if}
|
||||
|
||||
{#if !$wsConnected}
|
||||
<div class="connection-warning">
|
||||
⚠ Control service disconnected
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<style global>
|
||||
|
||||
@ -3,13 +3,25 @@
|
||||
import {
|
||||
heroWelcomeText,
|
||||
heroGuestText,
|
||||
welcomePrefix,
|
||||
welcomeSuffix,
|
||||
resortName,
|
||||
roomNumber,
|
||||
heroStatusBarBg,
|
||||
heroHeaderBg,
|
||||
heroAccentLineColor,
|
||||
heroWelcomePrefixColor,
|
||||
heroWelcomeTextColor,
|
||||
heroGuestTextColor
|
||||
heroGuestTextColor,
|
||||
enableHeaderBarOpacity,
|
||||
borderTrimColor,
|
||||
navElementsColor,
|
||||
heroBgStart,
|
||||
heroBgMid,
|
||||
heroBgEnd,
|
||||
navBarStart,
|
||||
navBarMid,
|
||||
navBarEnd,
|
||||
} from '../lib/configStore.js';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
@ -195,9 +207,9 @@
|
||||
<div class="accent-line" style="background-color: {$heroAccentLineColor};"></div>
|
||||
|
||||
<!-- 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="welcome" style="color: {$heroWelcomeTextColor};">{$heroWelcomeText}</div>
|
||||
<div class="welcome" style="color: {$heroWelcomePrefixColor};">{$welcomePrefix}</div>
|
||||
<div class="guest-name" style="color: {$heroGuestTextColor};">{$heroGuestText}</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
@ -277,14 +289,14 @@
|
||||
|
||||
<!-- Hero Image 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>
|
||||
|
||||
<!-- Border Above Nav -->
|
||||
<div class="border-above-nav"></div>
|
||||
<div class="border-above-nav" style="background-color: {$borderTrimColor};"></div>
|
||||
|
||||
<!-- 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)}
|
||||
<button
|
||||
class="nav-item"
|
||||
@ -297,8 +309,9 @@
|
||||
on:keydown={(e) => {
|
||||
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">
|
||||
{#if item.icon === 'play'}
|
||||
<!-- Plex Logo -->
|
||||
@ -325,7 +338,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Bottom Trim -->
|
||||
<div class="bottom-trim"></div>
|
||||
<div class="bottom-trim" style="background-color: {$borderTrimColor};"></div>
|
||||
|
||||
<!-- Weather Icon Test Grid -->
|
||||
<div class="weather-test">
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
@ -85,7 +85,8 @@
|
||||
font-size: 2.5rem;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.1em;
|
||||
color: #333;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.guest-family {
|
||||
@ -94,7 +95,8 @@
|
||||
font-size: 7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: #333;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
|
||||
@ -5,7 +5,7 @@ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
|
||||
const DEFAULT_CONFIG = {
|
||||
title: 'Guest',
|
||||
welcomePrefix: 'WELCOME',
|
||||
welcomeSuffix: 'Family',
|
||||
welcomeSuffix: " Family",
|
||||
useCustomSuffix: false,
|
||||
use_ip_location: false,
|
||||
manual_location: '',
|
||||
|
||||
@ -13,7 +13,7 @@ export const welcomeTitle = derived(config, ($config) => $config?.title || 'Gues
|
||||
|
||||
// Derived store for welcome display
|
||||
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
|
||||
export const heroWelcomeText = derived(config, ($config) => $config?.heroWelcomeText || 'WELCOME');
|
||||
@ -30,17 +30,48 @@ export const backgroundVideoPath = derived(config, ($config) => {
|
||||
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
|
||||
export const heroStatusBarBg = derived(config, ($config) => $config?.heroStatusBarBg || '#1B4965');
|
||||
export const heroHeaderBg = derived(config, ($config) => $config?.heroHeaderBg || '#1E8E9F');
|
||||
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 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 idleTitleColor = derived(config, ($config) => $config?.idleTitleColor || '#ffffff');
|
||||
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
|
||||
export const idleTimeoutMs = derived(
|
||||
|
||||
BIN
media/background_audio/pop_century.mp3
Normal file
BIN
media/background_audio/pop_century.mp3
Normal file
Binary file not shown.
BIN
media/background_audio/when_you_wish_upon_a_star.mp3
Executable file
BIN
media/background_audio/when_you_wish_upon_a_star.mp3
Executable file
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"title": "Guest",
|
||||
"welcomePrefix": "WELCOME",
|
||||
"welcomeSuffix": "'s Family",
|
||||
"welcomeSuffix": " Family",
|
||||
"heroWelcomeText": "WELCOME",
|
||||
"heroGuestText": "Guest",
|
||||
"use_ip_location": false,
|
||||
@ -10,16 +10,31 @@
|
||||
"plex_enabled": true,
|
||||
"restaurants_enabled": true,
|
||||
"attractions_enabled": true,
|
||||
"brand_color": "#1f2937",
|
||||
"resortName": "Mojo Dojo Casa House",
|
||||
"roomNumber": "ROOM 201",
|
||||
"backgroundVideoPath": "/media/background.mp4",
|
||||
"backgroundAudioPath": "/media/background_audio/pop_century.mp3",
|
||||
"heroStatusBarBg": "#1B4965",
|
||||
"heroHeaderBg": "#1E8E9F",
|
||||
"heroAccentLineColor": "#FF6F61",
|
||||
"heroWelcomePrefixColor": "#FF6F61",
|
||||
"heroWelcomeTextColor": "#FF6F61",
|
||||
"heroGuestTextColor": "#ffffff",
|
||||
"borderTrimColor": "#1B4965",
|
||||
"navElementsColor": "#1B4965",
|
||||
"heroBgStart": "#1B6B7A",
|
||||
"heroBgMid": "#2EC4B6",
|
||||
"heroBgEnd": "#0F4C5C",
|
||||
"navBarStart": "#F4E1C1",
|
||||
"navBarMid": "#EFDBAB",
|
||||
"navBarEnd": "#EAD5A0",
|
||||
"idleBgColor": "#1a1a1a",
|
||||
"idleTitleColor": "#ffffff",
|
||||
"idleSuffixColor": "#d4af37"
|
||||
"idleSuffixColor": "#d4af37",
|
||||
"idleStatusBarStart": "#113CCF",
|
||||
"idleStatusBarEnd": "#0f2fa8",
|
||||
"idleBottomBarStart": "#113CCF",
|
||||
"idleBottomBarEnd": "#0f2fa8",
|
||||
"enableHeaderBarOpacity": true,
|
||||
"videoTopOffset": "0"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user