/** * Settings Manager - Handles offline mode and application settings */ const Settings = { // Default settings defaults: { 'offline.enabled': false, 'offline.assets_source': 'cdn', 'offline.fonts_source': 'cdn', 'offline.tile_provider': 'openstreetmap', 'offline.tile_server_url': '' }, // Tile provider configurations tileProviders: { openstreetmap: { url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap contributors', subdomains: 'abc' }, cartodb_dark: { url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', attribution: '© OpenStreetMap contributors © CARTO', subdomains: 'abcd' }, cartodb_light: { url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', attribution: '© OpenStreetMap contributors © CARTO', subdomains: 'abcd' }, esri_world: { url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community', subdomains: null } }, // Current settings cache _cache: {}, /** * Initialize settings - load from server/localStorage */ async init() { try { const response = await fetch('/offline/settings'); if (response.ok) { const data = await response.json(); this._cache = { ...this.defaults, ...data.settings }; } else { // Fall back to localStorage this._loadFromLocalStorage(); } } catch (e) { console.warn('Failed to load settings from server, using localStorage:', e); this._loadFromLocalStorage(); } this._updateUI(); return this._cache; }, /** * Load settings from localStorage */ _loadFromLocalStorage() { const stored = localStorage.getItem('intercept_settings'); if (stored) { try { this._cache = { ...this.defaults, ...JSON.parse(stored) }; } catch (e) { this._cache = { ...this.defaults }; } } else { this._cache = { ...this.defaults }; } }, /** * Save a setting to server and localStorage */ async _save(key, value) { this._cache[key] = value; // Save to localStorage as backup localStorage.setItem('intercept_settings', JSON.stringify(this._cache)); // Save to server try { await fetch('/offline/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, value }) }); } catch (e) { console.warn('Failed to save setting to server:', e); } }, /** * Get a setting value */ get(key) { return this._cache[key] ?? this.defaults[key]; }, /** * Toggle offline mode master switch */ async toggleOfflineMode(enabled) { await this._save('offline.enabled', enabled); if (enabled) { // When enabling offline mode, also switch assets and fonts to local await this._save('offline.assets_source', 'local'); await this._save('offline.fonts_source', 'local'); } this._updateUI(); this._showReloadPrompt(); }, /** * Set asset source (cdn or local) */ async setAssetSource(source) { await this._save('offline.assets_source', source); this._showReloadPrompt(); }, /** * Set fonts source (cdn or local) */ async setFontsSource(source) { await this._save('offline.fonts_source', source); this._showReloadPrompt(); }, /** * Set tile provider */ async setTileProvider(provider) { await this._save('offline.tile_provider', provider); // Show/hide custom URL input const customRow = document.getElementById('customTileUrlRow'); if (customRow) { customRow.style.display = provider === 'custom' ? 'block' : 'none'; } // If not custom and we have a map, update tiles immediately if (provider !== 'custom') { this._updateMapTiles(); } }, /** * Set custom tile server URL */ async setCustomTileUrl(url) { await this._save('offline.tile_server_url', url); this._updateMapTiles(); }, /** * Get current tile configuration */ getTileConfig() { const provider = this.get('offline.tile_provider'); if (provider === 'custom') { const customUrl = this.get('offline.tile_server_url'); return { url: customUrl || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: 'Custom Tile Server', subdomains: 'abc' }; } return this.tileProviders[provider] || this.tileProviders.openstreetmap; }, /** * Check if local assets are available */ async checkAssets() { const assets = { leaflet: [ '/static/vendor/leaflet/leaflet.js', '/static/vendor/leaflet/leaflet.css' ], chartjs: [ '/static/vendor/chartjs/chart.umd.min.js' ], inter: [ '/static/vendor/fonts/Inter-Regular.woff2' ], jetbrains: [ '/static/vendor/fonts/JetBrainsMono-Regular.woff2' ] }; const results = {}; for (const [name, urls] of Object.entries(assets)) { const statusEl = document.getElementById(`status${name.charAt(0).toUpperCase() + name.slice(1)}`); if (statusEl) { statusEl.textContent = 'Checking...'; statusEl.className = 'asset-badge checking'; } let available = true; for (const url of urls) { try { const response = await fetch(url, { method: 'HEAD' }); if (!response.ok) { available = false; break; } } catch (e) { available = false; break; } } results[name] = available; if (statusEl) { statusEl.textContent = available ? 'Available' : 'Missing'; statusEl.className = `asset-badge ${available ? 'available' : 'missing'}`; } } return results; }, /** * Update UI elements to reflect current settings */ _updateUI() { // Offline mode toggle const offlineEnabled = document.getElementById('offlineEnabled'); if (offlineEnabled) { offlineEnabled.checked = this.get('offline.enabled'); } // Assets source const assetsSource = document.getElementById('assetsSource'); if (assetsSource) { assetsSource.value = this.get('offline.assets_source'); } // Fonts source const fontsSource = document.getElementById('fontsSource'); if (fontsSource) { fontsSource.value = this.get('offline.fonts_source'); } // Tile provider const tileProvider = document.getElementById('tileProvider'); if (tileProvider) { tileProvider.value = this.get('offline.tile_provider'); } // Custom tile URL const customTileUrl = document.getElementById('customTileUrl'); if (customTileUrl) { customTileUrl.value = this.get('offline.tile_server_url') || ''; } // Show/hide custom URL row const customRow = document.getElementById('customTileUrlRow'); if (customRow) { customRow.style.display = this.get('offline.tile_provider') === 'custom' ? 'block' : 'none'; } }, /** * Update map tiles if a map exists */ _updateMapTiles() { // Look for common map variable names const maps = [ window.map, window.leafletMap, window.aprsMap, window.adsbMap ].filter(m => m && typeof m.eachLayer === 'function'); if (maps.length === 0) return; const config = this.getTileConfig(); maps.forEach(map => { // Remove existing tile layers map.eachLayer(layer => { if (layer instanceof L.TileLayer) { map.removeLayer(layer); } }); // Add new tile layer const options = { attribution: config.attribution }; if (config.subdomains) { options.subdomains = config.subdomains; } L.tileLayer(config.url, options).addTo(map); }); }, /** * Show reload prompt */ _showReloadPrompt() { // Create or update reload prompt let prompt = document.getElementById('settingsReloadPrompt'); if (!prompt) { prompt = document.createElement('div'); prompt.id = 'settingsReloadPrompt'; prompt.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: var(--bg-dark, #0a0a0f); border: 1px solid var(--accent-cyan, #00d4ff); border-radius: 8px; padding: 12px 16px; display: flex; align-items: center; gap: 12px; z-index: 10001; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); `; prompt.innerHTML = ` Reload to apply changes `; document.body.appendChild(prompt); } } }; // Settings modal functions function showSettings() { const modal = document.getElementById('settingsModal'); if (modal) { modal.classList.add('active'); Settings.init().then(() => { Settings.checkAssets(); }); } } function hideSettings() { const modal = document.getElementById('settingsModal'); if (modal) { modal.classList.remove('active'); } } function switchSettingsTab(tabName) { // Update tab buttons document.querySelectorAll('.settings-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.tab === tabName); }); // Update sections document.querySelectorAll('.settings-section').forEach(section => { section.classList.toggle('active', section.id === `settings-${tabName}`); }); // Load tools/dependencies when that tab is selected if (tabName === 'tools') { loadSettingsTools(); } } /** * Load tool dependencies into settings modal */ function loadSettingsTools() { const content = document.getElementById('settingsToolsContent'); if (!content) return; content.innerHTML = '