Files
SignageHTML/config.js

845 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;