Compare commits

...

1 Commits

Author SHA1 Message Date
Alex Tran
2c82e4ce8b fix: edit date time doesn't update asset position on the timeline 2025-12-21 03:40:15 +00:00
11 changed files with 81 additions and 35 deletions

View File

@@ -342,7 +342,7 @@
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem /> <DownloadAction menuItem />
<ChangeDate menuItem /> <ChangeDate menuItem onDateChange={() => init(page)} />
<ChangeDescription menuItem /> <ChangeDescription menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={handleDeleteOrArchiveAssets} /> <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={handleDeleteOrArchiveAssets} />

View File

@@ -1,27 +1,44 @@
<script lang="ts"> <script lang="ts">
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte'; import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import type { DateChangeResult } from '$lib/modals/AssetSelectionChangeDateModal.svelte';
import AssetSelectionChangeDateModal from '$lib/modals/AssetSelectionChangeDateModal.svelte'; import AssetSelectionChangeDateModal from '$lib/modals/AssetSelectionChangeDateModal.svelte';
import type { TimelineDateTime } from '$lib/utils/timeline-util';
import { modalManager } from '@immich/ui'; import { modalManager } from '@immich/ui';
import { mdiCalendarEditOutline } from '@mdi/js'; import { mdiCalendarEditOutline } from '@mdi/js';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { interface Props {
menuItem?: boolean; menuItem?: boolean;
onDateChange?: (ids: string[], updateAsset: (asset: TimelineAsset) => void) => void;
} }
let { menuItem = false }: Props = $props(); let { menuItem = false, onDateChange }: Props = $props();
const { clearSelect, getOwnedAssets } = getAssetControlContext(); const { clearSelect, getOwnedAssets } = getAssetControlContext();
const handleChangeDate = async () => { const handleChangeDate = async () => {
const success = await modalManager.show(AssetSelectionChangeDateModal, { const result = await modalManager.show(AssetSelectionChangeDateModal, {
initialDate: DateTime.now(), initialDate: DateTime.now(),
assets: getOwnedAssets(), assets: getOwnedAssets(),
}); });
if (success) { if (result) {
const ids = result.assets.map((a) => a.id);
onDateChange?.(ids, (asset) => {
const updatedDateTime = getUpdatedDateTime(asset, result);
asset.localDateTime = updatedDateTime.toObject() as TimelineDateTime;
});
clearSelect(); clearSelect();
} }
}; };
function getUpdatedDateTime(asset: TimelineAsset, result: DateChangeResult): DateTime {
const { localDateTime } = asset;
const currentDateTime = DateTime.fromObject(localDateTime, { zone: result.timeZone });
return result.type === 'relative' ? currentDateTime.plus({ minutes: result.offsetMinutes }) : result.newDateTime;
}
</script> </script>
{#if menuItem} {#if menuItem}

View File

@@ -1,8 +1,26 @@
<script lang="ts" module>
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import type { DateTime as DateTimeType } from 'luxon';
export type DateChangeResult =
| {
type: 'relative';
offsetMinutes: number;
timeZone: string;
assets: TimelineAsset[];
}
| {
type: 'absolute';
newDateTime: DateTimeType;
timeZone: string;
assets: TimelineAsset[];
};
</script>
<script lang="ts"> <script lang="ts">
import Combobox from '$lib/components/shared-components/combobox.svelte'; import Combobox from '$lib/components/shared-components/combobox.svelte';
import DateInput from '$lib/elements/DateInput.svelte'; import DateInput from '$lib/elements/DateInput.svelte';
import DurationInput from '$lib/elements/DurationInput.svelte'; import DurationInput from '$lib/elements/DurationInput.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { 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 { user } from '$lib/stores/user.store';
import { getOwnedAssetsWithWarning } from '$lib/utils/asset-utils'; import { getOwnedAssetsWithWarning } from '$lib/utils/asset-utils';
@@ -17,7 +35,7 @@
initialDate?: DateTime; initialDate?: DateTime;
initialTimeZone?: string; initialTimeZone?: string;
assets: TimelineAsset[]; assets: TimelineAsset[];
onClose: (success: boolean) => void; onClose: (result: DateChangeResult | undefined) => void;
} }
let { initialDate = DateTime.now(), initialTimeZone, assets, onClose }: Props = $props(); let { initialDate = DateTime.now(), initialTimeZone, assets, onClose }: Props = $props();
@@ -32,24 +50,35 @@
const handleConfirm = async () => { const handleConfirm = async () => {
const ids = getOwnedAssetsWithWarning(assets, $user); const ids = getOwnedAssetsWithWarning(assets, $user);
const timeZone = selectedOption?.value ?? 'UTC';
try { try {
if (showRelative && (selectedDuration || selectedOption)) { if (showRelative && (selectedDuration || selectedOption)) {
await updateAssets({ await updateAssets({
assetBulkUpdateDto: { assetBulkUpdateDto: {
ids, ids,
dateTimeRelative: selectedDuration, dateTimeRelative: selectedDuration,
timeZone: selectedOption?.value, timeZone,
}, },
}); });
onClose(true); onClose({
type: 'relative',
offsetMinutes: selectedDuration,
timeZone,
assets,
});
return; return;
} }
const isoDate = toIsoDate(selectedDate, selectedOption); const isoDate = toIsoDate(selectedDate, selectedOption);
await updateAssets({ assetBulkUpdateDto: { ids, dateTimeOriginal: isoDate } }); await updateAssets({ assetBulkUpdateDto: { ids, dateTimeOriginal: isoDate } });
onClose(true); onClose({
type: 'absolute',
newDateTime: date,
timeZone,
assets,
});
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_change_date')); handleError(error, $t('errors.unable_to_change_date'));
onClose(false); onClose(undefined);
} }
}; };
@@ -63,7 +92,7 @@
const date = $derived(DateTime.fromISO(selectedDate, { zone: selectedOption?.value, setZone: true })); const date = $derived(DateTime.fromISO(selectedDate, { zone: selectedOption?.value, setZone: true }));
</script> </script>
<Modal title={$t('edit_date_and_time')} icon={mdiCalendarEdit} onClose={() => onClose(false)} size="small"> <Modal title={$t('edit_date_and_time')} icon={mdiCalendarEdit} onClose={() => onClose(undefined)} size="small">
<ModalBody> <ModalBody>
<Field label={$t('edit_date_and_time_by_offset')}> <Field label={$t('edit_date_and_time_by_offset')}>
<Switch data-testid="edit-by-offset-switch" bind:checked={showRelative} class="mb-2" /> <Switch data-testid="edit-by-offset-switch" bind:checked={showRelative} class="mb-2" />
@@ -117,7 +146,7 @@
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<HStack fullWidth> <HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose(false)}> <Button shape="round" color="secondary" fullWidth onclick={() => onClose(undefined)}>
{$t('cancel')} {$t('cancel')}
</Button> </Button>
<Button shape="round" color="primary" fullWidth onclick={handleConfirm} disabled={!date.isValid}> <Button shape="round" color="primary" fullWidth onclick={handleConfirm} disabled={!date.isValid}>

