diff --git a/.ruby-version b/.ruby-version index 47b322c9..1cf82530 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.1 +3.4.6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c847f1e..24c3f80f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `GET /api/v1/points will now return correct latitude and longitude values. #1502 - Deleting an import will now trigger stats recalculation for affected months. #1789 - Importing process should now schedule visits suggestions job a lot faster. +- Importing GPX files that start with ` { - if (!this.isAddingVisit) { - button.style.backgroundColor = '#f0f0f0'; - } - }); - - button.addEventListener('mouseleave', () => { - if (!this.isAddingVisit) { - button.style.backgroundColor = 'white'; - } - }); - // Toggle add visit mode on button click L.DomEvent.on(button, 'click', () => { this.toggleAddVisitMode(button); @@ -150,9 +140,8 @@ export default class extends Controller { exitAddVisitMode(button) { this.isAddingVisit = false; - // Reset button style - button.style.backgroundColor = 'white'; - button.style.color = 'black'; + // Reset button style with theme-aware styling + applyThemeToButton(button, this.userThemeValue || 'dark'); button.innerHTML = '➕'; // Reset cursor @@ -446,6 +435,16 @@ export default class extends Controller { }); } + handleThemeChange(event) { + console.log('Add visit controller: Theme changed to', event.detail.theme); + this.userThemeValue = event.detail.theme; + + // Update button theme if it exists + if (this.addVisitButton && !this.isAddingVisit) { + applyThemeToButton(this.addVisitButton, this.userThemeValue); + } + } + cleanup() { if (this.map) { this.map.off('click', this.onMapClick, this); diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 90d96342..cd7c695f 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -43,6 +43,8 @@ import { initializeFogCanvas, drawFogCanvas, createFogOverlay } from "../maps/fo import { TileMonitor } from "../maps/tile_monitor"; import BaseController from "./base_controller"; import { createAllMapLayers } from "../maps/layers"; +import { applyThemeToControl, applyThemeToButton, applyThemeToPanel } from "../maps/theme_utils"; +import { injectThemeStyles } from "../maps/theme_styles"; export default class extends BaseController { static targets = ["container"]; @@ -61,6 +63,10 @@ export default class extends BaseController { this.apiKey = this.element.dataset.api_key; this.selfHosted = this.element.dataset.self_hosted; + this.userTheme = this.element.dataset.user_theme || 'dark'; + + // Inject theme styles for Leaflet controls + injectThemeStyles(this.userTheme); try { this.markers = this.element.dataset.coordinates ? JSON.parse(this.element.dataset.coordinates) : []; @@ -134,10 +140,11 @@ export default class extends BaseController { const unit = this.distanceUnit === 'km' ? 'km' : 'mi'; div.innerHTML = `${distance} ${unit} | ${pointsNumber} points`; - div.style.backgroundColor = 'white'; - div.style.padding = '0 5px'; - div.style.marginRight = '5px'; - div.style.display = 'inline-block'; + applyThemeToControl(div, this.userTheme, { + padding: '0 5px', + marginRight: '5px', + display: 'inline-block' + }); return div; } }); @@ -195,8 +202,8 @@ export default class extends BaseController { } // Initialize the visits manager - this.visitsManager = new VisitsManager(this.map, this.apiKey); - + this.visitsManager = new VisitsManager(this.map, this.apiKey, this.userTheme); + // Expose visits manager globally for location search integration window.visitsManager = this.visitsManager; @@ -385,7 +392,7 @@ export default class extends BaseController { baseMaps() { let selectedLayerName = this.userSettings.preferred_map_layer || "OpenStreetMap"; - let maps = createAllMapLayers(this.map, selectedLayerName, this.selfHosted); + let maps = createAllMapLayers(this.map, selectedLayerName, this.selfHosted, this.userTheme); // Add custom map if it exists in settings if (this.userSettings.maps && this.userSettings.maps.url) { @@ -396,40 +403,28 @@ export default class extends BaseController { // If this is the preferred layer, add it to the map immediately if (selectedLayerName === this.userSettings.maps.name) { - customLayer.addTo(this.map); - // Remove any other base layers that might be active + // Remove any existing base layers first Object.values(maps).forEach(layer => { if (this.map.hasLayer(layer)) { this.map.removeLayer(layer); } }); + customLayer.addTo(this.map); } maps[this.userSettings.maps.name] = customLayer; } else { - // If no custom map is set, ensure a default layer is added - // First check if maps object has any entries + // If no maps were created (fallback case), add OSM if (Object.keys(maps).length === 0) { - // Fallback to OSM if no maps are configured - maps["OpenStreetMap"] = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { + console.warn('No map layers available, adding OSM fallback'); + const osmLayer = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap" }); + osmLayer.addTo(this.map); + maps["OpenStreetMap"] = osmLayer; } - - // Now try to get the selected layer or fall back to alternatives - const defaultLayer = maps[selectedLayerName] || Object.values(maps)[0]; - - if (defaultLayer) { - defaultLayer.addTo(this.map); - } else { - console.error("Could not find any default map layer"); - // Ultimate fallback - create and add OSM layer directly - L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { - maxZoom: 19, - attribution: "© OpenStreetMap" - }).addTo(this.map); - } + // Note: createAllMapLayers already added the appropriate theme-aware layer to the map } return maps; @@ -731,13 +726,10 @@ export default class extends BaseController { const button = L.DomUtil.create('button', 'map-settings-button'); button.innerHTML = '⚙️'; // Gear icon - // Style the button - button.style.backgroundColor = 'white'; + // Style the button with theme-aware styling + applyThemeToButton(button, this.userTheme); button.style.width = '32px'; button.style.height = '32px'; - button.style.border = 'none'; - button.style.cursor = 'pointer'; - button.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)'; // Disable map interactions when clicking the button L.DomEvent.disableClickPropagation(button); @@ -863,11 +855,9 @@ export default class extends BaseController { `; - // Style the panel - div.style.backgroundColor = 'white'; + // Style the panel with theme-aware styling + applyThemeToPanel(div, this.userTheme); div.style.padding = '10px'; - div.style.border = '1px solid #ccc'; - div.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)'; // Prevent map interactions when interacting with the form L.DomEvent.disableClickPropagation(div); @@ -1010,6 +1000,22 @@ export default class extends BaseController { const mapElement = document.getElementById('map'); if (mapElement) { mapElement.setAttribute('data-user_settings', JSON.stringify(this.userSettings)); + // Update theme if it changed + if (newSettings.theme && newSettings.theme !== this.userTheme) { + this.userTheme = newSettings.theme; + mapElement.setAttribute('data-user_theme', this.userTheme); + injectThemeStyles(this.userTheme); + + // Update location search theme if it exists + if (this.locationSearch) { + this.locationSearch.updateTheme(this.userTheme); + } + + // Dispatch theme change event for other controllers + document.dispatchEvent(new CustomEvent('theme:changed', { + detail: { theme: this.userTheme } + })); + } } // Store current layer states @@ -1091,12 +1097,10 @@ export default class extends BaseController { const button = L.DomUtil.create('button', 'toggle-panel-button'); button.innerHTML = '📅'; + // Style the button with theme-aware styling + applyThemeToButton(button, controller.userTheme); button.style.width = '48px'; button.style.height = '48px'; - button.style.border = 'none'; - button.style.cursor = 'pointer'; - button.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)'; - button.style.backgroundColor = 'white'; button.style.borderRadius = '4px'; button.style.padding = '0'; button.style.lineHeight = '48px'; @@ -1131,12 +1135,12 @@ export default class extends BaseController { const RouteTracksControl = L.Control.extend({ onAdd: function(map) { const container = L.DomUtil.create('div', 'routes-tracks-selector leaflet-bar'); - container.style.backgroundColor = 'white'; - container.style.padding = '8px'; - container.style.borderRadius = '4px'; - container.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)'; - container.style.fontSize = '12px'; - container.style.lineHeight = '1.2'; + applyThemeToControl(container, controller.userTheme, { + padding: '8px', + borderRadius: '4px', + fontSize: '12px', + lineHeight: '1.2' + }); // Get saved preference or default to 'routes' const savedPreference = localStorage.getItem('mapRouteMode') || 'routes'; @@ -1395,10 +1399,8 @@ export default class extends BaseController { this.fetchAndDisplayTrackedMonths(div, currentYear, currentMonth, allMonths); - div.style.backgroundColor = 'white'; + applyThemeToPanel(div, this.userTheme); div.style.padding = '10px'; - div.style.border = '1px solid #ccc'; - div.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)'; div.style.marginRight = '10px'; div.style.marginTop = '10px'; div.style.width = '300px'; @@ -1840,7 +1842,7 @@ export default class extends BaseController { initializeLocationSearch() { if (this.map && this.apiKey && this.features.reverse_geocoding) { - this.locationSearch = new LocationSearch(this.map, this.apiKey); + this.locationSearch = new LocationSearch(this.map, this.apiKey, this.userTheme); } } } diff --git a/app/javascript/controllers/public_stat_map_controller.js b/app/javascript/controllers/public_stat_map_controller.js index e8bac6c3..a16d5f29 100644 --- a/app/javascript/controllers/public_stat_map_controller.js +++ b/app/javascript/controllers/public_stat_map_controller.js @@ -50,7 +50,7 @@ export default class extends BaseController { try { // Use appropriate default layer based on self-hosted mode const selectedLayerName = this.selfHosted === "true" ? "OpenStreetMap" : "Light"; - const maps = createAllMapLayers(this.map, selectedLayerName, this.selfHosted); + const maps = createAllMapLayers(this.map, selectedLayerName, this.selfHosted, 'dark'); // If no layers were created, fall back to OSM if (Object.keys(maps).length === 0) { diff --git a/app/javascript/controllers/stat_page_controller.js b/app/javascript/controllers/stat_page_controller.js index 97b54b24..e2e94184 100644 --- a/app/javascript/controllers/stat_page_controller.js +++ b/app/javascript/controllers/stat_page_controller.js @@ -264,7 +264,7 @@ export default class extends BaseController { try { // Use appropriate default layer based on self-hosted mode const selectedLayerName = this.selfHosted === "true" ? "OpenStreetMap" : "Light"; - const maps = createAllMapLayers(this.map, selectedLayerName, this.selfHosted); + const maps = createAllMapLayers(this.map, selectedLayerName, this.selfHosted, 'dark'); // If no layers were created, fall back to OSM if (Object.keys(maps).length === 0) { diff --git a/app/javascript/controllers/trip_map_controller.js b/app/javascript/controllers/trip_map_controller.js index c95620e7..588a8e3b 100644 --- a/app/javascript/controllers/trip_map_controller.js +++ b/app/javascript/controllers/trip_map_controller.js @@ -53,7 +53,7 @@ export default class extends BaseController { this.userSettingsValue.preferred_map_layer || "OpenStreetMap" : "OpenStreetMap"; - let maps = createAllMapLayers(this.map, selectedLayerName); + let maps = createAllMapLayers(this.map, selectedLayerName, "false", 'dark'); // Add custom map if it exists in settings if (this.hasUserSettingsValue && this.userSettingsValue.maps && this.userSettingsValue.maps.url) { diff --git a/app/javascript/controllers/trips_controller.js b/app/javascript/controllers/trips_controller.js index 6275716a..b9c15d2f 100644 --- a/app/javascript/controllers/trips_controller.js +++ b/app/javascript/controllers/trips_controller.js @@ -168,7 +168,7 @@ export default class extends BaseController { baseMaps() { let selectedLayerName = this.userSettings.preferred_map_layer || "OpenStreetMap"; - let maps = createAllMapLayers(this.map, selectedLayerName); + let maps = createAllMapLayers(this.map, selectedLayerName, "false", 'dark'); // Add custom map if it exists in settings if (this.userSettings.maps && this.userSettings.maps.url) { diff --git a/app/javascript/maps/layers.js b/app/javascript/maps/layers.js index 6125beef..f00dc020 100644 --- a/app/javascript/maps/layers.js +++ b/app/javascript/maps/layers.js @@ -45,12 +45,54 @@ export function createMapLayer(map, selectedLayerName, layerKey, selfHosted) { } } +// Helper function to apply theme-aware layer selection +function getThemeAwareLayerName(preferredLayerName, userTheme, selfHosted) { + // Only apply theme-aware logic for non-self-hosted (vector) maps + if (selfHosted === "true") { + return preferredLayerName; + } + + // Define light and dark layer groups + const lightLayers = ["Light", "White", "Grayscale"]; + const darkLayers = ["Dark", "Black"]; + + let finalLayerName = preferredLayerName; + + if (userTheme === "light") { + // If user theme is light and preferred layer is light-compatible, keep it + if (lightLayers.includes(preferredLayerName)) { + finalLayerName = preferredLayerName; + } + // If user theme is light but preferred layer is dark, default to White + else if (darkLayers.includes(preferredLayerName)) { + finalLayerName = "White"; + } + } else if (userTheme === "dark") { + // If user theme is dark and preferred layer is dark-compatible, keep it + if (darkLayers.includes(preferredLayerName)) { + finalLayerName = preferredLayerName; + } + // If user theme is dark but preferred layer is light, default to Dark + else if (lightLayers.includes(preferredLayerName)) { + finalLayerName = "Dark"; + } + } + + return finalLayerName; +} + // Helper function to create all map layers -export function createAllMapLayers(map, selectedLayerName, selfHosted) { +export function createAllMapLayers(map, selectedLayerName, selfHosted, userTheme = 'dark') { const layers = {}; const mapsConfig = selfHosted === "true" ? rasterMapsConfig : vectorMapsConfig; + + // Apply theme-aware selection + const themeAwareLayerName = getThemeAwareLayerName(selectedLayerName, userTheme, selfHosted); + Object.keys(mapsConfig).forEach(layerKey => { - layers[layerKey] = createMapLayer(map, selectedLayerName, layerKey, selfHosted); + // Create the layer and add it to the map if it's the theme-aware selected layer + const layer = createMapLayer(map, themeAwareLayerName, layerKey, selfHosted); + layers[layerKey] = layer; }); return layers; diff --git a/app/javascript/maps/location_search.js b/app/javascript/maps/location_search.js index 4479dd93..645290b2 100644 --- a/app/javascript/maps/location_search.js +++ b/app/javascript/maps/location_search.js @@ -1,8 +1,11 @@ // Location search functionality for the map +import { applyThemeToButton } from "./theme_utils"; + class LocationSearch { - constructor(map, apiKey) { + constructor(map, apiKey, userTheme = 'dark') { this.map = map; this.apiKey = apiKey; + this.userTheme = userTheme; this.searchResults = []; this.searchMarkersLayer = null; this.currentSearchQuery = ''; @@ -22,12 +25,10 @@ class LocationSearch { onAdd: function(map) { const button = L.DomUtil.create('button', 'location-search-toggle'); button.innerHTML = '🔍'; + // Style the button with theme-aware styling + applyThemeToButton(button, this.userTheme); button.style.width = '48px'; button.style.height = '48px'; - button.style.border = 'none'; - button.style.cursor = 'pointer'; - button.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)'; - button.style.backgroundColor = 'white'; button.style.borderRadius = '4px'; button.style.padding = '0'; button.style.fontSize = '18px'; @@ -1158,6 +1159,16 @@ class LocationSearch { return new Date(dateString).toLocaleDateString() + ' ' + new Date(dateString).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); } + + updateTheme(newTheme) { + this.userTheme = newTheme; + + // Update search button theme if it exists + const searchButton = document.getElementById('location-search-toggle'); + if (searchButton) { + applyThemeToButton(searchButton, newTheme); + } + } } export { LocationSearch }; diff --git a/app/javascript/maps/theme_styles.js b/app/javascript/maps/theme_styles.js new file mode 100644 index 00000000..93820aba --- /dev/null +++ b/app/javascript/maps/theme_styles.js @@ -0,0 +1,156 @@ +// Dynamic CSS injection for theme-aware Leaflet controls +export function injectThemeStyles(userTheme) { + // Remove existing theme styles if any + const existingStyle = document.getElementById('leaflet-theme-styles'); + if (existingStyle) { + existingStyle.remove(); + } + + const themeColors = getThemeColors(userTheme); + + const css = ` + /* Leaflet default controls theme override */ + .leaflet-control-layers, + .leaflet-control-zoom, + .leaflet-control-attribution, + .leaflet-bar a, + .leaflet-control-layers-toggle, + .leaflet-control-layers-list, + .leaflet-control-draw { + background-color: ${themeColors.backgroundColor} !important; + color: ${themeColors.textColor} !important; + border-color: ${themeColors.borderColor} !important; + box-shadow: 0 1px 4px ${themeColors.shadowColor} !important; + } + + /* Leaflet zoom buttons */ + .leaflet-control-zoom a { + background-color: ${themeColors.backgroundColor} !important; + color: ${themeColors.textColor} !important; + border-bottom: 1px solid ${themeColors.borderColor} !important; + } + + .leaflet-control-zoom a:hover { + background-color: ${themeColors.hoverColor} !important; + } + + /* Leaflet layer control */ + .leaflet-control-layers-toggle { + background-color: ${themeColors.backgroundColor} !important; + color: ${themeColors.textColor} !important; + } + + .leaflet-control-layers-expanded { + background-color: ${themeColors.backgroundColor} !important; + color: ${themeColors.textColor} !important; + } + + .leaflet-control-layers label { + color: ${themeColors.textColor} !important; + } + + /* Leaflet Draw controls */ + .leaflet-draw-toolbar a { + background-color: ${themeColors.backgroundColor} !important; + color: ${themeColors.textColor} !important; + border-bottom: 1px solid ${themeColors.borderColor} !important; + } + + .leaflet-draw-toolbar a:hover { + background-color: ${themeColors.hoverColor} !important; + } + + .leaflet-draw-actions a { + background-color: ${themeColors.backgroundColor} !important; + color: ${themeColors.textColor} !important; + } + + /* Leaflet popups */ + .leaflet-popup-content-wrapper { + background-color: ${themeColors.backgroundColor} !important; + color: ${themeColors.textColor} !important; + } + + .leaflet-popup-tip { + background-color: ${themeColors.backgroundColor} !important; + } + + /* Attribution control */ + .leaflet-control-attribution a { + color: ${userTheme === 'light' ? '#0066cc' : '#66b3ff'} !important; + } + + /* Custom control buttons */ + .leaflet-control-button, + .add-visit-button, + .leaflet-bar button { + background-color: ${themeColors.backgroundColor} !important; + color: ${themeColors.textColor} !important; + border: 1px solid ${themeColors.borderColor} !important; + box-shadow: 0 1px 4px ${themeColors.shadowColor} !important; + } + + .leaflet-control-button:hover, + .add-visit-button:hover, + .leaflet-bar button:hover { + background-color: ${themeColors.hoverColor} !important; + } + + /* Any other custom controls */ + .leaflet-top .leaflet-control button, + .leaflet-bottom .leaflet-control button, + .leaflet-left .leaflet-control button, + .leaflet-right .leaflet-control button { + background-color: ${themeColors.backgroundColor} !important; + color: ${themeColors.textColor} !important; + border: 1px solid ${themeColors.borderColor} !important; + } + + /* Location search button */ + .location-search-toggle, + #location-search-toggle { + background-color: ${themeColors.backgroundColor} !important; + color: ${themeColors.textColor} !important; + border: 1px solid ${themeColors.borderColor} !important; + box-shadow: 0 1px 4px ${themeColors.shadowColor} !important; + } + + .location-search-toggle:hover, + #location-search-toggle:hover { + background-color: ${themeColors.hoverColor} !important; + } + + /* Distance scale control - minimal theming to avoid duplication */ + .leaflet-control-scale { + background: rgba(${userTheme === 'light' ? '255, 255, 255' : '55, 65, 81'}, 0.9) !important; + border-radius: 3px !important; + padding: 2px !important; + } + `; + + // Inject the CSS + const style = document.createElement('style'); + style.id = 'leaflet-theme-styles'; + style.textContent = css; + document.head.appendChild(style); +} + +function getThemeColors(userTheme) { + if (userTheme === 'light') { + return { + backgroundColor: '#ffffff', + textColor: '#000000', + borderColor: '#e5e7eb', + shadowColor: 'rgba(0, 0, 0, 0.1)', + hoverColor: '#f3f4f6' + }; + } else { + return { + backgroundColor: '#374151', + textColor: '#ffffff', + borderColor: '#4b5563', + shadowColor: 'rgba(0, 0, 0, 0.3)', + hoverColor: '#4b5563' + }; + } +} \ No newline at end of file diff --git a/app/javascript/maps/theme_utils.js b/app/javascript/maps/theme_utils.js new file mode 100644 index 00000000..822d0466 --- /dev/null +++ b/app/javascript/maps/theme_utils.js @@ -0,0 +1,78 @@ +// Theme utility functions for map controls and buttons + +/** + * Get theme-aware styles for map controls based on user theme + * @param {string} userTheme - 'light' or 'dark' + * @returns {Object} Object containing CSS properties for the theme + */ +export function getThemeStyles(userTheme) { + if (userTheme === 'light') { + return { + backgroundColor: '#ffffff', + color: '#000000', + borderColor: '#e5e7eb', + shadowColor: 'rgba(0, 0, 0, 0.1)' + }; + } else { + return { + backgroundColor: '#374151', + color: '#ffffff', + borderColor: '#4b5563', + shadowColor: 'rgba(0, 0, 0, 0.3)' + }; + } +} + +/** + * Apply theme-aware styles to a control element + * @param {HTMLElement} element - DOM element to style + * @param {string} userTheme - 'light' or 'dark' + * @param {Object} additionalStyles - Optional additional CSS properties + */ +export function applyThemeToControl(element, userTheme, additionalStyles = {}) { + const themeStyles = getThemeStyles(userTheme); + + // Apply base theme styles + element.style.backgroundColor = themeStyles.backgroundColor; + element.style.color = themeStyles.color; + element.style.border = `1px solid ${themeStyles.borderColor}`; + element.style.boxShadow = `0 1px 4px ${themeStyles.shadowColor}`; + + // Apply any additional styles + Object.assign(element.style, additionalStyles); +} + +/** + * Apply theme-aware styles to a button element + * @param {HTMLElement} button - Button element to style + * @param {string} userTheme - 'light' or 'dark' + */ +export function applyThemeToButton(button, userTheme) { + applyThemeToControl(button, userTheme, { + border: 'none', + cursor: 'pointer' + }); + + // Add hover effects + const themeStyles = getThemeStyles(userTheme); + const hoverBg = userTheme === 'light' ? '#f3f4f6' : '#4b5563'; + + button.addEventListener('mouseenter', () => { + button.style.backgroundColor = hoverBg; + }); + + button.addEventListener('mouseleave', () => { + button.style.backgroundColor = themeStyles.backgroundColor; + }); +} + +/** + * Apply theme-aware styles to a panel/container element + * @param {HTMLElement} panel - Panel element to style + * @param {string} userTheme - 'light' or 'dark' + */ +export function applyThemeToPanel(panel, userTheme) { + applyThemeToControl(panel, userTheme, { + borderRadius: '4px' + }); +} \ No newline at end of file diff --git a/app/javascript/maps/visits.js b/app/javascript/maps/visits.js index 4b907587..f8c6e6af 100644 --- a/app/javascript/maps/visits.js +++ b/app/javascript/maps/visits.js @@ -1,13 +1,15 @@ import L from "leaflet"; import { showFlashMessage } from "./helpers"; +import { applyThemeToButton } from "./theme_utils"; /** * Manages visits functionality including displaying, fetching, and interacting with visits */ export class VisitsManager { - constructor(map, apiKey) { + constructor(map, apiKey, userTheme = 'dark') { this.map = map; this.apiKey = apiKey; + this.userTheme = userTheme; // Create custom panes for different visit types if (!map.getPane('confirmedVisitsPane')) { @@ -67,12 +69,10 @@ export class VisitsManager { onAdd: (map) => { const button = L.DomUtil.create('button', 'leaflet-control-button drawer-button'); button.innerHTML = '⬅️'; // Left arrow icon + // Style the button with theme-aware styling + applyThemeToButton(button, this.userTheme); button.style.width = '48px'; button.style.height = '48px'; - button.style.border = 'none'; - button.style.cursor = 'pointer'; - button.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)'; - button.style.backgroundColor = 'white'; button.style.borderRadius = '4px'; button.style.padding = '0'; button.style.lineHeight = '48px'; @@ -104,12 +104,10 @@ export class VisitsManager { button.innerHTML = '⚓️'; button.title = 'Select Area'; button.id = 'selection-tool-button'; + // Style the button with theme-aware styling + applyThemeToButton(button, this.userTheme); button.style.width = '48px'; button.style.height = '48px'; - button.style.border = 'none'; - button.style.cursor = 'pointer'; - button.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)'; - button.style.backgroundColor = 'white'; button.style.borderRadius = '4px'; button.style.padding = '0'; button.style.lineHeight = '48px'; diff --git a/app/services/imports/source_detector.rb b/app/services/imports/source_detector.rb index 7acbb081..a1bfd004 100644 --- a/app/services/imports/source_detector.rb +++ b/app/services/imports/source_detector.rb @@ -40,7 +40,7 @@ class Imports::SourceDetector ] }, geojson: { - required_keys: ['type', 'features'], + required_keys: %w[type features], required_values: { 'type' => 'FeatureCollection' }, nested_patterns: [ ['features', 0, 'type'], @@ -79,9 +79,7 @@ class Imports::SourceDetector DETECTION_RULES.each do |format, rules| next if format == :owntracks # Already handled above - if matches_format?(json_data, rules) - return format - end + return format if matches_format?(json_data, rules) end nil @@ -105,14 +103,17 @@ class Imports::SourceDetector return false unless filename.downcase.end_with?('.gpx') # Check content for GPX structure - content_to_check = if file_path && File.exist?(file_path) - # Read first 1KB for GPX detection - File.open(file_path, 'rb') { |f| f.read(1024) } - else - file_content - end - - content_to_check.strip.start_with?('= current.length + current = current[key] elsif current.is_a?(Hash) return false unless current.key?(key) + current = current[key] else return false diff --git a/app/views/map/index.html.erb b/app/views/map/index.html.erb index 6cc51f5e..412741cd 100644 --- a/app/views/map/index.html.erb +++ b/app/views/map/index.html.erb @@ -68,6 +68,7 @@ data-api_key="<%= current_user.api_key %>" data-self_hosted="<%= @self_hosted %>" data-user_settings='<%= (current_user.settings || {}).to_json.html_safe %>' + data-user_theme="<%= current_user&.theme || 'dark' %>" data-coordinates='<%= @coordinates.to_json.html_safe %>' data-tracks='<%= @tracks.to_json.html_safe %>' data-distance="<%= @distance %>" diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index aed33719..ee6edf40 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM ruby:3.4.1-slim +FROM ruby:3.4.6-slim ENV APP_PATH=/var/app ENV BUNDLE_VERSION=2.5.21 @@ -13,6 +13,7 @@ ENV SIDEKIQ_PASSWORD=password ENV PGSSENCMODE=disable RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + curl \ wget \ build-essential \ git \ @@ -24,10 +25,12 @@ RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no libgeos-dev libgeos++-dev \ imagemagick \ tzdata \ - nodejs \ - yarn \ less \ libjemalloc2 libjemalloc-dev \ + cmake \ + && curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \ + && apt-get install -y nodejs \ + && npm install -g yarn \ && mkdir -p $APP_PATH \ && rm -rf /var/lib/apt/lists/* @@ -42,7 +45,7 @@ RUN if [ "$(uname -m)" = "x86_64" ]; then \ ENV RUBY_YJIT_ENABLE=1 # Update RubyGems and install Bundler -RUN gem update --system 3.6.2 \ +RUN gem update --system 3.6.9 \ && gem install bundler --version "$BUNDLE_VERSION" \ && rm -rf $GEM_HOME/cache/* @@ -52,7 +55,7 @@ COPY ../Gemfile ../Gemfile.lock ../.ruby-version ../vendor ./ RUN bundle config set --local path 'vendor/bundle' \ && bundle install --jobs 4 --retry 3 \ - && rm -rf vendor/bundle/ruby/3.4.1/cache/*.gem + && rm -rf vendor/bundle/ruby/3.4.0/cache/*.gem COPY ../. ./ diff --git a/docker/Dockerfile.prod b/docker/Dockerfile.prod index e5fd1d61..66eced7a 100644 --- a/docker/Dockerfile.prod +++ b/docker/Dockerfile.prod @@ -1,4 +1,4 @@ -FROM ruby:3.4.1-slim +FROM ruby:3.4.6-slim ENV APP_PATH=/var/app ENV BUNDLE_VERSION=2.5.21 @@ -8,6 +8,7 @@ ENV RAILS_PORT=3000 ENV RAILS_ENV=production RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + curl \ wget \ build-essential \ git \ @@ -19,10 +20,12 @@ RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no libgeos-dev libgeos++-dev \ imagemagick \ tzdata \ - nodejs \ - yarn \ less \ libjemalloc2 libjemalloc-dev \ + cmake \ + && curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \ + && apt-get install -y nodejs \ + && npm install -g yarn \ && mkdir -p $APP_PATH \ && rm -rf /var/lib/apt/lists/* @@ -37,7 +40,7 @@ RUN if [ "$(uname -m)" = "x86_64" ]; then \ ENV RUBY_YJIT_ENABLE=1 # Update gem system and install bundler -RUN gem update --system 3.6.2 \ +RUN gem update --system 3.6.9 \ && gem install bundler --version "$BUNDLE_VERSION" \ && rm -rf $GEM_HOME/cache/* @@ -49,7 +52,7 @@ COPY ../Gemfile ../Gemfile.lock ../.ruby-version ../vendor ./ RUN bundle config set --local path 'vendor/bundle' \ && bundle config set --local without 'development test' \ && bundle install --jobs 4 --retry 3 \ - && rm -rf vendor/bundle/ruby/3.4.1/cache/*.gem + && rm -rf vendor/bundle/ruby/3.4.0/cache/*.gem COPY ../. ./ diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 00000000..e680f656 --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Dawarich", + "short_name": "Dawarich", + "icons": [ + { + "src": "/assets/favicon/android-chrome-192x192-f9610e2af28e4e48ff0472572c0cb9e3902d29bccc2b07f8f03aabf684822355.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/assets/favicon/android-chrome-512x512-c2ec8132d773ae99f53955360cdd5691bb38e0ed141bddebd39d896b78b5afb6.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file