mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
feat(web): add cover images to individual shares (#9988)
* feat(web): add cover images to individual shares * Update wording in share modal * Use translation function * Add and use new translations * Fix formatting * Update with suggestions * Update test language * Update test and language file per suggestions * Fix formatting * Remove unused translation
This commit is contained in:
parent
78f600ebce
commit
aea1c46bea
13 changed files with 214 additions and 30 deletions
|
@ -0,0 +1,42 @@
|
||||||
|
import AlbumCover from '$lib/components/album-page/album-cover.svelte';
|
||||||
|
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||||
|
import { albumFactory } from '@test-data';
|
||||||
|
import { render } from '@testing-library/svelte';
|
||||||
|
|
||||||
|
vi.mock('$lib/utils');
|
||||||
|
|
||||||
|
describe('AlbumCover component', () => {
|
||||||
|
it('renders an image when the album has a thumbnail', () => {
|
||||||
|
vi.mocked(getAssetThumbnailUrl).mockReturnValue('/asdf');
|
||||||
|
const component = render(AlbumCover, {
|
||||||
|
album: albumFactory.build({
|
||||||
|
albumName: 'someName',
|
||||||
|
albumThumbnailAssetId: '123',
|
||||||
|
}),
|
||||||
|
preload: false,
|
||||||
|
class: 'text',
|
||||||
|
});
|
||||||
|
const img = component.getByTestId('album-image') as HTMLImageElement;
|
||||||
|
expect(img.alt).toBe('someName');
|
||||||
|
expect(img.getAttribute('loading')).toBe('lazy');
|
||||||
|
expect(img.className).toBe('z-0 rounded-xl object-cover text');
|
||||||
|
expect(img.getAttribute('src')).toBe('/asdf');
|
||||||
|
expect(getAssetThumbnailUrl).toHaveBeenCalledWith({ id: '123' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders an image when the album has no thumbnail', () => {
|
||||||
|
const component = render(AlbumCover, {
|
||||||
|
album: albumFactory.build({
|
||||||
|
albumName: '',
|
||||||
|
albumThumbnailAssetId: null,
|
||||||
|
}),
|
||||||
|
preload: true,
|
||||||
|
class: 'asdf',
|
||||||
|
});
|
||||||
|
const img = component.getByTestId('album-image') as HTMLImageElement;
|
||||||
|
expect(img.alt).toBe('unnamed_album');
|
||||||
|
expect(img.getAttribute('loading')).toBe('eager');
|
||||||
|
expect(img.className).toBe('z-0 rounded-xl object-cover asdf');
|
||||||
|
expect(img.getAttribute('src')).toStrictEqual(expect.any(String));
|
||||||
|
});
|
||||||
|
});
|
|
@ -46,7 +46,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<AlbumCover {album} {preload} css="h-full w-full transition-all duration-300 hover:shadow-lg" />
|
<AlbumCover {album} {preload} class="h-full w-full transition-all duration-300 hover:shadow-lg" />
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<p
|
<p
|
||||||
|
|
|
@ -1,35 +1,23 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||||
import { type AlbumResponseDto } from '@immich/sdk';
|
import { type AlbumResponseDto } from '@immich/sdk';
|
||||||
|
import NoCover from '$lib/components/sharedlinks-page/covers/no-cover.svelte';
|
||||||
|
import AssetCover from '$lib/components/sharedlinks-page/covers/asset-cover.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let album: AlbumResponseDto | undefined;
|
export let album: AlbumResponseDto;
|
||||||
export let preload = false;
|
export let preload = false;
|
||||||
export let css = '';
|
let className = '';
|
||||||
|
export { className as class };
|
||||||
|
|
||||||
$: thumbnailUrl =
|
$: alt = album.albumName || $t('unnamed_album');
|
||||||
album && album.albumThumbnailAssetId ? getAssetThumbnailUrl({ id: album.albumThumbnailAssetId }) : null;
|
$: thumbnailUrl = album.albumThumbnailAssetId ? getAssetThumbnailUrl({ id: album.albumThumbnailAssetId }) : null;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative aspect-square">
|
<div class="relative aspect-square">
|
||||||
{#if thumbnailUrl}
|
{#if thumbnailUrl}
|
||||||
<img
|
<AssetCover {alt} class={className} src={thumbnailUrl} {preload} />
|
||||||
loading={preload ? 'eager' : 'lazy'}
|
|
||||||
src={thumbnailUrl}
|
|
||||||
alt={album?.albumName ?? $t('unknown_album')}
|
|
||||||
class="z-0 rounded-xl object-cover {css}"
|
|
||||||
data-testid="album-image"
|
|
||||||
draggable="false"
|
|
||||||
/>
|
|
||||||
{:else}
|
{:else}
|
||||||
<enhanced:img
|
<NoCover {alt} class={className} {preload} />
|
||||||
loading={preload ? 'eager' : 'lazy'}
|
|
||||||
src="$lib/assets/no-thumbnail.png"
|
|
||||||
sizes="min(271px,186px)"
|
|
||||||
alt={album?.albumName ?? $t('empty_album')}
|
|
||||||
class="z-0 rounded-xl object-cover {css}"
|
|
||||||
data-testid="album-image"
|
|
||||||
draggable="false"
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<form on:submit|preventDefault={handleUpdateAlbumInfo} autocomplete="off" id="edit-album-form">
|
<form on:submit|preventDefault={handleUpdateAlbumInfo} autocomplete="off" id="edit-album-form">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="hidden sm:flex">
|
<div class="hidden sm:flex">
|
||||||
<AlbumCover {album} css="h-[200px] w-[200px] m-4 shadow-lg" />
|
<AlbumCover {album} class="h-[200px] w-[200px] m-4 shadow-lg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
|
|
|
@ -193,9 +193,8 @@
|
||||||
<div>Let anyone with the link see the selected photo(s)</div>
|
<div>Let anyone with the link see the selected photo(s)</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
Individual shared | <span class="text-immich-primary dark:text-immich-dark-primary"
|
{$t('individual_share')} |
|
||||||
>{editingLink.description || ''}</span
|
<span class="text-immich-primary dark:text-immich-dark-primary">{editingLink.description || ''}</span>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import AssetCover from '$lib/components/sharedlinks-page/covers/asset-cover.svelte';
|
||||||
|
import { render } from '@testing-library/svelte';
|
||||||
|
|
||||||
|
describe('AssetCover component', () => {
|
||||||
|
it('renders correctly', () => {
|
||||||
|
const component = render(AssetCover, {
|
||||||
|
alt: '123',
|
||||||
|
preload: true,
|
||||||
|
src: 'wee',
|
||||||
|
class: 'asdf',
|
||||||
|
});
|
||||||
|
const img = component.getByTestId('album-image') as HTMLImageElement;
|
||||||
|
expect(img.alt).toBe('123');
|
||||||
|
expect(img.getAttribute('src')).toBe('wee');
|
||||||
|
expect(img.getAttribute('loading')).toBe('eager');
|
||||||
|
expect(img.className).toBe('z-0 rounded-xl object-cover asdf');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,17 @@
|
||||||
|
import NoCover from '$lib/components/sharedlinks-page/covers/no-cover.svelte';
|
||||||
|
import { render } from '@testing-library/svelte';
|
||||||
|
|
||||||
|
describe('NoCover component', () => {
|
||||||
|
it('renders correctly', () => {
|
||||||
|
const component = render(NoCover, {
|
||||||
|
alt: '123',
|
||||||
|
preload: true,
|
||||||
|
class: 'asdf',
|
||||||
|
});
|
||||||
|
const img = component.getByTestId('album-image') as HTMLImageElement;
|
||||||
|
expect(img.alt).toBe('123');
|
||||||
|
expect(img.className).toBe('z-0 rounded-xl object-cover asdf');
|
||||||
|
expect(img.getAttribute('loading')).toBe('eager');
|
||||||
|
expect(img.src).toStrictEqual(expect.any(String));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,60 @@
|
||||||
|
import ShareCover from '$lib/components/sharedlinks-page/covers/share-cover.svelte';
|
||||||
|
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||||
|
import type { SharedLinkResponseDto } from '@immich/sdk';
|
||||||
|
import { albumFactory } from '@test-data';
|
||||||
|
import { render } from '@testing-library/svelte';
|
||||||
|
|
||||||
|
vi.mock('$lib/utils');
|
||||||
|
|
||||||
|
describe('ShareCover component', () => {
|
||||||
|
it('renders an image when the shared link is an album', () => {
|
||||||
|
const component = render(ShareCover, {
|
||||||
|
link: {
|
||||||
|
album: albumFactory.build({
|
||||||
|
albumName: '123',
|
||||||
|
}),
|
||||||
|
} as SharedLinkResponseDto,
|
||||||
|
preload: false,
|
||||||
|
class: 'text',
|
||||||
|
});
|
||||||
|
const img = component.getByTestId('album-image') as HTMLImageElement;
|
||||||
|
expect(img.alt).toBe('123');
|
||||||
|
expect(img.getAttribute('loading')).toBe('lazy');
|
||||||
|
expect(img.className).toBe('z-0 rounded-xl object-cover text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders an image when the shared link is an individual share', () => {
|
||||||
|
vi.mocked(getAssetThumbnailUrl).mockReturnValue('/asdf');
|
||||||
|
const component = render(ShareCover, {
|
||||||
|
link: {
|
||||||
|
assets: [
|
||||||
|
{
|
||||||
|
id: 'someId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as SharedLinkResponseDto,
|
||||||
|
preload: false,
|
||||||
|
class: 'text',
|
||||||
|
});
|
||||||
|
const img = component.getByTestId('album-image') as HTMLImageElement;
|
||||||
|
expect(img.alt).toBe('individual_share');
|
||||||
|
expect(img.getAttribute('loading')).toBe('lazy');
|
||||||
|
expect(img.className).toBe('z-0 rounded-xl object-cover text');
|
||||||
|
expect(img.getAttribute('src')).toBe('/asdf');
|
||||||
|
expect(getAssetThumbnailUrl).toHaveBeenCalledWith('someId');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders an image when the shared link has no album or assets', () => {
|
||||||
|
const component = render(ShareCover, {
|
||||||
|
link: {
|
||||||
|
assets: [],
|
||||||
|
} as unknown as SharedLinkResponseDto,
|
||||||
|
preload: false,
|
||||||
|
class: 'text',
|
||||||
|
});
|
||||||
|
const img = component.getByTestId('album-image') as HTMLImageElement;
|
||||||
|
expect(img.alt).toBe('unnamed_share');
|
||||||
|
expect(img.getAttribute('loading')).toBe('lazy');
|
||||||
|
expect(img.className).toBe('z-0 rounded-xl object-cover text');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let alt;
|
||||||
|
export let preload = false;
|
||||||
|
export let src: string;
|
||||||
|
let className = '';
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<img
|
||||||
|
{alt}
|
||||||
|
class="z-0 rounded-xl object-cover {className}"
|
||||||
|
data-testid="album-image"
|
||||||
|
draggable="false"
|
||||||
|
loading={preload ? 'eager' : 'lazy'}
|
||||||
|
{src}
|
||||||
|
/>
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let alt = '';
|
||||||
|
export let preload = false;
|
||||||
|
let className = '';
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<enhanced:img
|
||||||
|
{alt}
|
||||||
|
class="z-0 rounded-xl object-cover {className}"
|
||||||
|
data-testid="album-image"
|
||||||
|
draggable="false"
|
||||||
|
loading={preload ? 'eager' : 'lazy'}
|
||||||
|
sizes="min(271px,186px)"
|
||||||
|
src="$lib/assets/no-thumbnail.png"
|
||||||
|
/>
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { SharedLinkResponseDto } from '@immich/sdk';
|
||||||
|
import AlbumCover from '$lib/components/album-page/album-cover.svelte';
|
||||||
|
import NoCover from '$lib/components/sharedlinks-page/covers/no-cover.svelte';
|
||||||
|
import AssetCover from '$lib/components/sharedlinks-page/covers/asset-cover.svelte';
|
||||||
|
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
export let link: SharedLinkResponseDto;
|
||||||
|
export let preload = false;
|
||||||
|
let className = '';
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative aspect-square">
|
||||||
|
{#if link?.album}
|
||||||
|
<AlbumCover album={link.album} class={className} {preload} />
|
||||||
|
{:else if link.assets[0]}
|
||||||
|
<AssetCover
|
||||||
|
alt={$t('individual_share')}
|
||||||
|
class={className}
|
||||||
|
{preload}
|
||||||
|
src={getAssetThumbnailUrl(link.assets[0].id)}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<NoCover alt={$t('unnamed_share')} class={className} {preload} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
|
@ -7,7 +7,7 @@
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import AlbumCover from '$lib/components/album-page/album-cover.svelte';
|
import ShareCover from '$lib/components/sharedlinks-page/covers/share-cover.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let link: SharedLinkResponseDto;
|
export let link: SharedLinkResponseDto;
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
class="flex w-full gap-4 border-b border-gray-200 py-4 transition-all hover:border-immich-primary dark:border-gray-600 dark:text-immich-gray dark:hover:border-immich-dark-primary"
|
class="flex w-full gap-4 border-b border-gray-200 py-4 transition-all hover:border-immich-primary dark:border-gray-600 dark:text-immich-gray dark:hover:border-immich-dark-primary"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<AlbumCover album={link?.album} css="h-[100px] w-[100px] transition-all duration-300 hover:shadow-lg" />
|
<ShareCover class="h-[100px] w-[100px] transition-all duration-300 hover:shadow-lg" {link} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col justify-between">
|
<div class="flex flex-col justify-between">
|
||||||
|
|
|
@ -445,7 +445,6 @@
|
||||||
"edited": "Edited",
|
"edited": "Edited",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"empty_album": "Empty Album",
|
|
||||||
"empty_trash": "Empty trash",
|
"empty_trash": "Empty trash",
|
||||||
"end_date": "End date",
|
"end_date": "End date",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
|
@ -868,11 +867,12 @@
|
||||||
"unfavorite": "Unfavorite",
|
"unfavorite": "Unfavorite",
|
||||||
"unhide_person": "Unhide person",
|
"unhide_person": "Unhide person",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"unknown_album": "Unknown Album",
|
|
||||||
"unknown_year": "Unknown Year",
|
"unknown_year": "Unknown Year",
|
||||||
"unlimited": "Unlimited",
|
"unlimited": "Unlimited",
|
||||||
"unlink_oauth": "Unlink Oauth",
|
"unlink_oauth": "Unlink Oauth",
|
||||||
"unlinked_oauth_account": "Unlinked OAuth account",
|
"unlinked_oauth_account": "Unlinked OAuth account",
|
||||||
|
"unnamed_album": "Unnamed Album",
|
||||||
|
"unnamed_share": "Unnamed Share",
|
||||||
"unselect_all": "Unselect all",
|
"unselect_all": "Unselect all",
|
||||||
"unstack": "Un-stack",
|
"unstack": "Un-stack",
|
||||||
"untracked_files": "Untracked files",
|
"untracked_files": "Untracked files",
|
||||||
|
|
Loading…
Reference in a new issue