View File

@@ -563,7 +563,7 @@
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')} offset={{ x: 175, y: 25 }}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')} offset={{ x: 175, y: 25 }}>
<DownloadAction menuItem filename="{album.albumName}.zip" /> <DownloadAction menuItem filename="{album.albumName}.zip" />
{#if assetInteraction.isAllUserOwned} {#if assetInteraction.isAllUserOwned}
<ChangeDate menuItem /> <ChangeDate menuItem onDateChange={(ids, updateFn) => timelineManager.update(ids, updateFn)} />
<ChangeDescription menuItem /> <ChangeDescription menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
{#if assetInteraction.selectedAssets.length === 1} {#if assetInteraction.selectedAssets.length === 1}

View File

@@ -79,7 +79,7 @@
</ButtonContextMenu> </ButtonContextMenu>
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem /> <DownloadAction menuItem />
<ChangeDate menuItem /> <ChangeDate menuItem onDateChange={(ids, updateFn) => timelineManager.update(ids, updateFn)} />
<ChangeDescription menuItem /> <ChangeDescription menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
<ArchiveAction <ArchiveAction

View File

@@ -151,7 +151,7 @@
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem /> <DownloadAction menuItem />
<ChangeDate menuItem /> <ChangeDate menuItem onDateChange={() => triggerAssetUpdate()} />
<ChangeDescription menuItem /> <ChangeDescription menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={triggerAssetUpdate} /> <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={triggerAssetUpdate} />

View File

@@ -80,7 +80,7 @@
<SetVisibilityAction unlock onVisibilitySet={handleMoveOffLockedFolder} /> <SetVisibilityAction unlock onVisibilitySet={handleMoveOffLockedFolder} />
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem /> <DownloadAction menuItem />
<ChangeDate menuItem /> <ChangeDate menuItem onDateChange={(ids, updateFn) => timelineManager.update(ids, updateFn)} />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
<DeleteAssets menuItem force onAssetDelete={(assetIds) => timelineManager.removeAssets(assetIds)} /> <DeleteAssets menuItem force onAssetDelete={(assetIds) => timelineManager.removeAssets(assetIds)} />
</ButtonContextMenu> </ButtonContextMenu>

View File

@@ -501,7 +501,7 @@
text={$t('fix_incorrect_match')} text={$t('fix_incorrect_match')}
onClick={handleReassignAssets} onClick={handleReassignAssets}
/> />
<ChangeDate menuItem /> <ChangeDate menuItem onDateChange={(ids, updateFn) => timelineManager.update(ids, updateFn)} />
<ChangeDescription menuItem /> <ChangeDescription menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
<ArchiveAction <ArchiveAction

View File

@@ -139,7 +139,7 @@
onUnlink={handleUnlink} onUnlink={handleUnlink}
/> />
{/if} {/if}
<ChangeDate menuItem /> <ChangeDate menuItem onDateChange={(ids, updateFn) => timelineManager.update(ids, updateFn)} />
<ChangeDescription menuItem /> <ChangeDescription menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
<ArchiveAction <ArchiveAction

View File

@@ -291,7 +291,7 @@
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem /> <DownloadAction menuItem />
<ChangeDate menuItem /> <ChangeDate menuItem onDateChange={() => onSearchQueryUpdate()} />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} /> <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
@@ -432,7 +432,7 @@
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem /> <DownloadAction menuItem />
<ChangeDate menuItem /> <ChangeDate menuItem onDateChange={() => onSearchQueryUpdate()} />
<ChangeDescription menuItem /> <ChangeDescription menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} /> <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />

