Review comments

This commit is contained in:
midzelis 2025-10-14 12:26:16 +00:00
parent a72f2d8e0c
commit c23ca56adb
10 changed files with 80 additions and 112 deletions

View file

@ -1,12 +1,9 @@
<script lang="ts">
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
import AssetSelectionChangeDateModal from '$lib/modals/AssetSelectionChangeDateModal.svelte';
import { user } from '$lib/stores/user.store';
import { getSelectedAssets } from '$lib/utils/asset-utils';
import { fromTimelinePlainDateTime } from '$lib/utils/timeline-util.js';
import { modalManager } from '@immich/ui';
import { mdiCalendarEditOutline } from '@mdi/js';
import { DateTime, Duration } from 'luxon';
import { DateTime } from 'luxon';
import { t } from 'svelte-i18n';
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
interface Props {
@ -16,37 +13,15 @@
let { menuItem = false }: Props = $props();
const { clearSelect, getOwnedAssets } = getAssetControlContext();
const getCurrentInterval = () => {
const ids = getSelectedAssets(getOwnedAssets(), $user);
const assets = getOwnedAssets().filter((asset) => ids.includes(asset.id));
const imageTimestamps = assets.map((asset) => {
let localDateTime = fromTimelinePlainDateTime(asset.localDateTime);
let fileCreatedAt = fromTimelinePlainDateTime(asset.fileCreatedAt);
let offsetMinutes = localDateTime.diff(fileCreatedAt, 'minutes').shiftTo('minutes').minutes;
const timeZone = `UTC${offsetMinutes >= 0 ? '+' : ''}${Duration.fromObject({ minutes: offsetMinutes }).toFormat('hh:mm')}`;
return fileCreatedAt.setZone('utc', { keepLocalTime: true }).setZone(timeZone);
});
let minTimestamp = imageTimestamps[0];
let maxTimestamp = imageTimestamps[0];
for (let current of imageTimestamps) {
if (current < minTimestamp) {
minTimestamp = current;
}
if (current > maxTimestamp) {
maxTimestamp = current;
}
}
return { start: minTimestamp, end: maxTimestamp };
};
const showChangeDate = async () =>
await modalManager.show(AssetSelectionChangeDateModal, {
const showChangeDate = async () => {
const success = await modalManager.show(AssetSelectionChangeDateModal, {
initialDate: DateTime.now(),
assets: getOwnedAssets(),
currentInterval: getCurrentInterval(),
clearSelect,
});
if (success) {
clearSelect();
}
};
</script>
{#if menuItem}

View file

@ -2,7 +2,7 @@
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
import AssetUpdateDescriptionConfirmModal from '$lib/modals/AssetUpdateDescriptionConfirmModal.svelte';
import { user } from '$lib/stores/user.store';
import { getSelectedAssets } from '$lib/utils/asset-utils';
import { getOwnedAssetsWithWarning } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error';
import { updateAssets } from '@immich/sdk';
import { modalManager } from '@immich/ui';
@ -20,7 +20,7 @@
const handleUpdateDescription = async () => {
const description = await modalManager.show(AssetUpdateDescriptionConfirmModal);
if (description) {
const ids = getSelectedAssets(getOwnedAssets(), $user);
const ids = getOwnedAssetsWithWarning(getOwnedAssets(), $user);
try {
await updateAssets({ assetBulkUpdateDto: { ids, description } });

View file

@ -2,7 +2,7 @@
import ChangeLocation from '$lib/components/shared-components/change-location.svelte';
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
import { user } from '$lib/stores/user.store';
import { getSelectedAssets } from '$lib/utils/asset-utils';
import { getOwnedAssetsWithWarning } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error';
import { updateAssets } from '@immich/sdk';
import { mdiMapMarkerMultipleOutline } from '@mdi/js';
@ -25,7 +25,7 @@
return;
}
const ids = getSelectedAssets(getOwnedAssets(), $user);
const ids = getOwnedAssetsWithWarning(getOwnedAssets(), $user);
try {
await updateAssets({ assetBulkUpdateDto: { ids, latitude: point.lat, longitude: point.lng } });

View file

@ -10,7 +10,6 @@
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import NavigateToDateModal from '$lib/modals/NavigateToDateModal.svelte';
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
@ -22,7 +21,6 @@
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
import { AssetVisibility } from '@immich/sdk';
import { modalManager } from '@immich/ui';
let { isViewing: showAssetViewer } = assetViewingStore;
interface Props {
timelineManager: TimelineManager;
@ -40,6 +38,8 @@
scrollToAsset,
}: Props = $props();
const { isViewing: showAssetViewer } = assetViewingStore;
const trashOrDelete = async (force: boolean = false) => {
isShowDeleteConfirmation = false;
await deleteAssets(
@ -145,11 +145,14 @@
const setFocusTo = setFocusToInit.bind(undefined, scrollToAsset, timelineManager);
const setFocusAsset = setFocusAssetInit.bind(undefined, scrollToAsset);
const handleOpenDateModal = async () =>
await modalManager.show(NavigateToDateModal, {
const handleOpenDateModal = async () => {
const asset = await modalManager.show(NavigateToDateModal, {
timelineManager,
onFocusOnAsset: setFocusAsset,
});
if (asset) {
setFocusAsset(asset);
}
};
let shortcutList = $derived(
(() => {

View file

@ -1,5 +1,7 @@
<script lang="ts">
interface Props {
import type { HTMLInputAttributes } from 'svelte/elements';
interface Props extends HTMLInputAttributes {
type: 'date' | 'datetime-local';
value?: string;
min?: string;

View file

@ -40,8 +40,8 @@
onClose(true);
} catch (error) {
handleError(error, $t('errors.unable_to_change_date'));
}
onClose(false);
}
};
// when changing the time zone, assume the configured date/time is meant for that time zone (instead of updating it)

View file

@ -12,10 +12,6 @@ describe('DateSelectionModal component', () => {
const initialDate = DateTime.fromISO('2024-01-01');
const initialTimeZone = 'Europe/Berlin';
const currentInterval = {
start: DateTime.fromISO('2000-02-01T14:00:00+01:00'),
end: DateTime.fromISO('2001-02-01T14:00:00+01:00'),
};
const onClose = vi.fn();
const getRelativeInputToggle = () => screen.getByTestId('edit-by-offset-switch');
@ -43,7 +39,7 @@ describe('DateSelectionModal component', () => {
initialDate,
initialTimeZone,
assets: [],
clearSelect: vitest.fn(),
onClose,
});
expect(getDateInput().value).toBe('2024-01-01T00:00');
@ -52,7 +48,7 @@ describe('DateSelectionModal component', () => {
test('calls onConfirm with correct date on confirm', async () => {
render(AssetSelectionChangeDateModal, {
props: { initialDate, initialTimeZone, assets: [], clearSelect: vitest.fn(), onClose },
props: { initialDate, initialTimeZone, assets: [], onClose },
});
await fireEvent.click(getConfirmButton());
@ -67,7 +63,7 @@ describe('DateSelectionModal component', () => {
test('calls onCancel on cancel', async () => {
render(AssetSelectionChangeDateModal, {
props: { initialDate, initialTimeZone, assets: [], clearSelect: vitest.fn(), onClose },
props: { initialDate, initialTimeZone, assets: [], onClose },
});
await fireEvent.click(getCancelButton());
@ -83,7 +79,6 @@ describe('DateSelectionModal component', () => {
initialDate: dstDate,
initialTimeZone,
assets: [],
clearSelect: vitest.fn(),
onClose,
});
@ -92,7 +87,7 @@ describe('DateSelectionModal component', () => {
test('calls onConfirm with correct date on confirm', async () => {
render(AssetSelectionChangeDateModal, {
props: { initialDate: dstDate, initialTimeZone, assets: [], clearSelect: vitest.fn(), onClose },
props: { initialDate: dstDate, initialTimeZone, assets: [], onClose },
});
await fireEvent.click(getConfirmButton());
@ -108,7 +103,7 @@ describe('DateSelectionModal component', () => {
test('calls onConfirm with correct offset in relative mode', async () => {
render(AssetSelectionChangeDateModal, {
props: { initialDate, initialTimeZone, currentInterval, assets: [], clearSelect: vitest.fn(), onClose },
props: { initialDate, initialTimeZone, assets: [], onClose },
});
await fireEvent.click(getRelativeInputToggle());
@ -139,7 +134,7 @@ describe('DateSelectionModal component', () => {
test('calls onConfirm with correct timeZone in relative mode', async () => {
const user = userEvent.setup();
render(AssetSelectionChangeDateModal, {
props: { initialDate, initialTimeZone, currentInterval, assets: [], clearSelect: vitest.fn(), onClose },
props: { initialDate, initialTimeZone, assets: [], onClose },
});
await user.click(getRelativeInputToggle());

View file

@ -3,38 +3,23 @@
import DateInput from '$lib/elements/DateInput.svelte';
import DurationInput from '$lib/elements/DurationInput.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import {
calcNewDate,
getPreferredTimeZone,
getTimezones,
toIsoDate,
type ZoneOption,
} from '$lib/modals/timezone-utils';
import { getPreferredTimeZone, getTimezones, toIsoDate, type ZoneOption } from '$lib/modals/timezone-utils';
import { user } from '$lib/stores/user.store';
import { getSelectedAssets } from '$lib/utils/asset-utils';
import { getOwnedAssetsWithWarning } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error';
import { updateAssets } from '@immich/sdk';
import { Button, Field, HStack, Modal, ModalBody, ModalFooter, Switch, VStack } from '@immich/ui';
import { mdiCalendarEdit } from '@mdi/js';
import { DateTime } from 'luxon';
import { t } from 'svelte-i18n';
interface Props {
initialDate?: DateTime;
initialTimeZone?: string;
currentInterval?: { start: DateTime; end: DateTime };
assets: TimelineAsset[];
clearSelect: () => void;
onClose: (success: boolean) => void;
}
let {
initialDate = DateTime.now(),
initialTimeZone,
currentInterval,
assets,
clearSelect,
onClose,
}: Props = $props();
let { initialDate = DateTime.now(), initialTimeZone, assets, onClose }: Props = $props();
let showRelative = $state(false);
let selectedDuration = $state(0);
@ -46,7 +31,7 @@
let selectedOption = $derived(getPreferredTimeZone(initialDate, initialTimeZone, timezones, lastSelectedTimezone));
const handleConfirm = async () => {
const ids = getSelectedAssets(assets, $user);
const ids = getOwnedAssetsWithWarning(assets, $user);
try {
if (showRelative && (selectedDuration || selectedOption)) {
await updateAssets({
@ -56,33 +41,33 @@
timeZone: selectedOption?.value,
},
});
clearSelect();
onClose(true);
return;
}
const isoDate = toIsoDate(selectedDate, selectedOption);
await updateAssets({ assetBulkUpdateDto: { ids, dateTimeOriginal: isoDate } });
clearSelect();
onClose(true);
} catch (error) {
handleError(error, $t('errors.unable_to_change_date'));
onClose(false);
}
};
let intervalFrom = $derived(
currentInterval ? calcNewDate(currentInterval.start, selectedDuration, selectedOption?.value) : undefined,
);
let intervalTo = $derived(
currentInterval ? calcNewDate(currentInterval.end, selectedDuration, selectedOption?.value) : undefined,
);
// let before = $derived(DateTime.fromObject(assets[0].localDateTime).toFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"));
// let after = $derived(
// currentInterval ? calcNewDate(currentInterval.end, selectedDuration, selectedOption?.value) : undefined,
// );
// when changing the time zone, assume the configured date/time is meant for that time zone (instead of updating it)
const date = $derived(DateTime.fromISO(selectedDate, { zone: selectedOption?.value, setZone: true }));
</script>
<Modal title={$t('edit_date_and_time')} icon={mdiCalendarEdit} onClose={() => onClose(false)} size="large">
<Modal title={$t('edit_date_and_time')} icon={mdiCalendarEdit} onClose={() => onClose(false)} size="small">
<ModalBody>
<VStack fullWidth>
<HStack fullWidth>
<Field color="muted" label={$t('edit_date_and_time_by_offset')}>
<Field color="muted" label="Adjust selection by fixed duration (time zone adjust)">
<Switch data-testid="edit-by-offset-switch" bind:checked={showRelative} />
</Field>
</HStack>
@ -110,17 +95,32 @@
onSelect={(option) => (lastSelectedTimezone = option as ZoneOption)}
></Combobox>
</div>
<VStack fullWidth class={!showRelative || !currentInterval ? 'invisible' : ''}>
<HStack fullWidth>
<div class="immich-form-label" data-testid="interval-preview">{$t('new_date_range')}</div>
</HStack>
<HStack fullWidth>
<label class="immich-form-label" for="from">From</label>
<DateInput class="immich-form-input" id="from" type="datetime-local" bind:value={intervalFrom} />
<label class="immich-form-label" for="from">To</label>
<DateInput class="immich-form-input" id="to" type="datetime-local" bind:value={intervalTo} />
</HStack>
</VStack>
<!-- <Card color="secondary" class={!showRelative || !currentInterval ? 'invisible' : ''}>
<CardBody class="p-2">
<div class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-3 items-center">
<div class="col-span-2 immich-form-label" data-testid="interval-preview">Preview</div>
<Text size="small" class="-mt-2 immich-form-label col-span-2"
>Showing changes for first selected asset only</Text
>
<label class="immich-form-label" for="from">Before</label>
<DateInput
class="dark:text-gray-300 text-gray-700 text-base"
id="from"
type="datetime-local"
readonly
bind:value={before}
/>
<label class="immich-form-label" for="to">After</label>
<DateInput
class="dark:text-gray-300 text-gray-700 text-base"
id="to"
type="datetime-local"
readonly
bind:value={after}
/>
</div>
</CardBody>
</Card> -->
</VStack>
</ModalBody>
<ModalFooter>

View file

@ -9,11 +9,10 @@
import { t } from 'svelte-i18n';
interface Props {
timelineManager: TimelineManager;
onFocusOnAsset: (asset: TimelineAsset) => void;
onClose: (success: boolean) => void;
onClose: (asset?: TimelineAsset) => void;
}
let { timelineManager, onFocusOnAsset, onClose }: Props = $props();
let { timelineManager, onClose }: Props = $props();
const initialDate = DateTime.now();
let selectedDate = $state(initialDate.toFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"));
@ -23,27 +22,21 @@
const handleClose = async () => {
if (!date.isValid || !selectedOption) {
onClose(false);
onClose();
return;
}
// Get the local date/time components from the selected string using neutral timezone
const dateTime = toDatetime(selectedDate, selectedOption);
const asset = await timelineManager.getClosestAssetToDate(dateTime);
if (asset) {
onFocusOnAsset(asset);
onClose(true);
return;
}
onClose(false);
const dateTime = toDatetime(selectedDate, selectedOption) as DateTime<true>;
const asset = await timelineManager.getClosestAssetToDate(dateTime.toObject());
onClose(asset);
};
// when changing the time zone, assume the configured date/time is meant for that time zone (instead of updating it)
const date = $derived(DateTime.fromISO(selectedDate, { zone: selectedOption?.value, setZone: true }));
</script>
<Modal title={$t('navigate_to_time')} icon={mdiNavigationVariantOutline} onClose={() => onClose(false)}>
<Modal title={$t('navigate_to_time')} icon={mdiNavigationVariantOutline} onClose={() => onClose()}>
<ModalBody>
<VStack fullWidth>
<HStack fullWidth>
@ -61,7 +54,7 @@
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose(false)}>{$t('cancel')}</Button>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth onclick={handleClose}>{$t('confirm')}</Button>
</HStack>
</ModalFooter>

View file

@ -405,7 +405,7 @@ export const getAssetType = (type: AssetTypeEnum) => {
}
};
export const getSelectedAssets = (assets: TimelineAsset[], user: UserResponseDto | null): string[] => {
export const getOwnedAssetsWithWarning = (assets: TimelineAsset[], user: UserResponseDto | null): string[] => {
const ids = [...assets].filter((a) => user && a.ownerId === user.id).map((a) => a.id);
const numberOfIssues = [...assets].filter((a) => user && a.ownerId !== user.id).length;