/** * 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 = `Background preview`; } else { preview.innerHTML = '
No image selected
'; } } }; // 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 = ` `; 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 = `

Settings

×
Sunrise: --:-- | Sunset: --:--
${this.config.backgroundImage ? `Background preview` : '
No image selected
'}
${this.generateSitesHTML()}
${this.generateRssFeedsHTML()}
`; 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 '
No sites configured
'; } return this.config.sites.map((site, index) => `
ID:
`).join(''); } /** * Generate HTML for RSS feeds configuration */ generateRssFeedsHTML() { if (!this.config.rssFeeds || this.config.rssFeeds.length === 0) { return '
No RSS feeds configured
'; } return this.config.rssFeeds.map((feed, index) => `
URL:
`).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;