View File

@@ -1,24 +1,13 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte'; import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte'; import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
import Sidebar from '$lib/components/sidebar/sidebar.svelte'; import Sidebar from '$lib/components/sidebar/sidebar.svelte';
import Timeline from '$lib/components/timeline/Timeline.svelte';
import { AppRoute, AssetAction, QueryParameter } from '$lib/constants';
import SkipLink from '$lib/elements/SkipLink.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import TagCreateModal from '$lib/modals/TagCreateModal.svelte';
import TagEditModal from '$lib/modals/TagEditModal.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { joinPaths, TreeNode } from '$lib/utils/tree-utils';
import { deleteTag, getAllTags, type TagResponseDto } from '@immich/sdk';
import { Button, HStack, modalManager, Text } from '@immich/ui';
import { mdiDotsVertical, mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
import Timeline from '$lib/components/timeline/Timeline.svelte';
import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte'; import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte'; import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte'; import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
@@ -31,8 +20,19 @@
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte'; import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte'; import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
import TagAction from '$lib/components/timeline/actions/TagAction.svelte'; import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import { AppRoute, AssetAction, QueryParameter } from '$lib/constants';
import SkipLink from '$lib/elements/SkipLink.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import TagCreateModal from '$lib/modals/TagCreateModal.svelte';
import TagEditModal from '$lib/modals/TagEditModal.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { preferences, user } from '$lib/stores/user.store'; import { preferences, user } from '$lib/stores/user.store';
import { joinPaths, TreeNode } from '$lib/utils/tree-utils';
import { deleteTag, getAllTags, type TagResponseDto } from '@immich/sdk';
import { Button, HStack, modalManager, Text } from '@immich/ui';
import { mdiDotsVertical, mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
interface Props { interface Props {
data: PageData; data: PageData;
@@ -172,7 +172,7 @@
></FavoriteAction> ></FavoriteAction>
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem /> <DownloadAction menuItem />
<ChangeDate menuItem /> <ChangeDate menuItem onDateChange={(ids, updateFn) => timelineManager.update(ids, updateFn)} />
<ChangeDescription menuItem /> <ChangeDescription menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
<ArchiveAction <ArchiveAction