mirror of
https://github.com/immich-app/immich.git
synced 2025-12-21 17:25:35 +03:00
Compare commits
2 Commits
v2.4.1
...
fix-update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c82e4ce8b | ||
|
|
3d2196b0f2 |
@@ -0,0 +1,90 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { newMediumService } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
const { ctx } = newMediumService(BaseService, {
|
||||
database: db || defaultDatabase,
|
||||
real: [],
|
||||
mock: [LoggingRepository],
|
||||
});
|
||||
return { ctx, sut: ctx.get(AssetRepository) };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(AssetRepository.name, () => {
|
||||
describe('upsertExif', () => {
|
||||
it('should append to locked columns', async () => {
|
||||
const { ctx, sut } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({
|
||||
assetId: asset.id,
|
||||
dateTimeOriginal: '2023-11-19T18:11:00',
|
||||
lockedProperties: ['dateTimeOriginal'],
|
||||
});
|
||||
|
||||
await expect(
|
||||
ctx.database
|
||||
.selectFrom('asset_exif')
|
||||
.select('lockedProperties')
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({ lockedProperties: ['dateTimeOriginal'] });
|
||||
|
||||
await sut.upsertExif(
|
||||
{ assetId: asset.id, lockedProperties: ['description'] },
|
||||
{ lockedPropertiesBehavior: 'append' },
|
||||
);
|
||||
|
||||
await expect(
|
||||
ctx.database
|
||||
.selectFrom('asset_exif')
|
||||
.select('lockedProperties')
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({ lockedProperties: ['description', 'dateTimeOriginal'] });
|
||||
});
|
||||
|
||||
it('should deduplicate locked columns', async () => {
|
||||
const { ctx, sut } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({
|
||||
assetId: asset.id,
|
||||
dateTimeOriginal: '2023-11-19T18:11:00',
|
||||
lockedProperties: ['dateTimeOriginal', 'description'],
|
||||
});
|
||||
|
||||
await expect(
|
||||
ctx.database
|
||||
.selectFrom('asset_exif')
|
||||
.select('lockedProperties')
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({ lockedProperties: ['dateTimeOriginal', 'description'] });
|
||||
|
||||
await sut.upsertExif(
|
||||
{ assetId: asset.id, lockedProperties: ['description'] },
|
||||
{ lockedPropertiesBehavior: 'append' },
|
||||
);
|
||||
|
||||
await expect(
|
||||
ctx.database
|
||||
.selectFrom('asset_exif')
|
||||
.select('lockedProperties')
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({ lockedProperties: ['description', 'dateTimeOriginal'] });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -270,13 +270,13 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update dateTimeOriginal', async () => {
|
||||
it('should automatically lock lockable columns', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, description: 'test' });
|
||||
await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00' });
|
||||
|
||||
await expect(
|
||||
ctx.database
|
||||
@@ -285,7 +285,14 @@ describe(AssetService.name, () => {
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({ lockedProperties: null });
|
||||
await sut.update(auth, asset.id, { dateTimeOriginal: '2023-11-19T18:11:00' });
|
||||
|
||||
await sut.update(auth, asset.id, {
|
||||
latitude: 42,
|
||||
longitude: 42,
|
||||
rating: 3,
|
||||
description: 'foo',
|
||||
dateTimeOriginal: '2023-11-19T18:11:00+01:00',
|
||||
});
|
||||
|
||||
await expect(
|
||||
ctx.database
|
||||
@@ -293,7 +300,21 @@ describe(AssetService.name, () => {
|
||||
.select('lockedProperties')
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({ lockedProperties: ['dateTimeOriginal'] });
|
||||
).resolves.toEqual({
|
||||
lockedProperties: ['timeZone', 'rating', 'description', 'latitude', 'longitude', 'dateTimeOriginal'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should update dateTimeOriginal', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, description: 'test' });
|
||||
|
||||
await sut.update(auth, asset.id, { dateTimeOriginal: '2023-11-19T18:11:00' });
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-19T18:11:00+00:00', timeZone: null }),
|
||||
@@ -309,22 +330,8 @@ describe(AssetService.name, () => {
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, description: 'test' });
|
||||
|
||||
await expect(
|
||||
ctx.database
|
||||
.selectFrom('asset_exif')
|
||||
.select('lockedProperties')
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({ lockedProperties: null });
|
||||
await sut.update(auth, asset.id, { dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
|
||||
|
||||
await expect(
|
||||
ctx.database
|
||||
.selectFrom('asset_exif')
|
||||
.select('lockedProperties')
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({ lockedProperties: ['timeZone', 'dateTimeOriginal'] });
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00+00:00', timeZone: 'UTC-7' }),
|
||||
@@ -334,6 +341,42 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
|
||||
describe('updateAll', () => {
|
||||
it('should automatically lock lockable columns', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00' });
|
||||
|
||||
await expect(
|
||||
ctx.database
|
||||
.selectFrom('asset_exif')
|
||||
.select('lockedProperties')
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({ lockedProperties: null });
|
||||
|
||||
await sut.updateAll(auth, {
|
||||
ids: [asset.id],
|
||||
latitude: 42,
|
||||
description: 'foo',
|
||||
longitude: 42,
|
||||
rating: 3,
|
||||
dateTimeOriginal: '2023-11-19T18:11:00+01:00',
|
||||
});
|
||||
|
||||
await expect(
|
||||
ctx.database
|
||||
.selectFrom('asset_exif')
|
||||
.select('lockedProperties')
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({
|
||||
lockedProperties: ['timeZone', 'rating', 'description', 'latitude', 'longitude', 'dateTimeOriginal'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should relatively update assets', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
|
||||
@@ -344,13 +387,6 @@ describe(AssetService.name, () => {
|
||||
|
||||
await sut.updateAll(auth, { ids: [asset.id], dateTimeRelative: -11 });
|
||||
|
||||
await expect(
|
||||
ctx.database
|
||||
.selectFrom('asset_exif')
|
||||
.select('lockedProperties')
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({ lockedProperties: ['timeZone', 'dateTimeOriginal'] });
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
exifInfo: expect.objectContaining({
|
||||
@@ -359,66 +395,39 @@ describe(AssetService.name, () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update dateTimeOriginal', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, description: 'test' });
|
||||
it('should update dateTimeOriginal', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, description: 'test' });
|
||||
|
||||
await expect(
|
||||
ctx.database
|
||||
.selectFrom('asset_exif')
|
||||
.select('lockedProperties')
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({ lockedProperties: null });
|
||||
await sut.updateAll(auth, { ids: [asset.id], dateTimeOriginal: '2023-11-19T18:11:00' });
|
||||
await sut.updateAll(auth, { ids: [asset.id], dateTimeOriginal: '2023-11-19T18:11:00' });
|
||||
|
||||
await expect(
|
||||
ctx.database
|
||||
.selectFrom('asset_exif')
|
||||
.select('lockedProperties')
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({ lockedProperties: ['dateTimeOriginal'] });
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-19T18:11:00+00:00', timeZone: null }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-19T18:11:00+00:00', timeZone: null }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update dateTimeOriginal with time zone', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, description: 'test' });
|
||||
it('should update dateTimeOriginal with time zone', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, description: 'test' });
|
||||
|
||||
await expect(
|
||||
ctx.database
|
||||
.selectFrom('asset_exif')
|
||||
.select('lockedProperties')
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({ lockedProperties: null });
|
||||
await sut.updateAll(auth, { ids: [asset.id], dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
|
||||
await expect(
|
||||
ctx.database
|
||||
.selectFrom('asset_exif')
|
||||
.select('lockedProperties')
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow(),
|
||||
).resolves.toEqual({ lockedProperties: ['timeZone', 'dateTimeOriginal'] });
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00+00:00', timeZone: 'UTC-7' }),
|
||||
}),
|
||||
);
|
||||
await sut.updateAll(auth, { ids: [asset.id], dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00+00:00', timeZone: 'UTC-7' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -342,7 +342,7 @@
|
||||
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
<ChangeDate menuItem />
|
||||
<ChangeDate menuItem onDateChange={() => init(page)} />
|
||||
<ChangeDescription menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={handleDeleteOrArchiveAssets} />
|
||||
|
||||
@@ -1,27 +1,44 @@
|
||||
<script lang="ts">
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.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 type { TimelineDateTime } from '$lib/utils/timeline-util';
|
||||
import { modalManager } from '@immich/ui';
|
||||
import { mdiCalendarEditOutline } from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
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 handleChangeDate = async () => {
|
||||
const success = await modalManager.show(AssetSelectionChangeDateModal, {
|
||||
const result = await modalManager.show(AssetSelectionChangeDateModal, {
|
||||
initialDate: DateTime.now(),
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
{#if menuItem}
|
||||
|
||||
@@ -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">
|
||||
import Combobox from '$lib/components/shared-components/combobox.svelte';
|
||||
import DateInput from '$lib/elements/DateInput.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 { user } from '$lib/stores/user.store';
|
||||
import { getOwnedAssetsWithWarning } from '$lib/utils/asset-utils';
|
||||
@@ -17,7 +35,7 @@
|
||||
initialDate?: DateTime;
|
||||
initialTimeZone?: string;
|
||||
assets: TimelineAsset[];
|
||||
onClose: (success: boolean) => void;
|
||||
onClose: (result: DateChangeResult | undefined) => void;
|
||||
}
|
||||
let { initialDate = DateTime.now(), initialTimeZone, assets, onClose }: Props = $props();
|
||||
|
||||
@@ -32,24 +50,35 @@
|
||||
|
||||
const handleConfirm = async () => {
|
||||
const ids = getOwnedAssetsWithWarning(assets, $user);
|
||||
const timeZone = selectedOption?.value ?? 'UTC';
|
||||
try {
|
||||
if (showRelative && (selectedDuration || selectedOption)) {
|
||||
await updateAssets({
|
||||
assetBulkUpdateDto: {
|
||||
ids,
|
||||
dateTimeRelative: selectedDuration,
|
||||
timeZone: selectedOption?.value,
|
||||
timeZone,
|
||||
},
|
||||
});
|
||||
onClose(true);
|
||||
onClose({
|
||||
type: 'relative',
|
||||
offsetMinutes: selectedDuration,
|
||||
timeZone,
|
||||
assets,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const isoDate = toIsoDate(selectedDate, selectedOption);
|
||||
await updateAssets({ assetBulkUpdateDto: { ids, dateTimeOriginal: isoDate } });
|
||||
onClose(true);
|
||||
onClose({
|
||||
type: 'absolute',
|
||||
newDateTime: date,
|
||||
timeZone,
|
||||
assets,
|
||||
});
|
||||
} catch (error) {
|
||||
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 }));
|
||||
</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>
|
||||
<Field label={$t('edit_date_and_time_by_offset')}>
|
||||
<Switch data-testid="edit-by-offset-switch" bind:checked={showRelative} class="mb-2" />
|
||||
@@ -117,7 +146,7 @@
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<HStack fullWidth>
|
||||
<Button shape="round" color="secondary" fullWidth onclick={() => onClose(false)}>
|
||||
<Button shape="round" color="secondary" fullWidth onclick={() => onClose(undefined)}>
|
||||
{$t('cancel')}
|
||||
</Button>
|
||||
<Button shape="round" color="primary" fullWidth onclick={handleConfirm} disabled={!date.isValid}>
|
||||
|
||||
@@ -563,7 +563,7 @@
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')} offset={{ x: 175, y: 25 }}>
|
||||
<DownloadAction menuItem filename="{album.albumName}.zip" />
|
||||
{#if assetInteraction.isAllUserOwned}
|
||||
<ChangeDate menuItem />
|
||||
<ChangeDate menuItem onDateChange={(ids, updateFn) => timelineManager.update(ids, updateFn)} />
|
||||
<ChangeDescription menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
{#if assetInteraction.selectedAssets.length === 1}
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
</ButtonContextMenu>
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
<ChangeDate menuItem />
|
||||
<ChangeDate menuItem onDateChange={(ids, updateFn) => timelineManager.update(ids, updateFn)} />
|
||||
<ChangeDescription menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
<ChangeDate menuItem />
|
||||
<ChangeDate menuItem onDateChange={() => triggerAssetUpdate()} />
|
||||
<ChangeDescription menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={triggerAssetUpdate} />
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<SetVisibilityAction unlock onVisibilitySet={handleMoveOffLockedFolder} />
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
<ChangeDate menuItem />
|
||||
<ChangeDate menuItem onDateChange={(ids, updateFn) => timelineManager.update(ids, updateFn)} />
|
||||
<ChangeLocation menuItem />
|
||||
<DeleteAssets menuItem force onAssetDelete={(assetIds) => timelineManager.removeAssets(assetIds)} />
|
||||
</ButtonContextMenu>
|
||||
|
||||
@@ -501,7 +501,7 @@
|
||||
text={$t('fix_incorrect_match')}
|
||||
onClick={handleReassignAssets}
|
||||
/>
|
||||
<ChangeDate menuItem />
|
||||
<ChangeDate menuItem onDateChange={(ids, updateFn) => timelineManager.update(ids, updateFn)} />
|
||||
<ChangeDescription menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
onUnlink={handleUnlink}
|
||||
/>
|
||||
{/if}
|
||||
<ChangeDate menuItem />
|
||||
<ChangeDate menuItem onDateChange={(ids, updateFn) => timelineManager.update(ids, updateFn)} />
|
||||
<ChangeDescription menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
<ChangeDate menuItem />
|
||||
<ChangeDate menuItem onDateChange={() => onSearchQueryUpdate()} />
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
|
||||
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
|
||||
@@ -432,7 +432,7 @@
|
||||
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
<ChangeDate menuItem />
|
||||
<ChangeDate menuItem onDateChange={() => onSearchQueryUpdate()} />
|
||||
<ChangeDescription menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
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 TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
|
||||
import TreeItems from '$lib/components/shared-components/tree/tree-items.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 Timeline from '$lib/components/timeline/Timeline.svelte';
|
||||
import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
|
||||
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
|
||||
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
|
||||
@@ -31,8 +20,19 @@
|
||||
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
||||
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.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 { 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 {
|
||||
data: PageData;
|
||||
@@ -172,7 +172,7 @@
|
||||
></FavoriteAction>
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
<ChangeDate menuItem />
|
||||
<ChangeDate menuItem onDateChange={(ids, updateFn) => timelineManager.update(ids, updateFn)} />
|
||||
<ChangeDescription menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction
|
||||
|
||||
Reference in New Issue
Block a user