mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00: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
c229b4d71a
commit
cc6d64e259
9 changed files with 82 additions and 25 deletions
|
|
@ -1339,6 +1339,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_updated": "Network requirements changed, resetting backup queue",
|
"network_requirements_updated": "Network requirements changed, resetting backup queue",
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
"build:stats": "BUILD_STATS=true vite build",
|
"build:stats": "BUILD_STATS=true vite build",
|
||||||
"package": "svelte-kit package",
|
"package": "svelte-kit package",
|
||||||
"preview": "vite preview",
|
"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:typescript": "tsc --noEmit",
|
||||||
"check:watch": "npm run check:svelte -- --watch",
|
"check:watch": "npm run check:svelte -- --watch",
|
||||||
"check:code": "npm run format && npm run lint:p && npm run check:svelte && npm run check:typescript",
|
"check:code": "npm run format && npm run lint:p && npm run check:svelte && npm run check:typescript",
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@
|
||||||
setFocusToAsset as setFocusAssetInit,
|
setFocusToAsset as setFocusAssetInit,
|
||||||
setFocusTo as setFocusToInit,
|
setFocusTo as setFocusToInit,
|
||||||
} from '$lib/components/photos-page/actions/focus-actions';
|
} from '$lib/components/photos-page/actions/focus-actions';
|
||||||
import ChangeDate, { AbsoluteResult, RelativeResult } 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 { AppRoute } from '$lib/constants';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||||
|
|
@ -20,7 +22,10 @@
|
||||||
import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions';
|
import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions';
|
||||||
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
||||||
import { AssetVisibility } from '@immich/sdk';
|
import { AssetVisibility } from '@immich/sdk';
|
||||||
|
import { modalManager } from '@immich/ui';
|
||||||
|
import { mdiCalendarBlankOutline } from '@mdi/js';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
import DeleteAssetDialog from './delete-asset-dialog.svelte';
|
import DeleteAssetDialog from './delete-asset-dialog.svelte';
|
||||||
|
|
||||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||||
|
|
@ -197,15 +202,16 @@
|
||||||
|
|
||||||
{#if isShowSelectDate}
|
{#if isShowSelectDate}
|
||||||
<ChangeDate
|
<ChangeDate
|
||||||
title="Navigate to Time"
|
withDuration={false}
|
||||||
|
icon={mdiCalendarBlankOutline}
|
||||||
|
confirmText={$t('navigate')}
|
||||||
|
title={$t('navigate_to_time')}
|
||||||
initialDate={DateTime.now()}
|
initialDate={DateTime.now()}
|
||||||
timezoneInput={false}
|
timezoneInput={false}
|
||||||
onConfirm={async (result: AbsoluteResult | RelativeResult) => {
|
onConfirm={async (result: AbsoluteResult | RelativeResult) => {
|
||||||
isShowSelectDate = false;
|
isShowSelectDate = false;
|
||||||
if (result.mode === 'absolute') {
|
if (result.mode === 'absolute') {
|
||||||
const asset = await timelineManager.getClosestAssetToDate(
|
const asset = await timelineManager.getClosestAssetToDate(result.dateTime.toObject());
|
||||||
(DateTime.fromISO(result.date) as DateTime<true>).toObject(),
|
|
||||||
);
|
|
||||||
if (asset) {
|
if (asset) {
|
||||||
setFocusAsset(asset);
|
setFocusAsset(asset);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import { type ScrubberListener } from '$lib/utils/timeline-util';
|
import { getTimes, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||||
import { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
import { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
|
|
@ -82,7 +82,7 @@
|
||||||
onThumbnailClick,
|
onThumbnailClick,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let { isViewing: showAssetViewer, gridScrollTarget, mutex, viewingAsset } = assetViewingStore;
|
let { isViewing: showAssetViewer, gridScrollTarget, asset: viewingAsset } = assetViewingStore;
|
||||||
|
|
||||||
let element: HTMLElement | undefined = $state();
|
let element: HTMLElement | undefined = $state();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
|
@ -43,7 +46,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 () => {
|
||||||
|
|
@ -58,7 +65,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 });
|
||||||
|
|
||||||
|
|
@ -72,7 +81,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,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ConfirmModal } from '@immich/ui';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { mdiCalendarEditOutline } from '@mdi/js';
|
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 { DateTime, Duration } from 'luxon';
|
||||||
import { t } from 'svelte-i18n';
|
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 { 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 {
|
interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
@ -18,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;
|
||||||
}
|
}
|
||||||
|
|
@ -29,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();
|
||||||
|
|
@ -36,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 = {
|
||||||
|
|
@ -192,9 +196,13 @@
|
||||||
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 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)) {
|
if (showRelative && (selectedDuration || selectedRelativeOption)) {
|
||||||
|
|
@ -238,7 +246,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())}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
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,10 +524,14 @@ export class TimelineManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getClosestAssetToDate(dateTime: TimelineDateTime) {
|
async getClosestAssetToDate(dateTime: TimelineDateTime) {
|
||||||
const monthGroup = findMonthGroupForDate(this, dateTime);
|
let monthGroup = findMonthGroupForDate(this, dateTime);
|
||||||
|
if (!monthGroup) {
|
||||||
|
// if exact match not found, find closest
|
||||||
|
monthGroup = findClosestGroupForDate(this, dateTime);
|
||||||
if (!monthGroup) {
|
if (!monthGroup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
await this.loadMonthGroup(dateTime, { cancelable: false });
|
await this.loadMonthGroup(dateTime, { cancelable: false });
|
||||||
const asset = monthGroup.findClosest(dateTime);
|
const asset = monthGroup.findClosest(dateTime);
|
||||||
if (asset) {
|
if (asset) {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,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') },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue