Initial commit: Digital signage system for transit departures, weather, and news ticker

This commit is contained in:
2025-12-31 13:53:19 +01:00
commit a0c997f7d4
21 changed files with 5320 additions and 0 deletions

844
config.js Normal file
View File

@@ -0,0 +1,844 @@
/**
* 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">&times;</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;