845 lines
34 KiB
JavaScript
845 lines
34 KiB
JavaScript
/**
|
||
* config.js - A modular configuration component for the SL Transport Departures app
|
||
* Allows changing settings on the fly, such as orientation (portrait/landscape)
|
||
*/
|
||
|
||
class ConfigManager {
|
||
constructor(options = {}) {
|
||
// Default options
|
||
this.options = {
|
||
configButtonId: 'config-button',
|
||
configModalId: 'config-modal',
|
||
saveToLocalStorage: true,
|
||
defaultOrientation: 'normal',
|
||
defaultDarkMode: 'auto', // 'auto', 'on', or 'off'
|
||
...options
|
||
};
|
||
|
||
// Add updateBackgroundPreview function
|
||
this.updateBackgroundPreview = function() {
|
||
const preview = document.getElementById('background-preview');
|
||
const imageUrl = document.getElementById('background-image-url').value;
|
||
|
||
if (preview) {
|
||
if (imageUrl && imageUrl.trim() !== '') {
|
||
preview.innerHTML = `<img src="${imageUrl}" alt="Background preview">`;
|
||
} else {
|
||
preview.innerHTML = '<div class="no-image">No image selected</div>';
|
||
}
|
||
}
|
||
};
|
||
|
||
// Configuration state
|
||
this.config = {
|
||
orientation: this.options.defaultOrientation,
|
||
darkMode: this.options.defaultDarkMode,
|
||
backgroundImage: '',
|
||
backgroundOpacity: 0.3, // Default opacity (30%)
|
||
tickerSpeed: 60, // Default ticker speed in seconds
|
||
sites: [
|
||
{
|
||
id: '1411',
|
||
name: 'Ambassaderna',
|
||
enabled: true
|
||
}
|
||
],
|
||
rssFeeds: [
|
||
{
|
||
name: "Travel Alerts",
|
||
url: "https://travel.state.gov/content/travel/en/rss/rss.xml",
|
||
enabled: true
|
||
}
|
||
],
|
||
combineSameDirection: true, // Combine departures going in the same direction
|
||
...this.loadConfig() // Load saved config if available
|
||
};
|
||
|
||
// Sync configuration with server
|
||
this.syncConfig();
|
||
|
||
// Create UI elements
|
||
this.createConfigButton();
|
||
this.createConfigModal();
|
||
|
||
// Add keyboard shortcuts for testing
|
||
this.setupKeyboardShortcuts();
|
||
|
||
// Apply initial configuration
|
||
this.applyConfig();
|
||
}
|
||
|
||
/**
|
||
* Create the configuration button (gear icon)
|
||
*/
|
||
createConfigButton() {
|
||
const buttonContainer = document.createElement('div');
|
||
buttonContainer.id = this.options.configButtonId;
|
||
buttonContainer.className = 'config-button';
|
||
buttonContainer.title = 'Settings';
|
||
|
||
buttonContainer.innerHTML = `
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||
<path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.65.07-.97 0-.32-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1 0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66z" fill="#fff"/>
|
||
</svg>
|
||
`;
|
||
|
||
buttonContainer.addEventListener('click', () => this.toggleConfigModal());
|
||
document.body.appendChild(buttonContainer);
|
||
}
|
||
|
||
/**
|
||
* Create the configuration modal
|
||
*/
|
||
createConfigModal() {
|
||
const modalContainer = document.createElement('div');
|
||
modalContainer.id = this.options.configModalId;
|
||
modalContainer.className = 'config-modal';
|
||
modalContainer.style.display = 'none';
|
||
|
||
modalContainer.innerHTML = `
|
||
<div class="config-modal-content">
|
||
<div class="config-modal-header">
|
||
<h2>Settings</h2>
|
||
<span class="config-modal-close">×</span>
|
||
</div>
|
||
<div class="config-modal-body">
|
||
<div class="config-option">
|
||
<label for="orientation-select">Screen Orientation:</label>
|
||
<select id="orientation-select">
|
||
<option value="normal" ${this.config.orientation === 'normal' ? 'selected' : ''}>Normal (0°)</option>
|
||
<option value="vertical" ${this.config.orientation === 'vertical' ? 'selected' : ''}>Vertical (90°)</option>
|
||
<option value="upsidedown" ${this.config.orientation === 'upsidedown' ? 'selected' : ''}>Upside Down (180°)</option>
|
||
<option value="vertical-reverse" ${this.config.orientation === 'vertical-reverse' ? 'selected' : ''}>Vertical Reverse (270°)</option>
|
||
<option value="landscape" ${this.config.orientation === 'landscape' ? 'selected' : ''}>Landscape (2-column)</option>
|
||
</select>
|
||
</div>
|
||
<div class="config-option">
|
||
<label for="dark-mode-select">Dark Mode:</label>
|
||
<select id="dark-mode-select">
|
||
<option value="auto" ${this.config.darkMode === 'auto' ? 'selected' : ''}>Automatic (Sunset/Sunrise)</option>
|
||
<option value="on" ${this.config.darkMode === 'on' ? 'selected' : ''}>Always On</option>
|
||
<option value="off" ${this.config.darkMode === 'off' ? 'selected' : ''}>Always Off</option>
|
||
</select>
|
||
<div class="sun-times" id="sun-times">
|
||
<small>Sunrise: <span id="sunrise-time">--:--</span> | Sunset: <span id="sunset-time">--:--</span></small>
|
||
</div>
|
||
</div>
|
||
<div class="config-option">
|
||
<label for="background-image-url">Background Image:</label>
|
||
<input type="text" id="background-image-url" placeholder="Enter image URL" value="${this.config.backgroundImage}">
|
||
<div style="display: flex; gap: 10px; margin-top: 5px;">
|
||
<button id="test-image-button" style="padding: 5px 10px;">Use Test Image</button>
|
||
<label for="local-image-input" style="padding: 5px 10px; background-color: #ddd; border-radius: 4px; cursor: pointer;">
|
||
Select Local Image
|
||
</label>
|
||
<input type="file" id="local-image-input" accept="image/*" style="display: none;">
|
||
</div>
|
||
<div class="background-preview" id="background-preview">
|
||
${this.config.backgroundImage ? `<img src="${this.config.backgroundImage}" alt="Background preview">` : '<div class="no-image">No image selected</div>'}
|
||
</div>
|
||
</div>
|
||
<div class="config-option">
|
||
<label for="background-opacity">Background Opacity: <span id="opacity-value">${Math.round(this.config.backgroundOpacity * 100)}%</span></label>
|
||
<input type="range" id="background-opacity" min="0" max="1" step="0.05" value="${this.config.backgroundOpacity}">
|
||
</div>
|
||
<div class="config-option">
|
||
<label for="ticker-speed">Ticker Speed: <span id="ticker-speed-value">${this.config.tickerSpeed}</span> seconds</label>
|
||
<input type="range" id="ticker-speed" min="10" max="120" step="5" value="${this.config.tickerSpeed}">
|
||
</div>
|
||
<div class="config-option">
|
||
<label>Transit Sites:</label>
|
||
<div id="sites-container">
|
||
${this.generateSitesHTML()}
|
||
</div>
|
||
<div style="margin-top: 10px;">
|
||
<button id="add-site-button" style="padding: 5px 10px;">Add Site</button>
|
||
</div>
|
||
</div>
|
||
<div class="config-option">
|
||
<label>RSS Feeds:</label>
|
||
<div id="rss-feeds-container">
|
||
${this.generateRssFeedsHTML()}
|
||
</div>
|
||
<div style="margin-top: 10px;">
|
||
<button id="add-feed-button" style="padding: 5px 10px;">Add Feed</button>
|
||
</div>
|
||
</div>
|
||
<div class="config-option">
|
||
<label for="combine-directions">
|
||
<input type="checkbox" id="combine-directions" ${this.config.combineSameDirection ? 'checked' : ''}>
|
||
Combine departures going in the same direction
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="config-modal-footer">
|
||
<button id="config-save-button">Save</button>
|
||
<button id="config-cancel-button">Cancel</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modalContainer);
|
||
|
||
// Add event listeners
|
||
modalContainer.querySelector('.config-modal-close').addEventListener('click', () => this.hideConfigModal());
|
||
modalContainer.querySelector('#config-cancel-button').addEventListener('click', () => this.hideConfigModal());
|
||
modalContainer.querySelector('#config-save-button').addEventListener('click', () => this.saveAndApplyConfig());
|
||
modalContainer.querySelector('#test-image-button').addEventListener('click', () => {
|
||
const testImageUrl = 'https://images.unsplash.com/photo-1509356843151-3e7d96241e11?q=80&w=1000';
|
||
document.getElementById('background-image-url').value = testImageUrl;
|
||
this.updateBackgroundPreview();
|
||
});
|
||
|
||
// Add event listener for add site button
|
||
const addSiteButton = modalContainer.querySelector('#add-site-button');
|
||
if (addSiteButton) {
|
||
addSiteButton.addEventListener('click', () => this.addNewSite());
|
||
}
|
||
|
||
// Add event listener for add feed button
|
||
const addFeedButton = modalContainer.querySelector('#add-feed-button');
|
||
if (addFeedButton) {
|
||
addFeedButton.addEventListener('click', () => this.addNewFeed());
|
||
}
|
||
|
||
// Add event listener for local image selection
|
||
const localImageInput = modalContainer.querySelector('#local-image-input');
|
||
if (localImageInput) {
|
||
localImageInput.addEventListener('change', (event) => {
|
||
const file = event.target.files[0];
|
||
if (file) {
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
const imageDataUrl = e.target.result;
|
||
document.getElementById('background-image-url').value = imageDataUrl;
|
||
this.updateBackgroundPreview();
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Close modal when clicking outside the content
|
||
modalContainer.addEventListener('click', (event) => {
|
||
if (event.target === modalContainer) {
|
||
this.hideConfigModal();
|
||
}
|
||
});
|
||
|
||
// Add event listeners for site and feed management
|
||
this.setupSiteEventListeners();
|
||
this.setupFeedEventListeners();
|
||
}
|
||
|
||
/**
|
||
* Setup event listeners for site management
|
||
*/
|
||
setupSiteEventListeners() {
|
||
const sitesContainer = document.getElementById('sites-container');
|
||
if (!sitesContainer) return;
|
||
|
||
const removeButtons = sitesContainer.querySelectorAll('.remove-site-button');
|
||
removeButtons.forEach(button => {
|
||
button.addEventListener('click', (event) => {
|
||
const siteItem = event.target.closest('.site-item');
|
||
if (siteItem) {
|
||
const index = parseInt(siteItem.dataset.index);
|
||
this.removeSite(index);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Setup event listeners for RSS feed management
|
||
*/
|
||
setupFeedEventListeners() {
|
||
const feedsContainer = document.getElementById('rss-feeds-container');
|
||
if (!feedsContainer) return;
|
||
|
||
const removeButtons = feedsContainer.querySelectorAll('.remove-feed-button');
|
||
removeButtons.forEach(button => {
|
||
button.addEventListener('click', (event) => {
|
||
const feedItem = event.target.closest('.feed-item');
|
||
if (feedItem) {
|
||
const index = parseInt(feedItem.dataset.index);
|
||
this.removeFeed(index);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Add a new site to the configuration
|
||
*/
|
||
addNewSite() {
|
||
this.config.sites.push({
|
||
id: '',
|
||
name: 'New Site',
|
||
enabled: true
|
||
});
|
||
|
||
const sitesContainer = document.getElementById('sites-container');
|
||
if (sitesContainer) {
|
||
sitesContainer.innerHTML = this.generateSitesHTML();
|
||
this.setupSiteEventListeners();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Add a new RSS feed to the configuration
|
||
*/
|
||
addNewFeed() {
|
||
this.config.rssFeeds.push({
|
||
name: 'New Feed',
|
||
url: '',
|
||
enabled: true
|
||
});
|
||
|
||
const feedsContainer = document.getElementById('rss-feeds-container');
|
||
if (feedsContainer) {
|
||
feedsContainer.innerHTML = this.generateRssFeedsHTML();
|
||
this.setupFeedEventListeners();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Remove a site from the configuration
|
||
*/
|
||
removeSite(index) {
|
||
if (index >= 0 && index < this.config.sites.length) {
|
||
this.config.sites.splice(index, 1);
|
||
|
||
const sitesContainer = document.getElementById('sites-container');
|
||
if (sitesContainer) {
|
||
sitesContainer.innerHTML = this.generateSitesHTML();
|
||
this.setupSiteEventListeners();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Remove an RSS feed from the configuration
|
||
*/
|
||
removeFeed(index) {
|
||
if (index >= 0 && index < this.config.rssFeeds.length) {
|
||
this.config.rssFeeds.splice(index, 1);
|
||
|
||
const feedsContainer = document.getElementById('rss-feeds-container');
|
||
if (feedsContainer) {
|
||
feedsContainer.innerHTML = this.generateRssFeedsHTML();
|
||
this.setupFeedEventListeners();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Toggle the configuration modal visibility
|
||
*/
|
||
toggleConfigModal() {
|
||
const modal = document.getElementById(this.options.configModalId);
|
||
if (modal.style.display === 'none') {
|
||
this.showConfigModal();
|
||
} else {
|
||
this.hideConfigModal();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Hide the configuration modal
|
||
*/
|
||
hideConfigModal() {
|
||
const modal = document.getElementById(this.options.configModalId);
|
||
modal.style.display = 'none';
|
||
}
|
||
|
||
/**
|
||
* Show the configuration modal
|
||
*/
|
||
showConfigModal() {
|
||
const modal = document.getElementById(this.options.configModalId);
|
||
modal.style.display = 'flex';
|
||
|
||
// Update form values to match current config
|
||
document.getElementById('orientation-select').value = this.config.orientation;
|
||
document.getElementById('dark-mode-select').value = this.config.darkMode;
|
||
document.getElementById('combine-directions').checked = this.config.combineSameDirection;
|
||
|
||
// Update sites container
|
||
const sitesContainer = document.getElementById('sites-container');
|
||
if (sitesContainer) {
|
||
sitesContainer.innerHTML = this.generateSitesHTML();
|
||
this.setupSiteEventListeners();
|
||
}
|
||
|
||
// Update RSS feeds container
|
||
const feedsContainer = document.getElementById('rss-feeds-container');
|
||
if (feedsContainer) {
|
||
feedsContainer.innerHTML = this.generateRssFeedsHTML();
|
||
this.setupFeedEventListeners();
|
||
}
|
||
|
||
// Update background image, opacity, and ticker speed values
|
||
const backgroundImageUrl = document.getElementById('background-image-url');
|
||
const backgroundOpacity = document.getElementById('background-opacity');
|
||
const opacityValue = document.getElementById('opacity-value');
|
||
const tickerSpeed = document.getElementById('ticker-speed');
|
||
const tickerSpeedValue = document.getElementById('ticker-speed-value');
|
||
|
||
if (backgroundImageUrl) {
|
||
backgroundImageUrl.value = this.config.backgroundImage || '';
|
||
}
|
||
|
||
if (backgroundOpacity) {
|
||
backgroundOpacity.value = this.config.backgroundOpacity;
|
||
}
|
||
|
||
if (opacityValue) {
|
||
opacityValue.textContent = `${Math.round(this.config.backgroundOpacity * 100)}%`;
|
||
}
|
||
|
||
if (tickerSpeed) {
|
||
tickerSpeed.value = this.config.tickerSpeed;
|
||
}
|
||
|
||
if (tickerSpeedValue) {
|
||
tickerSpeedValue.textContent = this.config.tickerSpeed;
|
||
}
|
||
|
||
// Update background preview
|
||
this.updateBackgroundPreview();
|
||
|
||
// Update sun times display
|
||
this.updateSunTimesDisplay();
|
||
|
||
// Add event listeners
|
||
this.setupModalEventListeners();
|
||
}
|
||
|
||
/**
|
||
* Setup event listeners for the modal
|
||
*/
|
||
setupModalEventListeners() {
|
||
// Background image URL input
|
||
const imageUrlInput = document.getElementById('background-image-url');
|
||
if (imageUrlInput) {
|
||
const newInput = imageUrlInput.cloneNode(true);
|
||
imageUrlInput.parentNode.replaceChild(newInput, imageUrlInput);
|
||
newInput.addEventListener('input', () => this.updateBackgroundPreview());
|
||
}
|
||
|
||
// Opacity slider
|
||
const opacitySlider = document.getElementById('background-opacity');
|
||
if (opacitySlider) {
|
||
const newSlider = opacitySlider.cloneNode(true);
|
||
opacitySlider.parentNode.replaceChild(newSlider, opacitySlider);
|
||
newSlider.addEventListener('input', (e) => {
|
||
document.getElementById('opacity-value').textContent = `${Math.round(e.target.value * 100)}%`;
|
||
});
|
||
}
|
||
|
||
// Ticker speed slider
|
||
const tickerSpeedSlider = document.getElementById('ticker-speed');
|
||
if (tickerSpeedSlider) {
|
||
const newSlider = tickerSpeedSlider.cloneNode(true);
|
||
tickerSpeedSlider.parentNode.replaceChild(newSlider, tickerSpeedSlider);
|
||
newSlider.addEventListener('input', (e) => {
|
||
document.getElementById('ticker-speed-value').textContent = e.target.value;
|
||
});
|
||
}
|
||
|
||
// Local image selection
|
||
const localImageInput = document.getElementById('local-image-input');
|
||
if (localImageInput) {
|
||
const newInput = localImageInput.cloneNode(true);
|
||
localImageInput.parentNode.replaceChild(newInput, localImageInput);
|
||
newInput.addEventListener('change', (event) => {
|
||
const file = event.target.files[0];
|
||
if (file) {
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
document.getElementById('background-image-url').value = e.target.result;
|
||
this.updateBackgroundPreview();
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Save and apply the configuration from the form
|
||
*/
|
||
saveAndApplyConfig() {
|
||
try {
|
||
// Get values from form
|
||
const orientationSelect = document.getElementById('orientation-select');
|
||
const darkModeSelect = document.getElementById('dark-mode-select');
|
||
const backgroundImageUrl = document.getElementById('background-image-url');
|
||
const backgroundOpacity = document.getElementById('background-opacity');
|
||
const tickerSpeed = document.getElementById('ticker-speed');
|
||
const combineDirections = document.getElementById('combine-directions');
|
||
|
||
// Get sites and feeds configuration from form
|
||
const sitesConfig = this.getSitesFromForm();
|
||
const feedsConfig = this.getRssFeedsFromForm();
|
||
|
||
// Get the background image URL directly from the DOM
|
||
const imageUrlValue = document.querySelector('#background-image-url').value;
|
||
|
||
// Update config
|
||
this.config.orientation = orientationSelect.value;
|
||
this.config.darkMode = darkModeSelect.value;
|
||
this.config.backgroundImage = imageUrlValue;
|
||
this.config.backgroundOpacity = parseFloat(backgroundOpacity.value);
|
||
this.config.tickerSpeed = parseInt(tickerSpeed.value);
|
||
this.config.combineSameDirection = combineDirections.checked;
|
||
this.config.sites = sitesConfig;
|
||
this.config.rssFeeds = feedsConfig;
|
||
|
||
// Save config
|
||
this.saveConfig();
|
||
|
||
// Sync with server
|
||
this.syncConfig();
|
||
|
||
// Update ticker speed if changed
|
||
if (window.tickerManager && this.config.tickerSpeed) {
|
||
window.tickerManager.setScrollSpeed(this.config.tickerSpeed);
|
||
}
|
||
|
||
// Apply config
|
||
this.applyConfig();
|
||
|
||
// Hide modal
|
||
this.hideConfigModal();
|
||
} catch (error) {
|
||
console.error('Error saving configuration:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Apply the current configuration to the page
|
||
*/
|
||
applyConfig() {
|
||
// Apply orientation
|
||
document.body.classList.remove('normal', 'landscape', 'vertical', 'upsidedown', 'vertical-reverse');
|
||
document.body.classList.add(this.config.orientation);
|
||
|
||
// Apply dark mode setting
|
||
this.applyDarkModeSetting();
|
||
|
||
// Apply background image and opacity
|
||
this.applyBackgroundImage();
|
||
|
||
// Apply ticker speed
|
||
this.applyTickerSpeed();
|
||
|
||
// Ensure content wrapper exists when changing to rotated modes
|
||
if (['vertical', 'upsidedown', 'vertical-reverse'].includes(this.config.orientation)) {
|
||
this.ensureContentWrapper();
|
||
}
|
||
|
||
// Dispatch event for other components to react to config changes
|
||
const event = new CustomEvent('configChanged', { detail: { config: this.config } });
|
||
document.dispatchEvent(event);
|
||
}
|
||
|
||
/**
|
||
* Apply dark mode setting based on configuration
|
||
*/
|
||
applyDarkModeSetting() {
|
||
// Always update sun times display for informational purposes
|
||
this.updateSunTimesDisplay();
|
||
|
||
// If we have a WeatherManager instance
|
||
if (window.weatherManager) {
|
||
if (this.config.darkMode === 'auto') {
|
||
window.weatherManager.updateDarkModeBasedOnTime();
|
||
} else {
|
||
const isDarkMode = this.config.darkMode === 'on';
|
||
window.weatherManager.setDarkMode(isDarkMode);
|
||
}
|
||
} else {
|
||
const isDarkMode = this.config.darkMode === 'on';
|
||
document.body.classList.toggle('dark-mode', isDarkMode);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Apply background image and opacity settings
|
||
*/
|
||
applyBackgroundImage() {
|
||
// Remove any existing background overlay
|
||
const existingOverlay = document.getElementById('background-overlay');
|
||
if (existingOverlay) {
|
||
existingOverlay.remove();
|
||
}
|
||
|
||
// If there's a background image URL, create and apply the overlay
|
||
if (this.config.backgroundImage && this.config.backgroundImage.trim() !== '') {
|
||
const overlay = document.createElement('div');
|
||
overlay.id = 'background-overlay';
|
||
overlay.style.position = 'fixed';
|
||
overlay.style.top = '0';
|
||
overlay.style.left = '0';
|
||
overlay.style.width = '100vw';
|
||
overlay.style.height = '100vh';
|
||
overlay.style.backgroundImage = `url(${this.config.backgroundImage})`;
|
||
overlay.style.backgroundSize = 'cover';
|
||
overlay.style.backgroundPosition = 'center';
|
||
overlay.style.opacity = this.config.backgroundOpacity;
|
||
overlay.style.zIndex = '-1';
|
||
overlay.style.pointerEvents = 'none';
|
||
|
||
// Adjust background rotation based on orientation
|
||
if (this.config.orientation === 'vertical') {
|
||
overlay.style.transform = 'rotate(90deg) scale(2)';
|
||
overlay.style.transformOrigin = 'center center';
|
||
} else if (this.config.orientation === 'upsidedown') {
|
||
overlay.style.transform = 'rotate(180deg) scale(1.5)';
|
||
overlay.style.transformOrigin = 'center center';
|
||
} else if (this.config.orientation === 'vertical-reverse') {
|
||
overlay.style.transform = 'rotate(270deg) scale(2)';
|
||
overlay.style.transformOrigin = 'center center';
|
||
} else {
|
||
overlay.style.transform = 'scale(1.2)';
|
||
overlay.style.transformOrigin = 'center center';
|
||
}
|
||
|
||
// Insert as the first child of body
|
||
document.body.insertBefore(overlay, document.body.firstChild);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Apply ticker speed setting
|
||
*/
|
||
applyTickerSpeed() {
|
||
if (window.tickerManager) {
|
||
window.tickerManager.setScrollSpeed(this.config.tickerSpeed);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Update the sun times display in the config modal
|
||
*/
|
||
updateSunTimesDisplay() {
|
||
if (window.weatherManager) {
|
||
const sunriseElement = document.getElementById('sunrise-time');
|
||
const sunsetElement = document.getElementById('sunset-time');
|
||
|
||
if (sunriseElement && sunsetElement) {
|
||
const sunriseTime = window.weatherManager.getSunriseTime();
|
||
const sunsetTime = window.weatherManager.getSunsetTime();
|
||
|
||
sunriseElement.textContent = sunriseTime;
|
||
sunsetElement.textContent = sunsetTime;
|
||
|
||
const weatherSunTimesElement = document.querySelector('#custom-weather .sun-times');
|
||
if (weatherSunTimesElement) {
|
||
weatherSunTimesElement.textContent = `Sunrise: ${sunriseTime} | Sunset: ${sunsetTime}`;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Generate HTML for sites configuration
|
||
*/
|
||
generateSitesHTML() {
|
||
if (!this.config.sites || this.config.sites.length === 0) {
|
||
return '<div class="no-sites">No sites configured</div>';
|
||
}
|
||
|
||
return this.config.sites.map((site, index) => `
|
||
<div class="site-item" data-index="${index}">
|
||
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||
<input type="checkbox" class="site-enabled" ${site.enabled ? 'checked' : ''}>
|
||
<input type="text" class="site-name" value="${site.name}" placeholder="Site Name" style="flex: 1; margin: 0 5px;">
|
||
<button class="remove-site-button" style="padding: 2px 5px;">×</button>
|
||
</div>
|
||
<div style="display: flex; align-items: center;">
|
||
<span style="margin-right: 5px;">ID:</span>
|
||
<input type="text" class="site-id" value="${site.id}" placeholder="Site ID" style="width: 100px;">
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
/**
|
||
* Generate HTML for RSS feeds configuration
|
||
*/
|
||
generateRssFeedsHTML() {
|
||
if (!this.config.rssFeeds || this.config.rssFeeds.length === 0) {
|
||
return '<div class="no-feeds">No RSS feeds configured</div>';
|
||
}
|
||
|
||
return this.config.rssFeeds.map((feed, index) => `
|
||
<div class="feed-item" data-index="${index}">
|
||
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||
<input type="checkbox" class="feed-enabled" ${feed.enabled ? 'checked' : ''}>
|
||
<input type="text" class="feed-name" value="${feed.name}" placeholder="Feed Name" style="flex: 1; margin: 0 5px;">
|
||
<button class="remove-feed-button" style="padding: 2px 5px;">×</button>
|
||
</div>
|
||
<div style="display: flex; align-items: center;">
|
||
<span style="margin-right: 5px;">URL:</span>
|
||
<input type="text" class="feed-url" value="${feed.url}" placeholder="Feed URL" style="flex: 1;">
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
/**
|
||
* Get sites configuration from form
|
||
*/
|
||
getSitesFromForm() {
|
||
const sitesContainer = document.getElementById('sites-container');
|
||
const siteItems = sitesContainer.querySelectorAll('.site-item');
|
||
|
||
return Array.from(siteItems).map(item => {
|
||
return {
|
||
id: item.querySelector('.site-id').value,
|
||
name: item.querySelector('.site-name').value,
|
||
enabled: item.querySelector('.site-enabled').checked
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Get RSS feeds configuration from form
|
||
*/
|
||
getRssFeedsFromForm() {
|
||
const feedsContainer = document.getElementById('rss-feeds-container');
|
||
const feedItems = feedsContainer.querySelectorAll('.feed-item');
|
||
|
||
return Array.from(feedItems).map(item => {
|
||
return {
|
||
name: item.querySelector('.feed-name').value,
|
||
url: item.querySelector('.feed-url').value,
|
||
enabled: item.querySelector('.feed-enabled').checked
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Sync configuration with server
|
||
*/
|
||
syncConfig() {
|
||
fetch('/api/config/update', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(this.config)
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
console.log('Configuration synced with server:', data);
|
||
})
|
||
.catch(error => {
|
||
console.error('Error syncing configuration with server:', error);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Save the configuration to localStorage
|
||
*/
|
||
saveConfig() {
|
||
if (this.options.saveToLocalStorage && window.localStorage) {
|
||
try {
|
||
localStorage.setItem('sl-departures-config', JSON.stringify(this.config));
|
||
console.log('Configuration saved to localStorage');
|
||
} catch (error) {
|
||
console.error('Error saving configuration to localStorage:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Load the configuration from localStorage
|
||
*/
|
||
loadConfig() {
|
||
if (this.options.saveToLocalStorage && window.localStorage) {
|
||
try {
|
||
const savedConfig = localStorage.getItem('sl-departures-config');
|
||
if (savedConfig) {
|
||
console.log('Configuration loaded from localStorage');
|
||
return JSON.parse(savedConfig);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading configuration from localStorage:', error);
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
/**
|
||
* Setup keyboard shortcuts for testing
|
||
*/
|
||
setupKeyboardShortcuts() {
|
||
document.addEventListener('keydown', (event) => {
|
||
// Only handle keyboard shortcuts if not in an input field
|
||
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
|
||
return;
|
||
}
|
||
|
||
// Ctrl/Cmd + Shift + O: Toggle orientation
|
||
if (event.key === 'O' && (event.ctrlKey || event.metaKey) && event.shiftKey) {
|
||
event.preventDefault();
|
||
const orientations = ['normal', 'vertical', 'upsidedown', 'vertical-reverse', 'landscape'];
|
||
const currentIndex = orientations.indexOf(this.config.orientation);
|
||
const nextIndex = (currentIndex + 1) % orientations.length;
|
||
this.config.orientation = orientations[nextIndex];
|
||
this.applyConfig();
|
||
this.saveConfig();
|
||
}
|
||
|
||
// Ctrl/Cmd + Shift + D: Toggle dark mode
|
||
if (event.key === 'D' && (event.ctrlKey || event.metaKey) && event.shiftKey) {
|
||
event.preventDefault();
|
||
const modes = ['auto', 'on', 'off'];
|
||
const currentIndex = modes.indexOf(this.config.darkMode);
|
||
const nextIndex = (currentIndex + 1) % modes.length;
|
||
this.config.darkMode = modes[nextIndex];
|
||
this.applyConfig();
|
||
this.saveConfig();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Ensure content wrapper exists for rotated orientations
|
||
*/
|
||
ensureContentWrapper() {
|
||
if (!document.getElementById('content-wrapper')) {
|
||
console.log('Creating content wrapper');
|
||
const wrapper = document.createElement('div');
|
||
wrapper.id = 'content-wrapper';
|
||
|
||
// Move all body children to the wrapper except config elements
|
||
const configElements = ['config-button', 'config-modal', 'background-overlay'];
|
||
|
||
// Create an array of nodes to move (can't modify while iterating)
|
||
const nodesToMove = [];
|
||
for (let i = 0; i < document.body.children.length; i++) {
|
||
const child = document.body.children[i];
|
||
if (!configElements.includes(child.id) && child.id !== 'content-wrapper') {
|
||
nodesToMove.push(child);
|
||
}
|
||
}
|
||
|
||
// Move the nodes to the wrapper
|
||
nodesToMove.forEach(node => {
|
||
wrapper.appendChild(node);
|
||
});
|
||
|
||
// Add the wrapper back to the body
|
||
document.body.appendChild(wrapper);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Export the ConfigManager class for use in other modules
|
||
window.ConfigManager = ConfigManager;
|