mirror of
https://github.com/dedicatedcode/reitti.git
synced 2026-01-10 09:57:57 -05:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -44,6 +44,6 @@ test.describe('Memory Tests', () => {
|
||||
await page.getByRole('button', { name: 'Create Memory' }).click();
|
||||
await expect(page.getByText('Test Memory')).toBeVisible();
|
||||
await expect(page.getByText('One fine description')).toBeVisible();
|
||||
await expect(page.getByText('March 22, 2018 - March 23, 2018')).toBeVisible();
|
||||
await expect(page.getByText('March 22, 2018 — March 23, 2018')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.model.security.UserSettings;
|
||||
import com.dedicatedcode.reitti.repository.UserJdbcService;
|
||||
import com.dedicatedcode.reitti.repository.UserSettingsJdbcService;
|
||||
import com.dedicatedcode.reitti.service.ContextPathHolder;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -21,13 +22,15 @@ public class CustomAuthenticationSuccessHandler implements AuthenticationSuccess
|
||||
private final UserJdbcService userJdbcService;
|
||||
private final UserSettingsJdbcService userSettingsJdbcService;
|
||||
private final LocaleResolver localeResolver;
|
||||
private final ContextPathHolder contextPathHolder;
|
||||
|
||||
public CustomAuthenticationSuccessHandler(UserJdbcService userJdbcService,
|
||||
UserSettingsJdbcService userSettingsJdbcService,
|
||||
LocaleResolver localeResolver) {
|
||||
public CustomAuthenticationSuccessHandler(UserJdbcService userJdbcService,
|
||||
UserSettingsJdbcService userSettingsJdbcService,
|
||||
LocaleResolver localeResolver, ContextPathHolder contextPathHolder) {
|
||||
this.userJdbcService = userJdbcService;
|
||||
this.userSettingsJdbcService = userSettingsJdbcService;
|
||||
this.localeResolver = localeResolver;
|
||||
this.contextPathHolder = contextPathHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -46,6 +49,6 @@ public class CustomAuthenticationSuccessHandler implements AuthenticationSuccess
|
||||
}
|
||||
|
||||
// Redirect to default success URL
|
||||
response.sendRedirect("/");
|
||||
response.sendRedirect(contextPathHolder.getContextPath() + "/");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.dedicatedcode.reitti.config;
|
||||
|
||||
import com.dedicatedcode.reitti.service.ContextPathHolder;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
@@ -10,6 +11,12 @@ import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class HtmxAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
private final ContextPathHolder contextPathHolder;
|
||||
|
||||
public HtmxAuthenticationEntryPoint(ContextPathHolder contextPathHolder) {
|
||||
this.contextPathHolder = contextPathHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authException) throws IOException {
|
||||
@@ -17,11 +24,11 @@ public class HtmxAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
// Check if the request is coming from HTMX
|
||||
if ("true".equals(request.getHeader("HX-Request"))) {
|
||||
// Tell HTMX to redirect the whole window to the login page
|
||||
response.setHeader("HX-Redirect", "/login");
|
||||
response.setHeader("HX-Redirect", contextPathHolder.getContextPath() + "/login");
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
} else {
|
||||
// Standard behavior for non-HTMX requests (regular 302 redirect)
|
||||
response.sendRedirect("/login");
|
||||
response.sendRedirect(contextPathHolder.getContextPath() + "/login");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.dedicatedcode.reitti.model.security.TokenUser;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.repository.MagicLinkJdbcService;
|
||||
import com.dedicatedcode.reitti.repository.UserJdbcService;
|
||||
import com.dedicatedcode.reitti.service.ContextPathHolder;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -27,10 +28,12 @@ public class MagicLinkAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final MagicLinkJdbcService magicLinkJdbcService;
|
||||
private final UserJdbcService userJdbcService;
|
||||
private final ContextPathHolder contextPathHolder;
|
||||
|
||||
public MagicLinkAuthenticationFilter(MagicLinkJdbcService magicLinkJdbcService, UserJdbcService userJdbcService) {
|
||||
public MagicLinkAuthenticationFilter(MagicLinkJdbcService magicLinkJdbcService, UserJdbcService userJdbcService, ContextPathHolder contextPathHolder) {
|
||||
this.magicLinkJdbcService = magicLinkJdbcService;
|
||||
this.userJdbcService = userJdbcService;
|
||||
this.contextPathHolder = contextPathHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -41,8 +44,8 @@ public class MagicLinkAuthenticationFilter extends OncePerRequestFilter {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
boolean isMemoryRequest = request.getRequestURI().startsWith("/memories/");
|
||||
if (!("/access".equals(request.getRequestURI()) || isMemoryRequest)) {
|
||||
boolean isMemoryRequest = request.getRequestURI().startsWith(contextPathHolder.getContextPath() + "/memories/");
|
||||
if (!((contextPathHolder.getContextPath() + "/access").equals(request.getRequestURI()) || isMemoryRequest)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
@@ -53,25 +56,25 @@ public class MagicLinkAuthenticationFilter extends OncePerRequestFilter {
|
||||
Optional<MagicLinkToken> magicLinkToken = magicLinkJdbcService.findByRawToken(token);
|
||||
|
||||
if (magicLinkToken.isEmpty()) {
|
||||
response.sendRedirect("/error/magic-link?error=invalid");
|
||||
response.sendRedirect(contextPathHolder.getContextPath() + "/error/magic-link?error=invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
MagicLinkToken linkToken = magicLinkToken.get();
|
||||
|
||||
if (linkToken.getExpiryDate() != null && linkToken.getExpiryDate().isBefore(Instant.now())) {
|
||||
response.sendRedirect("/error/magic-link?error=expired");
|
||||
response.sendRedirect(contextPathHolder.getContextPath() + "/error/magic-link?error=expired");
|
||||
return;
|
||||
}
|
||||
Long resourceId = null;
|
||||
if (isMemoryRequest) {
|
||||
try {
|
||||
resourceId = Long.parseLong(request.getRequestURI().substring("/memories/".length()));
|
||||
resourceId = Long.parseLong(request.getRequestURI().substring((contextPathHolder.getContextPath() + "/memories/").length()));
|
||||
} catch (NumberFormatException e) {
|
||||
//ignored
|
||||
}
|
||||
if (linkToken.getResourceType() != MagicLinkResourceType.MEMORY || resourceId == null || resourceId.longValue() != linkToken.getResourceId()) {
|
||||
response.sendRedirect("/error/magic-link?error=invalid");
|
||||
response.sendRedirect(contextPathHolder.getContextPath() + "/error/magic-link?error=invalid");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -79,7 +82,7 @@ public class MagicLinkAuthenticationFilter extends OncePerRequestFilter {
|
||||
Optional<User> user = magicLinkJdbcService.findUserIdByToken(linkToken.getId()).flatMap(userJdbcService::findById);
|
||||
|
||||
if (user.isEmpty()) {
|
||||
response.sendRedirect("/error/magic-link?error=user-not-found");
|
||||
response.sendRedirect(contextPathHolder.getContextPath() + "/error/magic-link?error=user-not-found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,13 +101,13 @@ public class MagicLinkAuthenticationFilter extends OncePerRequestFilter {
|
||||
SecurityContextHolder.getContext());
|
||||
|
||||
if (linkToken.getResourceType() != MagicLinkResourceType.MEMORY) {
|
||||
response.sendRedirect("/");
|
||||
response.sendRedirect(contextPathHolder.getContextPath() + "/");
|
||||
} else {
|
||||
response.sendRedirect("/memories/" + linkToken.getResourceId());
|
||||
response.sendRedirect(contextPathHolder.getContextPath() + "/memories/" + linkToken.getResourceId());
|
||||
}
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
response.sendRedirect("/error/magic-link?error=processing");
|
||||
response.sendRedirect(contextPathHolder.getContextPath() + "/error/magic-link?error=processing");
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
@@ -14,8 +14,6 @@ public class RabbitMQConfig {
|
||||
public static final String EXCHANGE_NAME = "reitti-exchange";
|
||||
public static final String SIGNIFICANT_PLACE_QUEUE = "reitti.place.created.v2";
|
||||
public static final String SIGNIFICANT_PLACE_ROUTING_KEY = "reitti.place.created.v2";
|
||||
public static final String RECALCULATE_TRIP_QUEUE = "reitti.trip.recalculate.v2";
|
||||
public static final String DETECT_TRIP_RECALCULATION_ROUTING_KEY = "reitti.trip.recalculate.v2";
|
||||
public static final String TRIGGER_PROCESSING_PIPELINE_QUEUE = "reitti.processing.v2";
|
||||
public static final String TRIGGER_PROCESSING_PIPELINE_ROUTING_KEY = "reitti.processing.start.v2";
|
||||
|
||||
@@ -37,14 +35,6 @@ public class RabbitMQConfig {
|
||||
return new TopicExchange(DLX_NAME);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Queue recaluclateTripQueue() {
|
||||
return QueueBuilder.durable(RECALCULATE_TRIP_QUEUE)
|
||||
.withArgument("x-dead-letter-exchange", DLX_NAME)
|
||||
.withArgument("x-dead-letter-routing-key", DLQ_NAME)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Queue significantPlaceQueue() {
|
||||
return QueueBuilder.durable(SIGNIFICANT_PLACE_QUEUE)
|
||||
@@ -74,11 +64,6 @@ public class RabbitMQConfig {
|
||||
return BindingBuilder.bind(significantPlaceQueue).to(exchange).with(SIGNIFICANT_PLACE_ROUTING_KEY);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Binding recalculateTripBinding(Queue recaluclateTripQueue , TopicExchange exchange) {
|
||||
return BindingBuilder.bind(recaluclateTripQueue).to(exchange).with(DETECT_TRIP_RECALCULATION_ROUTING_KEY);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Binding triggerProcessingBinding(Queue triggerProcessingQueue, TopicExchange exchange) {
|
||||
return BindingBuilder.bind(triggerProcessingQueue).to(exchange).with(TRIGGER_PROCESSING_PIPELINE_ROUTING_KEY);
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.dedicatedcode.reitti.config;
|
||||
|
||||
import com.dedicatedcode.reitti.model.Role;
|
||||
import com.dedicatedcode.reitti.repository.UserJdbcService;
|
||||
import com.dedicatedcode.reitti.service.ContextPathHolder;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -14,11 +15,13 @@ import java.io.IOException;
|
||||
public class SetupFilter implements Filter {
|
||||
|
||||
private final UserJdbcService userService;
|
||||
private final ContextPathHolder contextPathHolder;
|
||||
private final boolean localLoginDisabled;
|
||||
|
||||
public SetupFilter(UserJdbcService userService,
|
||||
public SetupFilter(UserJdbcService userService, ContextPathHolder contextPathHolder,
|
||||
@Value("${reitti.security.local-login.disable:false}") boolean localLoginDisabled) {
|
||||
this.userService = userService;
|
||||
this.contextPathHolder = contextPathHolder;
|
||||
this.localLoginDisabled = localLoginDisabled;
|
||||
}
|
||||
|
||||
@@ -37,19 +40,19 @@ public class SetupFilter implements Filter {
|
||||
String requestURI = httpRequest.getRequestURI();
|
||||
|
||||
// Skip setup check for setup pages, static resources, and health checks
|
||||
if (requestURI.startsWith("/setup") ||
|
||||
requestURI.startsWith("/css") ||
|
||||
requestURI.startsWith("/js") ||
|
||||
requestURI.startsWith("/images") ||
|
||||
requestURI.startsWith("/img") ||
|
||||
requestURI.equals("/actuator/health")) {
|
||||
if (requestURI.contains("/setup") ||
|
||||
requestURI.contains("/css") ||
|
||||
requestURI.contains("/js") ||
|
||||
requestURI.contains("/images") ||
|
||||
requestURI.contains("/img") ||
|
||||
requestURI.contains("/actuator/health")) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if admin has empty password
|
||||
if (hasAdminWithEmptyPassword()) {
|
||||
httpResponse.sendRedirect("/setup");
|
||||
httpResponse.sendRedirect(contextPathHolder.getContextPath() + "/setup");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,7 @@ import com.dedicatedcode.reitti.model.security.TokenUser;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.repository.ProcessedVisitJdbcService;
|
||||
import com.dedicatedcode.reitti.repository.TripJdbcService;
|
||||
import com.dedicatedcode.reitti.service.I18nService;
|
||||
import com.dedicatedcode.reitti.service.MagicLinkTokenService;
|
||||
import com.dedicatedcode.reitti.service.MemoryService;
|
||||
import com.dedicatedcode.reitti.service.RequestHelper;
|
||||
import com.dedicatedcode.reitti.service.*;
|
||||
import com.dedicatedcode.reitti.service.integration.ImmichIntegrationService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -45,19 +42,21 @@ public class MemoryController {
|
||||
private final ImmichIntegrationService immichIntegrationService;
|
||||
private final MagicLinkTokenService magicLinkTokenService;
|
||||
private final I18nService i18n;
|
||||
|
||||
private final ContextPathHolder contextPathHolder;
|
||||
public MemoryController(MemoryService memoryService,
|
||||
TripJdbcService tripJdbcService,
|
||||
ProcessedVisitJdbcService processedVisitJdbcService,
|
||||
ImmichIntegrationService immichIntegrationService,
|
||||
MagicLinkTokenService magicLinkTokenService,
|
||||
I18nService i18n) {
|
||||
I18nService i18n,
|
||||
ContextPathHolder contextPathHolder) {
|
||||
this.memoryService = memoryService;
|
||||
this.tripJdbcService = tripJdbcService;
|
||||
this.processedVisitJdbcService = processedVisitJdbcService;
|
||||
this.immichIntegrationService = immichIntegrationService;
|
||||
this.magicLinkTokenService = magicLinkTokenService;
|
||||
this.i18n = i18n;
|
||||
this.contextPathHolder = contextPathHolder;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -211,7 +210,7 @@ public class MemoryController {
|
||||
|
||||
Memory created = memoryService.createMemory(user, memory);
|
||||
this.memoryService.recalculateMemory(user, created.getId(), timezone);
|
||||
response.setHeader("HX-Redirect", "/memories/" + created.getId() + "?timezone=" + timezone.getId());
|
||||
response.setHeader("HX-Redirect", contextPathHolder.getContextPath() + "/memories/" + created.getId() + "?timezone=" + timezone.getId());
|
||||
return "memories/fragments :: empty";
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -335,7 +334,7 @@ public class MemoryController {
|
||||
throw new ForbiddenException("You are not allowed to delete this memory");
|
||||
}
|
||||
memoryService.deleteMemory(user, id);
|
||||
response.setHeader("HX-Redirect", "/memories");
|
||||
response.setHeader("HX-Redirect", contextPathHolder.getContextPath() + "/memories");
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -361,7 +360,7 @@ public class MemoryController {
|
||||
throw new ForbiddenException("You are not allowed execute this action. Only the owner of the memory can do this.");
|
||||
}
|
||||
memoryService.recalculateMemory(user, id, timezone);
|
||||
httpResponse.setHeader("HX-Redirect", "/memories/" + id + "?timezone=" + timezone.getId());
|
||||
httpResponse.setHeader("HX-Redirect", contextPathHolder.getContextPath() + "/memories/" + id + "?timezone=" + timezone.getId());
|
||||
return "Ok";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.dedicatedcode.reitti.controller.api;
|
||||
|
||||
import com.dedicatedcode.reitti.config.ConditionalOnPropertyNotEmpty;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -13,7 +14,6 @@ import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.dedicatedcode.reitti.model.security.ApiToken;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.repository.RawLocationPointJdbcService;
|
||||
import com.dedicatedcode.reitti.service.ApiTokenService;
|
||||
import com.dedicatedcode.reitti.service.ContextPathHolder;
|
||||
import com.dedicatedcode.reitti.service.integration.ImmichIntegrationService;
|
||||
import com.dedicatedcode.reitti.service.integration.OwnTracksRecorderIntegrationService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -21,13 +22,12 @@ import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/settings/integrations")
|
||||
public class IntegrationsSettingsController {
|
||||
private final ContextPathHolder contextPathHolder;
|
||||
private final ApiTokenService apiTokenService;
|
||||
private final RawLocationPointJdbcService rawLocationPointJdbcService;
|
||||
private final ImmichIntegrationService immichIntegrationService;
|
||||
@@ -35,10 +35,14 @@ public class IntegrationsSettingsController {
|
||||
private final MessageSource messageSource;
|
||||
private final boolean dataManagementEnabled;
|
||||
|
||||
public IntegrationsSettingsController(ApiTokenService apiTokenService, RawLocationPointJdbcService rawLocationPointJdbcService, ImmichIntegrationService immichIntegrationService,
|
||||
public IntegrationsSettingsController(ContextPathHolder contextPathHolder,
|
||||
ApiTokenService apiTokenService,
|
||||
RawLocationPointJdbcService rawLocationPointJdbcService,
|
||||
ImmichIntegrationService immichIntegrationService,
|
||||
OwnTracksRecorderIntegrationService ownTracksRecorderIntegrationService,
|
||||
MessageSource messageSource,
|
||||
@Value("${reitti.data-management.enabled:false}") boolean dataManagementEnabled) {
|
||||
this.contextPathHolder = contextPathHolder;
|
||||
this.apiTokenService = apiTokenService;
|
||||
this.rawLocationPointJdbcService = rawLocationPointJdbcService;
|
||||
this.immichIntegrationService = immichIntegrationService;
|
||||
@@ -81,6 +85,7 @@ public class IntegrationsSettingsController {
|
||||
|
||||
model.addAttribute("openSection", openSection);
|
||||
model.addAttribute("serverUrl", calculateServerUrl(request));
|
||||
model.addAttribute("contextPath", contextPathHolder.getContextPath());
|
||||
|
||||
return "settings/integrations";
|
||||
}
|
||||
@@ -121,6 +126,7 @@ public class IntegrationsSettingsController {
|
||||
|
||||
model.addAttribute("openSection", openSection);
|
||||
model.addAttribute("serverUrl", calculateServerUrl(request));
|
||||
model.addAttribute("contextPath", contextPathHolder.getContextPath());
|
||||
|
||||
return "settings/integrations :: integrations-content";
|
||||
}
|
||||
@@ -145,7 +151,7 @@ public class IntegrationsSettingsController {
|
||||
@GetMapping("/reitti.properties")
|
||||
public ResponseEntity<String> getGpsLoggerProperties(@RequestParam String token, HttpServletRequest request) {
|
||||
String serverUrl = calculateServerUrl(request);
|
||||
String url = serverUrl + "/api/v1/ingest/owntracks?token=" + token;
|
||||
String url = serverUrl + contextPathHolder.getContextPath() + "/api/v1/ingest/owntracks?token=" + token;
|
||||
String properties = "log_customurl_url=" + url + "\n" +
|
||||
"log_customurl_method=POST\n" +
|
||||
"log_customurl_body={\"_type\" : \"location\",\"t\": \"u\",\"acc\": \"%ACC\",\"alt\": \"%ALT\",\"batt\": \"%BATT\",\"bs\": \"%ISCHARGING\",\"lat\": \"%LAT\",\"lon\": \"%LON\",\"tst\": \"%TIMESTAMP\",\"vel\": \"%SPD\"}\n" +
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
package com.dedicatedcode.reitti.controller.settings;
|
||||
|
||||
import com.dedicatedcode.reitti.config.RabbitMQConfig;
|
||||
import com.dedicatedcode.reitti.event.RecalculateTripEvent;
|
||||
import com.dedicatedcode.reitti.model.Role;
|
||||
import com.dedicatedcode.reitti.model.UnitSystem;
|
||||
import com.dedicatedcode.reitti.model.geo.RawLocationPoint;
|
||||
import com.dedicatedcode.reitti.model.geo.TransportMode;
|
||||
import com.dedicatedcode.reitti.model.geo.TransportModeConfig;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.model.security.UserSettings;
|
||||
import com.dedicatedcode.reitti.repository.RawLocationPointJdbcService;
|
||||
import com.dedicatedcode.reitti.repository.TransportModeJdbcService;
|
||||
import com.dedicatedcode.reitti.repository.TripJdbcService;
|
||||
import com.dedicatedcode.reitti.repository.UserSettingsJdbcService;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import com.dedicatedcode.reitti.service.processing.TransportModeService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -19,10 +21,11 @@ import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -30,20 +33,25 @@ import java.util.stream.Collectors;
|
||||
@RequestMapping("/settings/transportation-modes")
|
||||
public class TransportationModesController {
|
||||
|
||||
private final RabbitTemplate rabbitTemplate;
|
||||
private static final Logger log = LoggerFactory.getLogger(TransportationModesController.class);
|
||||
private final TripJdbcService tripJdbcService;
|
||||
private final TransportModeJdbcService transportModeJdbcService;
|
||||
private final UserSettingsJdbcService userSettingsJdbcService;
|
||||
private final TransportModeService transportModeService;
|
||||
private final RawLocationPointJdbcService rawLocationPointJdbcService;
|
||||
private final boolean dataManagementEnabled;
|
||||
|
||||
public TransportationModesController(RabbitTemplate rabbitTemplate, TripJdbcService tripJdbcService,
|
||||
public TransportationModesController(TripJdbcService tripJdbcService,
|
||||
TransportModeJdbcService transportModeJdbcService,
|
||||
UserSettingsJdbcService userSettingsJdbcService,
|
||||
TransportModeService transportModeService,
|
||||
RawLocationPointJdbcService rawLocationPointJdbcService,
|
||||
@Value("${reitti.data-management.enabled:false}") boolean dataManagementEnabled) {
|
||||
this.rabbitTemplate = rabbitTemplate;
|
||||
this.tripJdbcService = tripJdbcService;
|
||||
this.transportModeJdbcService = transportModeJdbcService;
|
||||
this.userSettingsJdbcService = userSettingsJdbcService;
|
||||
this.transportModeService = transportModeService;
|
||||
this.rawLocationPointJdbcService = rawLocationPointJdbcService;
|
||||
this.dataManagementEnabled = dataManagementEnabled;
|
||||
}
|
||||
|
||||
@@ -188,19 +196,21 @@ public class TransportationModesController {
|
||||
return mph * 1.60934;
|
||||
}
|
||||
|
||||
private Double kmhToMph(Double kmh) {
|
||||
return kmh / 1.60934;
|
||||
}
|
||||
|
||||
@PostMapping("/reclassify")
|
||||
public String reclassifyTrips(@AuthenticationPrincipal User user, Model model) {
|
||||
try {
|
||||
// Start async reclassification
|
||||
CompletableFuture.runAsync(() -> {
|
||||
tripJdbcService.findIdsByUser(user).forEach(tripId -> {
|
||||
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,
|
||||
RabbitMQConfig.DETECT_TRIP_RECALCULATION_ROUTING_KEY,
|
||||
new RecalculateTripEvent(user.getUsername(), tripId, UUID.randomUUID().toString()));
|
||||
tripJdbcService.findByUser(user).forEach(trip -> {
|
||||
Instant startTime = trip.getStartTime();
|
||||
Instant endTime = trip.getEndTime();
|
||||
List<RawLocationPoint> tripPoints = this.rawLocationPointJdbcService.findByUserAndTimestampBetweenOrderByTimestampAsc(user, startTime, endTime.plus(1, ChronoUnit.MILLIS));
|
||||
TransportMode transportMode = this.transportModeService.inferTransportMode(user, tripPoints, startTime, endTime);
|
||||
if (transportMode != trip.getTransportModeInferred()) {
|
||||
log.trace("Reclassified trip {} from {} to {} to mode {}", trip.getId(), startTime, endTime, transportMode);
|
||||
trip = trip.withTransportMode(transportMode);
|
||||
this.tripJdbcService.update(trip);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.dedicatedcode.reitti.service;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ContextPathHolder {
|
||||
private final String contextPath;
|
||||
|
||||
public ContextPathHolder(@Value("${server.servlet.context-path:}") String contextPath) {
|
||||
if (contextPath.endsWith("/")) {
|
||||
this.contextPath = contextPath.substring(0, contextPath.length() - 1);
|
||||
} else {
|
||||
this.contextPath = contextPath;
|
||||
}
|
||||
}
|
||||
|
||||
public String getContextPath() {
|
||||
return contextPath;
|
||||
}
|
||||
}
|
||||
@@ -10,16 +10,16 @@ public class TilesCustomizationProvider {
|
||||
private final UserSettingsDTO.TilesCustomizationDTO tilesConfiguration;
|
||||
|
||||
public TilesCustomizationProvider(
|
||||
@Value("${reitti.ui.tile.cache.url:null}") String cacheUrl,
|
||||
@Value("${reitti.ui.tiles.cache.url:}") String cacheUrl,
|
||||
@Value("${reitti.ui.tiles.default.service}") String defaultService,
|
||||
@Value("${reitti.ui.tiles.default.attribution}") String defaultAttribution,
|
||||
@Value("${reitti.ui.tiles.custom.service:}") String customService,
|
||||
@Value("${reitti.ui.tiles.custom.attribution:}") String customAttribution) {
|
||||
@Value("${reitti.ui.tiles.custom.attribution:}") String customAttribution, ContextPathHolder contextPathHolder) {
|
||||
String serviceUrl;
|
||||
if (StringUtils.hasText(customService)) {
|
||||
serviceUrl = customService;
|
||||
} else if (StringUtils.hasText(cacheUrl)) {
|
||||
serviceUrl = "/api/v1/tiles/{z}/{x}/{y}.png";
|
||||
serviceUrl = contextPathHolder.getContextPath() + "/api/v1/tiles/{z}/{x}/{y}.png";
|
||||
} else {
|
||||
serviceUrl = defaultService;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.dedicatedcode.reitti.model.geo.*;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.repository.TransportModeJdbcService;
|
||||
import com.dedicatedcode.reitti.repository.TransportModeOverrideJdbcService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
@@ -12,7 +14,7 @@ import java.util.*;
|
||||
|
||||
@Service
|
||||
public class TransportModeService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TransportModeService.class);
|
||||
private final TransportModeJdbcService transportModeJdbcService;
|
||||
private final TransportModeOverrideJdbcService transportModeOverrideJdbcService;
|
||||
|
||||
@@ -23,9 +25,9 @@ public class TransportModeService {
|
||||
}
|
||||
|
||||
public TransportMode inferTransportMode(User user, List<RawLocationPoint> tripPoints, Instant startTime, Instant endTime) {
|
||||
|
||||
Optional<TransportMode> override = this.transportModeOverrideJdbcService.getTransportModeOverride(user, startTime, endTime);
|
||||
if (override.isPresent()) {
|
||||
log.trace("Found transport mode override for user [{}] and time range [{} - {}]", user.getUsername(), startTime, endTime);
|
||||
return override.get();
|
||||
}
|
||||
List<TransportModeConfig> config = transportModeJdbcService.getTransportModeConfigs(user);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
server.servlet.context-path=${BASE_PATH:/}
|
||||
|
||||
# PostgreSQL configuration (commented out for now, uncomment for production)
|
||||
spring.datasource.url=jdbc:postgresql://${POSTGIS_HOST:postgis}:${POSTGIS_PORT:5432}/${POSTGIS_DB:reittidb}
|
||||
spring.datasource.username=${POSTGIS_USER:reitti}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
# Server configuration
|
||||
server.port=8080
|
||||
|
||||
server.servlet.context-path=/
|
||||
server.forward-headers-strategy=framework
|
||||
|
||||
# Logging configuration
|
||||
@@ -91,7 +91,7 @@ reitti.geocoding.photon.base-url=
|
||||
|
||||
# Tiles Configuration
|
||||
reitti.ui.tiles.cache.url=
|
||||
reitti.ui.tiles.default.service=https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png
|
||||
reitti.ui.tiles.default.service=https://tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||
reitti.ui.tiles.default.attribution=© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Tiles style by <a href="https://www.hotosm.org/" target="_blank">Humanitarian OpenStreetMap Team</a> hosted by <a href="https://openstreetmap.fr/" target="_blank">OpenStreetMap France</a>
|
||||
|
||||
# You can set custom tiles service and attribution by uncommenting the next two keys and setting them to appropriate values
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
font-family: 'Fraunces';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src: url('/fonts/fraunces-v37-latin_latin-ext_vietnamese-100.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
src: url('../fonts/fraunces-v37-latin_latin-ext_vietnamese-100.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* fraunces-regular - latin_latin-ext_vietnamese */
|
||||
@@ -13,7 +13,7 @@
|
||||
font-family: 'Fraunces';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('/fonts/fraunces-v37-latin_latin-ext_vietnamese-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
src: url('../fonts/fraunces-v37-latin_latin-ext_vietnamese-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* fraunces-900 - latin_latin-ext_vietnamese */
|
||||
@@ -22,7 +22,7 @@
|
||||
font-family: 'Fraunces';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url('/fonts/fraunces-v37-latin_latin-ext_vietnamese-900.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
src: url('../fonts/fraunces-v37-latin_latin-ext_vietnamese-900.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -30,7 +30,7 @@
|
||||
font-family: 'Noto Serif SC';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src: url('/fonts/noto-serif-sc-chinese-simplified-200-normal.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
src: url('../fonts/noto-serif-sc-chinese-simplified-200-normal.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -38,7 +38,7 @@
|
||||
font-family: 'Noto Serif SC';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('/fonts/noto-serif-sc-chinese-simplified-400-normal.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
src: url('../fonts/noto-serif-sc-chinese-simplified-400-normal.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -46,7 +46,7 @@
|
||||
font-family: 'Noto Serif SC';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url('/fonts/noto-serif-sc-chinese-simplified-900-normal.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
src: url('../fonts/noto-serif-sc-chinese-simplified-900-normal.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
|
||||
@@ -20,7 +20,7 @@ class GpxDownloader {
|
||||
relevantData: relevantData ? 'true' : 'false'
|
||||
});
|
||||
|
||||
const response = await fetch(`/settings/export-data/gpx?${params}`, {
|
||||
const response = await fetch(window.contextPath + `/settings/export-data/gpx?${params}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/xml'
|
||||
|
||||
@@ -32,7 +32,7 @@ class PhotoClient {
|
||||
|
||||
const iconHtml = `
|
||||
<div class="photo-marker-icon" style="width: ${iconSize}; height: ${iconSize};">
|
||||
<img src="${firstPhoto.thumbnailUrl}"
|
||||
<img src="${window.contextPath + firstPhoto.thumbnailUrl}"
|
||||
alt="Cluster of ${count} photos"
|
||||
onerror="this.style.display='none'; this.parentElement.innerHTML='📷';">
|
||||
<div class="photo-count-indicator">${count}</div>
|
||||
@@ -75,7 +75,7 @@ class PhotoClient {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/api/v1/photos/immich/range?timezone=${timezone}&startDate=${start}&endDate=${end}`);
|
||||
const response = await fetch(window.contextPath + `/api/v1/photos/immich/range?timezone=${timezone}&startDate=${start}&endDate=${end}`);
|
||||
if (!response.ok) {
|
||||
this.photos = [];
|
||||
} else {
|
||||
@@ -129,7 +129,7 @@ class PhotoClient {
|
||||
|
||||
const iconHtml = `
|
||||
<div class="photo-marker-icon" style="width: ${iconSize}; height: ${iconSize};">
|
||||
<img src="${photo.thumbnailUrl}"
|
||||
<img src="${window.contextPath + photo.thumbnailUrl}"
|
||||
alt="${photo.fileName || 'Photo'}"
|
||||
onerror="this.style.display='none'; this.parentElement.innerHTML='📷';">
|
||||
</div>
|
||||
@@ -202,7 +202,7 @@ class PhotoClient {
|
||||
photoElement.appendChild(spinner);
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = photo.fullImageUrl;
|
||||
img.src = window.contextPath + photo.fullImageUrl;
|
||||
img.alt = photo.fileName || 'Photo';
|
||||
|
||||
// Handle image load
|
||||
@@ -307,7 +307,7 @@ class PhotoClient {
|
||||
|
||||
// Create image
|
||||
const img = document.createElement('img');
|
||||
img.src = photo.fullImageUrl;
|
||||
img.src = window.contextPath + photo.fullImageUrl;
|
||||
img.alt = photo.fileName || 'Photo';
|
||||
|
||||
// Handle image load
|
||||
|
||||
@@ -114,7 +114,7 @@ class RawLocationLoader {
|
||||
}
|
||||
|
||||
// Create fetch promise for raw location points with index to maintain order
|
||||
const fetchPromise = fetch(urlWithParams).then(response => {
|
||||
const fetchPromise = fetch(window.contextPath + urlWithParams).then(response => {
|
||||
if (!response.ok) {
|
||||
console.warn('Could not fetch raw location points');
|
||||
return { points: [], index: i, config: config };
|
||||
@@ -177,7 +177,7 @@ class RawLocationLoader {
|
||||
'zoom=' + currentZoom + (config.respectBounds && withBounds ? ('&minLat=' + bbox.minLat +'&minLng=' + bbox.minLng +'&maxLat=' + bbox.maxLat +'&maxLng=' + bbox.maxLng) : '');
|
||||
|
||||
// Create fetch promise for raw location points with index to maintain order
|
||||
const fetchPromise = fetch(urlWithParams).then(response => {
|
||||
const fetchPromise = fetch(window.contextPath + urlWithParams).then(response => {
|
||||
if (!response.ok) {
|
||||
console.warn('Could not fetch raw location points');
|
||||
return { points: [], index: i, config: config };
|
||||
@@ -315,7 +315,7 @@ class RawLocationLoader {
|
||||
const urlWithParams = config.url + separator + 'zoom=' + currentZoom;
|
||||
|
||||
// Create fetch promise for raw location points with index to maintain order
|
||||
const fetchPromise = fetch(urlWithParams).then(response => {
|
||||
const fetchPromise = fetch(window.contextPath + urlWithParams).then(response => {
|
||||
if (!response.ok) {
|
||||
console.warn('Could not fetch raw location points');
|
||||
return { points: [], index: i, config: config };
|
||||
|
||||
@@ -172,4 +172,8 @@
|
||||
</div>
|
||||
|
||||
</body>
|
||||
<script th:inline="javascript">
|
||||
window.contextPath = /*[[@{/}]]*/ "/";
|
||||
window.contextPath = window.contextPath.replace(/\/$/, '');
|
||||
</script>
|
||||
</html>
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
<div class="toggle-buttons">
|
||||
<button type="button"
|
||||
th:class="${mode == 'simple' ? 'btn active' : 'btn'}"
|
||||
th:hx-get="${configurationForm.id != null ? '/settings/visit-sensitivity/edit/' + configurationForm.id + '?new-mode=simple' : '/settings/visit-sensitivity/new?new-mode=simple'}"
|
||||
th:hx-get="@{${configurationForm.id != null ? '/settings/visit-sensitivity/edit/' + configurationForm.id + '?new-mode=simple' : '/settings/visit-sensitivity/new?new-mode=simple'}}"
|
||||
hx-target="#edit-form"
|
||||
hx-include="#configuration-form"
|
||||
th:text="#{visit.sensitivity.mode.simple}"
|
||||
th:title="#{visit.sensitivity.mode.simple.description}">Simple</button>
|
||||
<button type="button"
|
||||
th:class="${mode == 'advanced' ? 'btn active' : 'btn'}"
|
||||
th:hx-get="${configurationForm.id != null ? '/settings/visit-sensitivity/edit/' + configurationForm.id + '?new-mode=advanced' : '/settings/visit-sensitivity/new?new-mode=advanced'}"
|
||||
th:hx-get="@{${configurationForm.id != null ? '/settings/visit-sensitivity/edit/' + configurationForm.id + '?new-mode=advanced' : '/settings/visit-sensitivity/new?new-mode=advanced'}}"
|
||||
hx-target="#edit-form"
|
||||
hx-include="#configuration-form"
|
||||
th:text="#{visit.sensitivity.mode.advanced}"
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="configuration-form" hx-post="/settings/visit-sensitivity/save"
|
||||
<form id="configuration-form" th:hx-post="@{/settings/visit-sensitivity/save}"
|
||||
hx-target="body" th:object="${configurationForm}"
|
||||
onchange="handleFormChange(event)">
|
||||
<input type="hidden" th:field="*{id}">
|
||||
@@ -161,7 +161,7 @@
|
||||
<div class="form-actions">
|
||||
<button type="button"
|
||||
id="preview-btn"
|
||||
hx-post="/settings/visit-sensitivity/preview"
|
||||
th:hx-post="@{/settings/visit-sensitivity/preview}"
|
||||
hx-vals='js:{"timezone": getUserTimezone()}'
|
||||
hx-target="#preview-area"
|
||||
hx-include="#configuration-form"
|
||||
@@ -169,7 +169,7 @@
|
||||
th:text="#{visit.sensitivity.preview}">Preview</button>
|
||||
<button type="submit" class="btn btn-success" th:text="#{visit.sensitivity.save}">Save</button>
|
||||
<button type="button"
|
||||
hx-get="/settings/visit-sensitivity"
|
||||
th:hx-get="@{/settings/visit-sensitivity}"
|
||||
hx-target="body"
|
||||
hx-swap="innerHTML"
|
||||
class="btn btn-secondary"
|
||||
@@ -225,7 +225,7 @@
|
||||
formData.set('previewDate', currentDate);
|
||||
|
||||
// Trigger the preview update
|
||||
htmx.ajax('POST', '/settings/visit-sensitivity/preview', {
|
||||
htmx.ajax('POST', window.contextPath + '/settings/visit-sensitivity/preview', {
|
||||
values: Object.fromEntries(formData),
|
||||
target: '#preview-area',
|
||||
swap: 'innerHTML'
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<input type="date"
|
||||
id="preview-date"
|
||||
th:value="${previewDate}"
|
||||
hx-post="/settings/visit-sensitivity/preview"
|
||||
th:hx-post="@{/settings/visit-sensitivity/preview}"
|
||||
hx-vals='js:{"timezone": getUserTimezone()}'
|
||||
hx-target="#preview-area"
|
||||
hx-include="#configuration-form"
|
||||
@@ -119,8 +119,8 @@
|
||||
.setView([window.userSettings?.homeLatitude || 60.1699, window.userSettings?.homeLongitude || 24.9384], 12);
|
||||
|
||||
// Add tile layer
|
||||
const tilesUrl = window.userSettings?.tiles?.service || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
const tilesAttribution = window.userSettings?.tiles?.attribution || '© OpenStreetMap contributors';
|
||||
const tilesUrl = window.userSettings.tiles.service;
|
||||
const tilesAttribution = window.userSettings.tiles.attribution;
|
||||
|
||||
const tileLayer = window.userSettings?.preferColoredMap ? L.tileLayer : L.tileLayer.grayscale;
|
||||
tileLayer(tilesUrl, {
|
||||
@@ -141,8 +141,8 @@
|
||||
.setView([window.userSettings?.homeLatitude || 60.1699, window.userSettings?.homeLongitude || 24.9384], 12);
|
||||
|
||||
// Add tile layer
|
||||
const tilesUrl = window.userSettings?.tiles?.service || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
const tilesAttribution = window.userSettings?.tiles?.attribution || '© OpenStreetMap contributors';
|
||||
const tilesUrl = window.userSettings.tiles.service;
|
||||
const tilesAttribution = window.userSettings.tiles.attribution;
|
||||
|
||||
const tileLayer = window.userSettings?.preferColoredMap ? L.tileLayer : L.tileLayer.grayscale;
|
||||
tileLayer(tilesUrl, {
|
||||
@@ -159,7 +159,7 @@
|
||||
if (!currentMap) return;
|
||||
|
||||
// Fetch current processed visits/trips for the selected date
|
||||
fetch(`/api/v1/timeline?date=${previewDate}&timezone=${getUserTimezone()}`)
|
||||
fetch(window.contextPath + `/api/v1/timeline?date=${previewDate}&timezone=${getUserTimezone()}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateTimelineContent('current-timeline-content', data);
|
||||
@@ -176,7 +176,7 @@
|
||||
if (!previewMap || !previewReady) return;
|
||||
|
||||
// Fetch preview processed visits/trips
|
||||
fetch(`/api/v1/preview/${previewId}/timeline?date=${previewDate}&timezone=${getUserTimezone()}`)
|
||||
fetch(window.contextPath + `/api/v1/preview/${previewId}/timeline?date=${previewDate}&timezone=${getUserTimezone()}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateTimelineContent('preview-timeline-content', data);
|
||||
@@ -354,7 +354,7 @@
|
||||
}
|
||||
|
||||
// Fetch and display raw location points
|
||||
fetch(rawLocationPointsUrl)
|
||||
fetch(window.contextPath + rawLocationPointsUrl)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
console.warn('Could not fetch raw location points');
|
||||
@@ -422,7 +422,7 @@
|
||||
}
|
||||
}
|
||||
function checkPreviewStatus() {
|
||||
fetch(`/api/v1/preview/${previewId}/status`)
|
||||
fetch(window.contextPath + `/api/v1/preview/${previewId}/status`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.ready) {
|
||||
@@ -460,7 +460,7 @@
|
||||
|
||||
function connectSSE() {
|
||||
console.log('Configuration Preview: Connecting to SSE...');
|
||||
eventSource = new EventSource('/events');
|
||||
eventSource = new EventSource(window.contextPath + '/events');
|
||||
|
||||
eventSource.onopen = function() {
|
||||
console.log('Configuration Preview: SSE connection opened.');
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<div th:fragment="main-navigation(activeSection)" class="navigation-container">
|
||||
<div class="navbar">
|
||||
<span><img class="logo" th:src="@{/img/logo.svg}" alt="reitti logo" title="reitti" src="/img/logo.svg"></span>
|
||||
<a id="nav-timeline" sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_MAGIC_LINK_FULL_ACCESS')" th:classappend="${activeSection == 'index'} ? 'active' : ''" href="/" class="nav-link" th:title="#{nav.timeline}"><i class="lni lni-route-1"></i></a>
|
||||
<a id="nav-memories" sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')" href="/memories" class="nav-link" th:classappend="${activeSection == 'memories'} ? 'active' : ''"th:title="#{nav.memories}"><i class="lni lni-agenda"></i></a>
|
||||
<a id="nav-statistics" sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')" href="/statistics" class="nav-link" th:classappend="${activeSection == 'statistics'} ? 'active' : ''" th:title="#{nav.statistics}"><i class="lni lni-bar-chart-4"></i></a>
|
||||
<a id="nav-settings" sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')" href="/settings" class="nav-link" th:classappend="${activeSection == 'settings'} ? 'active' : ''" th:title="#{nav.settings.tooltip}"><i class="lni lni-gear-1"></i></a>
|
||||
<a id="nav-timeline" sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_MAGIC_LINK_FULL_ACCESS')" th:classappend="${activeSection == 'index'} ? 'active' : ''" th:href="@{/}" class="nav-link" th:title="#{nav.timeline}"><i class="lni lni-route-1"></i></a>
|
||||
<a id="nav-memories" sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')" th:href="@{/memories}" class="nav-link" th:classappend="${activeSection == 'memories'} ? 'active' : ''"th:title="#{nav.memories}"><i class="lni lni-agenda"></i></a>
|
||||
<a id="nav-statistics" sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')" th:href="@{/statistics}" class="nav-link" th:classappend="${activeSection == 'statistics'} ? 'active' : ''" th:title="#{nav.statistics}"><i class="lni lni-bar-chart-4"></i></a>
|
||||
<a id="nav-settings" sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')" th:href="@{/settings}" class="nav-link" th:classappend="${activeSection == 'settings'} ? 'active' : ''" th:title="#{nav.settings.tooltip}"><i class="lni lni-gear-1"></i></a>
|
||||
<a sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_MAGIC_LINK_FULL_ACCESS')" th:if="${activeSection == 'index'}" type="button" class="nav-link" id="auto-update-btn" onclick="toggleAutoUpdate()" th:title="#{map.auto-update.enable.title}" title="Auto Update"><i class="lni lni-play"></i></a>
|
||||
<a sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_MAGIC_LINK_FULL_ACCESS')" th:if="${activeSection == 'index'}" type="button" class="nav-link" onclick="toggleFullscreen()" th:title="#{map.fullscreen.toggle.title}" title="Toggle Fullscreen"><i class="lni lni-arrow-all-direction"></i></a>
|
||||
<form th:action="@{/logout}" method="post">
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<p th:text="#{integrations.immich.description}">Immich is a self-hosted photo and video backup solution. Connect
|
||||
your Immich instance to show photos taken at specific locations and dates on your timeline map.</p>
|
||||
|
||||
<form hx-post="/settings/integrations/immich-integration" hx-target="#photos-section" hx-swap="innerHTML" class="immich-form"
|
||||
<form th:hx-post="@{/settings/integrations/immich-integration}" hx-target="#photos-section" hx-swap="innerHTML" class="immich-form"
|
||||
style="margin-top: 20px;" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label for="serverUrl" th:text="#{integrations.immich.server.url}">Server URL</label>
|
||||
@@ -80,7 +80,7 @@
|
||||
resultDiv.innerHTML = '<div style="color: var(--color-text-white);">Testing connection...</div>';
|
||||
|
||||
// Make AJAX request to test connection
|
||||
fetch('/settings/integrations/immich-integration/test', {
|
||||
fetch(window.contextPath + '/settings/integrations/immich-integration/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
|
||||
@@ -2,84 +2,84 @@
|
||||
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.w3.org/1999/xhtml">
|
||||
<body>
|
||||
<div th:fragment="settings-nav(activeSection, dataManagementEnabled, isAdmin)" class="settings-nav">
|
||||
<a href="/settings/job-status"
|
||||
<a th:href="@{/settings/job-status}"
|
||||
class="settings-nav-item"
|
||||
th:classappend="${activeSection == 'job-status'} ? 'active' : ''"
|
||||
th:title="#{settings.job.status.description}"
|
||||
th:text="#{settings.job.status}">Job Status</a>
|
||||
|
||||
<a href="/settings/import"
|
||||
<a th:href="@{/settings/import}"
|
||||
class="settings-nav-item"
|
||||
th:classappend="${activeSection == 'file-upload'} ? 'active' : ''"
|
||||
th:title="#{settings.import.data.description}"
|
||||
th:text="#{settings.import.data}">Import Data</a>
|
||||
|
||||
<a href="/settings/export-data"
|
||||
<a th:href="@{/settings/export-data}"
|
||||
class="settings-nav-item"
|
||||
th:classappend="${activeSection == 'export-data'} ? 'active' : ''"
|
||||
th:title="#{export.title.description}"
|
||||
th:text="#{export.title}">Export Data</a>
|
||||
|
||||
<a href="/settings/api-tokens"
|
||||
<a th:href="@{/settings/api-tokens}"
|
||||
class="settings-nav-item"
|
||||
th:classappend="${activeSection == 'api-tokens'} ? 'active' : ''"
|
||||
th:title="#{settings.api.tokens.description}"
|
||||
th:text="#{settings.api.tokens}">API Tokens</a>
|
||||
|
||||
<a href="/settings/share-access"
|
||||
<a th:href="@{/settings/share-access}"
|
||||
class="settings-nav-item"
|
||||
th:classappend="${activeSection == 'sharing'} ? 'active' : ''"
|
||||
th:title="#{settings.share.access.description}"
|
||||
th:text="#{settings.share.access}">Share Access</a>
|
||||
|
||||
<a href="/settings/user-management"
|
||||
<a th:href="@{/settings/user-management}"
|
||||
class="settings-nav-item"
|
||||
th:classappend="${activeSection == 'user-management'} ? 'active' : ''"
|
||||
th:title="#{settings.user.management.description}"
|
||||
th:text="#{settings.user.management}">User Management</a>
|
||||
|
||||
<a href="/settings/places"
|
||||
<a th:href="@{/settings/places}"
|
||||
class="settings-nav-item"
|
||||
th:classappend="${activeSection == 'places'} ? 'active' : ''"
|
||||
th:title="#{settings.places.description}"
|
||||
th:text="#{settings.places}">Places</a>
|
||||
|
||||
<a href="/settings/visit-sensitivity"
|
||||
<a th:href="@{/settings/visit-sensitivity}"
|
||||
class="settings-nav-item"
|
||||
th:classappend="${activeSection == 'visit-sensitivity'} ? 'active' : ''"
|
||||
th:title="#{visit.sensitivity.title.description}"
|
||||
th:text="#{visit.sensitivity.title}">Visit Sensitivity</a>
|
||||
<a href="/settings/transportation-modes"
|
||||
<a th:href="@{/settings/transportation-modes}"
|
||||
class="settings-nav-item"
|
||||
th:classappend="${activeSection == 'transportation-modes'} ? 'active' : ''"
|
||||
th:title="#{settings.transportation-modes.description}"
|
||||
th:text="#{settings.transportation-modes}">Transportion Modes</a>
|
||||
|
||||
<a href="/settings/geocode-services"
|
||||
<a th:href="@{/settings/geocode-services}"
|
||||
class="settings-nav-item"
|
||||
th:classappend="${activeSection == 'geocode-services'} ? 'active' : ''"
|
||||
th:title="#{settings.geocoding.description}"
|
||||
th:text="#{settings.geocoding}">Geocoding</a>
|
||||
|
||||
<a href="/settings/manage-data"
|
||||
<a th:href="@{/settings/manage-data}"
|
||||
th:if="${dataManagementEnabled}"
|
||||
class="settings-nav-item"
|
||||
th:classappend="${activeSection == 'manage-data'} ? 'active' : ''"
|
||||
th:title="#{settings.manage.data.description}"
|
||||
th:text="#{settings.manage.data}">Manage Data</a>
|
||||
|
||||
<a href="/settings/integrations"
|
||||
<a th:href="@{/settings/integrations}"
|
||||
class="settings-nav-item"
|
||||
th:classappend="${activeSection == 'integrations'} ? 'active' : ''"
|
||||
th:title="#{settings.integrations.description}"
|
||||
th:text="#{settings.integrations}">Integrations</a>
|
||||
<a href="/settings/logging"
|
||||
<a th:href="@{/settings/logging}"
|
||||
th:if="${isAdmin}"
|
||||
class="settings-nav-item"
|
||||
th:classappend="${activeSection == 'logging'} ? 'active' : ''"
|
||||
th:title="#{settings.logging.description}"
|
||||
th:text="#{settings.logging}">Logging</a>
|
||||
<a href="/settings/about"
|
||||
<a th:href="@{/settings/about}"
|
||||
class="settings-nav-item"
|
||||
th:classappend="${activeSection == 'about'} ? 'active' : ''"
|
||||
th:title="#{settings.about.description}"
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<form id="reitti-integration-form" hx-post="/settings/integrations/reitti-integrations" hx-target="#shared-instances-section" hx-swap="innerHTML" class="reitti-integration-form" style="margin-top: 20px;" autocomplete="off">
|
||||
<form id="reitti-integration-form" th:hx-post="@{/settings/integrations/reitti-integrations}" hx-target="#shared-instances-section" hx-swap="innerHTML" class="reitti-integration-form" style="margin-top: 20px;" autocomplete="off">
|
||||
<h4 id="form-title" th:text="#{integrations.reitti.add.title}">Add New Reitti Integration</h4>
|
||||
|
||||
<input type="hidden" id="integrationId" name="id" value="">
|
||||
@@ -164,7 +164,7 @@
|
||||
|
||||
// Set form action to update
|
||||
const form = document.getElementById('reitti-integration-form');
|
||||
form.setAttribute('hx-post', '/settings/integrations/reitti-integrations/' + id + '/update');
|
||||
form.setAttribute('hx-post', window.contextPath + '/settings/integrations/reitti-integrations/' + id + '/update');
|
||||
|
||||
// Tell HTMX to process the form again after changing attributes
|
||||
htmx.process(form);
|
||||
@@ -194,7 +194,7 @@
|
||||
|
||||
// Reset form action to create
|
||||
const form = document.getElementById('reitti-integration-form');
|
||||
form.setAttribute('hx-post', '/settings/integrations/reitti-integrations');
|
||||
form.setAttribute('hx-post', window.contextPath + '/settings/integrations/reitti-integrations');
|
||||
|
||||
// Tell HTMX to process the form again after changing attributes
|
||||
htmx.process(form);
|
||||
@@ -231,7 +231,7 @@
|
||||
resultDiv.innerHTML = '<div style="color: var(--color-text-white);">' + loadingMessage + '</div>';
|
||||
|
||||
// Make AJAX request to test connection
|
||||
fetch('/settings/integrations/reitti-integrations/test', {
|
||||
fetch(window.contextPath + '/settings/integrations/reitti-integrations/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
@@ -263,7 +263,7 @@
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
||||
<h4 style="margin: 0; color: var(--color-accent);" th:text="#{integrations.reitti.info.title}">Remote Instance Information</h4>
|
||||
<button class="btn btn-secondary"
|
||||
hx-get="/settings/integrations/shared-instances-content"
|
||||
th:hx-get="@{/settings/integrations/shared-instances-content}"
|
||||
hx-target="#shared-instances-section"
|
||||
hx-swap="innerHTML"
|
||||
th:text="#{form.close}">Close</button>
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
<div class="years-navigation" th:fragment="years-navigation">
|
||||
<div class="timeline-entry trip active" id="overall-entry" data-year="overall"
|
||||
hx-get="/statistics/overall"
|
||||
th:hx-get="@{/statistics/overall}"
|
||||
hx-target="#statistics-panel"
|
||||
hx-trigger="load, click[shouldTrigger(this)]"
|
||||
hx-swap="outerHTML">
|
||||
<div class="entry-description" th:text="#{statistics.overall}">Overall</div>
|
||||
</div>
|
||||
<div class="timeline-entry trip" th:each="year : ${years}"
|
||||
th:attr="hx-get='/statistics/'+ ${year}, data-year=${year}"
|
||||
th:attr="hx-get=@{'/statistics/'+ ${year}}, data-year=${year}"
|
||||
hx-target="#statistics-panel"
|
||||
hx-trigger="click[shouldTrigger(this)]"
|
||||
hx-swap="outerHTML">
|
||||
@@ -125,7 +125,7 @@
|
||||
<h3 th:text="#{statistics.monthly.breakdown}">Monthly Breakdown</h3>
|
||||
<div class="months-grid">
|
||||
<div class="month-item" th:each="month, iterStats : ${months}"
|
||||
th:attr="hx-get='/statistics/' + ${year} + '/' + ${month},
|
||||
th:attr="hx-get=@{'/statistics/' + ${year} + '/' + ${month}},
|
||||
hx-target='#month-details-' + ${month},
|
||||
data-year=${year},
|
||||
data-month=${month},
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
th:classappend="${iterStat.first} ? 'active' : ''"
|
||||
th:data-user-id="${userData.userId}"
|
||||
onclick="selectUser(this)">
|
||||
<img th:src="${userData.userAvatarUrl}"
|
||||
<img th:src="@{${userData.userAvatarUrl}}"
|
||||
th:alt="${userData.avatarFallback}"
|
||||
class="avatar">
|
||||
<div th:text="${userData.displayName}" class="username">Username</div>
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
|
||||
<div th:fragment="view-mode" class="trip-transport-mode-container">
|
||||
<span th:text="#{'timeline.transport.' + ${transportMode} + '.label'}">by foot</span>
|
||||
</span>
|
||||
<i class="lni lni-pencil-1 edit-icon"
|
||||
th:hx-get="@{/timeline/trips/edit-form/{id}(id=${tripId}, date=${date}, timezone=${timezone})}"
|
||||
hx-target="closest .trip-transport-mode-container"
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h3 th:text="#{users.existing}">Existing Users</h3>
|
||||
<button th:if="${isAdmin && addUserAvailable}" class="btn"
|
||||
hx-get="/settings/user-form"
|
||||
th:hx-get="@{/settings/user-form}"
|
||||
hx-target="#user-management"
|
||||
hx-swap="innerHTML"
|
||||
th:text="#{users.add.title}">Add New User</button>
|
||||
@@ -68,7 +68,7 @@
|
||||
<!-- User Form Page Fragment -->
|
||||
<div th:fragment="user-form-page">
|
||||
<form hx-target="#user-management" hx-swap="innerHTML" class="user-form" autocomplete="off"
|
||||
th:attr="hx-post=${userId != null ? '/settings/users/update' : '/settings/users'}"
|
||||
th:attr="hx-post=@{${userId != null ? '/settings/users/update' : '/settings/users'}}"
|
||||
enctype="multipart/form-data">
|
||||
<input type="hidden" name="userId" th:value="${userId}">
|
||||
<h3 th:text="${userId != null ? #messages.msg('users.update.title') : #messages.msg('users.add.title')}">Add New User</h3>
|
||||
@@ -363,7 +363,7 @@
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button type="submit" class="btn" th:text="${userId != null ? #messages.msg('form.update') : #messages.msg('form.create')}">Create User</button>
|
||||
<button type="button" class="btn"
|
||||
hx-get="/settings/users-content"
|
||||
th:hx-get="@{/settings/users-content}"
|
||||
hx-target="#user-management"
|
||||
hx-swap="innerHTML"
|
||||
th:text="#{form.cancel}">Cancel</button>
|
||||
|
||||
@@ -7,34 +7,27 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/leaflet.css">
|
||||
<link rel="stylesheet" href="/css/date-picker.css">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<link rel="stylesheet" href="/css/photo-client.css">
|
||||
<link rel="stylesheet" href="/css/inline-edit.css">
|
||||
<link rel="stylesheet" href="/css/avatar-marker.css">
|
||||
<link rel="stylesheet" th:href="@{/css/leaflet.css}" href="/css/leaflet.css">
|
||||
<link rel="stylesheet" th:href="@{/css/date-picker.css}" href="/css/date-picker.css">
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}" href="/css/main.css">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}" href="/css/lineicons.css">
|
||||
<link rel="stylesheet" th:href="@{/css/photo-client.css}" href="/css/photo-client.css">
|
||||
<link rel="stylesheet" th:href="@{/css/inline-edit.css}" href="/css/inline-edit.css">
|
||||
<link rel="stylesheet" th:href="@{/css/avatar-marker.css}" href="/css/avatar-marker.css">
|
||||
<link th:if="${userSettings.customCssUrl}" rel="stylesheet" th:href="${userSettings.customCssUrl}">
|
||||
<style>
|
||||
.canvas-visit-tooltip {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
<script src="/js/HumanizeDuration.js"></script>
|
||||
<script src="/js/date-picker-combined.js"></script>
|
||||
<script src="/js/timeline-scroll-indicator.js"></script>
|
||||
<script src="/js/photo-client.js"></script>
|
||||
<script src="/js/raw-location-loader.js"></script>
|
||||
<script src="/js/canvas-visit-renderer.js"></script>
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/leaflet.js"></script>
|
||||
<script src="/js/TileLayer.Grayscale.js"></script>
|
||||
<script src="/js/polygon-editor.js"></script>
|
||||
<script src="/js/leaflet.geodesic.2.7.2.js"></script>
|
||||
<script src="/js/leaflet.markercluster.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<script th:src="@{/js/HumanizeDuration.js}"></script>
|
||||
<script th:src="@{/js/date-picker-combined.js}"></script>
|
||||
<script th:src="@{/js/timeline-scroll-indicator.js}"></script>
|
||||
<script th:src="@{/js/photo-client.js}"></script>
|
||||
<script th:src="@{/js/raw-location-loader.js}"></script>
|
||||
<script th:src="@{/js/canvas-visit-renderer.js}"></script>
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/leaflet.js}"></script>
|
||||
<script th:src="@{/js/TileLayer.Grayscale.js}"></script>
|
||||
<script th:src="@{/js/polygon-editor.js}"></script>
|
||||
<script th:src="@{/js/leaflet.geodesic.2.7.2.js}"></script>
|
||||
<script th:src="@{/js/leaflet.markercluster.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="message-container">
|
||||
@@ -57,7 +50,7 @@
|
||||
<div class="navigation-container" th:replace="~{fragments/main-navigation :: main-navigation('index')}"></div>
|
||||
<div class="timeline">
|
||||
<div class="timeline-container"
|
||||
hx-get="/timeline/content/range"
|
||||
th:hx-get="@{/timeline/content/range}"
|
||||
hx-trigger="dateChanged from:body"
|
||||
hx-vals="js:{startDate: getTimelineParams().startDate, endDate: getTimelineParams().endDate, timezone: getTimelineParams().timezone}"
|
||||
hx-indicator="#loading-indicator">
|
||||
@@ -82,6 +75,9 @@
|
||||
</button>
|
||||
|
||||
<script th:inline="javascript">
|
||||
window.contextPath = /*[[@{/}]]*/ "/";
|
||||
window.contextPath = window.contextPath.replace(/\/$/, '');
|
||||
|
||||
// Locale object for JavaScript
|
||||
window.locale = {
|
||||
today: /*[[#{datepicker.today}]]*/ 'Today',
|
||||
@@ -464,7 +460,7 @@
|
||||
const fullUrl = baseUrl.includes('?') ? `${baseUrl}&${urlParams}` : `${baseUrl}?${urlParams}`;
|
||||
|
||||
fetchPromises.push(
|
||||
fetch(fullUrl)
|
||||
fetch(window.contextPath + fullUrl)
|
||||
.then(response => response.json())
|
||||
.then(data => ({ userId, data, color: userConfigs.get(userId)?.color }))
|
||||
.catch(error => {
|
||||
@@ -858,7 +854,7 @@
|
||||
}
|
||||
|
||||
console.log('Connecting to SSE...');
|
||||
eventSource = new EventSource('/events');
|
||||
eventSource = new EventSource(window.contextPath + '/events');
|
||||
|
||||
eventSource.onopen = function() {
|
||||
console.log('SSE connection opened.');
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/leaflet.css">
|
||||
<script src="/js/leaflet.js"></script>
|
||||
<script src="/js/TileLayer.Grayscale.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/leaflet.css}">
|
||||
<script th:src="@{/js/leaflet.js}"></script>
|
||||
<script th:src="@{/js/TileLayer.Grayscale.js}"></script>
|
||||
<style>
|
||||
:root {
|
||||
--color-highlight: #F5DEB3FF;
|
||||
@@ -168,7 +168,7 @@
|
||||
</form>
|
||||
<div th:if="${oidcEnabled}">
|
||||
<hr th:if="${localLoginEnabled}" style="margin: 30px 0; border: none; border-top: 1px solid var(--color-highlight);">
|
||||
<a href="/oauth2/authorization/oauth" style="text-decoration: none;">
|
||||
<a th:href="@{/oauth2/authorization/oauth}" style="text-decoration: none;">
|
||||
<button type="button" th:text="#{login.oauth.button}">Log in with OAuth</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -173,10 +173,10 @@
|
||||
<h4 th:text="#{memory.block.gallery.selected.title}">Selected Photos</h4>
|
||||
<div class="selected-photos-combined" id="selectedPhotos">
|
||||
<div th:each="image : ${imageBlock.images}" class="selected-photo-item">
|
||||
<img th:src="${image.imageUrl}" alt="Uploaded image">
|
||||
<img th:src="@{${image.imageUrl}}" alt="Uploaded image">
|
||||
<input type="hidden" name="uploadedUrls" th:value="${image.imageUrl}">
|
||||
<button type="button" class="btn-remove"
|
||||
hx-get="/memories/fragments/empty"
|
||||
th:hx-get="@{/memories/fragments/empty}"
|
||||
hx-target="closest .selected-photo-item"
|
||||
hx-swap="delete"
|
||||
th:title="#{memory.block.gallery.remove}">
|
||||
|
||||
@@ -6,25 +6,10 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}" href="../../static/css/main.css">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}" href="../../static/css/lineicons.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
</head>
|
||||
<body class="memories-page">
|
||||
<div class="settings-container">
|
||||
<div class="settings-nav">
|
||||
<div class="navbar">
|
||||
<a th:href="@{/}" href="../index.html"><img class="logo" th:src="@{/img/logo.svg}"
|
||||
src="../../static/img/logo.svg" alt="reitti logo"
|
||||
title="reitti"></a>
|
||||
<a href="/" class="nav-link" th:title="#{nav.timeline}"><i class="lni lni-route-1"></i></a>
|
||||
<a href="/statistics" class="nav-link" th:title="#{nav.statistics}"><i class="lni lni-bar-chart-4"></i></a>
|
||||
<form th:action="@{/logout}" method="post">
|
||||
<button type="submit" class="nav-link" style="font-size: 1.4rem;" th:title="#{nav.logout.tooltip}"><i
|
||||
class="lni lni-exit"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-content-area">
|
||||
<div id="edit-fragment" class="memory-header" th:fragment="edit-memory">
|
||||
<div th:if="${error}" class="alert alert-error">
|
||||
@@ -72,30 +57,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
// User settings for timezone handling
|
||||
window.userSettings = /*[[${userSettings}]]*/ {};
|
||||
|
||||
function getUserTimezone() {
|
||||
if (window.userSettings.timeZoneOverride) {
|
||||
return window.userSettings.timeZoneOverride;
|
||||
} else {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
}
|
||||
// Show/hide image URL field based on header type selection
|
||||
document.querySelectorAll('input[name="headerType"]').forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
const imageUrlGroup = document.getElementById('imageUrlGroup');
|
||||
if (this.value === 'IMAGE') {
|
||||
imageUrlGroup.style.display = 'block';
|
||||
} else {
|
||||
imageUrlGroup.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<div class="form-actions">
|
||||
<button type="button"
|
||||
class="btn btn-secondary"
|
||||
hx-get="/memories/fragments/empty"
|
||||
th:hx-get="@{/memories/fragments/empty}"
|
||||
hx-target=".block-type-selection"
|
||||
hx-swap="delete"
|
||||
th:text="#{memory.block.cancel}">Cancel</button>
|
||||
@@ -77,7 +77,7 @@
|
||||
<div class="form-actions">
|
||||
<button type="button"
|
||||
class="btn btn-secondary"
|
||||
hx-get="/memories/fragments/empty"
|
||||
th:hx-get="@{/memories/fragments/empty}"
|
||||
hx-target=".block-form"
|
||||
hx-swap="delete"
|
||||
th:text="#{memory.block.cancel}">Cancel</button>
|
||||
@@ -118,7 +118,7 @@
|
||||
<div class="form-actions">
|
||||
<button type="button"
|
||||
class="btn btn-secondary"
|
||||
hx-get="/memories/fragments/empty"
|
||||
th:hx-get="@{/memories/fragments/empty}"
|
||||
hx-target=".block-form"
|
||||
hx-swap="delete"
|
||||
th:text="#{memory.block.cancel}">Cancel</button>
|
||||
@@ -159,7 +159,7 @@
|
||||
<div class="form-actions">
|
||||
<button type="button"
|
||||
class="btn btn-secondary"
|
||||
hx-get="/memories/fragments/empty"
|
||||
th:hx-get="@{/memories/fragments/empty}"
|
||||
hx-target=".block-form"
|
||||
hx-swap="delete"
|
||||
th:text="#{memory.block.cancel}">Cancel</button>
|
||||
@@ -233,10 +233,10 @@
|
||||
<!-- Uploaded photos fragment -->
|
||||
<th:block th:fragment="uploaded-photos">
|
||||
<div th:each="url : ${urls}" class="selected-photo-item">
|
||||
<img th:src="${url}" alt="Uploaded image">
|
||||
<img th:src="@{${url}}" alt="Uploaded image">
|
||||
<input type="hidden" name="uploadedUrls" th:value="${url}">
|
||||
<button type="button" class="btn-remove"
|
||||
hx-get="/memories/fragments/empty"
|
||||
th:hx-get="@{/memories/fragments/empty}"
|
||||
hx-target="closest .selected-photo-item"
|
||||
hx-swap="delete"
|
||||
th:title="#{memory.block.gallery.remove}">
|
||||
@@ -245,7 +245,6 @@
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<!-- Immich photos grid fragment -->
|
||||
<div class="immich-photos-grid" th:fragment="immich-photos-grid">
|
||||
<div th:if="${photos.isEmpty()}" class="no-photos" th:text="#{memory.block.gallery.immich.no.photos}">
|
||||
No photos found for this date range
|
||||
@@ -255,7 +254,7 @@
|
||||
th:attr="hx-post=@{/memories/{id}/blocks/fetch-immich-photo(id=${memoryId}, assetId=${photo.id})}"
|
||||
hx-target="#selectedPhotos"
|
||||
hx-swap="beforeend">
|
||||
<img th:src="${photo.thumbnailUrl}" th:alt="${photo.fileName}">
|
||||
<img th:src="@{${photo.thumbnailUrl}}" th:alt="${photo.fileName}">
|
||||
<div class="photo-overlay">
|
||||
<i class="lni lni-plus"></i>
|
||||
</div>
|
||||
@@ -281,7 +280,7 @@
|
||||
|
||||
<div class="years-navigation" th:fragment="years-navigation">
|
||||
<div class="timeline-entry trip active" id="overall-entry" data-year="overall"
|
||||
hx-get="/memories/all"
|
||||
th:hx-get="@{/memories/all}"
|
||||
hx-target=".memories-overview"
|
||||
hx-trigger="load, click[shouldTrigger(this)]"
|
||||
hx-vals="js:{timezone: getUserTimezone()}"
|
||||
@@ -289,7 +288,7 @@
|
||||
<div class="entry-description" th:text="#{memory.list.all}">All</div>
|
||||
</div>
|
||||
<div class="timeline-entry trip" th:each="year : ${years}"
|
||||
th:attr="hx-get='/memories/year/'+ ${year}, data-year=${year}"
|
||||
th:attr="hx-get=@{'/memories/year/'+ ${year}}, data-year=${year}"
|
||||
hx-target=".memories-overview"
|
||||
hx-trigger="click[shouldTrigger(this)]"
|
||||
hx-vals="js:{timezone: getUserTimezone()}"
|
||||
@@ -335,7 +334,6 @@
|
||||
<div th:if="${memories.isEmpty()}" class="empty-state">
|
||||
<i class="lni lni-image"></i>
|
||||
<h2>No memories yet</h2>
|
||||
<p>Create your first memory by selecting a date range on the timeline</p>
|
||||
</div>
|
||||
|
||||
<div class="memories-grid" th:unless="${memories.isEmpty()}">
|
||||
@@ -369,7 +367,7 @@
|
||||
function navigateToMemoryWithTimezone(event, memoryId) {
|
||||
event.preventDefault();
|
||||
const timezone = getUserTimezone();
|
||||
window.location.href = `/memories/${memoryId}?timezone=${timezone}`;
|
||||
window.location.href = window.contextPath + `/memories/${memoryId}?timezone=${timezone}`;
|
||||
}
|
||||
|
||||
(function () {
|
||||
@@ -386,7 +384,7 @@
|
||||
tap: false,
|
||||
touchZoom: false
|
||||
}).setView([51.505, -0.09], 13);
|
||||
L.tileLayer.grayscale('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
L.tileLayer.grayscale(window.userSettings.tiles.service, {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(memoryMap);
|
||||
@@ -417,7 +415,7 @@
|
||||
<h2 th:text="#{memory.share.title}">Share Memory</h2>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
hx-get="/memories/fragments/empty"
|
||||
th:hx-get="@{/memories/fragments/empty}"
|
||||
hx-target="#share-overlay-container"
|
||||
hx-swap="innerHTML">×</button>
|
||||
</div>
|
||||
@@ -428,8 +426,7 @@
|
||||
<p class="memory-date-range">
|
||||
<i class="lni lni-calendar"></i>
|
||||
<span th:text="${#temporals.format(memory.startDate, 'MMMM d, yyyy')}">Start Date</span>
|
||||
<span th:if="${memory.startDate != memory.endDate}">
|
||||
- <span th:text="${#temporals.format(memory.endDate, 'MMMM d, yyyy')}">End Date</span>
|
||||
<span th:if="${memory.startDate != memory.endDate}"> — <span th:text="${#temporals.format(memory.endDate, 'MMMM d, yyyy')}">End Date</span>
|
||||
</span>
|
||||
</p>
|
||||
<p th:if="${memory.description != null && !memory.description.isEmpty()}"
|
||||
@@ -496,7 +493,7 @@
|
||||
<h2 th:text="#{memory.share.configure.title}">Configure Share Link</h2>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
hx-get="/memories/fragments/empty"
|
||||
th:hx-get="@{/memories/fragments/empty}"
|
||||
hx-target="#share-overlay-container"
|
||||
hx-swap="innerHTML">×</button>
|
||||
</div>
|
||||
@@ -550,7 +547,7 @@
|
||||
<h2 th:text="#{memory.share.result.title}">Share Link Created</h2>
|
||||
<button type="button"
|
||||
class="btn-close"
|
||||
hx-get="/memories/fragments/empty"
|
||||
th:hx-get="@{/memories/fragments/empty}"
|
||||
hx-target="#share-overlay-container"
|
||||
hx-swap="innerHTML">×</button>
|
||||
</div>
|
||||
@@ -605,7 +602,7 @@
|
||||
<div class="form-actions">
|
||||
<button type="button"
|
||||
class="btn btn-primary"
|
||||
hx-get="/memories/fragments/empty"
|
||||
th:hx-get="@{/memories/fragments/empty}"
|
||||
hx-target="#share-overlay-container"
|
||||
hx-swap="innerHTML">
|
||||
<i class="lni lni-checkmark"></i> <span th:text="#{memory.share.result.done}">Done</span>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="timeline">
|
||||
<div class="timeline-container">
|
||||
<div id="years-navigation"
|
||||
hx-get="/memories/years-navigation"
|
||||
th:hx-get="@{/memories/years-navigation}"
|
||||
hx-trigger="load">
|
||||
<div class="photo-modal-loading-spinner htmx-indicator" th:text="#{timeline.loading}">Loading...</div>
|
||||
</div>
|
||||
@@ -68,6 +68,8 @@
|
||||
|
||||
<script th:inline="javascript">
|
||||
window.userSettings = /*[[${userSettings}]]*/ {};
|
||||
window.contextPath = /*[[@{/}]]*/ "/";
|
||||
window.contextPath = window.contextPath.replace(/\/$/, '');
|
||||
|
||||
function getUserTimezone() {
|
||||
if (window.userSettings.timeZoneOverride) {
|
||||
|
||||
@@ -20,16 +20,6 @@
|
||||
</head>
|
||||
<body class="memories-page">
|
||||
<div class="settings-container">
|
||||
<nav class="settings-nav">
|
||||
<div class="settings-header">
|
||||
<a href="/memories" class="back-link">
|
||||
<i class="lni lni-arrow-left"></i>
|
||||
<span th:text="#{memory.new.back.to.memories}">Back to Memories</span>
|
||||
</a>
|
||||
<h1 th:text="#{memory.new.title}">New Memory</h1>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div th:fragment="new-memory" class="settings-content-area">
|
||||
<div th:if="${error}" class="alert alert-error">
|
||||
<i class="lni lni-warning"></i>
|
||||
@@ -112,7 +102,6 @@
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,13 +11,17 @@
|
||||
<link rel="stylesheet" th:href="@{/css/photo-client.css}" href="../../static/css/photo-client.css">
|
||||
<link rel="stylesheet" th:href="@{/css/leaflet.css}" href="../../static/css/leaflet.css">
|
||||
<link rel="stylesheet" th:href="@{/css/share-overlay.css}" href="../../static/css/share-overlay.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/leaflet.js"></script>
|
||||
<script src="/js/leaflet.geodesic.2.7.2.js"></script>
|
||||
<script src="/js/TileLayer.Grayscale.js"></script>
|
||||
<script src="/js/raw-location-loader.js"></script>
|
||||
<script src="/js/HumanizeDuration.js"></script>
|
||||
<script src="/js/photo-client.js"></script>
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/leaflet.js}"></script>
|
||||
<script th:src="@{/js/leaflet.geodesic.2.7.2.js}"></script>
|
||||
<script th:src="@{/js/TileLayer.Grayscale.js}"></script>
|
||||
<script th:src="@{/js/raw-location-loader.js}"></script>
|
||||
<script th:src="@{/js/HumanizeDuration.js}"></script>
|
||||
<script th:src="@{/js/photo-client.js}"></script>
|
||||
<script type="application/javascript" th:inline="javascript">
|
||||
window.contextPath = /*[[@{/}]]*/ "/";
|
||||
window.contextPath = window.contextPath.replace(/\/$/, '');
|
||||
</script>
|
||||
</head>
|
||||
<body class="memories-page">
|
||||
<div class="settings-container">
|
||||
@@ -80,8 +84,8 @@
|
||||
<p class="memory-date-range-large">
|
||||
<i class="lni lni-calendar"></i>
|
||||
<span th:text="${#temporals.format(memory.startDate, 'MMMM d, yyyy')}">Start Date</span>
|
||||
<span th:if="${memory.startDate != memory.endDate}">
|
||||
- <span th:text="${#temporals.format(memory.endDate, 'MMMM d, yyyy')}">End Date</span>
|
||||
<span th:if="${memory.startDate != memory.endDate}"> — <span
|
||||
th:text="${#temporals.format(memory.endDate, 'MMMM d, yyyy')}">End Date</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -114,9 +118,9 @@
|
||||
<div class="gallery-images-grid" th:id="${'gallery_' + block.blockId}">
|
||||
<div th:each="image : ${block.images}"
|
||||
class="gallery-image-item"
|
||||
th:attr="data-image-url=${image.imageUrl},data-caption=${image.caption}">
|
||||
th:attr="data-image-url=@{${image.imageUrl}},data-caption=${image.caption}">
|
||||
<div class="photo-loading-spinner"></div>
|
||||
<img th:data-src="${image.imageUrl}"
|
||||
<img th:data-src="@{${image.imageUrl}}"
|
||||
class="lazy-image"
|
||||
onload="this.classList.add('loaded'); this.previousElementSibling.style.display='none';"
|
||||
onerror="this.style.display='none'; this.previousElementSibling.style.display='none'; this.parentElement.innerHTML='📷';">
|
||||
@@ -148,7 +152,7 @@
|
||||
imageContainer.appendChild(modalSpinner);
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = allImages[currentIndex].imageUrl;
|
||||
img.src = window.contextPath + allImages[currentIndex].imageUrl;
|
||||
img.alt = allImages[currentIndex].caption || 'Gallery image';
|
||||
|
||||
img.addEventListener('load', () => {
|
||||
@@ -297,12 +301,12 @@
|
||||
|
||||
let tileLayer;
|
||||
if (userSettings.preferColoredMap) {
|
||||
tileLayer = L.tileLayer(userSettings.tiles.service, {
|
||||
tileLayer = L.tileLayer(window.contextPath + userSettings.tiles.service, {
|
||||
maxZoom: 19,
|
||||
attribution: userSettings.tiles.attribution
|
||||
});
|
||||
} else {
|
||||
tileLayer = L.tileLayer.grayscale(userSettings.tiles.service, {
|
||||
tileLayer = L.tileLayer.grayscale(window.contextPath + userSettings.tiles.service, {
|
||||
maxZoom: 19,
|
||||
attribution: userSettings.tiles.attribution
|
||||
});
|
||||
@@ -430,12 +434,12 @@
|
||||
|
||||
let tileLayer;
|
||||
if (userSettings.preferColoredMap) {
|
||||
tileLayer = L.tileLayer(userSettings.tiles.service, {
|
||||
tileLayer = L.tileLayer(window.contextPath + userSettings.tiles.service, {
|
||||
maxZoom: 19,
|
||||
attribution: userSettings.tiles.attribution
|
||||
});
|
||||
} else {
|
||||
tileLayer = L.tileLayer.grayscale(userSettings.tiles.service, {
|
||||
tileLayer = L.tileLayer.grayscale(window.contextPath + userSettings.tiles.service, {
|
||||
maxZoom: 19,
|
||||
attribution: userSettings.tiles.attribution
|
||||
});
|
||||
@@ -575,12 +579,12 @@
|
||||
|
||||
|
||||
if (userSettings.preferColoredMap) {
|
||||
L.tileLayer(userSettings.tiles.service, {
|
||||
L.tileLayer(window.contextPath + userSettings.tiles.service, {
|
||||
maxZoom: 19,
|
||||
attribution: userSettings.tiles.attribution
|
||||
}).addTo(memoryMap);
|
||||
} else {
|
||||
L.tileLayer.grayscale(userSettings.tiles.service, {
|
||||
L.tileLayer.grayscale(window.contextPath + userSettings.tiles.service, {
|
||||
maxZoom: 19,
|
||||
attribution: userSettings.tiles.attribution
|
||||
}).addTo(memoryMap);
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<link rel="stylesheet" href="/css/acknowledgments.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<script src="/js/fireworks.min.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/acknowledgments.css}">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
<script th:src="@{/js/fireworks.min.js}"></script>
|
||||
</head>
|
||||
<body class="settings-page">
|
||||
<div class="navigation-container" th:replace="~{fragments/main-navigation :: main-navigation('settings')}"></div>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
</head>
|
||||
<body class="settings-page">
|
||||
<div class="navigation-container" th:replace="~{fragments/main-navigation :: main-navigation('settings')}"></div>
|
||||
@@ -64,7 +64,7 @@
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<form hx-post="/settings/api-tokens" hx-target="#api-tokens" hx-swap="innerHTML" class="token-form"
|
||||
<form th:hx-post="@{/settings/api-tokens}" hx-target="#api-tokens" hx-swap="innerHTML" class="token-form"
|
||||
style="margin-bottom: 20px;">
|
||||
<div class="form-group">
|
||||
<label for="tokenName" th:text="#{tokens.name.label}">Token Name</label>
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title th:text="#{edit-place.page.title}">Edit Place - Reitti</title>
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/leaflet.css">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<link rel="stylesheet" th:href="@{/css/leaflet.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
@@ -155,10 +155,10 @@
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
<script src="/js/leaflet.js"></script>
|
||||
<script src="/js/polygon-editor.js"></script>
|
||||
<script src="/js/TileLayer.Grayscale.js"></script>
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script th:src="@{/js/leaflet.js}"></script>
|
||||
<script th:src="@{/js/polygon-editor.js}"></script>
|
||||
<script th:src="@{/js/TileLayer.Grayscale.js}"></script>
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
@@ -242,7 +242,7 @@
|
||||
<button type="button" id="save-btn" class="btn btn-default btn-block">
|
||||
<span class="btn-text" th:text="#{form.save}">Save</span>
|
||||
</button>
|
||||
<a th:href="${returnUrl}" class="btn btn-default btn-block" th:text="#{form.cancel}">Cancel</a>
|
||||
<a th:href="@{${returnUrl}}" class="btn btn-default btn-block" th:text="#{form.cancel}">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -254,6 +254,8 @@
|
||||
|
||||
<script th:inline="javascript">
|
||||
const placeData = /*[[${place}]]*/ {};
|
||||
window.contextPath = /*[[@{/}]]*/ "/";
|
||||
window.contextPath = window.contextPath.replace(/\/$/, '');
|
||||
window.userSettings = /*[[${userSettings}]]*/ {};
|
||||
|
||||
// Initialize the map
|
||||
@@ -425,7 +427,7 @@
|
||||
|
||||
const formData = new FormData(document.getElementById('polygon-form'));
|
||||
|
||||
fetch(`/settings/places/${placeData.id}/check-update`, {
|
||||
fetch(window.contextPath + `/settings/places/${placeData.id}/check-update`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<script src="/js/gpx-downloader.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
<script th:src="@{/js/gpx-downloader.js}"></script>
|
||||
</head>
|
||||
<body class="settings-page">
|
||||
<div class="navigation-container" th:replace="~{fragments/main-navigation :: main-navigation('settings')}"></div>
|
||||
@@ -40,7 +40,7 @@
|
||||
id="startDate"
|
||||
name="startDate"
|
||||
th:value="${startDate}"
|
||||
hx-get="/settings/export-data/data-content"
|
||||
th:hx-get="@{/settings/export-data/data-content}"
|
||||
hx-target="#data-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="change"
|
||||
@@ -56,7 +56,7 @@
|
||||
id="endDate"
|
||||
name="endDate"
|
||||
th:value="${endDate}"
|
||||
hx-get="/settings/export-data/data-content"
|
||||
th:hx-get="@{/settings/export-data/data-content}"
|
||||
hx-target="#data-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="load, change"
|
||||
@@ -113,7 +113,7 @@
|
||||
<label for="pageSizeSelect" th:text="#{export.raw.data.show}">Show:</label>
|
||||
<select id="pageSizeSelect"
|
||||
name="size"
|
||||
hx-get="/settings/export-data/data-content"
|
||||
th:hx-get="@{/settings/export-data/data-content}"
|
||||
hx-target="#data-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="#startDate, #endDate"
|
||||
@@ -130,7 +130,7 @@
|
||||
<button type="button"
|
||||
class="btn btn-sm"
|
||||
th:disabled="${!hasPrevious}"
|
||||
hx-get="/settings/export-data/data-content"
|
||||
th:hx-get="@{/settings/export-data/data-content}"
|
||||
hx-target="#data-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="#startDate, #endDate, #pageSizeSelect"
|
||||
@@ -147,7 +147,7 @@
|
||||
<button type="button"
|
||||
class="btn btn-sm"
|
||||
th:disabled="${!hasNext}"
|
||||
hx-get="/settings/export-data/data-content"
|
||||
th:hx-get="@{/settings/export-data/data-content}"
|
||||
hx-target="#data-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="#startDate, #endDate, #pageSizeSelect"
|
||||
@@ -193,6 +193,8 @@
|
||||
|
||||
</div>
|
||||
<script th:inline="javascript">
|
||||
window.contextPath = /*[[@{/}]]*/ "/";
|
||||
window.contextPath = window.contextPath.replace(/\/$/, '');
|
||||
window.userSettings = /*[[${userSettings}]]*/ {}
|
||||
|
||||
function handleGpxDownload(buttonElement) {
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
</head>
|
||||
<body class="settings-page">
|
||||
<div class="navigation-container" th:replace="~{fragments/main-navigation :: main-navigation('settings')}"></div>
|
||||
@@ -109,7 +109,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="settings-card">
|
||||
<form hx-post="/settings/geocode-services" hx-target="#geocode-services" hx-swap="innerHTML"
|
||||
<form th:hx-post="@{/settings/geocode-services}" hx-target="#geocode-services" hx-swap="innerHTML"
|
||||
class="geocode-service-form" style="margin-bottom: 20px;">
|
||||
<h3 th:text="#{geocoding.add.title}">Add New Geocoding Service</h3>
|
||||
<div class="form-group">
|
||||
@@ -137,7 +137,7 @@
|
||||
<p th:text="#{geocoding.run.description}">Process all significant places that haven't been
|
||||
geocoded yet</p>
|
||||
<button class="btn"
|
||||
hx-post="/settings/geocode-services/run-geocoding"
|
||||
th:hx-post="@{/settings/geocode-services/run-geocoding}"
|
||||
hx-target="#geocode-services"
|
||||
hx-swap="innerHTML"
|
||||
th:attr="hx-confirm=#{geocoding.run.confirm}"
|
||||
@@ -155,7 +155,7 @@
|
||||
<strong>⚠️ <span th:text="#{label.warning}">Warning:</span></strong> <span th:text="#{geocoding.clear.warning}">This will clear all existing address information and re-geocode all places</span>
|
||||
</div>
|
||||
<button class="btn btn-danger"
|
||||
hx-post="/settings/geocode-services/clear-and-rerun"
|
||||
th:hx-post="@{/settings/geocode-services/clear-and-rerun}"
|
||||
hx-target="#geocode-services"
|
||||
hx-swap="innerHTML"
|
||||
th:attr="hx-confirm=#{geocoding.clear.confirm}"
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
</head>
|
||||
<body class="settings-page">
|
||||
<div class="navigation-container" th:replace="~{fragments/main-navigation :: main-navigation('settings')}"></div>
|
||||
@@ -38,7 +38,7 @@
|
||||
tracks, and routes with timestamps that can be processed into your location history.
|
||||
</p>
|
||||
<form id="gpx-upload-form"
|
||||
hx-post="/settings/import/gpx"
|
||||
th:hx-post="@{/settings/import/gpx}"
|
||||
hx-target="#file-upload"
|
||||
hx-encoding="multipart/form-data">
|
||||
<div class="form-group">
|
||||
@@ -62,7 +62,7 @@
|
||||
Supports both single Feature and FeatureCollection formats.
|
||||
</p>
|
||||
<form id="geojson-upload-form"
|
||||
hx-post="/settings/import/geojson"
|
||||
th:hx-post="@{/settings/import/geojson}"
|
||||
hx-target="#file-upload"
|
||||
hx-swap="innerHTML"
|
||||
hx-encoding="multipart/form-data">
|
||||
@@ -85,7 +85,7 @@
|
||||
<p class="description" th:text="#{upload.google.android.format.description}">This exports a timeline.json file with your recent location data from Android devices.</p>
|
||||
|
||||
<form id="timeline-android-upload-form"
|
||||
hx-post="/settings/import/google-timeline-android"
|
||||
th:hx-post="@{/settings/import/google-timeline-android}"
|
||||
hx-target="#file-upload"
|
||||
hx-swap="innerHTML"
|
||||
hx-encoding="multipart/form-data">
|
||||
@@ -108,7 +108,7 @@
|
||||
<p class="description" th:text="#{upload.google.ios.format.description}">This exports a timeline.json file with your recent location data from iOS devices.</p>
|
||||
|
||||
<form id="timeline-ios-upload-form"
|
||||
hx-post="/settings/import/google-timeline-ios"
|
||||
th:hx-post="@{/settings/import/google-timeline-ios}"
|
||||
hx-target="#file-upload"
|
||||
hx-swap="innerHTML"
|
||||
hx-encoding="multipart/form-data">
|
||||
@@ -130,7 +130,7 @@
|
||||
<p th:text="#{upload.google.old.format.instructions}">From Google Takeout: Export your data from takeout.google.com and upload the Records.json file from the Location History folder.</p>
|
||||
<p class="description" th:text="#{upload.google.old.format.description}">This contains your complete historical location data.</p>
|
||||
<form id="records-upload-form"
|
||||
hx-post="/settings/import/google-records"
|
||||
th:hx-post="@{/settings/import/google-records}"
|
||||
hx-target="#file-upload"
|
||||
hx-swap="innerHTML"
|
||||
hx-encoding="multipart/form-data">
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
</head>
|
||||
<body class="settings-page">
|
||||
<div class="navigation-container" th:replace="~{fragments/main-navigation :: main-navigation('settings')}"></div>
|
||||
@@ -43,7 +43,7 @@
|
||||
<!-- Token Selection Dropdown -->
|
||||
<div th:if="${hasToken && tokens.size() > 1}" >
|
||||
<label for="tokenSelect" th:text="#{integrations.token.select.label}">Select API Token:</label>
|
||||
<select id="tokenSelect" name="selectedToken" class="form-control" hx-get="/settings/integrations/integrations-content" hx-vals='{"openSection": "data-ingestion"}' hx-target="#integrations" hx-swap="innerHTML" hx-trigger="change">
|
||||
<select id="tokenSelect" name="selectedToken" class="form-control" th:hx-get="@{/settings/integrations/integrations-content}" hx-vals='{"openSection": "data-ingestion"}' hx-target="#integrations" hx-swap="innerHTML" hx-trigger="change">
|
||||
<option th:each="token : ${tokens}" th:value="${token.token}" th:text="${token.name}" th:selected="${token.token == selectedToken}"></option>
|
||||
</select>
|
||||
<small th:text="#{integrations.token.select.help}">Choose the API token to use in the setup URLs below. The selected token will be automatically inserted into the example URLs.</small>
|
||||
@@ -59,8 +59,8 @@
|
||||
<li th:text="#{integrations.gpslogger.step1}">Download GPSLogger from the Google Play Store</li>
|
||||
<li th:utext="#{integrations.gpslogger.step2}">Open GPSLogger and go to <strong>Logging details → Log to custom URL</strong></li>
|
||||
<li th:utext="#{integrations.gpslogger.step3}">Enable "Log to custom URL"</li>
|
||||
<li th:if="${hasToken}" th:utext="#{integrations.gpslogger.step4.with.token(${serverUrl + '/api/v1/ingest/owntracks?token=' + selectedToken})}">Set the URL to: <code th:text="${serverUrl + '/api/v1/ingest/owntracks?token=' + selectedToken}">https://your-domain.com/api/v1/location-data</code></li>
|
||||
<li th:unless="${hasToken}" th:utext="#{integrations.gpslogger.step4.without.token(${serverUrl + '/api/v1/ingest/owntracks?token=<YOUR TOKEN>'})}">Set the URL to: <code th:text="${serverUrl + '/api/v1/ingest/owntracks?token=<YOUR TOKEN>'}">https://your-domain.com/api/v1/location-data</code></li>
|
||||
<li th:if="${hasToken}" th:utext="#{integrations.gpslogger.step4.with.token(${serverUrl + contextPath + '/api/v1/ingest/owntracks?token=' + selectedToken})}">Set the URL to: <code th:text="${serverUrl + '/api/v1/ingest/owntracks?token=' + selectedToken}">https://your-domain.com/api/v1/location-data</code></li>
|
||||
<li th:unless="${hasToken}" th:utext="#{integrations.gpslogger.step4.without.token(${serverUrl + contextPath + '/api/v1/ingest/owntracks?token=<YOUR TOKEN>'})}">Set the URL to: <code th:text="${serverUrl + '/api/v1/ingest/owntracks?token=<YOUR TOKEN>'}">https://your-domain.com/api/v1/location-data</code></li>
|
||||
<li th:utext="#{integrations.gpslogger.step5}">Set HTTP Method to <strong>POST</strong></li>
|
||||
<li><span th:utext="#{integrations.gpslogger.step6}">Set HTTP Body to:</span> <code>{
|
||||
"_type" : "location",
|
||||
@@ -93,8 +93,8 @@
|
||||
<li th:text="#{integrations.owntracks.step1}">Download OwnTracks from the App Store or Google Play Store</li>
|
||||
<li th:utext="#{integrations.owntracks.step2}">Open OwnTracks and go to <strong>Settings → Connection</strong></li>
|
||||
<li th:utext="#{integrations.owntracks.step3}">Set Mode to <strong>HTTP</strong></li>
|
||||
<li th:if="${hasToken}" th:utext="#{integrations.owntracks.step4.with.token(${serverUrl + '/api/v1/ingest/owntracks?token=' + selectedToken})}">Set the Endpoint to: <code th:text="${serverUrl + '/api/v1/ingest/owntracks?token=' + selectedToken}">https://your-domain.com/api/v1/location-data</code></li>
|
||||
<li th:unless="${hasToken}" th:utext="#{integrations.owntracks.step4.without.token(${serverUrl + '/api/v1/ingest/owntracks?token=<YOUR TOKEN>'})}">Set the Endpoint to: <code th:text="${serverUrl + '/api/v1/ingest/owntracks?token=<YOUR TOKEN>'}">https://your-domain.com/api/v1/location-data</code></li>
|
||||
<li th:if="${hasToken}" th:utext="#{integrations.owntracks.step4.with.token(${serverUrl + contextPath + '/api/v1/ingest/owntracks?token=' + selectedToken})}">Set the Endpoint to: <code th:text="${serverUrl + '/api/v1/ingest/owntracks?token=' + selectedToken}">https://your-domain.com/api/v1/location-data</code></li>
|
||||
<li th:unless="${hasToken}" th:utext="#{integrations.owntracks.step4.without.token(${serverUrl + contextPath + '/api/v1/ingest/owntracks?token=<YOUR TOKEN>'})}">Set the Endpoint to: <code th:text="${serverUrl + '/api/v1/ingest/owntracks?token=<YOUR TOKEN>'}">https://your-domain.com/api/v1/location-data</code></li>
|
||||
<li th:utext="#{integrations.owntracks.step5}">Disable <strong>Authentication</strong> (we use the token in the URL instead)</li>
|
||||
<li th:text="#{integrations.owntracks.step6}">Configure tracking settings as desired. Make sure that Owntracks records a point at least every 30 seconds.</li>
|
||||
<li th:utext="#{integrations.owntracks.step7}">On the map view, set tracking mode to "Movement"</li>
|
||||
@@ -121,8 +121,8 @@
|
||||
<li th:utext="#{integrations.overland.step2}">Open Overland and go to the <strong>Settings</strong> tab</li>
|
||||
<li th:utext="#{integrations.overland.step3}"><strong>Important:</strong> Tap the <strong>Request Permission</strong> button to grant location access - Overland will not track anything without this permission</li>
|
||||
<li th:utext="#{integrations.overland.step4}">Tap on <strong>Receiver Endpoint</strong></li>
|
||||
<li th:if="${hasToken}" th:utext="#{integrations.overland.step5.with.token(${serverUrl + '/api/v1/ingest/overland?token=' + selectedToken})}">Set the Endpoint URL to: <code th:text="${serverUrl + '/api/v1/ingest/overland?token=' + selectedToken}">https://your-domain.com/api/v1/ingest/overland?token=YOUR_TOKEN</code></li>
|
||||
<li th:unless="${hasToken}" th:utext="#{integrations.overland.step5.without.token(${serverUrl + '/api/v1/ingest/overland?token=<YOUR TOKEN>'})}">Set the Endpoint URL to: <code th:text="${serverUrl + '/api/v1/ingest/overland?token=<YOUR TOKEN>'}">https://your-domain.com/api/v1/ingest/overland?token=YOUR_TOKEN</code></li>
|
||||
<li th:if="${hasToken}" th:utext="#{integrations.overland.step5.with.token(${serverUrl + contextPath + '/api/v1/ingest/overland?token=' + selectedToken})}">Set the Endpoint URL to: <code th:text="${serverUrl + '/api/v1/ingest/overland?token=' + selectedToken}">https://your-domain.com/api/v1/ingest/overland?token=YOUR_TOKEN</code></li>
|
||||
<li th:unless="${hasToken}" th:utext="#{integrations.overland.step5.without.token(${serverUrl + contextPath + '/api/v1/ingest/overland?token=<YOUR TOKEN>'})}">Set the Endpoint URL to: <code th:text="${serverUrl + '/api/v1/ingest/overland?token=<YOUR TOKEN>'}">https://your-domain.com/api/v1/ingest/overland?token=YOUR_TOKEN</code></li>
|
||||
<li th:utext="#{integrations.overland.step6}">Leave the <strong>Device ID</strong> field empty or set a custom identifier</li>
|
||||
<li th:utext="#{integrations.overland.step7}">Leave the <strong>Access Token</strong> field empty (we use the token in the URL)</li>
|
||||
<li th:utext="#{integrations.overland.step8}">Configure tracking settings:
|
||||
@@ -161,7 +161,7 @@
|
||||
<p style="margin-bottom: 15px; color: var(--color-text-white);" th:text="#{integrations.data.quality.description}">Check the quality and frequency of your incoming location data to ensure optimal tracking performance.</p>
|
||||
<button class="btn"
|
||||
id="data-quality-btn"
|
||||
hx-get="/settings/integrations/data-quality-content"
|
||||
th:hx-get="@{/settings/integrations/data-quality-content}"
|
||||
hx-target="#integrations"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#data-quality-spinner">
|
||||
@@ -191,7 +191,7 @@
|
||||
<span th:text="${errorMessage}">Error message</span>
|
||||
</div>
|
||||
|
||||
<form hx-post="/settings/integrations/owntracks-recorder-integration" hx-target="#integrations" hx-swap="innerHTML" class="owntracks-recorder-form" style="margin-top: 20px;" autocomplete="off">
|
||||
<form th:hx-post="@{/settings/integrations/owntracks-recorder-integration}" hx-target="#integrations" hx-swap="innerHTML" class="owntracks-recorder-form" style="margin-top: 20px;" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label for="recorderBaseUrl" th:text="#{integrations.owntracks.recorder.base.url}">Base URL</label>
|
||||
<input type="url"
|
||||
@@ -293,7 +293,7 @@
|
||||
resultDiv.innerHTML = '<div style="color: var(--color-text-white);">' + loadingMessage + '</div>';
|
||||
|
||||
// Make AJAX request to test connection
|
||||
fetch('/settings/integrations/owntracks-recorder-integration/test', {
|
||||
fetch(window.contextPath + '/settings/integrations/owntracks-recorder-integration/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
@@ -349,7 +349,7 @@
|
||||
<summary>
|
||||
<i class="lni lni-photos"></i> <span th:text="#{integrations.photos.title}">Photos</span>
|
||||
</summary>
|
||||
<div id="photos-section" hx-get="/settings/integrations/photos-content" hx-trigger="load" hx-swap="innerHTML">
|
||||
<div id="photos-section" th:hx-get="@{/settings/integrations/photos-content}" hx-trigger="load" hx-swap="innerHTML">
|
||||
<!-- <span th:text="#{integrations.photos.loading}">Photos content will be loaded here</span> -->
|
||||
</div>
|
||||
</details>
|
||||
@@ -357,7 +357,7 @@
|
||||
<summary>
|
||||
<i class="lni lni-network"></i> <span th:text="#{integrations.shared.instances.title}">Shared Instances</span>
|
||||
</summary>
|
||||
<div id="shared-instances-section" hx-get="/settings/integrations/shared-instances-content" hx-trigger="load" hx-swap="innerHTML">
|
||||
<div id="shared-instances-section" th:hx-get="@{/settings/integrations/shared-instances-content}" hx-trigger="load" hx-swap="innerHTML">
|
||||
<!-- <span th:text="#{integrations.shared.instances.loading}">Shared instances content will be loaded here</span> -->
|
||||
</div>
|
||||
</details>
|
||||
@@ -397,7 +397,7 @@
|
||||
}
|
||||
|
||||
function generateOwnTracksConfig(token, serverUrl) {
|
||||
const url = serverUrl + '/api/v1/ingest/owntracks?token=' + token;
|
||||
const url = serverUrl + window.contextPath + '/api/v1/ingest/owntracks?token=' + token;
|
||||
const json = JSON.stringify({
|
||||
"_type": "configuration",
|
||||
"url": url,
|
||||
@@ -409,11 +409,11 @@
|
||||
}
|
||||
|
||||
function generateGpsLoggerPropertiesUrl(token, serverUrl) {
|
||||
return serverUrl + '/settings/integrations/reitti.properties?token=' + token;
|
||||
return serverUrl + window.contextPath + '/settings/integrations/reitti.properties?token=' + token;
|
||||
}
|
||||
|
||||
function generateOverlandUrl(token, serverUrl) {
|
||||
const ingestUrl = serverUrl + '/api/v1/ingest/overland?token=' + token;
|
||||
const ingestUrl = serverUrl + window.contextPath + '/api/v1/ingest/overland?token=' + token;
|
||||
const encodedUrl = encodeURIComponent(ingestUrl);
|
||||
return 'overland://setup?url=' + encodedUrl + '&device_id=1&unique_id=yes';
|
||||
}
|
||||
@@ -534,7 +534,7 @@
|
||||
<!-- Action Buttons -->
|
||||
<div style="margin-top: 25px; text-align: center; display: flex; gap: 15px; justify-content: center;">
|
||||
<button class="btn"
|
||||
hx-get="/settings/integrations/data-quality-content"
|
||||
th:hx-get="@{/settings/integrations/data-quality-content}"
|
||||
hx-target="#integrations"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#refresh-data-spinner">
|
||||
@@ -544,7 +544,7 @@
|
||||
</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary"
|
||||
hx-get="/settings/integrations/integrations-content"
|
||||
th:hx-get="@{/settings/integrations/integrations-content}"
|
||||
hx-target="#integrations"
|
||||
hx-swap="innerHTML"
|
||||
th:text="#{form.close}">
|
||||
@@ -556,6 +556,8 @@
|
||||
|
||||
<span th:text="${firstToken}" id="current-token" style="display:none;"></span>
|
||||
<script th:inline="javascript">
|
||||
window.contextPath = /*[[@{/}]]*/ "/";
|
||||
window.contextPath = window.contextPath.replace(/\/$/, '');
|
||||
window.userSettings = /*[[${userSettings}]]*/ {};
|
||||
window.locale = {
|
||||
'integrations.owntracks.recorder.test.missing.fields': /*[[#{integrations.owntracks.recorder.test.missing.fields}]]*/ 'Please fill in Base URL, Username, and Device ID',
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
</head>
|
||||
<body class="settings-page">
|
||||
<div class="navigation-container" th:replace="~{fragments/main-navigation :: main-navigation('settings')}"></div>
|
||||
@@ -36,7 +36,7 @@
|
||||
<div style="margin-top: 20px;">
|
||||
<button class="btn"
|
||||
hx-trigger="every 2s"
|
||||
hx-get="/settings/queue-stats-content"
|
||||
th:hx-get="@{/settings/queue-stats-content}"
|
||||
hx-target="#job-status"
|
||||
hx-swap="innerHTML"
|
||||
th:text="#{jobs.refresh}">Refresh Status</button>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
<style>
|
||||
.log-output {
|
||||
background: #1e1e1e;
|
||||
@@ -107,7 +107,7 @@
|
||||
|
||||
<div id="loggingSettingsCard" th:fragment="logging-settings-card" class="settings-card">
|
||||
<form id="loggingSettingsForm"
|
||||
hx-post="/settings/logging/update"
|
||||
th:hx-post="@{/settings/logging/update}"
|
||||
hx-target="#loggingSettingsCard"
|
||||
hx-swap="outerHTML">
|
||||
<div class="form-group">
|
||||
@@ -160,7 +160,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<form class="logger-level-form"
|
||||
hx-post="/settings/logging/update"
|
||||
th:hx-post="@{/settings/logging/update}"
|
||||
hx-target="#loggingSettingsCard"
|
||||
hx-swap="outerHTML">
|
||||
<input type="hidden" name="logger" th:value="${logger.name}">
|
||||
@@ -181,7 +181,7 @@
|
||||
<td>
|
||||
<button type="button" class="btn btn-danger"
|
||||
th:if="${logger.name != 'ROOT'}"
|
||||
hx-post="/settings/logging/remove"
|
||||
th:hx-post="@{/settings/logging/remove}"
|
||||
th:hx-vals="${'{"logger":"' + logger.name + '"}'}"
|
||||
hx-target="#loggingSettingsCard"
|
||||
hx-swap="outerHTML"
|
||||
@@ -201,6 +201,8 @@
|
||||
</div>
|
||||
|
||||
<script th:inline="javascript">
|
||||
window.contextPath = /*[[@{/}]]*/ "/";
|
||||
window.contextPath = window.contextPath.replace(/\/$/, '');
|
||||
window.userSettings = /*[[${userSettings}]]*/ {}
|
||||
|
||||
// Localized messages
|
||||
@@ -240,7 +242,7 @@
|
||||
}
|
||||
|
||||
function connectEventSource() {
|
||||
eventSource = new EventSource('/settings/logging/stream');
|
||||
eventSource = new EventSource(window.contextPath + '/settings/logging/stream');
|
||||
|
||||
eventSource.onopen = function() {
|
||||
logOutput.innerHTML = `<div class="log-line">${messages.connected}</div>`;
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
</head>
|
||||
<body class="settings-page">
|
||||
<div class="navigation-container" th:replace="~{fragments/main-navigation :: main-navigation('settings')}"></div>
|
||||
@@ -40,7 +40,7 @@
|
||||
<h3 th:text="#{data.process.visits.title}">Process Visits and Trips</h3>
|
||||
<p th:text="#{data.process.visits.description}">Manually trigger the processing of raw location data into visits and trips. This will analyze unprocessed location points and create meaningful visits and trips from them.</p>
|
||||
<button class="btn"
|
||||
hx-post="/settings/manage-data/process-visits-trips"
|
||||
th:hx-post="@{/settings/manage-data/process-visits-trips}"
|
||||
hx-target="#manage-data"
|
||||
hx-swap="innerHTML"
|
||||
th:attr="hx-confirm=#{data.process.visits.confirm}"
|
||||
@@ -56,7 +56,7 @@
|
||||
<strong>⚠️ <span th:text="#{label.warning}">Warning:</span></strong> <span th:text="#{data.clear.reprocess.warning}">This action will permanently delete all visits, trips, and processed visits. This cannot be undone.</span>
|
||||
</div>
|
||||
<button class="btn btn-danger"
|
||||
hx-post="/settings/manage-data/clear-and-reprocess"
|
||||
th:hx-post="@{/settings/manage-data/clear-and-reprocess}"
|
||||
hx-target="#manage-data"
|
||||
hx-swap="innerHTML"
|
||||
th:attr="hx-confirm=#{data.clear.reprocess.confirm}"
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
<button class="btn btn-danger"
|
||||
id="delete-all-confirmed-btn"
|
||||
hx-post="/settings/manage-data/remove-all-data"
|
||||
th:hx-post="@{/settings/manage-data/remove-all-data}"
|
||||
hx-target="#manage-data"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="#delete-verification-input"
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
<div th:unless="${deleteAllRequiresVerification}">
|
||||
<button class="btn btn-danger"
|
||||
hx-post="/settings/manage-data/remove-all-data"
|
||||
thhx-post="@{/settings/manage-data/remove-all-data}"
|
||||
hx-target="#manage-data"
|
||||
hx-swap="innerHTML"
|
||||
th:attr="hx-confirm=#{data.remove.all.confirm}"
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/leaflet.css">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/leaflet.js"></script>
|
||||
<script src="/js/TileLayer.Grayscale.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/leaflet.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/leaflet.js}"></script>
|
||||
<script th:src="@{/js/TileLayer.Grayscale.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
</head>
|
||||
<body class="settings-page">
|
||||
<div class="navigation-container" th:replace="~{fragments/main-navigation :: main-navigation('settings')}"></div>
|
||||
@@ -33,7 +33,8 @@
|
||||
</div>
|
||||
|
||||
<div class="search-container" style="margin-bottom: 20px;">
|
||||
<input type="text" id="search-input" name="search" th:value="${search}" th:placeholder="#{places.search.placeholder}" placeholder="Search places..." style="width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px;" hx-get="/settings/places/places-content" hx-target="#places-management" hx-swap="innerHTML" hx-trigger="input changed delay:300ms" hx-vals='{"page": 0}' />
|
||||
<input type="text" id="search-input" name="search" th:value="${search}" th:placeholder="#{places.search.placeholder}" placeholder="Search places..." style="width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px;"
|
||||
th:hx-get="@{/settings/places/places-content}" hx-target="#places-management" hx-swap="innerHTML" hx-trigger="input changed delay:300ms" hx-vals='{"page": 0}' />
|
||||
</div>
|
||||
|
||||
<div class="places-grid" th:if="${!isEmpty}">
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
</head>
|
||||
<body class="settings-page">
|
||||
<div class="navigation-container" th:replace="~{fragments/main-navigation :: main-navigation('settings')}"></div>
|
||||
@@ -85,7 +85,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="settings-card">
|
||||
<form hx-post="/settings/share-access/magic-links" hx-target="#magic-links" hx-swap="innerHTML"
|
||||
<form th:hx-post="@{/settings/share-access/magic-links}" hx-target="#magic-links" hx-swap="innerHTML"
|
||||
class="magic-link-form"
|
||||
style="margin-bottom: 20px;">
|
||||
<div class="form-group">
|
||||
@@ -158,7 +158,7 @@
|
||||
<div class="settings-card" th:if="${!availableUsers.isEmpty()}">
|
||||
<p th:text="#{share-with.users.description}">Select users you want to share your location data with. They will be able to view your timeline and location history.</p>
|
||||
|
||||
<form hx-post="/settings/share-access/users" hx-target="#share-with" hx-swap="innerHTML">
|
||||
<form th:hx-post="@{/settings/share-access/users}" hx-target="#share-with" hx-swap="innerHTML">
|
||||
<div class="user-sharing-selection">
|
||||
<div class="language-buttons">
|
||||
<div th:each="user : ${availableUsers}" class="user-sharing-button-wrapper">
|
||||
@@ -171,7 +171,7 @@
|
||||
onchange="this.form.requestSubmit()"
|
||||
style="display: none;">
|
||||
<div class="user-sharing-content">
|
||||
<img th:src="${user.avatarUrl()}"
|
||||
<img th:src="@{${user.avatarUrl()}}"
|
||||
th:alt="${user.avatarFallback()}"
|
||||
class="avatar">
|
||||
<div class="user-info">
|
||||
@@ -220,7 +220,7 @@
|
||||
<tr th:each="sharing : ${sharedWithMeUsers}">
|
||||
<td>
|
||||
<div class="user-info-row" style="display: flex; align-items: center; gap: 10px;">
|
||||
<img th:src="${sharing.sharingUser().avatarUrl()}"
|
||||
<img th:src="@{${sharing.sharingUser().avatarUrl()}}"
|
||||
th:alt="${sharing.sharingUser().avatarFallback()}"
|
||||
class="avatar small">
|
||||
<div class="user-details">
|
||||
@@ -261,7 +261,7 @@ function updateSharingColor(colorInput) {
|
||||
const sharingId = colorInput.getAttribute('data-sharing-id');
|
||||
const color = colorInput.value;
|
||||
|
||||
fetch(`/settings/share-access/shared-with-me/${sharingId}/color`, {
|
||||
fetch(window.contextPath + `/settings/share-access/shared-with-me/${sharingId}/color`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
@@ -287,6 +287,8 @@ function updateSharingColor(colorInput) {
|
||||
}
|
||||
</script>
|
||||
<script th:inline="javascript">
|
||||
window.contextPath = /*[[@{/}]]*/ "/";
|
||||
window.contextPath = window.contextPath.replace(/\/$/, '');
|
||||
window.userSettings = /*[[${userSettings}]]*/ {}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
</head>
|
||||
<body class="settings-page">
|
||||
<div class="navigation-container" th:replace="~{fragments/main-navigation :: main-navigation('settings')}"></div>
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
<div class="settings-card" th:if="${!availableModes.isEmpty()}">
|
||||
<h3 th:text="#{transportation.modes.add.title}">Add Transportation Mode</h3>
|
||||
<form hx-post="/settings/transportation-modes/add"
|
||||
<form th:hx-post="@{/settings/transportation-modes/add}"
|
||||
hx-target="body"
|
||||
hx-swap="outerHTML"
|
||||
class="token-form"
|
||||
@@ -110,7 +110,7 @@
|
||||
|
||||
<button id="reclassify-btn"
|
||||
class="btn btn-primary"
|
||||
hx-post="/settings/transportation-modes/reclassify"
|
||||
th:hx-post="@{/settings/transportation-modes/reclassify}"
|
||||
hx-target="#reclassify-status"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#reclassify-spinner"
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/leaflet.css">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/leaflet.js"></script>
|
||||
<script src="/js/TileLayer.Grayscale.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/leaflet.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/leaflet.js}"></script>
|
||||
<script th:src="@{/js/TileLayer.Grayscale.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
</head>
|
||||
<body class="settings-page">
|
||||
<div class="navigation-container" th:replace="~{fragments/main-navigation :: main-navigation('settings')}"></div>
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/leaflet.css">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<script src="/js/HumanizeDuration.js"></script>
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/leaflet.js"></script>
|
||||
<script src="/js/TileLayer.Grayscale.js"></script>
|
||||
<script src="/js/leaflet.geodesic.2.7.2.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/leaflet.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<script th:src="@{/js/HumanizeDuration.js}"></script>
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/leaflet.js}"></script>
|
||||
<script th:src="@{/js/TileLayer.Grayscale.js}"></script>
|
||||
<script th:src="@{/js/leaflet.geodesic.2.7.2.js}"></script>
|
||||
<script th:src="@{/js/util.js}"></script>
|
||||
</head>
|
||||
<body class="settings-page">
|
||||
<div class="navigation-container" th:replace="~{fragments/main-navigation :: main-navigation('settings')}"></div>
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="alert-actions">
|
||||
<button type="button"
|
||||
class="btn btn-warning"
|
||||
hx-post="/settings/visit-sensitivity/recalculate"
|
||||
th:hx-post="@{/settings/visit-sensitivity/recalculate}"
|
||||
hx-target="body"
|
||||
th:hx-confirm="#{visit.sensitivity.recalculation.confirm}"
|
||||
hx-indicator="#recalculate-spinner">
|
||||
@@ -50,7 +50,7 @@
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-secondary"
|
||||
hx-post="/settings/visit-sensitivity/dismiss-recalculation"
|
||||
th:hx-post="@{/settings/visit-sensitivity/dismiss-recalculation}"
|
||||
hx-target="body"
|
||||
th:text="#{visit.sensitivity.recalculation.dismiss}">Dismiss</button>
|
||||
</div>
|
||||
@@ -72,6 +72,8 @@
|
||||
</div>
|
||||
|
||||
<script th:inline="javascript">
|
||||
window.contextPath = /*[[@{/}]]*/ "/";
|
||||
window.contextPath = window.contextPath.replace(/\/$/, '');
|
||||
window.userSettings = /*[[${userSettings}]]*/ {}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/leaflet.css">
|
||||
<script src="/js/leaflet.js"></script>
|
||||
<script src="/js/TileLayer.Grayscale.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/leaflet.css}">
|
||||
<script th:src="@{/js/leaflet.js}"></script>
|
||||
<script th:src="@{/js/TileLayer.Grayscale.js}"></script>
|
||||
<style>
|
||||
:root {
|
||||
--color-highlight: #fddca1;
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
<link rel="icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="apple-touch-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="shortcut icon" type="image/x-icon" th:href="@{/img/logo.svg}">
|
||||
<link rel="stylesheet" href="/css/leaflet.css">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/lineicons.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/chart.js"></script>
|
||||
<script src="/js/leaflet.js"></script>
|
||||
<script src="/js/TileLayer.Grayscale.js"></script>
|
||||
<link rel="stylesheet" th:href="@{/css/leaflet.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/lineicons.css}">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<script th:src="@{/js/chart.js}"></script>
|
||||
<script th:src="@{/js/leaflet.js}"></script>
|
||||
<script th:src="@{/js/TileLayer.Grayscale.js}"></script>
|
||||
</head>
|
||||
<body class="background-dark">
|
||||
<div id="statistics-panel"></div>
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="timeline">
|
||||
<div class="timeline-container">
|
||||
<div id="years-navigation"
|
||||
hx-get="/statistics/years-navigation"
|
||||
th:hx-get="@{/statistics/years-navigation}"
|
||||
hx-trigger="load">
|
||||
<div class="photo-modal-loading-spinner htmx-indicator" th:text="#{timeline.loading}">Loading...</div>
|
||||
</div>
|
||||
@@ -30,6 +30,8 @@
|
||||
</div>
|
||||
<script th:inline="javascript">
|
||||
window.userSettings = /*[[${userSettings}]]*/ {}
|
||||
window.contextPath = /*[[@{/}]]*/ "/";
|
||||
window.contextPath = window.contextPath.replace(/\/$/, '');
|
||||
|
||||
function shouldTrigger(element) {
|
||||
return !element.classList.contains('active');
|
||||
|
||||
@@ -16,7 +16,7 @@ class TilesCustomizationProviderTest {
|
||||
String customAttribution = "Custom Attribution";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
null, defaultService, defaultAttribution, customService, customAttribution
|
||||
null, defaultService, defaultAttribution, customService, customAttribution, new ContextPathHolder("")
|
||||
);
|
||||
|
||||
// When
|
||||
@@ -36,7 +36,7 @@ class TilesCustomizationProviderTest {
|
||||
String customAttribution = "Custom Attribution";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
null, defaultService, defaultAttribution, customService, customAttribution
|
||||
null, defaultService, defaultAttribution, customService, customAttribution, new ContextPathHolder("")
|
||||
);
|
||||
|
||||
// When
|
||||
@@ -56,7 +56,7 @@ class TilesCustomizationProviderTest {
|
||||
String customAttribution = "";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
null, defaultService, defaultAttribution, customService, customAttribution
|
||||
null, defaultService, defaultAttribution, customService, customAttribution, new ContextPathHolder("")
|
||||
);
|
||||
|
||||
// When
|
||||
@@ -76,7 +76,7 @@ class TilesCustomizationProviderTest {
|
||||
String customAttribution = null;
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
null, defaultService, defaultAttribution, customService, customAttribution
|
||||
null, defaultService, defaultAttribution, customService, customAttribution, new ContextPathHolder("")
|
||||
);
|
||||
|
||||
// When
|
||||
@@ -96,7 +96,7 @@ class TilesCustomizationProviderTest {
|
||||
String customAttribution = "\t\n";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
null, defaultService, defaultAttribution, customService, customAttribution
|
||||
null, defaultService, defaultAttribution, customService, customAttribution, new ContextPathHolder("")
|
||||
);
|
||||
|
||||
// When
|
||||
@@ -116,7 +116,7 @@ class TilesCustomizationProviderTest {
|
||||
String customAttribution = "";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
null, defaultService, defaultAttribution, customService, customAttribution
|
||||
null, defaultService, defaultAttribution, customService, customAttribution, new ContextPathHolder("")
|
||||
);
|
||||
|
||||
// When
|
||||
@@ -136,7 +136,7 @@ class TilesCustomizationProviderTest {
|
||||
String customAttribution = "";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
null, defaultService, defaultAttribution, customService, customAttribution
|
||||
null, defaultService, defaultAttribution, customService, customAttribution, new ContextPathHolder("")
|
||||
);
|
||||
|
||||
// When
|
||||
@@ -156,7 +156,7 @@ class TilesCustomizationProviderTest {
|
||||
String customAttribution = "Custom Attribution";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
null, defaultService, defaultAttribution, customService, customAttribution
|
||||
null, defaultService, defaultAttribution, customService, customAttribution, new ContextPathHolder("")
|
||||
);
|
||||
|
||||
// When
|
||||
@@ -177,7 +177,7 @@ class TilesCustomizationProviderTest {
|
||||
String customAttribution = "Custom Attribution";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
cacheUrl, defaultService, defaultAttribution, customService, customAttribution
|
||||
cacheUrl, defaultService, defaultAttribution, customService, customAttribution, new ContextPathHolder("")
|
||||
);
|
||||
|
||||
// When
|
||||
@@ -198,7 +198,7 @@ class TilesCustomizationProviderTest {
|
||||
String customAttribution = "Custom Attribution";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
cacheUrl, defaultService, defaultAttribution, customService, customAttribution
|
||||
cacheUrl, defaultService, defaultAttribution, customService, customAttribution, new ContextPathHolder("")
|
||||
);
|
||||
|
||||
// When
|
||||
|
||||
Reference in New Issue
Block a user