mirror of
https://github.com/immich-app/immich.git
synced 2025-08-30 23:02:39 -04:00
Fix and enhance navigate to time functionality
• Add i18n support for navigate button and dialog title • Update ChangeDate component to return DateTime object alongside ISO string • Fix closest date finding algorithm to handle cases where exact month doesn't exist • Add navigate to time (g) shortcut to keyboard shortcuts modal
This commit is contained in:
parent
4016d18960
commit
c22a4e146f
@ -1344,6 +1344,8 @@
|
||||
"my_albums": "My albums",
|
||||
"name": "Name",
|
||||
"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_videos_upload": "Use cellular data to backup videos",
|
||||
"network_requirements_updated": "Network requirements changed, resetting backup queue",
|
||||
|
@ -9,7 +9,7 @@
|
||||
"build:stats": "BUILD_STATS=true vite build",
|
||||
"package": "svelte-kit package",
|
||||
"preview": "vite preview",
|
||||
"check:svelte": "svelte-check --no-tsconfig --fail-on-warnings --compiler-warnings 'reactive_declaration_non_reactive_property:ignore' --ignore src/lib/components/photos-page/asset-grid.svelte",
|
||||
"check:svelte": "svelte-check --no-tsconfig --fail-on-warnings",
|
||||
"check:typescript": "tsc --noEmit",
|
||||
"check:watch": "npm run check:svelte -- --watch",
|
||||
"check:code": "npm run format && npm run lint:p && npm run check:svelte && npm run check:typescript",
|
||||
|
@ -5,9 +5,11 @@
|
||||
setFocusToAsset as setFocusAssetInit,
|
||||
setFocusTo as setFocusToInit,
|
||||
} from '$lib/components/photos-page/actions/focus-actions';
|
||||
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
|
||||
import ChangeDate, {
|
||||
type AbsoluteResult,
|
||||
type RelativeResult,
|
||||
} from '$lib/components/shared-components/change-date.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||
@ -20,7 +22,10 @@
|
||||
import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions';
|
||||
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
import { modalManager } from '@immich/ui';
|
||||
import { mdiCalendarBlankOutline } from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
import DeleteAssetDialog from './delete-asset-dialog.svelte';
|
||||
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
@ -197,16 +202,19 @@
|
||||
|
||||
{#if isShowSelectDate}
|
||||
<ChangeDate
|
||||
title="Navigate to Time"
|
||||
withDuration={false}
|
||||
icon={mdiCalendarBlankOutline}
|
||||
confirmText={$t('navigate')}
|
||||
title={$t('navigate_to_time')}
|
||||
initialDate={DateTime.now()}
|
||||
timezoneInput={false}
|
||||
onConfirm={async (dateString: string) => {
|
||||
onConfirm={async (result: AbsoluteResult | RelativeResult) => {
|
||||
isShowSelectDate = false;
|
||||
const asset = await timelineManager.getClosestAssetToDate(
|
||||
(DateTime.fromISO(dateString) as DateTime<true>).toObject(),
|
||||
);
|
||||
if (asset) {
|
||||
setFocusAsset(asset);
|
||||
if (result.mode === 'absolute') {
|
||||
const asset = await timelineManager.getClosestAssetToDate(result.dateTime.toObject());
|
||||
if (asset) {
|
||||
setFocusAsset(asset);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onCancel={() => (isShowSelectDate = false)}
|
||||
|
@ -4,7 +4,6 @@
|
||||
import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer';
|
||||
import AssetDateGroupActions from '$lib/components/photos-page/asset-date-group-actions.svelte';
|
||||
import AssetGridActions from '$lib/components/photos-page/asset-grid-actions.svelte';
|
||||
import AssetViewerAndActions from '$lib/components/photos-page/asset-viewer-and-actions.svelte';
|
||||
import Skeleton from '$lib/components/photos-page/skeleton.svelte';
|
||||
import Scrubber from '$lib/components/shared-components/scrubber/scrubber.svelte';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
@ -15,12 +14,11 @@
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
import { type ScrubberListener, type TimelinePlainYearMonth } from '$lib/utils/timeline-util';
|
||||
import { AssetVisibility, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||
import { getTimes, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import type { UpdatePayload } from 'vite';
|
||||
import Portal from '../shared-components/portal/portal.svelte';
|
||||
import AssetDateGroup from './asset-date-group.svelte';
|
||||
|
||||
interface Props {
|
||||
@ -69,7 +67,7 @@
|
||||
empty,
|
||||
}: Props = $props();
|
||||
|
||||
let { isViewing: showAssetViewer, gridScrollTarget, mutex, viewingAsset} = assetViewingStore;
|
||||
let { isViewing: showAssetViewer, gridScrollTarget, asset: viewingAsset } = assetViewingStore;
|
||||
|
||||
let element: HTMLElement | undefined = $state();
|
||||
|
||||
|
@ -8,6 +8,9 @@ import ChangeDate from './change-date.svelte';
|
||||
describe('ChangeDate component', () => {
|
||||
const initialDate = DateTime.fromISO('2024-01-01');
|
||||
const initialTimeZone = 'Europe/Berlin';
|
||||
const targetDate = DateTime.fromISO('2024-01-01').setZone('UTC+1', {
|
||||
keepLocalTime: true,
|
||||
});
|
||||
const currentInterval = {
|
||||
start: DateTime.fromISO('2000-02-01T14:00:00+01:00'),
|
||||
end: DateTime.fromISO('2001-02-01T14:00:00+01:00'),
|
||||
@ -43,7 +46,11 @@ describe('ChangeDate component', () => {
|
||||
|
||||
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 () => {
|
||||
@ -58,7 +65,9 @@ describe('ChangeDate component', () => {
|
||||
|
||||
describe('when date is in daylight saving time', () => {
|
||||
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', () => {
|
||||
render(ChangeDate, { initialDate: dstDate, initialTimeZone, onCancel, onConfirm });
|
||||
|
||||
@ -72,7 +81,11 @@ describe('ChangeDate component', () => {
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,15 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { ConfirmModal } from '@immich/ui';
|
||||
import { mdiCalendarEditOutline } from '@mdi/js';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getDateTimeOffsetLocaleString } from '$lib/utils/timeline-util.js';
|
||||
import { ConfirmModal, Field, Switch } from '@immich/ui';
|
||||
import { mdiCalendarEdit } from '@mdi/js';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
import DateInput from '../elements/date-input.svelte';
|
||||
import Combobox, { type ComboBoxOption } from './combobox.svelte';
|
||||
import DurationInput from '../elements/duration-input.svelte';
|
||||
import { Field, Switch } from '@immich/ui';
|
||||
import { getDateTimeOffsetLocaleString } from '$lib/utils/timeline-util.js';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { get } from 'svelte/store';
|
||||
import DateInput from '../elements/date-input.svelte';
|
||||
import DurationInput from '../elements/duration-input.svelte';
|
||||
import Combobox, { type ComboBoxOption } from './combobox.svelte';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
@ -18,6 +17,8 @@
|
||||
timezoneInput?: boolean;
|
||||
withDuration?: boolean;
|
||||
currentInterval?: { start: DateTime; end: DateTime };
|
||||
icon?: string;
|
||||
confirmText?: string;
|
||||
onCancel: () => void;
|
||||
onConfirm: (result: AbsoluteResult | RelativeResult) => void;
|
||||
}
|
||||
@ -29,6 +30,8 @@
|
||||
timezoneInput = true,
|
||||
withDuration = true,
|
||||
currentInterval = undefined,
|
||||
icon = mdiCalendarEdit,
|
||||
confirmText,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: Props = $props();
|
||||
@ -36,6 +39,7 @@
|
||||
export type AbsoluteResult = {
|
||||
mode: 'absolute';
|
||||
date: string;
|
||||
dateTime: DateTime<true>;
|
||||
};
|
||||
|
||||
export type RelativeResult = {
|
||||
@ -192,9 +196,13 @@
|
||||
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 });
|
||||
const finalDateTime = DateTime.fromObject(dtComponents.toObject(), { zone: fixedOffsetZone }) as DateTime<true>;
|
||||
|
||||
onConfirm({ mode: 'absolute', date: finalDateTime.toISO({ includeOffset: true })! });
|
||||
onConfirm({
|
||||
mode: 'absolute',
|
||||
date: finalDateTime.toISO({ includeOffset: true }),
|
||||
dateTime: finalDateTime,
|
||||
});
|
||||
}
|
||||
|
||||
if (showRelative && (selectedDuration || selectedRelativeOption)) {
|
||||
@ -238,7 +246,8 @@
|
||||
<ConfirmModal
|
||||
confirmColor="primary"
|
||||
{title}
|
||||
icon={mdiCalendarEditOutline}
|
||||
{icon}
|
||||
{confirmText}
|
||||
prompt="Please select a new date:"
|
||||
disabled={!date.isValid}
|
||||
onClose={(confirmed) => (confirmed ? handleConfirm() : onCancel())}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
runAssetOperation,
|
||||
} from '$lib/managers/timeline-manager/internal/operations-support.svelte';
|
||||
import {
|
||||
findClosestGroupForDate,
|
||||
findMonthGroupForAsset as findMonthGroupForAssetUtil,
|
||||
findMonthGroupForDate,
|
||||
getAssetWithOffset,
|
||||
@ -523,9 +524,13 @@ export class TimelineManager {
|
||||
}
|
||||
|
||||
async getClosestAssetToDate(dateTime: TimelineDateTime) {
|
||||
const monthGroup = findMonthGroupForDate(this, dateTime);
|
||||
let monthGroup = findMonthGroupForDate(this, dateTime);
|
||||
if (!monthGroup) {
|
||||
return;
|
||||
// if exact match not found, find closest
|
||||
monthGroup = findClosestGroupForDate(this, dateTime);
|
||||
if (!monthGroup) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await this.loadMonthGroup(dateTime, { cancelable: false });
|
||||
const asset = monthGroup.findClosest(dateTime);
|
||||
|
@ -28,6 +28,7 @@
|
||||
{ key: ['D', 'd'], action: $t('previous_or_next_day') },
|
||||
{ key: ['M', 'm'], action: $t('previous_or_next_month') },
|
||||
{ key: ['Y', 'y'], action: $t('previous_or_next_year') },
|
||||
{ key: ['g'], action: $t('navigate_to_time') },
|
||||
{ key: ['x'], action: $t('select') },
|
||||
{ key: ['Esc'], action: $t('back_close_deselect') },
|
||||
{ key: ['Ctrl', 'k'], action: $t('search_your_photos') },
|
||||
|
Loading…
x
Reference in New Issue
Block a user