mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
fix(web): handling historical timezones in web client (#18905)
* fix handling historical timezones in web client * honor dst when calculating the timezone offset * fix variable used to construct timezones list to honor dst * remove unused variable. fix lint
This commit is contained in:
parent
84024f6cdc
commit
7b2237b86b
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime, Duration } from 'luxon';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import DateInput from '../elements/date-input.svelte';
|
import DateInput from '../elements/date-input.svelte';
|
||||||
import Combobox, { type ComboBoxOption } from './combobox.svelte';
|
import Combobox, { type ComboBoxOption } from './combobox.svelte';
|
||||||
@ -65,6 +65,9 @@
|
|||||||
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|
||||||
let selectedDate = $state(initialDate.toFormat("yyyy-MM-dd'T'HH:mm"));
|
let selectedDate = $state(initialDate.toFormat("yyyy-MM-dd'T'HH:mm"));
|
||||||
|
// Use a fixed modern date to calculate stable timezone offsets for the list
|
||||||
|
// This ensures that the offsets shown in the combobox are always current,
|
||||||
|
// regardless of the historical date selected by the user.
|
||||||
let timezones: ZoneOption[] = knownTimezones
|
let timezones: ZoneOption[] = knownTimezones
|
||||||
.map((zone) => zoneOptionForDate(zone, selectedDate))
|
.map((zone) => zoneOptionForDate(zone, selectedDate))
|
||||||
.filter((zone) => zone.valid)
|
.filter((zone) => zone.valid)
|
||||||
@ -73,12 +76,13 @@
|
|||||||
let selectedOption: ZoneOption | undefined = $state(getPreferredTimeZone(initialDate, userTimeZone, timezones));
|
let selectedOption: ZoneOption | undefined = $state(getPreferredTimeZone(initialDate, userTimeZone, timezones));
|
||||||
|
|
||||||
function zoneOptionForDate(zone: string, date: string) {
|
function zoneOptionForDate(zone: string, date: string) {
|
||||||
const dateAtZone: DateTime = DateTime.fromISO(date, { zone });
|
const { offsetMinutes, offsetFormat: zoneOffsetAtDate } = getModernOffsetForZoneAndDate(zone, date);
|
||||||
const zoneOffsetAtDate = dateAtZone.toFormat('ZZ');
|
// For validity, we still need to check if the exact date/time exists in the *original* timezone (for gaps/overlaps).
|
||||||
const valid = dateAtZone.isValid && date.toString() === dateAtZone.toFormat("yyyy-MM-dd'T'HH:mm");
|
const dateForValidity = DateTime.fromISO(date, { zone });
|
||||||
|
const valid = dateForValidity.isValid && date === dateForValidity.toFormat("yyyy-MM-dd'T'HH:mm");
|
||||||
return {
|
return {
|
||||||
value: zone,
|
value: zone,
|
||||||
offsetMinutes: dateAtZone.offset,
|
offsetMinutes,
|
||||||
label: zone + ' (' + zoneOffsetAtDate + ')' + (valid ? '' : ' [invalid date!]'),
|
label: zone + ' (' + zoneOffsetAtDate + ')' + (valid ? '' : ' [invalid date!]'),
|
||||||
valid,
|
valid,
|
||||||
};
|
};
|
||||||
@ -118,6 +122,24 @@
|
|||||||
return previousSelection ?? fromInitialTimeZone ?? sameAsUserTimeZone ?? firstWithSameOffset ?? utcFallback;
|
return previousSelection ?? fromInitialTimeZone ?? sameAsUserTimeZone ?? firstWithSameOffset ?? utcFallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getModernOffsetForZoneAndDate(
|
||||||
|
zone: string,
|
||||||
|
dateString: string,
|
||||||
|
): { offsetMinutes: number; offsetFormat: string } {
|
||||||
|
const dt = DateTime.fromISO(dateString, { zone });
|
||||||
|
|
||||||
|
// we determine the *modern* offset for this zone based on its current rules.
|
||||||
|
// To do this, we "move" the date to the current year, keeping the local time components.
|
||||||
|
// This allows Luxon to apply current-year DST rules.
|
||||||
|
const modernYearDt = dt.set({ year: DateTime.now().year });
|
||||||
|
|
||||||
|
// Calculate the offset at that modern year's date.
|
||||||
|
const modernOffsetMinutes = modernYearDt.setZone(zone, { keepLocalTime: true }).offset;
|
||||||
|
const modernOffsetFormat = modernYearDt.setZone(zone, { keepLocalTime: true }).toFormat('ZZ');
|
||||||
|
|
||||||
|
return { offsetMinutes: modernOffsetMinutes, offsetFormat: modernOffsetFormat };
|
||||||
|
}
|
||||||
|
|
||||||
function sortTwoZones(zoneA: ZoneOption, zoneB: ZoneOption) {
|
function sortTwoZones(zoneA: ZoneOption, zoneB: ZoneOption) {
|
||||||
let offsetDifference = zoneA.offsetMinutes - zoneB.offsetMinutes;
|
let offsetDifference = zoneA.offsetMinutes - zoneB.offsetMinutes;
|
||||||
if (offsetDifference != 0) {
|
if (offsetDifference != 0) {
|
||||||
@ -127,9 +149,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
const value = date.toISO();
|
if (date.isValid && selectedOption) {
|
||||||
if (value) {
|
// Get the local date/time components from the selected string using neutral timezone
|
||||||
onConfirm(value);
|
const dtComponents = DateTime.fromISO(selectedDate, { zone: 'utc' });
|
||||||
|
|
||||||
|
// Determine the modern, DST-aware offset for the selected IANA zone
|
||||||
|
const { offsetMinutes } = getModernOffsetForZoneAndDate(selectedOption.value, selectedDate);
|
||||||
|
|
||||||
|
// Construct the final ISO string with a fixed-offset zone.
|
||||||
|
const fixedOffsetZone = `UTC${offsetMinutes >= 0 ? '+' : ''}${Duration.fromObject({ minutes: offsetMinutes }).toFormat('hh:mm')}`;
|
||||||
|
|
||||||
|
// Create a DateTime object in this fixed-offset zone, preserving the local time.
|
||||||
|
const finalDateTime = DateTime.fromObject(dtComponents.toObject(), { zone: fixedOffsetZone });
|
||||||
|
|
||||||
|
onConfirm(finalDateTime.toISO({ includeOffset: true })!);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user