mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
chore(web): refactor date section of asset viewer (#24514)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
|
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
|
||||||
import { expect, test } from '@playwright/test';
|
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 type { Socket } from 'socket.io-client';
|
||||||
import { utils } from 'src/utils';
|
import { testAssetDir, utils } from 'src/utils';
|
||||||
|
|
||||||
test.describe('Detail Panel', () => {
|
test.describe('Detail Panel', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
@@ -83,4 +85,42 @@ test.describe('Detail Panel', () => {
|
|||||||
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id });
|
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id });
|
||||||
await expect(textarea).toHaveValue('new description');
|
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
Submodule e2e/test-assets updated: 163c251744...0eac5a3738
@@ -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">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
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 DetailPanelDescription from '$lib/components/asset-viewer/detail-panel-description.svelte';
|
||||||
import DetailPanelLocation from '$lib/components/asset-viewer/detail-panel-location.svelte';
|
import DetailPanelLocation from '$lib/components/asset-viewer/detail-panel-location.svelte';
|
||||||
import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.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 { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
import AssetChangeDateModal from '$lib/modals/AssetChangeDateModal.svelte';
|
|
||||||
import { Route } from '$lib/route';
|
import { Route } from '$lib/route';
|
||||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
import { delay, getDimensions } from '$lib/utils/asset-utils';
|
import { delay, getDimensions } from '$lib/utils/asset-utils';
|
||||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { fromISODateTime, fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util';
|
|
||||||
import { getParentPath } from '$lib/utils/tree-utils';
|
import { getParentPath } from '$lib/utils/tree-utils';
|
||||||
import {
|
import {
|
||||||
AssetMediaSize,
|
AssetMediaSize,
|
||||||
@@ -25,9 +24,8 @@
|
|||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
type AssetResponseDto,
|
type AssetResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { Icon, IconButton, LoadingSpinner, modalManager, Text } from '@immich/ui';
|
import { Icon, IconButton, LoadingSpinner, Text } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
mdiCalendar,
|
|
||||||
mdiCamera,
|
mdiCamera,
|
||||||
mdiCameraIris,
|
mdiCameraIris,
|
||||||
mdiClose,
|
mdiClose,
|
||||||
@@ -59,12 +57,6 @@
|
|||||||
let people = $derived(asset.people || []);
|
let people = $derived(asset.people || []);
|
||||||
let unassignedFaces = $derived(asset.unassignedFaces || []);
|
let unassignedFaces = $derived(asset.unassignedFaces || []);
|
||||||
let showingHiddenPeople = $state(false);
|
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(
|
let latlng = $derived(
|
||||||
(() => {
|
(() => {
|
||||||
const lat = asset.exifInfo?.latitude;
|
const lat = asset.exifInfo?.latitude;
|
||||||
@@ -126,18 +118,6 @@
|
|||||||
// Remove the last part of the path to get the parent path
|
// Remove the last part of the path to get the parent path
|
||||||
return Route.folders({ path: getParentPath(asset.originalPath) });
|
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>
|
</script>
|
||||||
|
|
||||||
<OnEvents onAlbumAddAssets={() => (albums = refreshAlbums())} />
|
<OnEvents onAlbumAddAssets={() => (albums = refreshAlbums())} />
|
||||||
@@ -291,65 +271,7 @@
|
|||||||
<Text size="small" color="muted">{$t('no_exif_info_available')}</Text>
|
<Text size="small" color="muted">{$t('no_exif_info_available')}</Text>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if dateTime}
|
<DetailPanelDate {asset} />
|
||||||
<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}
|
|
||||||
|
|
||||||
<div class="flex gap-4 py-4">
|
<div class="flex gap-4 py-4">
|
||||||
<div><Icon icon={mdiImageOutline} size="24" /></div>
|
<div><Icon icon={mdiImageOutline} size="24" /></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user