fix: navigate to time action

This commit is contained in:
midzelis 2025-08-14 20:06:22 +00:00
parent ba0cfb76ed
commit d5cc017bba
7 changed files with 70 additions and 17 deletions

View File

@ -1360,6 +1360,8 @@
"my_albums": "My albums", "my_albums": "My albums",
"name": "Name", "name": "Name",
"name_or_nickname": "Name or nickname", "name_or_nickname": "Name or nickname",
"navigate": "Navigate",
"navigate_to_time": "Navigate to Time",
"network_requirement_photos_upload": "Use cellular data to backup photos", "network_requirement_photos_upload": "Use cellular data to backup photos",
"network_requirement_videos_upload": "Use cellular data to backup videos", "network_requirement_videos_upload": "Use cellular data to backup videos",
"network_requirements": "Network Requirements", "network_requirements": "Network Requirements",

View File

@ -8,6 +8,9 @@ import ChangeDate from './change-date.svelte';
describe('ChangeDate component', () => { describe('ChangeDate component', () => {
const initialDate = DateTime.fromISO('2024-01-01'); const initialDate = DateTime.fromISO('2024-01-01');
const initialTimeZone = 'Europe/Berlin'; const initialTimeZone = 'Europe/Berlin';
const targetDate = DateTime.fromISO('2024-01-01').setZone('UTC+1', {
keepLocalTime: true,
});
const currentInterval = { const currentInterval = {
start: DateTime.fromISO('2000-02-01T14:00:00+01:00'), start: DateTime.fromISO('2000-02-01T14:00:00+01:00'),
end: DateTime.fromISO('2001-02-01T14:00:00+01:00'), end: DateTime.fromISO('2001-02-01T14:00:00+01:00'),
@ -50,7 +53,11 @@ describe('ChangeDate component', () => {
await fireEvent.click(getConfirmButton()); await fireEvent.click(getConfirmButton());
expect(onConfirm).toHaveBeenCalledWith({ mode: 'absolute', date: '2024-01-01T00:00:00.000+01:00' }); expect(onConfirm).toHaveBeenCalledWith({
mode: 'absolute',
date: '2024-01-01T00:00:00.000+01:00',
dateTime: targetDate,
});
}); });
test('calls onCancel on cancel', async () => { test('calls onCancel on cancel', async () => {
@ -65,7 +72,9 @@ describe('ChangeDate component', () => {
describe('when date is in daylight saving time', () => { describe('when date is in daylight saving time', () => {
const dstDate = DateTime.fromISO('2024-07-01'); const dstDate = DateTime.fromISO('2024-07-01');
const targetDate = DateTime.fromISO('2024-07-01').setZone('UTC+2', {
keepLocalTime: true,
});
test('should render correct timezone with offset', () => { test('should render correct timezone with offset', () => {
render(ChangeDate, { initialDate: dstDate, initialTimeZone, onCancel, onConfirm }); render(ChangeDate, { initialDate: dstDate, initialTimeZone, onCancel, onConfirm });
@ -79,7 +88,11 @@ describe('ChangeDate component', () => {
await fireEvent.click(getConfirmButton()); await fireEvent.click(getConfirmButton());
expect(onConfirm).toHaveBeenCalledWith({ mode: 'absolute', date: '2024-07-01T00:00:00.000+02:00' }); expect(onConfirm).toHaveBeenCalledWith({
mode: 'absolute',
date: '2024-07-01T00:00:00.000+02:00',
dateTime: targetDate,
});
}); });
}); });

View File

@ -4,7 +4,7 @@
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { getDateTimeOffsetLocaleString } from '$lib/utils/timeline-util.js'; import { getDateTimeOffsetLocaleString } from '$lib/utils/timeline-util.js';
import { ConfirmModal, Field, Switch } from '@immich/ui'; import { ConfirmModal, Field, Switch } from '@immich/ui';
import { mdiCalendarEditOutline } from '@mdi/js'; import { mdiCalendarEdit } from '@mdi/js';
import { DateTime, Duration } from 'luxon'; import { DateTime, Duration } from 'luxon';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
@ -17,6 +17,8 @@
timezoneInput?: boolean; timezoneInput?: boolean;
withDuration?: boolean; withDuration?: boolean;
currentInterval?: { start: DateTime; end: DateTime }; currentInterval?: { start: DateTime; end: DateTime };
icon?: string;
confirmText?: string;
onCancel: () => void; onCancel: () => void;
onConfirm: (result: AbsoluteResult | RelativeResult) => void; onConfirm: (result: AbsoluteResult | RelativeResult) => void;
} }
@ -28,6 +30,8 @@
timezoneInput = true, timezoneInput = true,
withDuration = true, withDuration = true,
currentInterval = undefined, currentInterval = undefined,
icon = mdiCalendarEdit,
confirmText,
onCancel, onCancel,
onConfirm, onConfirm,
}: Props = $props(); }: Props = $props();
@ -35,6 +39,7 @@
export type AbsoluteResult = { export type AbsoluteResult = {
mode: 'absolute'; mode: 'absolute';
date: string; date: string;
dateTime: DateTime<true>;
}; };
export type RelativeResult = { export type RelativeResult = {
@ -191,9 +196,15 @@
const fixedOffsetZone = `UTC${offsetMinutes >= 0 ? '+' : ''}${Duration.fromObject({ minutes: offsetMinutes }).toFormat('hh:mm')}`; 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. // Create a DateTime object in this fixed-offset zone, preserving the local time.
const finalDateTime = DateTime.fromObject(dtComponents.toObject(), { zone: fixedOffsetZone }); const fixedOffsetDateTime = DateTime.fromObject(dtComponents.toObject(), {
zone: fixedOffsetZone,
}) as DateTime<true>;
onConfirm({ mode: 'absolute', date: finalDateTime.toISO({ includeOffset: true })! }); onConfirm({
mode: 'absolute',
date: fixedOffsetDateTime.toISO({ includeOffset: true })!,
dateTime: fixedOffsetDateTime,
});
} }
if (showRelative && (selectedDuration || selectedRelativeOption)) { if (showRelative && (selectedDuration || selectedRelativeOption)) {
@ -237,7 +248,8 @@
<ConfirmModal <ConfirmModal
confirmColor="primary" confirmColor="primary"
{title} {title}
icon={mdiCalendarEditOutline} {icon}
{confirmText}
prompt="Please select a new date:" prompt="Please select a new date:"
disabled={!date.isValid} disabled={!date.isValid}
onClose={(confirmed) => (confirmed ? handleConfirm() : onCancel())} onClose={(confirmed) => (confirmed ? handleConfirm() : onCancel())}

View File

@ -833,15 +833,14 @@
title="Navigate to Time" title="Navigate to Time"
initialDate={DateTime.now()} initialDate={DateTime.now()}
timezoneInput={false} timezoneInput={false}
onConfirm={async (dateString: AbsoluteResult | RelativeResult) => { onConfirm={async (result: AbsoluteResult | RelativeResult) => {
isShowSelectDate = false; isShowSelectDate = false;
if (dateString.mode == 'absolute') { if (result.mode !== 'absolute') {
const asset = await timelineManager.getClosestAssetToDate( return;
(DateTime.fromISO(dateString.date) as DateTime<true>).toObject(), }
); const asset = await timelineManager.getClosestAssetToDate(result.dateTime.toObject());
if (asset) { if (asset) {
setFocusAsset(asset); setFocusAsset(asset);
}
} }
}} }}
onCancel={() => (isShowSelectDate = false)} onCancel={() => (isShowSelectDate = false)}

View File

@ -143,3 +143,24 @@ export function findMonthGroupForDate(timelineManager: TimelineManager, targetYe
} }
} }
} }
export function findClosestGroupForDate(timelineManager: TimelineManager, targetYearMonth: TimelineYearMonth) {
let closestMonth: MonthGroup | undefined;
let minDifference = Number.MAX_SAFE_INTEGER;
for (const month of timelineManager.months) {
const { year, month: monthNum } = month.yearMonth;
// Calculate the absolute difference in months
const yearDiff = Math.abs(year - targetYearMonth.year);
const monthDiff = Math.abs(monthNum - targetYearMonth.month);
const totalDiff = yearDiff * 12 + monthDiff;
if (totalDiff < minDifference) {
minDifference = totalDiff;
closestMonth = month;
}
}
return closestMonth;
}

View File

@ -16,6 +16,7 @@ import {
runAssetOperation, runAssetOperation,
} from '$lib/managers/timeline-manager/internal/operations-support.svelte'; } from '$lib/managers/timeline-manager/internal/operations-support.svelte';
import { import {
findClosestGroupForDate,
findMonthGroupForAsset as findMonthGroupForAssetUtil, findMonthGroupForAsset as findMonthGroupForAssetUtil,
findMonthGroupForDate, findMonthGroupForDate,
getAssetWithOffset, getAssetWithOffset,
@ -523,9 +524,13 @@ export class TimelineManager {
} }
async getClosestAssetToDate(dateTime: TimelineDateTime) { async getClosestAssetToDate(dateTime: TimelineDateTime) {
const monthGroup = findMonthGroupForDate(this, dateTime); let monthGroup = findMonthGroupForDate(this, dateTime);
if (!monthGroup) { if (!monthGroup) {
return; // if exact match not found, find closest
monthGroup = findClosestGroupForDate(this, dateTime);
if (!monthGroup) {
return;
}
} }
await this.loadMonthGroup(dateTime, { cancelable: false }); await this.loadMonthGroup(dateTime, { cancelable: false });
const asset = monthGroup.findClosest(dateTime); const asset = monthGroup.findClosest(dateTime);

View File

@ -27,6 +27,7 @@
{ key: ['D', 'd'], action: $t('previous_or_next_day') }, { key: ['D', 'd'], action: $t('previous_or_next_day') },
{ key: ['M', 'm'], action: $t('previous_or_next_month') }, { key: ['M', 'm'], action: $t('previous_or_next_month') },
{ key: ['Y', 'y'], action: $t('previous_or_next_year') }, { key: ['Y', 'y'], action: $t('previous_or_next_year') },
{ key: ['g'], action: $t('navigate_to_time') },
{ key: ['x'], action: $t('select') }, { key: ['x'], action: $t('select') },
{ key: ['Esc'], action: $t('back_close_deselect') }, { key: ['Esc'], action: $t('back_close_deselect') },
{ key: ['Ctrl', 'k'], action: $t('search_your_photos') }, { key: ['Ctrl', 'k'], action: $t('search_your_photos') },