Update map layers based on user theme preference (light/dark) and add theme-aware styling to map controls and buttons.

This commit is contained in:
Eugene Burmakin 2025-09-26 18:49:13 +02:00
parent d05e5d71d3
commit 7a7f0b09df
19 changed files with 442 additions and 131 deletions

View file

@ -1 +1 @@
3.4.1
3.4.6

View file

@ -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 `<gpx` tag will now be detected correctly. #1775
## Changed
@ -23,7 +24,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## Added
- Added foundation for upcoming authentication from iOS app.
- [Dawarich Cloud] Based on preferred theme (light or dark), the map page will now load with the corresponding map layer (light or dark).
- [Dawarich Cloud] Added foundation for upcoming authentication from iOS app.
- [Dawarich Cloud] Trial users can now create up to 5 imports. After that, they will be prompted to subscribe to a paid plan.

View file

@ -600,7 +600,7 @@ DEPENDENCIES
webmock
RUBY VERSION
ruby 3.4.1p0
ruby 3.4.6p54
BUNDLED WITH
2.5.21

View file

@ -1,11 +1,13 @@
import { Controller } from "@hotwired/stimulus";
import L from "leaflet";
import { showFlashMessage } from "../maps/helpers";
import { applyThemeToButton } from "../maps/theme_utils";
export default class extends Controller {
static targets = [""];
static values = {
apiKey: String
apiKey: String,
userTheme: String
}
connect() {
@ -17,12 +19,16 @@ export default class extends Controller {
this.currentPopup = null;
this.mapsController = null;
// Listen for theme changes
document.addEventListener('theme:changed', this.handleThemeChange.bind(this));
// Wait for the map to be initialized
this.waitForMap();
}
disconnect() {
this.cleanup();
document.removeEventListener('theme:changed', this.handleThemeChange.bind(this));
console.log("Add visit controller disconnected");
}
@ -76,13 +82,10 @@ export default class extends Controller {
button.innerHTML = '';
button.title = 'Add a visit';
// Style the button to match other map controls
// Style the button with theme-aware styling
applyThemeToButton(button, this.userThemeValue || 'dark');
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';
@ -93,19 +96,6 @@ export default class extends Controller {
// Disable map interactions when clicking the button
L.DomEvent.disableClickPropagation(button);
// Add hover effects
button.addEventListener('mouseenter', () => {
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);

View file

@ -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,7 +202,7 @@ 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: "&copy; <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a>"
});
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: "&copy; <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a>"
}).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 {
</form>
`;
// 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);
}
}
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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;

View file

@ -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 };

View file

@ -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'
};
}
}

View file

@ -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'
});
}

View file

@ -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';

View file

@ -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)
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?('<?xml') && content_to_check.include?('<gpx')
(
content_to_check.strip.start_with?('<?xml') ||
content_to_check.strip.start_with?('<gpx')
) && content_to_check.include?('<gpx')
end
def owntracks_file?
@ -169,19 +170,13 @@ class Imports::SourceDetector
return false unless structure_matches?(json_data, pattern[:structure])
# Check required keys
if pattern[:required_keys]
return false unless has_required_keys?(json_data, pattern[:required_keys])
end
return false if pattern[:required_keys] && !has_required_keys?(json_data, pattern[:required_keys])
# Check required values
if pattern[:required_values]
return false unless has_required_values?(json_data, pattern[:required_values])
end
return false if pattern[:required_values] && !has_required_values?(json_data, pattern[:required_values])
# Check nested patterns
if pattern[:nested_patterns]
return false unless has_nested_patterns?(json_data, pattern[:nested_patterns])
end
return false if pattern[:nested_patterns] && !has_nested_patterns?(json_data, pattern[:nested_patterns])
true
end
@ -221,9 +216,11 @@ class Imports::SourceDetector
if current.is_a?(Array)
return false if key >= current.length
current = current[key]
elsif current.is_a?(Hash)
return false unless current.key?(key)
current = current[key]
else
return false

View file

@ -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 %>"

View file

@ -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 ../. ./

View file

@ -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 ../. ./

19
public/site.webmanifest Normal file
View file

@ -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"
}