mirror of
https://github.com/immich-app/immich
synced 2026-04-21 13:37:38 +00:00
chore(web): refactor date section of asset viewer (#24514)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
parent
18c0228f1b
commit
b7eff33f90
4 changed files with 131 additions and 83 deletions
|
|
@ -1,7 +1,9 @@
|
|||
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { basename, join } from 'node:path';
|
||||
import type { Socket } from 'socket.io-client';
|
||||
import { utils } from 'src/utils';
|
||||
import { testAssetDir, utils } from 'src/utils';
|
||||
|
||||
test.describe('Detail Panel', () => {
|
||||
let admin: LoginResponseDto;
|
||||
|
|
@ -83,4 +85,42 @@ test.describe('Detail Panel', () => {
|
|||
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id });
|
||||
await expect(textarea).toHaveValue('new description');
|
||||
});
|
||||
|
||||
test.describe('Date editor', () => {
|
||||
test('displays inferred asset timezone', async ({ context, page }) => {
|
||||
const test = {
|
||||
filepath: 'metadata/dates/datetimeoriginal-gps.jpg',
|
||||
expected: {
|
||||
dateTime: '2025-12-01T11:30',
|
||||
// Test with a timezone which is NOT the first among timezones with the same offset
|
||||
// This is to check that the editor does not simply fall back to the first available timezone with that offset
|
||||
// America/Denver (-07:00) is not the first among timezones with offset -07:00
|
||||
timeZoneWithOffset: 'America/Denver (-07:00)',
|
||||
},
|
||||
};
|
||||
|
||||
const asset = await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
bytes: await readFile(join(testAssetDir, test.filepath)),
|
||||
filename: basename(test.filepath),
|
||||
},
|
||||
});
|
||||
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
|
||||
|
||||
// asset viewer -> detail panel -> date editor
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await page.waitForSelector('#immich-asset-viewer');
|
||||
|
||||
await page.getByRole('button', { name: 'Info' }).click();
|
||||
await page.getByTestId('detail-panel-edit-date-button').click();
|
||||
await page.waitForSelector('[role="dialog"]');
|
||||
|
||||
const datetime = page.locator('#datetime');
|
||||
await expect(datetime).toHaveValue(test.expected.dateTime);
|
||||
const timezone = page.getByRole('combobox', { name: 'Timezone' });
|
||||
await expect(timezone).toHaveValue(test.expected.timeZoneWithOffset);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 163c251744e0a35d7ecfd02682452043f149fc2b
|
||||
Subproject commit 0eac5a37384c151be88381b41f9e28d8d59a4466
|
||||
86
web/src/lib/components/asset-viewer/detail-panel-date.svelte
Normal file
86
web/src/lib/components/asset-viewer/detail-panel-date.svelte
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<script lang="ts">
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import AssetChangeDateModal from '$lib/modals/AssetChangeDateModal.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { fromISODateTime, fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
import { Icon, modalManager } from '@immich/ui';
|
||||
import { mdiCalendar, mdiPencil } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
asset: AssetResponseDto;
|
||||
};
|
||||
|
||||
const { asset }: Props = $props();
|
||||
|
||||
const timeZone = $derived(asset.exifInfo?.timeZone ?? undefined);
|
||||
const dateTime = $derived(
|
||||
timeZone && asset.exifInfo?.dateTimeOriginal
|
||||
? fromISODateTime(asset.exifInfo.dateTimeOriginal, timeZone)
|
||||
: fromISODateTimeUTC(asset.localDateTime),
|
||||
);
|
||||
const isOwner = $derived(authManager.authenticated && asset.ownerId === authManager.user.id);
|
||||
|
||||
const handleChangeDate = async () => {
|
||||
if (!isOwner) {
|
||||
return;
|
||||
}
|
||||
|
||||
await modalManager.show(AssetChangeDateModal, {
|
||||
asset: toTimelineAsset(asset),
|
||||
initialDate: dateTime,
|
||||
initialTimeZone: timeZone,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if dateTime}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full text-start justify-between place-items-start gap-4 py-4"
|
||||
onclick={handleChangeDate}
|
||||
title={isOwner ? $t('edit_date') : ''}
|
||||
class:hover:text-primary={isOwner}
|
||||
data-testid="detail-panel-edit-date-button"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<Icon icon={mdiCalendar} size="24" />
|
||||
|
||||
<div>
|
||||
<p>
|
||||
{dateTime.toLocaleString({ month: 'short', day: 'numeric', year: 'numeric' }, { locale: $locale })}
|
||||
</p>
|
||||
<div class="flex gap-2 text-sm">
|
||||
<p>
|
||||
{dateTime.toLocaleString(
|
||||
{
|
||||
weekday: 'short',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
timeZoneName: timeZone ? 'longOffset' : undefined,
|
||||
},
|
||||
{ locale: $locale },
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isOwner}
|
||||
<div class="p-1">
|
||||
<Icon icon={mdiPencil} size="20" />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{:else if !dateTime && isOwner}
|
||||
<div class="flex justify-between place-items-start gap-4 py-4">
|
||||
<div class="flex gap-4">
|
||||
<Icon icon={mdiCalendar} size="24" />
|
||||
</div>
|
||||
<div class="p-1">
|
||||
<Icon icon={mdiPencil} size="20" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import DetailPanelDate from '$lib/components/asset-viewer/detail-panel-date.svelte';
|
||||
import DetailPanelDescription from '$lib/components/asset-viewer/detail-panel-description.svelte';
|
||||
import DetailPanelLocation from '$lib/components/asset-viewer/detail-panel-location.svelte';
|
||||
import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.svelte';
|
||||
|
|
@ -8,7 +9,6 @@
|
|||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import AssetChangeDateModal from '$lib/modals/AssetChangeDateModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
|
|
@ -16,7 +16,6 @@
|
|||
import { delay, getDimensions } from '$lib/utils/asset-utils';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { fromISODateTime, fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { getParentPath } from '$lib/utils/tree-utils';
|
||||
import {
|
||||
AssetMediaSize,
|
||||
|
|
@ -25,9 +24,8 @@
|
|||
type AlbumResponseDto,
|
||||
type AssetResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { Icon, IconButton, LoadingSpinner, modalManager, Text } from '@immich/ui';
|
||||
import { Icon, IconButton, LoadingSpinner, Text } from '@immich/ui';
|
||||
import {
|
||||
mdiCalendar,
|
||||
mdiCamera,
|
||||
mdiCameraIris,
|
||||
mdiClose,
|
||||
|
|
@ -59,12 +57,6 @@
|
|||
let people = $derived(asset.people || []);
|
||||
let unassignedFaces = $derived(asset.unassignedFaces || []);
|
||||
let showingHiddenPeople = $state(false);
|
||||
let timeZone = $derived(asset.exifInfo?.timeZone ?? undefined);
|
||||
let dateTime = $derived(
|
||||
timeZone && asset.exifInfo?.dateTimeOriginal
|
||||
? fromISODateTime(asset.exifInfo.dateTimeOriginal, timeZone)
|
||||
: fromISODateTimeUTC(asset.localDateTime),
|
||||
);
|
||||
let latlng = $derived(
|
||||
(() => {
|
||||
const lat = asset.exifInfo?.latitude;
|
||||
|
|
@ -126,18 +118,6 @@
|
|||
// Remove the last part of the path to get the parent path
|
||||
return Route.folders({ path: getParentPath(asset.originalPath) });
|
||||
};
|
||||
|
||||
const handleChangeDate = async () => {
|
||||
if (!isOwner) {
|
||||
return;
|
||||
}
|
||||
|
||||
await modalManager.show(AssetChangeDateModal, {
|
||||
asset: toTimelineAsset(asset),
|
||||
initialDate: dateTime,
|
||||
initialTimeZone: timeZone,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<OnEvents onAlbumAddAssets={() => (albums = refreshAlbums())} />
|
||||
|
|
@ -291,65 +271,7 @@
|
|||
<Text size="small" color="muted">{$t('no_exif_info_available')}</Text>
|
||||
{/if}
|
||||
|
||||
{#if dateTime}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full text-start justify-between place-items-start gap-4 py-4"
|
||||
onclick={handleChangeDate}
|
||||
title={isOwner ? $t('edit_date') : ''}
|
||||
class:hover:text-primary={isOwner}
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div>
|
||||
<Icon icon={mdiCalendar} size="24" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
{dateTime.toLocaleString(
|
||||
{
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
{ locale: $locale },
|
||||
)}
|
||||
</p>
|
||||
<div class="flex gap-2 text-sm">
|
||||
<p>
|
||||
{dateTime.toLocaleString(
|
||||
{
|
||||
weekday: 'short',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
timeZoneName: timeZone ? 'longOffset' : undefined,
|
||||
},
|
||||
{ locale: $locale },
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isOwner}
|
||||
<div class="p-1">
|
||||
<Icon icon={mdiPencil} size="20" />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{:else if !dateTime && isOwner}
|
||||
<div class="flex justify-between place-items-start gap-4 py-4">
|
||||
<div class="flex gap-4">
|
||||
<div>
|
||||
<Icon icon={mdiCalendar} size="24" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-1">
|
||||
<Icon icon={mdiPencil} size="20" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<DetailPanelDate {asset} />
|
||||
|
||||
<div class="flex gap-4 py-4">
|
||||
<div><Icon icon={mdiImageOutline} size="24" /></div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue