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
|
- `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
|
- Deleting an import will now trigger stats recalculation for affected months. #1789
|
||||||
- Importing process should now schedule visits suggestions job a lot faster.
|
- 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
|
## Changed
|
||||||
|
|
||||||
|
|
@ -23,7 +24,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
## Added
|
## 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.
|
- [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
|
webmock
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.4.1p0
|
ruby 3.4.6p54
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.21
|
2.5.21
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import { Controller } from "@hotwired/stimulus";
|
import { Controller } from "@hotwired/stimulus";
|
||||||
import L from "leaflet";
|
import L from "leaflet";
|
||||||
import { showFlashMessage } from "../maps/helpers";
|
import { showFlashMessage } from "../maps/helpers";
|
||||||
|
import { applyThemeToButton } from "../maps/theme_utils";
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = [""];
|
static targets = [""];
|
||||||
static values = {
|
static values = {
|
||||||
apiKey: String
|
apiKey: String,
|
||||||
|
userTheme: String
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
|
|
@ -17,12 +19,16 @@ export default class extends Controller {
|
||||||
this.currentPopup = null;
|
this.currentPopup = null;
|
||||||
this.mapsController = null;
|
this.mapsController = null;
|
||||||
|
|
||||||
|
// Listen for theme changes
|
||||||
|
document.addEventListener('theme:changed', this.handleThemeChange.bind(this));
|
||||||
|
|
||||||
// Wait for the map to be initialized
|
// Wait for the map to be initialized
|
||||||
this.waitForMap();
|
this.waitForMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
|
document.removeEventListener('theme:changed', this.handleThemeChange.bind(this));
|
||||||
console.log("Add visit controller disconnected");
|
console.log("Add visit controller disconnected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,13 +82,10 @@ export default class extends Controller {
|
||||||
button.innerHTML = '➕';
|
button.innerHTML = '➕';
|
||||||
button.title = 'Add a visit';
|
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.width = '48px';
|
||||||
button.style.height = '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.borderRadius = '4px';
|
||||||
button.style.padding = '0';
|
button.style.padding = '0';
|
||||||
button.style.lineHeight = '48px';
|
button.style.lineHeight = '48px';
|
||||||
|
|
@ -93,19 +96,6 @@ export default class extends Controller {
|
||||||
// Disable map interactions when clicking the button
|
// Disable map interactions when clicking the button
|
||||||
L.DomEvent.disableClickPropagation(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
|
// Toggle add visit mode on button click
|
||||||
L.DomEvent.on(button, 'click', () => {
|
L.DomEvent.on(button, 'click', () => {
|
||||||
this.toggleAddVisitMode(button);
|
this.toggleAddVisitMode(button);
|
||||||
|
|
@ -150,9 +140,8 @@ export default class extends Controller {
|
||||||
exitAddVisitMode(button) {
|
exitAddVisitMode(button) {
|
||||||
this.isAddingVisit = false;
|
this.isAddingVisit = false;
|
||||||
|
|
||||||
// Reset button style
|
// Reset button style with theme-aware styling
|
||||||
button.style.backgroundColor = 'white';
|
applyThemeToButton(button, this.userThemeValue || 'dark');
|
||||||
button.style.color = 'black';
|
|
||||||
button.innerHTML = '➕';
|
button.innerHTML = '➕';
|
||||||
|
|
||||||
// Reset cursor
|
// 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() {
|
cleanup() {
|
||||||
if (this.map) {
|
if (this.map) {
|
||||||
this.map.off('click', this.onMapClick, this);
|
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 { TileMonitor } from "../maps/tile_monitor";
|
||||||
import BaseController from "./base_controller";
|
import BaseController from "./base_controller";
|
||||||
import { createAllMapLayers } from "../maps/layers";
|
import { createAllMapLayers } from "../maps/layers";
|
||||||
|
import { applyThemeToControl, applyThemeToButton, applyThemeToPanel } from "../maps/theme_utils";
|
||||||
|
import { injectThemeStyles } from "../maps/theme_styles";
|
||||||
|
|
||||||
export default class extends BaseController {
|
export default class extends BaseController {
|
||||||
static targets = ["container"];
|
static targets = ["container"];
|
||||||
|
|
@ -61,6 +63,10 @@ export default class extends BaseController {
|
||||||
|
|
||||||
this.apiKey = this.element.dataset.api_key;
|
this.apiKey = this.element.dataset.api_key;
|
||||||
this.selfHosted = this.element.dataset.self_hosted;
|
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 {
|
try {
|
||||||
this.markers = this.element.dataset.coordinates ? JSON.parse(this.element.dataset.coordinates) : [];
|
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';
|
const unit = this.distanceUnit === 'km' ? 'km' : 'mi';
|
||||||
div.innerHTML = `${distance} ${unit} | ${pointsNumber} points`;
|
div.innerHTML = `${distance} ${unit} | ${pointsNumber} points`;
|
||||||
div.style.backgroundColor = 'white';
|
applyThemeToControl(div, this.userTheme, {
|
||||||
div.style.padding = '0 5px';
|
padding: '0 5px',
|
||||||
div.style.marginRight = '5px';
|
marginRight: '5px',
|
||||||
div.style.display = 'inline-block';
|
display: 'inline-block'
|
||||||
|
});
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -195,8 +202,8 @@ export default class extends BaseController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the visits manager
|
// 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
|
// Expose visits manager globally for location search integration
|
||||||
window.visitsManager = this.visitsManager;
|
window.visitsManager = this.visitsManager;
|
||||||
|
|
||||||
|
|
@ -385,7 +392,7 @@ export default class extends BaseController {
|
||||||
|
|
||||||
baseMaps() {
|
baseMaps() {
|
||||||
let selectedLayerName = this.userSettings.preferred_map_layer || "OpenStreetMap";
|
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
|
// Add custom map if it exists in settings
|
||||||
if (this.userSettings.maps && this.userSettings.maps.url) {
|
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 this is the preferred layer, add it to the map immediately
|
||||||
if (selectedLayerName === this.userSettings.maps.name) {
|
if (selectedLayerName === this.userSettings.maps.name) {
|
||||||
customLayer.addTo(this.map);
|
// Remove any existing base layers first
|
||||||
// Remove any other base layers that might be active
|
|
||||||
Object.values(maps).forEach(layer => {
|
Object.values(maps).forEach(layer => {
|
||||||
if (this.map.hasLayer(layer)) {
|
if (this.map.hasLayer(layer)) {
|
||||||
this.map.removeLayer(layer);
|
this.map.removeLayer(layer);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
customLayer.addTo(this.map);
|
||||||
}
|
}
|
||||||
|
|
||||||
maps[this.userSettings.maps.name] = customLayer;
|
maps[this.userSettings.maps.name] = customLayer;
|
||||||
} else {
|
} else {
|
||||||
// If no custom map is set, ensure a default layer is added
|
// If no maps were created (fallback case), add OSM
|
||||||
// First check if maps object has any entries
|
|
||||||
if (Object.keys(maps).length === 0) {
|
if (Object.keys(maps).length === 0) {
|
||||||
// Fallback to OSM if no maps are configured
|
console.warn('No map layers available, adding OSM fallback');
|
||||||
maps["OpenStreetMap"] = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
const osmLayer = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
attribution: "© <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a>"
|
attribution: "© <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a>"
|
||||||
});
|
});
|
||||||
|
osmLayer.addTo(this.map);
|
||||||
|
maps["OpenStreetMap"] = osmLayer;
|
||||||
}
|
}
|
||||||
|
// Note: createAllMapLayers already added the appropriate theme-aware layer to the map
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return maps;
|
return maps;
|
||||||
|
|
@ -731,13 +726,10 @@ export default class extends BaseController {
|
||||||
const button = L.DomUtil.create('button', 'map-settings-button');
|
const button = L.DomUtil.create('button', 'map-settings-button');
|
||||||
button.innerHTML = '⚙️'; // Gear icon
|
button.innerHTML = '⚙️'; // Gear icon
|
||||||
|
|
||||||
// Style the button
|
// Style the button with theme-aware styling
|
||||||
button.style.backgroundColor = 'white';
|
applyThemeToButton(button, this.userTheme);
|
||||||
button.style.width = '32px';
|
button.style.width = '32px';
|
||||||
button.style.height = '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
|
// Disable map interactions when clicking the button
|
||||||
L.DomEvent.disableClickPropagation(button);
|
L.DomEvent.disableClickPropagation(button);
|
||||||
|
|
@ -863,11 +855,9 @@ export default class extends BaseController {
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Style the panel
|
// Style the panel with theme-aware styling
|
||||||
div.style.backgroundColor = 'white';
|
applyThemeToPanel(div, this.userTheme);
|
||||||
div.style.padding = '10px';
|
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
|
// Prevent map interactions when interacting with the form
|
||||||
L.DomEvent.disableClickPropagation(div);
|
L.DomEvent.disableClickPropagation(div);
|
||||||
|
|
@ -1010,6 +1000,22 @@ export default class extends BaseController {
|
||||||
const mapElement = document.getElementById('map');
|
const mapElement = document.getElementById('map');
|
||||||
if (mapElement) {
|
if (mapElement) {
|
||||||
mapElement.setAttribute('data-user_settings', JSON.stringify(this.userSettings));
|
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
|
// Store current layer states
|
||||||
|
|
@ -1091,12 +1097,10 @@ export default class extends BaseController {
|
||||||
const button = L.DomUtil.create('button', 'toggle-panel-button');
|
const button = L.DomUtil.create('button', 'toggle-panel-button');
|
||||||
button.innerHTML = '📅';
|
button.innerHTML = '📅';
|
||||||
|
|
||||||
|
// Style the button with theme-aware styling
|
||||||
|
applyThemeToButton(button, controller.userTheme);
|
||||||
button.style.width = '48px';
|
button.style.width = '48px';
|
||||||
button.style.height = '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.borderRadius = '4px';
|
||||||
button.style.padding = '0';
|
button.style.padding = '0';
|
||||||
button.style.lineHeight = '48px';
|
button.style.lineHeight = '48px';
|
||||||
|
|
@ -1131,12 +1135,12 @@ export default class extends BaseController {
|
||||||
const RouteTracksControl = L.Control.extend({
|
const RouteTracksControl = L.Control.extend({
|
||||||
onAdd: function(map) {
|
onAdd: function(map) {
|
||||||
const container = L.DomUtil.create('div', 'routes-tracks-selector leaflet-bar');
|
const container = L.DomUtil.create('div', 'routes-tracks-selector leaflet-bar');
|
||||||
container.style.backgroundColor = 'white';
|
applyThemeToControl(container, controller.userTheme, {
|
||||||
container.style.padding = '8px';
|
padding: '8px',
|
||||||
container.style.borderRadius = '4px';
|
borderRadius: '4px',
|
||||||
container.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)';
|
fontSize: '12px',
|
||||||
container.style.fontSize = '12px';
|
lineHeight: '1.2'
|
||||||
container.style.lineHeight = '1.2';
|
});
|
||||||
|
|
||||||
// Get saved preference or default to 'routes'
|
// Get saved preference or default to 'routes'
|
||||||
const savedPreference = localStorage.getItem('mapRouteMode') || 'routes';
|
const savedPreference = localStorage.getItem('mapRouteMode') || 'routes';
|
||||||
|
|
@ -1395,10 +1399,8 @@ export default class extends BaseController {
|
||||||
|
|
||||||
this.fetchAndDisplayTrackedMonths(div, currentYear, currentMonth, allMonths);
|
this.fetchAndDisplayTrackedMonths(div, currentYear, currentMonth, allMonths);
|
||||||
|
|
||||||
div.style.backgroundColor = 'white';
|
applyThemeToPanel(div, this.userTheme);
|
||||||
div.style.padding = '10px';
|
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.marginRight = '10px';
|
||||||
div.style.marginTop = '10px';
|
div.style.marginTop = '10px';
|
||||||
div.style.width = '300px';
|
div.style.width = '300px';
|
||||||
|
|
@ -1840,7 +1842,7 @@ export default class extends BaseController {
|
||||||
|
|
||||||
initializeLocationSearch() {
|
initializeLocationSearch() {
|
||||||
if (this.map && this.apiKey && this.features.reverse_geocoding) {
|
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 {
|
try {
|
||||||
// Use appropriate default layer based on self-hosted mode
|
// Use appropriate default layer based on self-hosted mode
|
||||||
const selectedLayerName = this.selfHosted === "true" ? "OpenStreetMap" : "Light";
|
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 no layers were created, fall back to OSM
|
||||||
if (Object.keys(maps).length === 0) {
|
if (Object.keys(maps).length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -264,7 +264,7 @@ export default class extends BaseController {
|
||||||
try {
|
try {
|
||||||
// Use appropriate default layer based on self-hosted mode
|
// Use appropriate default layer based on self-hosted mode
|
||||||
const selectedLayerName = this.selfHosted === "true" ? "OpenStreetMap" : "Light";
|
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 no layers were created, fall back to OSM
|
||||||
if (Object.keys(maps).length === 0) {
|
if (Object.keys(maps).length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export default class extends BaseController {
|
||||||
this.userSettingsValue.preferred_map_layer || "OpenStreetMap" :
|
this.userSettingsValue.preferred_map_layer || "OpenStreetMap" :
|
||||||
"OpenStreetMap";
|
"OpenStreetMap";
|
||||||
|
|
||||||
let maps = createAllMapLayers(this.map, selectedLayerName);
|
let maps = createAllMapLayers(this.map, selectedLayerName, "false", 'dark');
|
||||||
|
|
||||||
// Add custom map if it exists in settings
|
// Add custom map if it exists in settings
|
||||||
if (this.hasUserSettingsValue && this.userSettingsValue.maps && this.userSettingsValue.maps.url) {
|
if (this.hasUserSettingsValue && this.userSettingsValue.maps && this.userSettingsValue.maps.url) {
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ export default class extends BaseController {
|
||||||
|
|
||||||
baseMaps() {
|
baseMaps() {
|
||||||
let selectedLayerName = this.userSettings.preferred_map_layer || "OpenStreetMap";
|
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
|
// Add custom map if it exists in settings
|
||||||
if (this.userSettings.maps && this.userSettings.maps.url) {
|
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
|
// Helper function to create all map layers
|
||||||
export function createAllMapLayers(map, selectedLayerName, selfHosted) {
|
export function createAllMapLayers(map, selectedLayerName, selfHosted, userTheme = 'dark') {
|
||||||
const layers = {};
|
const layers = {};
|
||||||
const mapsConfig = selfHosted === "true" ? rasterMapsConfig : vectorMapsConfig;
|
const mapsConfig = selfHosted === "true" ? rasterMapsConfig : vectorMapsConfig;
|
||||||
|
|
||||||
|
// Apply theme-aware selection
|
||||||
|
const themeAwareLayerName = getThemeAwareLayerName(selectedLayerName, userTheme, selfHosted);
|
||||||
|
|
||||||
Object.keys(mapsConfig).forEach(layerKey => {
|
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;
|
return layers;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
// Location search functionality for the map
|
// Location search functionality for the map
|
||||||
|
import { applyThemeToButton } from "./theme_utils";
|
||||||
|
|
||||||
class LocationSearch {
|
class LocationSearch {
|
||||||
constructor(map, apiKey) {
|
constructor(map, apiKey, userTheme = 'dark') {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.apiKey = apiKey;
|
this.apiKey = apiKey;
|
||||||
|
this.userTheme = userTheme;
|
||||||
this.searchResults = [];
|
this.searchResults = [];
|
||||||
this.searchMarkersLayer = null;
|
this.searchMarkersLayer = null;
|
||||||
this.currentSearchQuery = '';
|
this.currentSearchQuery = '';
|
||||||
|
|
@ -22,12 +25,10 @@ class LocationSearch {
|
||||||
onAdd: function(map) {
|
onAdd: function(map) {
|
||||||
const button = L.DomUtil.create('button', 'location-search-toggle');
|
const button = L.DomUtil.create('button', 'location-search-toggle');
|
||||||
button.innerHTML = '🔍';
|
button.innerHTML = '🔍';
|
||||||
|
// Style the button with theme-aware styling
|
||||||
|
applyThemeToButton(button, this.userTheme);
|
||||||
button.style.width = '48px';
|
button.style.width = '48px';
|
||||||
button.style.height = '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.borderRadius = '4px';
|
||||||
button.style.padding = '0';
|
button.style.padding = '0';
|
||||||
button.style.fontSize = '18px';
|
button.style.fontSize = '18px';
|
||||||
|
|
@ -1158,6 +1159,16 @@ class LocationSearch {
|
||||||
return new Date(dateString).toLocaleDateString() + ' ' +
|
return new Date(dateString).toLocaleDateString() + ' ' +
|
||||||
new Date(dateString).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
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 };
|
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 L from "leaflet";
|
||||||
import { showFlashMessage } from "./helpers";
|
import { showFlashMessage } from "./helpers";
|
||||||
|
import { applyThemeToButton } from "./theme_utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages visits functionality including displaying, fetching, and interacting with visits
|
* Manages visits functionality including displaying, fetching, and interacting with visits
|
||||||
*/
|
*/
|
||||||
export class VisitsManager {
|
export class VisitsManager {
|
||||||
constructor(map, apiKey) {
|
constructor(map, apiKey, userTheme = 'dark') {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.apiKey = apiKey;
|
this.apiKey = apiKey;
|
||||||
|
this.userTheme = userTheme;
|
||||||
|
|
||||||
// Create custom panes for different visit types
|
// Create custom panes for different visit types
|
||||||
if (!map.getPane('confirmedVisitsPane')) {
|
if (!map.getPane('confirmedVisitsPane')) {
|
||||||
|
|
@ -67,12 +69,10 @@ export class VisitsManager {
|
||||||
onAdd: (map) => {
|
onAdd: (map) => {
|
||||||
const button = L.DomUtil.create('button', 'leaflet-control-button drawer-button');
|
const button = L.DomUtil.create('button', 'leaflet-control-button drawer-button');
|
||||||
button.innerHTML = '⬅️'; // Left arrow icon
|
button.innerHTML = '⬅️'; // Left arrow icon
|
||||||
|
// Style the button with theme-aware styling
|
||||||
|
applyThemeToButton(button, this.userTheme);
|
||||||
button.style.width = '48px';
|
button.style.width = '48px';
|
||||||
button.style.height = '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.borderRadius = '4px';
|
||||||
button.style.padding = '0';
|
button.style.padding = '0';
|
||||||
button.style.lineHeight = '48px';
|
button.style.lineHeight = '48px';
|
||||||
|
|
@ -104,12 +104,10 @@ export class VisitsManager {
|
||||||
button.innerHTML = '⚓️';
|
button.innerHTML = '⚓️';
|
||||||
button.title = 'Select Area';
|
button.title = 'Select Area';
|
||||||
button.id = 'selection-tool-button';
|
button.id = 'selection-tool-button';
|
||||||
|
// Style the button with theme-aware styling
|
||||||
|
applyThemeToButton(button, this.userTheme);
|
||||||
button.style.width = '48px';
|
button.style.width = '48px';
|
||||||
button.style.height = '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.borderRadius = '4px';
|
||||||
button.style.padding = '0';
|
button.style.padding = '0';
|
||||||
button.style.lineHeight = '48px';
|
button.style.lineHeight = '48px';
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class Imports::SourceDetector
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
geojson: {
|
geojson: {
|
||||||
required_keys: ['type', 'features'],
|
required_keys: %w[type features],
|
||||||
required_values: { 'type' => 'FeatureCollection' },
|
required_values: { 'type' => 'FeatureCollection' },
|
||||||
nested_patterns: [
|
nested_patterns: [
|
||||||
['features', 0, 'type'],
|
['features', 0, 'type'],
|
||||||
|
|
@ -79,9 +79,7 @@ class Imports::SourceDetector
|
||||||
DETECTION_RULES.each do |format, rules|
|
DETECTION_RULES.each do |format, rules|
|
||||||
next if format == :owntracks # Already handled above
|
next if format == :owntracks # Already handled above
|
||||||
|
|
||||||
if matches_format?(json_data, rules)
|
return format if matches_format?(json_data, rules)
|
||||||
return format
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
nil
|
nil
|
||||||
|
|
@ -105,14 +103,17 @@ class Imports::SourceDetector
|
||||||
return false unless filename.downcase.end_with?('.gpx')
|
return false unless filename.downcase.end_with?('.gpx')
|
||||||
|
|
||||||
# Check content for GPX structure
|
# Check content for GPX structure
|
||||||
content_to_check = if file_path && File.exist?(file_path)
|
content_to_check =
|
||||||
# Read first 1KB for GPX detection
|
if file_path && File.exist?(file_path)
|
||||||
File.open(file_path, 'rb') { |f| f.read(1024) }
|
# Read first 1KB for GPX detection
|
||||||
else
|
File.open(file_path, 'rb') { |f| f.read(1024) }
|
||||||
file_content
|
else
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def owntracks_file?
|
def owntracks_file?
|
||||||
|
|
@ -123,11 +124,11 @@ class Imports::SourceDetector
|
||||||
|
|
||||||
# Check for specific OwnTracks line format in content
|
# Check for specific OwnTracks line format in content
|
||||||
content_to_check = if file_path && File.exist?(file_path)
|
content_to_check = if file_path && File.exist?(file_path)
|
||||||
# For OwnTracks, read first few lines only
|
# For OwnTracks, read first few lines only
|
||||||
File.open(file_path, 'r') { |f| f.read(2048) }
|
File.open(file_path, 'r') { |f| f.read(2048) }
|
||||||
else
|
else
|
||||||
file_content
|
file_content
|
||||||
end
|
end
|
||||||
|
|
||||||
content_to_check.lines.any? { |line| line.include?('"_type":"location"') }
|
content_to_check.lines.any? { |line| line.include?('"_type":"location"') }
|
||||||
end
|
end
|
||||||
|
|
@ -169,19 +170,13 @@ class Imports::SourceDetector
|
||||||
return false unless structure_matches?(json_data, pattern[:structure])
|
return false unless structure_matches?(json_data, pattern[:structure])
|
||||||
|
|
||||||
# Check required keys
|
# Check required keys
|
||||||
if pattern[:required_keys]
|
return false if pattern[:required_keys] && !has_required_keys?(json_data, pattern[:required_keys])
|
||||||
return false unless has_required_keys?(json_data, pattern[:required_keys])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Check required values
|
# Check required values
|
||||||
if pattern[:required_values]
|
return false if pattern[:required_values] && !has_required_values?(json_data, pattern[:required_values])
|
||||||
return false unless has_required_values?(json_data, pattern[:required_values])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Check nested patterns
|
# Check nested patterns
|
||||||
if pattern[:nested_patterns]
|
return false if pattern[:nested_patterns] && !has_nested_patterns?(json_data, pattern[:nested_patterns])
|
||||||
return false unless has_nested_patterns?(json_data, pattern[:nested_patterns])
|
|
||||||
end
|
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
@ -221,9 +216,11 @@ class Imports::SourceDetector
|
||||||
|
|
||||||
if current.is_a?(Array)
|
if current.is_a?(Array)
|
||||||
return false if key >= current.length
|
return false if key >= current.length
|
||||||
|
|
||||||
current = current[key]
|
current = current[key]
|
||||||
elsif current.is_a?(Hash)
|
elsif current.is_a?(Hash)
|
||||||
return false unless current.key?(key)
|
return false unless current.key?(key)
|
||||||
|
|
||||||
current = current[key]
|
current = current[key]
|
||||||
else
|
else
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@
|
||||||
data-api_key="<%= current_user.api_key %>"
|
data-api_key="<%= current_user.api_key %>"
|
||||||
data-self_hosted="<%= @self_hosted %>"
|
data-self_hosted="<%= @self_hosted %>"
|
||||||
data-user_settings='<%= (current_user.settings || {}).to_json.html_safe %>'
|
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-coordinates='<%= @coordinates.to_json.html_safe %>'
|
||||||
data-tracks='<%= @tracks.to_json.html_safe %>'
|
data-tracks='<%= @tracks.to_json.html_safe %>'
|
||||||
data-distance="<%= @distance %>"
|
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 APP_PATH=/var/app
|
||||||
ENV BUNDLE_VERSION=2.5.21
|
ENV BUNDLE_VERSION=2.5.21
|
||||||
|
|
@ -13,6 +13,7 @@ ENV SIDEKIQ_PASSWORD=password
|
||||||
ENV PGSSENCMODE=disable
|
ENV PGSSENCMODE=disable
|
||||||
|
|
||||||
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
wget \
|
wget \
|
||||||
build-essential \
|
build-essential \
|
||||||
git \
|
git \
|
||||||
|
|
@ -24,10 +25,12 @@ RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no
|
||||||
libgeos-dev libgeos++-dev \
|
libgeos-dev libgeos++-dev \
|
||||||
imagemagick \
|
imagemagick \
|
||||||
tzdata \
|
tzdata \
|
||||||
nodejs \
|
|
||||||
yarn \
|
|
||||||
less \
|
less \
|
||||||
libjemalloc2 libjemalloc-dev \
|
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 \
|
&& mkdir -p $APP_PATH \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
@ -42,7 +45,7 @@ RUN if [ "$(uname -m)" = "x86_64" ]; then \
|
||||||
ENV RUBY_YJIT_ENABLE=1
|
ENV RUBY_YJIT_ENABLE=1
|
||||||
|
|
||||||
# Update RubyGems and install Bundler
|
# 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" \
|
&& gem install bundler --version "$BUNDLE_VERSION" \
|
||||||
&& rm -rf $GEM_HOME/cache/*
|
&& rm -rf $GEM_HOME/cache/*
|
||||||
|
|
||||||
|
|
@ -52,7 +55,7 @@ COPY ../Gemfile ../Gemfile.lock ../.ruby-version ../vendor ./
|
||||||
|
|
||||||
RUN bundle config set --local path 'vendor/bundle' \
|
RUN bundle config set --local path 'vendor/bundle' \
|
||||||
&& bundle install --jobs 4 --retry 3 \
|
&& 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 ../. ./
|
COPY ../. ./
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
FROM ruby:3.4.1-slim
|
FROM ruby:3.4.6-slim
|
||||||
|
|
||||||
ENV APP_PATH=/var/app
|
ENV APP_PATH=/var/app
|
||||||
ENV BUNDLE_VERSION=2.5.21
|
ENV BUNDLE_VERSION=2.5.21
|
||||||
|
|
@ -8,6 +8,7 @@ ENV RAILS_PORT=3000
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
|
|
||||||
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
wget \
|
wget \
|
||||||
build-essential \
|
build-essential \
|
||||||
git \
|
git \
|
||||||
|
|
@ -19,10 +20,12 @@ RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no
|
||||||
libgeos-dev libgeos++-dev \
|
libgeos-dev libgeos++-dev \
|
||||||
imagemagick \
|
imagemagick \
|
||||||
tzdata \
|
tzdata \
|
||||||
nodejs \
|
|
||||||
yarn \
|
|
||||||
less \
|
less \
|
||||||
libjemalloc2 libjemalloc-dev \
|
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 \
|
&& mkdir -p $APP_PATH \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
@ -37,7 +40,7 @@ RUN if [ "$(uname -m)" = "x86_64" ]; then \
|
||||||
ENV RUBY_YJIT_ENABLE=1
|
ENV RUBY_YJIT_ENABLE=1
|
||||||
|
|
||||||
# Update gem system and install bundler
|
# 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" \
|
&& gem install bundler --version "$BUNDLE_VERSION" \
|
||||||
&& rm -rf $GEM_HOME/cache/*
|
&& rm -rf $GEM_HOME/cache/*
|
||||||
|
|
||||||
|
|
@ -49,7 +52,7 @@ COPY ../Gemfile ../Gemfile.lock ../.ruby-version ../vendor ./
|
||||||
RUN bundle config set --local path 'vendor/bundle' \
|
RUN bundle config set --local path 'vendor/bundle' \
|
||||||
&& bundle config set --local without 'development test' \
|
&& bundle config set --local without 'development test' \
|
||||||
&& bundle install --jobs 4 --retry 3 \
|
&& 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 ../. ./
|
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