mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-09 08:47:11 -05:00
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:
parent
d05e5d71d3
commit
7a7f0b09df
19 changed files with 442 additions and 131 deletions
|
|
@ -1 +1 @@
|
|||
3.4.1
|
||||
3.4.6
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -600,7 +600,7 @@ DEPENDENCIES
|
|||
webmock
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.4.1p0
|
||||
ruby 3.4.6p54
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.21
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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: "© <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: "© <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
156
app/javascript/maps/theme_styles.js
Normal file
156
app/javascript/maps/theme_styles.js
Normal 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'
|
||||
};
|
||||
}
|
||||
}
|
||||
78
app/javascript/maps/theme_utils.js
Normal file
78
app/javascript/maps/theme_utils.js
Normal 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'
|
||||
});
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 %>"
|
||||
|
|
|
|||
|
|
@ -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 ../. ./
|
||||
|
||||
|
|
|
|||
|
|
@ -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
19
public/site.webmanifest
Normal 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"
|
||||
}
|
||||
Loading…
Reference in a new issue