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 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"]
|
||||||
|
|||||||
@ -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,27 +691,54 @@
|
|||||||
// 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
|
||||||
document.getElementById('backgroundVideoPath').value = settings.backgroundVideoPath || '/media/background.mp4';
|
document.getElementById('backgroundVideoPath').value = settings.backgroundVideoPath || '/media/background.mp4';
|
||||||
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>
|
||||||
|
|||||||
@ -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()));
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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: '',
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
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",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user