/** * 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%) sites: [ { id: '1411', name: 'Ambassaderna', 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 template = document.getElementById('config-modal-template'); const modalContainer = document.createElement('div'); modalContainer.id = this.options.configModalId; modalContainer.className = 'config-modal'; modalContainer.style.display = 'none'; // Clone the template content into the modal modalContainer.appendChild(template.content.cloneNode(true)); // Set dynamic values from current config modalContainer.querySelector('#orientation-select').value = this.config.orientation; modalContainer.querySelector('#dark-mode-select').value = this.config.darkMode; modalContainer.querySelector('#background-image-url').value = this.config.backgroundImage || ''; modalContainer.querySelector('#background-opacity').value = this.config.backgroundOpacity; modalContainer.querySelector('#opacity-value').textContent = `${Math.round(this.config.backgroundOpacity * 100)}%`; modalContainer.querySelector('#combine-directions').checked = this.config.combineSameDirection; // Populate sites const sitesContainer = modalContainer.querySelector('#sites-container'); if (sitesContainer) { sitesContainer.innerHTML = this.generateSitesHTML(); } // Update background preview const preview = modalContainer.querySelector('#background-preview'); if (preview && this.config.backgroundImage) { const img = document.createElement('img'); img.src = this.config.backgroundImage; img.alt = 'Background preview'; preview.innerHTML = ''; preview.appendChild(img); } document.body.appendChild(modalContainer); // Add tab switching functionality this.setupTabs(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 listeners for site search const searchSiteButton = modalContainer.querySelector('#search-site-button'); const siteSearchInput = modalContainer.querySelector('#site-search-input'); if (searchSiteButton && siteSearchInput) { searchSiteButton.addEventListener('click', () => this.searchSites()); siteSearchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { this.searchSites(); } }); } // Add event listener for map selection const selectFromMapButton = modalContainer.querySelector('#select-from-map-button'); if (selectFromMapButton) { selectFromMapButton.addEventListener('click', () => this.showMapSelector()); } // 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 management this.setupSiteEventListeners(); } /** * 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); } }); }); } /** * 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(); } } /** * 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(); } } } /** * Setup tab switching functionality */ setupTabs(modalContainer) { const tabs = modalContainer.querySelectorAll('.config-tab'); const tabContents = modalContainer.querySelectorAll('.config-tab-content'); tabs.forEach(tab => { tab.addEventListener('click', () => { const targetTab = tab.dataset.tab; // Remove active class from all tabs and contents tabs.forEach(t => t.classList.remove('active')); tabContents.forEach(content => content.classList.remove('active')); // Add active class to clicked tab and corresponding content tab.classList.add('active'); const targetContent = modalContainer.querySelector(`#tab-${targetTab}`); if (targetContent) { targetContent.classList.add('active'); } }); }); } /** * 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'; // Reset to first tab const tabs = modal.querySelectorAll('.config-tab'); const tabContents = modal.querySelectorAll('.config-tab-content'); tabs.forEach(t => t.classList.remove('active')); tabContents.forEach(content => content.classList.remove('active')); if (tabs.length > 0) { tabs[0].classList.add('active'); } if (tabContents.length > 0) { tabContents[0].classList.add('active'); } // 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 background image and opacity values const backgroundImageUrl = document.getElementById('background-image-url'); const backgroundOpacity = document.getElementById('background-opacity'); const opacityValue = document.getElementById('opacity-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)}%`; } // 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)}%`; }); } // 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 combineDirections = document.getElementById('combine-directions'); // Get sites configuration from form const sitesConfig = this.getSitesFromForm(); // 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.combineSameDirection = combineDirections.checked; this.config.sites = sitesConfig; // Save config this.saveConfig(); // Sync with server this.syncConfig(); // 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(); // 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.backgroundImage = `url(${this.config.backgroundImage})`; overlay.style.opacity = this.config.backgroundOpacity; overlay.className = `orientation-${this.config.orientation}`; // Insert as the first child of body document.body.insertBefore(overlay, document.body.firstChild); } } /** * 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(''); } /** * Search for transit sites */ async searchSites() { const searchInput = document.getElementById('site-search-input'); const resultsContainer = document.getElementById('site-search-results'); if (!searchInput || !resultsContainer) return; const query = searchInput.value.trim(); if (!query || query.length < 2) { resultsContainer.style.display = 'none'; return; } try { resultsContainer.style.display = 'block'; resultsContainer.textContent = 'Searching...'; const response = await fetch(`/api/sites/search?q=${encodeURIComponent(query)}`); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: `HTTP ${response.status}` })); throw new Error(errorData.error || `HTTP error! Status: ${response.status}`); } const data = await response.json(); if (!data.sites || data.sites.length === 0) { resultsContainer.textContent = 'No sites found. Try a different search term.'; return; } resultsContainer.innerHTML = ''; data.sites.forEach(site => { const resultDiv = document.createElement('div'); resultDiv.className = 'site-search-result'; resultDiv.dataset.siteId = site.id; resultDiv.dataset.siteName = site.name; const nameDiv = document.createElement('div'); nameDiv.textContent = site.name; const idDiv = document.createElement('div'); idDiv.textContent = `ID: ${site.id}`; resultDiv.appendChild(nameDiv); resultDiv.appendChild(idDiv); resultDiv.addEventListener('click', () => { this.addSiteFromSearch(site.id, site.name); searchInput.value = ''; resultsContainer.style.display = 'none'; }); resultsContainer.appendChild(resultDiv); }); } catch (error) { console.error('Error searching sites:', error); let errorMessage = error.message || 'Unknown error'; // Provide more helpful error messages if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) { errorMessage = 'Unable to connect to server. Please make sure the server is running.'; } else if (error.message.includes('HTTP')) { errorMessage = `Server error: ${error.message}`; } resultsContainer.textContent = `Error: ${errorMessage}`; } } /** * Add a site from search results */ addSiteFromSearch(siteId, siteName) { // Check if site already exists if (this.config.sites.some(site => site.id === siteId)) { alert(`Site "${siteName}" (ID: ${siteId}) is already in your list.`); return; } this.config.sites.push({ id: siteId, name: siteName, enabled: true }); const sitesContainer = document.getElementById('sites-container'); if (sitesContainer) { sitesContainer.innerHTML = this.generateSitesHTML(); this.setupSiteEventListeners(); } } /** * Show map selector for choosing transit sites */ async showMapSelector() { // Create map modal const mapModal = document.createElement('div'); mapModal.id = 'map-selector-modal'; mapModal.className = 'config-modal'; mapModal.style.display = 'flex'; mapModal.innerHTML = `

Select Transit Stop from Map

×
Instructions: Click on a marker to select a transit stop. Zoom and pan to explore the map.
`; document.body.appendChild(mapModal); // Wait for DOM to be ready await new Promise(resolve => setTimeout(resolve, 100)); // Initialize map centered on Stockholm const map = L.map('map-container').setView([59.3293, 18.0686], 13); // Add OpenStreetMap tiles L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 19 }).addTo(map); // Close handlers const closeMap = () => { map.remove(); document.body.removeChild(mapModal); }; mapModal.querySelector('.map-modal-close').addEventListener('click', closeMap); mapModal.querySelector('#map-close-button').addEventListener('click', closeMap); mapModal.addEventListener('click', (e) => { if (e.target === mapModal) closeMap(); }); // Load transit stops - search for common Stockholm areas const loadSitesOnMap = async () => { try { // Start with focused search to avoid too many markers const searchTerms = ['Ambassaderna']; const allSites = new Map(); for (const term of searchTerms) { try { const response = await fetch(`/api/sites/search?q=${encodeURIComponent(term)}`); const data = await response.json(); console.log(`Search "${term}" returned:`, data); if (data.sites) { data.sites.forEach(site => { // Only add sites with valid coordinates from API const lat = site.lat || site.latitude; const lon = site.lon || site.longitude; if (lat && lon && !isNaN(parseFloat(lat)) && !isNaN(parseFloat(lon))) { if (!allSites.has(site.id)) { allSites.set(site.id, { id: site.id, name: site.name, lat: parseFloat(lat), lon: parseFloat(lon) }); } } else { console.log(`Site ${site.id} (${site.name}) missing coordinates, skipping`); } }); } } catch (err) { console.error(`Error searching for ${term}:`, err); } } // Add known site with coordinates as fallback const knownSites = [ { id: '1411', name: 'Ambassaderna', lat: 59.3293, lon: 18.0686 } ]; knownSites.forEach(site => { if (!allSites.has(site.id)) { allSites.set(site.id, site); } }); const sitesArray = Array.from(allSites.values()); console.log(`Loading ${sitesArray.length} sites on map with coordinates:`, sitesArray); if (sitesArray.length > 0) { const markers = []; sitesArray.forEach(site => { const lat = site.lat; const lon = site.lon; if (!lat || !lon || isNaN(lat) || isNaN(lon)) { console.warn(`Invalid coordinates for site ${site.id}, skipping`); return; } // Create custom icon const customIcon = L.divIcon({ className: 'custom-marker', html: `
🚌
`, iconSize: [30, 30], iconAnchor: [15, 15] }); const marker = L.marker([lat, lon], { icon: customIcon }).addTo(map); const popupContent = `
${site.name}
ID: ${site.id}
`; marker.bindPopup(popupContent); markers.push(marker); // Make marker clickable to open popup marker.on('click', function() { this.openPopup(); }); }); // Fit map to show all markers, or center on first marker if (markers.length > 0) { if (markers.length === 1) { map.setView(markers[0].getLatLng(), 15); } else { const group = new L.featureGroup(markers); map.fitBounds(group.getBounds().pad(0.1)); } } else { // If no markers, show message console.log('No sites with coordinates found'); } } } catch (error) { console.error('Error loading sites on map:', error); } }; // Load sites after map is initialized setTimeout(() => { loadSitesOnMap(); }, 500); // Handle site selection from map popup - use event delegation on the modal mapModal.addEventListener('click', (e) => { if (e.target.classList.contains('select-site-from-map')) { e.preventDefault(); e.stopPropagation(); const siteId = e.target.dataset.siteId; const siteName = e.target.dataset.siteName; if (siteId && siteName) { console.log(`Selecting site: ${siteName} (ID: ${siteId})`); this.addSiteFromSearch(siteId, siteName); closeMap(); } } }); // Also handle popupopen event to ensure buttons are clickable map.on('popupopen', (e) => { const popup = e.popup; const content = popup.getElement(); if (content) { const selectBtn = content.querySelector('.select-site-from-map'); if (selectBtn) { selectBtn.addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation(); const siteId = selectBtn.dataset.siteId; const siteName = selectBtn.dataset.siteName; if (siteId && siteName) { console.log(`Selecting site from popup: ${siteName} (ID: ${siteId})`); this.addSiteFromSearch(siteId, siteName); closeMap(); } }); } } }); // Map search functionality const mapSearchInput = document.getElementById('map-search-input'); if (mapSearchInput) { mapSearchInput.addEventListener('keypress', async (e) => { if (e.key === 'Enter') { const query = mapSearchInput.value.trim(); if (query.length < 2) return; try { const response = await fetch(`/api/sites/search?q=${encodeURIComponent(query)}`); const data = await response.json(); if (data.sites && data.sites.length > 0) { // Clear existing markers map.eachLayer((layer) => { if (layer instanceof L.Marker) { map.removeLayer(layer); } }); // Clear existing markers map.eachLayer((layer) => { if (layer instanceof L.Marker) { map.removeLayer(layer); } }); // Add markers for search results const markers = []; data.sites.forEach(site => { let lat = site.lat || site.latitude; let lon = site.lon || site.longitude; // If no coordinates, use approximate location based on map center if (!lat || !lon) { const center = map.getCenter(); lat = center.lat + (Math.random() - 0.5) * 0.05; lon = center.lon + (Math.random() - 0.5) * 0.05; } // Create custom icon const customIcon = L.divIcon({ className: 'custom-transit-marker', html: `
🚌
`, iconSize: [32, 32], iconAnchor: [16, 16], popupAnchor: [0, -16] }); const marker = L.marker([lat, lon], { icon: customIcon, title: site.name }).addTo(map); const popupContent = document.createElement('div'); popupContent.style.minWidth = '220px'; popupContent.innerHTML = `
${site.name} ID: ${site.id}
`; marker.bindPopup(popupContent); markers.push(marker); marker.on('click', function() { this.openPopup(); }); }); // Fit map to show results if (markers.length > 0) { const group = new L.featureGroup(markers); map.fitBounds(group.getBounds().pad(0.1)); } } } catch (error) { console.error('Error searching on map:', error); } } }); } } /** * 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 }; }); } /** * 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); } } } // ES module export export { ConfigManager }; // Keep window reference for backward compatibility window.ConfigManager = ConfigManager;