mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
chore(web): migration svelte 5 syntax (#13883)
This commit is contained in:
parent
9203a61709
commit
0b3742cf13
310 changed files with 6435 additions and 4176 deletions
|
|
@ -1,14 +1,15 @@
|
|||
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||
import { albumFactory } from '@test-data/factories/album-factory';
|
||||
import '@testing-library/jest-dom';
|
||||
import { fireEvent, render, waitFor, type RenderResult } from '@testing-library/svelte';
|
||||
import { render, waitFor, type RenderResult } from '@testing-library/svelte';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { init, register, waitLocale } from 'svelte-i18n';
|
||||
import AlbumCard from '../album-card.svelte';
|
||||
|
||||
const onShowContextMenu = vi.fn();
|
||||
|
||||
describe('AlbumCard component', () => {
|
||||
let sut: RenderResult<AlbumCard>;
|
||||
let sut: RenderResult<typeof AlbumCard>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await init({ fallbackLocale: 'en-US' });
|
||||
|
|
@ -110,13 +111,9 @@ describe('AlbumCard component', () => {
|
|||
toJSON: () => ({}),
|
||||
});
|
||||
|
||||
await fireEvent(
|
||||
contextMenuButton,
|
||||
new MouseEvent('click', {
|
||||
clientX: 123,
|
||||
clientY: 456,
|
||||
}),
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
await user.click(contextMenuButton);
|
||||
|
||||
expect(onShowContextMenu).toHaveBeenCalledTimes(1);
|
||||
expect(onShowContextMenu).toHaveBeenCalledWith(expect.objectContaining({ x: 123, y: 456 }));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,28 +11,43 @@
|
|||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let albums: AlbumResponseDto[];
|
||||
export let group: AlbumGroup | undefined = undefined;
|
||||
export let showOwner = false;
|
||||
export let showDateRange = false;
|
||||
export let showItemCount = false;
|
||||
export let onShowContextMenu: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined =
|
||||
undefined;
|
||||
interface Props {
|
||||
albums: AlbumResponseDto[];
|
||||
group?: AlbumGroup | undefined;
|
||||
showOwner?: boolean;
|
||||
showDateRange?: boolean;
|
||||
showItemCount?: boolean;
|
||||
onShowContextMenu?: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined;
|
||||
}
|
||||
|
||||
$: isCollapsed = !!group && isAlbumGroupCollapsed($albumViewSettings, group.id);
|
||||
let {
|
||||
albums,
|
||||
group = undefined,
|
||||
showOwner = false,
|
||||
showDateRange = false,
|
||||
showItemCount = false,
|
||||
onShowContextMenu = undefined,
|
||||
}: Props = $props();
|
||||
|
||||
let isCollapsed = $derived(!!group && isAlbumGroupCollapsed($albumViewSettings, group.id));
|
||||
|
||||
const showContextMenu = (position: ContextMenuPosition, album: AlbumResponseDto) => {
|
||||
onShowContextMenu?.(position, album);
|
||||
};
|
||||
|
||||
$: iconRotation = isCollapsed ? 'rotate-0' : 'rotate-90';
|
||||
let iconRotation = $derived(isCollapsed ? 'rotate-0' : 'rotate-90');
|
||||
|
||||
const oncontextmenu = (event: MouseEvent, album: AlbumResponseDto) => {
|
||||
event.preventDefault();
|
||||
showContextMenu({ x: event.x, y: event.y }, album);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if group}
|
||||
<div class="grid">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => toggleAlbumGroupCollapsing(group.id)}
|
||||
onclick={() => toggleAlbumGroupCollapsing(group.id)}
|
||||
class="w-fit mt-2 pt-2 pr-2 mb-2 dark:text-immich-dark-fg"
|
||||
aria-expanded={!isCollapsed}
|
||||
>
|
||||
|
|
@ -56,7 +71,7 @@
|
|||
data-sveltekit-preload-data="hover"
|
||||
href="{AppRoute.ALBUMS}/{album.id}"
|
||||
animate:flip={{ duration: 400 }}
|
||||
on:contextmenu|preventDefault={(e) => showContextMenu({ x: e.x, y: e.y }, album)}
|
||||
oncontextmenu={(event) => oncontextmenu(event, album)}
|
||||
>
|
||||
<AlbumCard
|
||||
{album}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,23 @@
|
|||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let showOwner = false;
|
||||
export let showDateRange = false;
|
||||
export let showItemCount = false;
|
||||
export let preload = false;
|
||||
export let onShowContextMenu: ((position: ContextMenuPosition) => unknown) | undefined = undefined;
|
||||
interface Props {
|
||||
album: AlbumResponseDto;
|
||||
showOwner?: boolean;
|
||||
showDateRange?: boolean;
|
||||
showItemCount?: boolean;
|
||||
preload?: boolean;
|
||||
onShowContextMenu?: ((position: ContextMenuPosition) => unknown) | undefined;
|
||||
}
|
||||
|
||||
let {
|
||||
album,
|
||||
showOwner = false,
|
||||
showDateRange = false,
|
||||
showItemCount = false,
|
||||
preload = false,
|
||||
onShowContextMenu = undefined,
|
||||
}: Props = $props();
|
||||
|
||||
const showAlbumContextMenu = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -39,7 +50,7 @@
|
|||
size="20"
|
||||
padding="2"
|
||||
class="icon-white-drop-shadow"
|
||||
on:click={showAlbumContextMenu}
|
||||
onclick={showAlbumContextMenu}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,18 @@
|
|||
import AssetCover from '$lib/components/sharedlinks-page/covers/asset-cover.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let preload = false;
|
||||
let className = '';
|
||||
export { className as class };
|
||||
interface Props {
|
||||
album: AlbumResponseDto;
|
||||
preload?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
$: alt = album.albumName || $t('unnamed_album');
|
||||
$: thumbnailUrl = album.albumThumbnailAssetId ? getAssetThumbnailUrl({ id: album.albumThumbnailAssetId }) : null;
|
||||
let { album, preload = false, class: className = '' }: Props = $props();
|
||||
|
||||
let alt = $derived(album.albumName || $t('unnamed_album'));
|
||||
let thumbnailUrl = $derived(
|
||||
album.albumThumbnailAssetId ? getAssetThumbnailUrl({ id: album.albumThumbnailAssetId }) : null,
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if thumbnailUrl}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@
|
|||
import AutogrowTextarea from '$lib/components/shared-components/autogrow-textarea.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let id: string;
|
||||
export let description: string;
|
||||
export let isOwned: boolean;
|
||||
interface Props {
|
||||
id: string;
|
||||
description: string;
|
||||
isOwned: boolean;
|
||||
}
|
||||
|
||||
let { id, description = $bindable(), isOwned }: Props = $props();
|
||||
|
||||
const handleUpdateDescription = async (newDescription: string) => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -23,24 +23,38 @@
|
|||
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let order: AssetOrder | undefined;
|
||||
export let user: UserResponseDto; // Declare user as a prop
|
||||
export let onChangeOrder: (order: AssetOrder) => void;
|
||||
export let onClose: () => void;
|
||||
export let onToggleEnabledActivity: () => void;
|
||||
export let onShowSelectSharedUser: () => void;
|
||||
export let onRemove: (userId: string) => void;
|
||||
export let onRefreshAlbum: () => void;
|
||||
interface Props {
|
||||
album: AlbumResponseDto;
|
||||
order: AssetOrder | undefined;
|
||||
user: UserResponseDto;
|
||||
onChangeOrder: (order: AssetOrder) => void;
|
||||
onClose: () => void;
|
||||
onToggleEnabledActivity: () => void;
|
||||
onShowSelectSharedUser: () => void;
|
||||
onRemove: (userId: string) => void;
|
||||
onRefreshAlbum: () => void;
|
||||
}
|
||||
|
||||
let selectedRemoveUser: UserResponseDto | null = null;
|
||||
let {
|
||||
album,
|
||||
order,
|
||||
user,
|
||||
onChangeOrder,
|
||||
onClose,
|
||||
onToggleEnabledActivity,
|
||||
onShowSelectSharedUser,
|
||||
onRemove,
|
||||
onRefreshAlbum,
|
||||
}: Props = $props();
|
||||
|
||||
let selectedRemoveUser: UserResponseDto | null = $state(null);
|
||||
|
||||
const options: Record<AssetOrder, RenderedOption> = {
|
||||
[AssetOrder.Asc]: { icon: mdiArrowUpThin, title: $t('oldest_first') },
|
||||
[AssetOrder.Desc]: { icon: mdiArrowDownThin, title: $t('newest_first') },
|
||||
};
|
||||
|
||||
$: selectedOption = order ? options[order] : options[AssetOrder.Desc];
|
||||
let selectedOption = $derived(order ? options[order] : options[AssetOrder.Desc]);
|
||||
|
||||
const handleToggle = async (returnedOption: RenderedOption): Promise<void> => {
|
||||
if (selectedOption === returnedOption) {
|
||||
|
|
@ -125,7 +139,7 @@
|
|||
<div class="py-2">
|
||||
<div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
|
||||
<div class="p-2">
|
||||
<button type="button" class="flex items-center gap-2" on:click={onShowSelectSharedUser}>
|
||||
<button type="button" class="flex items-center gap-2" onclick={onShowSelectSharedUser}>
|
||||
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
|
||||
<div><Icon path={mdiPlus} size="25" /></div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@
|
|||
import type { AlbumResponseDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
interface Props {
|
||||
album: AlbumResponseDto;
|
||||
}
|
||||
|
||||
$: startDate = formatDate(album.startDate);
|
||||
$: endDate = formatDate(album.endDate);
|
||||
let { album }: Props = $props();
|
||||
|
||||
const formatDate = (date?: string) => {
|
||||
return date ? new Date(date).toLocaleDateString($locale, dateFormats.album) : undefined;
|
||||
|
|
@ -24,6 +25,8 @@
|
|||
|
||||
return '';
|
||||
};
|
||||
let startDate = $derived(formatDate(album.startDate));
|
||||
let endDate = $derived(formatDate(album.endDate));
|
||||
</script>
|
||||
|
||||
<span class="my-2 flex gap-2 text-sm font-medium text-gray-500" data-testid="album-details">
|
||||
|
|
|
|||
|
|
@ -4,12 +4,20 @@
|
|||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let id: string;
|
||||
export let albumName: string;
|
||||
export let isOwned: boolean;
|
||||
export let onUpdate: (albumName: string) => void;
|
||||
interface Props {
|
||||
id: string;
|
||||
albumName: string;
|
||||
isOwned: boolean;
|
||||
onUpdate: (albumName: string) => void;
|
||||
}
|
||||
|
||||
$: newAlbumName = albumName;
|
||||
let { id, albumName = $bindable(), isOwned, onUpdate }: Props = $props();
|
||||
|
||||
let newAlbumName = $state(albumName);
|
||||
|
||||
$effect(() => {
|
||||
newAlbumName = albumName;
|
||||
});
|
||||
|
||||
const handleUpdateName = async () => {
|
||||
if (newAlbumName === albumName) {
|
||||
|
|
@ -33,7 +41,7 @@
|
|||
|
||||
<input
|
||||
use:shortcut={{ shortcut: { key: 'Enter' }, onShortcut: (e) => e.currentTarget.blur() }}
|
||||
on:blur={handleUpdateName}
|
||||
onblur={handleUpdateName}
|
||||
class="w-[99%] mb-2 border-b-2 border-transparent text-2xl md:text-4xl lg:text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned
|
||||
? 'hover:border-gray-400'
|
||||
: 'hover:border-transparent'} bg-immich-bg focus:border-b-2 focus:border-immich-primary focus:outline-none dark:bg-immich-dark-bg dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray"
|
||||
|
|
|
|||
|
|
@ -21,11 +21,15 @@
|
|||
import { t } from 'svelte-i18n';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
export let user: UserResponseDto | undefined = undefined;
|
||||
interface Props {
|
||||
sharedLink: SharedLinkResponseDto;
|
||||
user?: UserResponseDto | undefined;
|
||||
}
|
||||
|
||||
let { sharedLink, user = undefined }: Props = $props();
|
||||
|
||||
const album = sharedLink.album as AlbumResponseDto;
|
||||
let innerWidth: number;
|
||||
let innerWidth: number = $state(0);
|
||||
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
|
|
@ -70,15 +74,15 @@
|
|||
</AssetSelectControlBar>
|
||||
{:else}
|
||||
<ControlAppBar showBackButton={false}>
|
||||
<svelte:fragment slot="leading">
|
||||
{#snippet leading()}
|
||||
<ImmichLogoSmallLink width={innerWidth} />
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
|
||||
<svelte:fragment slot="trailing">
|
||||
{#snippet trailing()}
|
||||
{#if sharedLink.allowUpload}
|
||||
<CircleIconButton
|
||||
title={$t('add_photos')}
|
||||
on:click={() => openFileUploadDialog({ albumId: album.id })}
|
||||
onclick={() => openFileUploadDialog({ albumId: album.id })}
|
||||
icon={mdiFileImagePlusOutline}
|
||||
/>
|
||||
{/if}
|
||||
|
|
@ -86,13 +90,13 @@
|
|||
{#if album.assetCount > 0 && sharedLink.allowDownload}
|
||||
<CircleIconButton
|
||||
title={$t('download')}
|
||||
on:click={() => downloadAlbum(album)}
|
||||
onclick={() => downloadAlbum(album)}
|
||||
icon={mdiFolderDownloadOutline}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<ThemeButton />
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -38,8 +38,12 @@
|
|||
import { fly } from 'svelte/transition';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let albumGroups: string[];
|
||||
export let searchQuery: string;
|
||||
interface Props {
|
||||
albumGroups: string[];
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
let { albumGroups, searchQuery = $bindable() }: Props = $props();
|
||||
|
||||
const flipOrdering = (ordering: string) => {
|
||||
return ordering === SortOrder.Asc ? SortOrder.Desc : SortOrder.Asc;
|
||||
|
|
@ -73,62 +77,38 @@
|
|||
$albumViewSettings.view === AlbumViewMode.Cover ? AlbumViewMode.List : AlbumViewMode.Cover;
|
||||
};
|
||||
|
||||
let selectedGroupOption: AlbumGroupOptionMetadata;
|
||||
let groupIcon: string;
|
||||
|
||||
$: selectedFilterOption = albumFilterNames[findFilterOption($albumViewSettings.filter)];
|
||||
|
||||
$: selectedSortOption = findSortOptionMetadata($albumViewSettings.sortBy);
|
||||
|
||||
$: {
|
||||
selectedGroupOption = findGroupOptionMetadata($albumViewSettings.groupBy);
|
||||
if (selectedGroupOption.isDisabled()) {
|
||||
selectedGroupOption = findGroupOptionMetadata(AlbumGroupBy.None);
|
||||
let groupIcon = $derived.by(() => {
|
||||
if (selectedGroupOption?.id === AlbumGroupBy.None) {
|
||||
return mdiFolderRemoveOutline;
|
||||
}
|
||||
}
|
||||
return $albumViewSettings.groupOrder === SortOrder.Desc ? mdiFolderArrowDownOutline : mdiFolderArrowUpOutline;
|
||||
});
|
||||
|
||||
// svelte-ignore reactive_declaration_non_reactive_property
|
||||
$: {
|
||||
if (selectedGroupOption.id === AlbumGroupBy.None) {
|
||||
groupIcon = mdiFolderRemoveOutline;
|
||||
} else {
|
||||
groupIcon =
|
||||
$albumViewSettings.groupOrder === SortOrder.Desc ? mdiFolderArrowDownOutline : mdiFolderArrowUpOutline;
|
||||
}
|
||||
}
|
||||
let albumFilterNames: Record<AlbumFilter, string> = $derived({
|
||||
[AlbumFilter.All]: $t('all'),
|
||||
[AlbumFilter.Owned]: $t('owned'),
|
||||
[AlbumFilter.Shared]: $t('shared'),
|
||||
});
|
||||
|
||||
// svelte-ignore reactive_declaration_non_reactive_property
|
||||
$: sortIcon = $albumViewSettings.sortOrder === SortOrder.Desc ? mdiArrowDownThin : mdiArrowUpThin;
|
||||
let selectedFilterOption = $derived(albumFilterNames[findFilterOption($albumViewSettings.filter)]);
|
||||
let selectedSortOption = $derived(findSortOptionMetadata($albumViewSettings.sortBy));
|
||||
let selectedGroupOption = $derived(findGroupOptionMetadata($albumViewSettings.groupBy));
|
||||
let sortIcon = $derived($albumViewSettings.sortOrder === SortOrder.Desc ? mdiArrowDownThin : mdiArrowUpThin);
|
||||
|
||||
// svelte-ignore reactive_declaration_non_reactive_property
|
||||
$: albumFilterNames = ((): Record<AlbumFilter, string> => {
|
||||
return {
|
||||
[AlbumFilter.All]: $t('all'),
|
||||
[AlbumFilter.Owned]: $t('owned'),
|
||||
[AlbumFilter.Shared]: $t('shared'),
|
||||
};
|
||||
})();
|
||||
let albumSortByNames: Record<AlbumSortBy, string> = $derived({
|
||||
[AlbumSortBy.Title]: $t('sort_title'),
|
||||
[AlbumSortBy.ItemCount]: $t('sort_items'),
|
||||
[AlbumSortBy.DateModified]: $t('sort_modified'),
|
||||
[AlbumSortBy.DateCreated]: $t('sort_created'),
|
||||
[AlbumSortBy.MostRecentPhoto]: $t('sort_recent'),
|
||||
[AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
|
||||
});
|
||||
|
||||
// svelte-ignore reactive_declaration_non_reactive_property
|
||||
$: albumSortByNames = ((): Record<AlbumSortBy, string> => {
|
||||
return {
|
||||
[AlbumSortBy.Title]: $t('sort_title'),
|
||||
[AlbumSortBy.ItemCount]: $t('sort_items'),
|
||||
[AlbumSortBy.DateModified]: $t('sort_modified'),
|
||||
[AlbumSortBy.DateCreated]: $t('sort_created'),
|
||||
[AlbumSortBy.MostRecentPhoto]: $t('sort_recent'),
|
||||
[AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
|
||||
};
|
||||
})();
|
||||
|
||||
// svelte-ignore reactive_declaration_non_reactive_property
|
||||
$: albumGroupByNames = ((): Record<AlbumGroupBy, string> => {
|
||||
return {
|
||||
[AlbumGroupBy.None]: $t('group_no'),
|
||||
[AlbumGroupBy.Owner]: $t('group_owner'),
|
||||
[AlbumGroupBy.Year]: $t('group_year'),
|
||||
};
|
||||
})();
|
||||
let albumGroupByNames: Record<AlbumGroupBy, string> = $derived({
|
||||
[AlbumGroupBy.None]: $t('group_no'),
|
||||
[AlbumGroupBy.Owner]: $t('group_owner'),
|
||||
[AlbumGroupBy.Year]: $t('group_year'),
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Filter Albums by Sharing Status (All, Owned, Shared) -->
|
||||
|
|
@ -147,7 +127,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Create Album -->
|
||||
<LinkButton on:click={() => createAlbumAndRedirect()}>
|
||||
<LinkButton onclick={() => createAlbumAndRedirect()}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiPlusBoxOutline} size="18" />
|
||||
<p class="hidden md:block">{$t('create_album')}</p>
|
||||
|
|
@ -184,7 +164,7 @@
|
|||
<!-- Expand Album Groups -->
|
||||
<div class="hidden xl:flex gap-0">
|
||||
<div class="block">
|
||||
<LinkButton title={$t('expand_all')} on:click={() => expandAllAlbumGroups()}>
|
||||
<LinkButton title={$t('expand_all')} onclick={() => expandAllAlbumGroups()}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiUnfoldMoreHorizontal} size="18" />
|
||||
</div>
|
||||
|
|
@ -193,7 +173,7 @@
|
|||
|
||||
<!-- Collapse Album Groups -->
|
||||
<div class="block">
|
||||
<LinkButton title={$t('collapse_all')} on:click={() => collapseAllAlbumGroups(albumGroups)}>
|
||||
<LinkButton title={$t('collapse_all')} onclick={() => collapseAllAlbumGroups(albumGroups)}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiUnfoldLessHorizontal} size="18" />
|
||||
</div>
|
||||
|
|
@ -204,7 +184,7 @@
|
|||
{/if}
|
||||
|
||||
<!-- Cover/List Display Toggle -->
|
||||
<LinkButton on:click={() => handleChangeListMode()}>
|
||||
<LinkButton onclick={() => handleChangeListMode()}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
{#if $albumViewSettings.view === AlbumViewMode.List}
|
||||
<Icon path={mdiViewGridOutline} size="18" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { groupBy } from 'lodash-es';
|
||||
import { addUsersToAlbum, deleteAlbum, type AlbumUserAddDto, type AlbumResponseDto, isHttpError } from '@immich/sdk';
|
||||
import { mdiDeleteOutline, mdiShareVariantOutline, mdiFolderDownloadOutline, mdiRenameOutline } from '@mdi/js';
|
||||
|
|
@ -38,14 +38,29 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
export let ownedAlbums: AlbumResponseDto[] = [];
|
||||
export let sharedAlbums: AlbumResponseDto[] = [];
|
||||
export let searchQuery: string = '';
|
||||
export let userSettings: AlbumViewSettings;
|
||||
export let allowEdit = false;
|
||||
export let showOwner = false;
|
||||
export let albumGroupIds: string[] = [];
|
||||
interface Props {
|
||||
ownedAlbums?: AlbumResponseDto[];
|
||||
sharedAlbums?: AlbumResponseDto[];
|
||||
searchQuery?: string;
|
||||
userSettings: AlbumViewSettings;
|
||||
allowEdit?: boolean;
|
||||
showOwner?: boolean;
|
||||
albumGroupIds?: string[];
|
||||
empty?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
ownedAlbums = $bindable([]),
|
||||
sharedAlbums = $bindable([]),
|
||||
searchQuery = '',
|
||||
userSettings,
|
||||
allowEdit = false,
|
||||
showOwner = false,
|
||||
albumGroupIds = $bindable([]),
|
||||
empty,
|
||||
}: Props = $props();
|
||||
|
||||
interface AlbumGroupOption {
|
||||
[option: string]: (order: SortOrder, albums: AlbumResponseDto[]) => AlbumGroup[];
|
||||
|
|
@ -118,25 +133,24 @@
|
|||
},
|
||||
};
|
||||
|
||||
let albums: AlbumResponseDto[] = [];
|
||||
let filteredAlbums: AlbumResponseDto[] = [];
|
||||
let groupedAlbums: AlbumGroup[] = [];
|
||||
let albums: AlbumResponseDto[] = $state([]);
|
||||
let filteredAlbums: AlbumResponseDto[] = $state([]);
|
||||
let groupedAlbums: AlbumGroup[] = $state([]);
|
||||
|
||||
let albumGroupOption: string = AlbumGroupBy.None;
|
||||
let albumGroupOption: string = $state(AlbumGroupBy.None);
|
||||
|
||||
let showShareByURLModal = false;
|
||||
let showShareByURLModal = $state(false);
|
||||
|
||||
let albumToEdit: AlbumResponseDto | null = null;
|
||||
let albumToShare: AlbumResponseDto | null = null;
|
||||
let albumToEdit: AlbumResponseDto | null = $state(null);
|
||||
let albumToShare: AlbumResponseDto | null = $state(null);
|
||||
let albumToDelete: AlbumResponseDto | null = null;
|
||||
|
||||
let contextMenuPosition: ContextMenuPosition = { x: 0, y: 0 };
|
||||
let contextMenuTargetAlbum: AlbumResponseDto | null = null;
|
||||
let isOpen = false;
|
||||
let contextMenuPosition: ContextMenuPosition = $state({ x: 0, y: 0 });
|
||||
let contextMenuTargetAlbum: AlbumResponseDto | undefined = $state();
|
||||
let isOpen = $state(false);
|
||||
|
||||
// Step 1: Filter between Owned and Shared albums, or both.
|
||||
// svelte-ignore reactive_declaration_non_reactive_property
|
||||
$: {
|
||||
run(() => {
|
||||
switch (userSettings.filter) {
|
||||
case AlbumFilter.Owned: {
|
||||
albums = ownedAlbums;
|
||||
|
|
@ -152,10 +166,10 @@
|
|||
albums = nonOwnedAlbums.length > 0 ? ownedAlbums.concat(nonOwnedAlbums) : ownedAlbums;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Step 2: Filter using the given search query.
|
||||
$: {
|
||||
run(() => {
|
||||
if (searchQuery) {
|
||||
const searchAlbumNormalized = normalizeSearchString(searchQuery);
|
||||
|
||||
|
|
@ -165,17 +179,17 @@
|
|||
} else {
|
||||
filteredAlbums = albums;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Step 3: Group albums.
|
||||
$: {
|
||||
run(() => {
|
||||
albumGroupOption = getSelectedAlbumGroupOption(userSettings);
|
||||
const groupFunc = groupOptions[albumGroupOption] ?? groupOptions[AlbumGroupBy.None];
|
||||
groupedAlbums = groupFunc(stringToSortOrder(userSettings.groupOrder), filteredAlbums);
|
||||
}
|
||||
});
|
||||
|
||||
// Step 4: Sort albums amongst each group.
|
||||
$: {
|
||||
run(() => {
|
||||
groupedAlbums = groupedAlbums.map((group) => ({
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
|
|
@ -183,9 +197,11 @@
|
|||
}));
|
||||
|
||||
albumGroupIds = groupedAlbums.map(({ id }) => id);
|
||||
}
|
||||
});
|
||||
|
||||
$: showFullContextMenu = allowEdit && contextMenuTargetAlbum && contextMenuTargetAlbum.ownerId === $user.id;
|
||||
let showFullContextMenu = $derived(
|
||||
allowEdit && contextMenuTargetAlbum && contextMenuTargetAlbum.ownerId === $user.id,
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
if (allowEdit) {
|
||||
|
|
@ -320,6 +336,10 @@
|
|||
};
|
||||
|
||||
const openShareModal = () => {
|
||||
if (!contextMenuTargetAlbum) {
|
||||
return;
|
||||
}
|
||||
|
||||
albumToShare = contextMenuTargetAlbum;
|
||||
closeAlbumContextMenu();
|
||||
};
|
||||
|
|
@ -359,7 +379,7 @@
|
|||
{/if}
|
||||
{:else}
|
||||
<!-- Empty Message -->
|
||||
<slot name="empty" />
|
||||
{@render empty?.()}
|
||||
{/if}
|
||||
|
||||
<!-- Context Menu -->
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@
|
|||
import type { AlbumSortOptionMetadata } from '$lib/utils/album-utils';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let option: AlbumSortOptionMetadata;
|
||||
interface Props {
|
||||
option: AlbumSortOptionMetadata;
|
||||
}
|
||||
|
||||
let { option }: Props = $props();
|
||||
|
||||
const handleSort = () => {
|
||||
if ($albumViewSettings.sortBy === option.id) {
|
||||
|
|
@ -13,24 +17,22 @@
|
|||
$albumViewSettings.sortOrder = option.defaultOrder;
|
||||
}
|
||||
};
|
||||
// svelte-ignore reactive_declaration_non_reactive_property
|
||||
$: albumSortByNames = ((): Record<AlbumSortBy, string> => {
|
||||
return {
|
||||
[AlbumSortBy.Title]: $t('sort_title'),
|
||||
[AlbumSortBy.ItemCount]: $t('sort_items'),
|
||||
[AlbumSortBy.DateModified]: $t('sort_modified'),
|
||||
[AlbumSortBy.DateCreated]: $t('sort_created'),
|
||||
[AlbumSortBy.MostRecentPhoto]: $t('sort_recent'),
|
||||
[AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
|
||||
};
|
||||
})();
|
||||
|
||||
let albumSortByNames: Record<AlbumSortBy, string> = $derived({
|
||||
[AlbumSortBy.Title]: $t('sort_title'),
|
||||
[AlbumSortBy.ItemCount]: $t('sort_items'),
|
||||
[AlbumSortBy.DateModified]: $t('sort_modified'),
|
||||
[AlbumSortBy.DateCreated]: $t('sort_created'),
|
||||
[AlbumSortBy.MostRecentPhoto]: $t('sort_recent'),
|
||||
[AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
|
||||
});
|
||||
</script>
|
||||
|
||||
<th class="text-sm font-medium {option.columnStyle}">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
|
||||
on:click={handleSort}
|
||||
onclick={handleSort}
|
||||
>
|
||||
{#if $albumViewSettings.sortBy === option.id}
|
||||
{#if $albumViewSettings.sortOrder === SortOrder.Desc}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@
|
|||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let onShowContextMenu: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined =
|
||||
undefined;
|
||||
interface Props {
|
||||
album: AlbumResponseDto;
|
||||
onShowContextMenu?: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined;
|
||||
}
|
||||
|
||||
let { album, onShowContextMenu = undefined }: Props = $props();
|
||||
|
||||
const showContextMenu = (position: ContextMenuPosition) => {
|
||||
onShowContextMenu?.(position, album);
|
||||
|
|
@ -20,12 +23,17 @@
|
|||
const dateLocaleString = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString($locale, dateFormats.album);
|
||||
};
|
||||
|
||||
const oncontextmenu = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
showContextMenu({ x: event.x, y: event.y });
|
||||
};
|
||||
</script>
|
||||
|
||||
<tr
|
||||
class="flex h-[50px] w-full place-items-center border-[3px] border-transparent p-2 text-center odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5"
|
||||
on:click={() => goto(`${AppRoute.ALBUMS}/${album.id}`)}
|
||||
on:contextmenu|preventDefault={(e) => showContextMenu({ x: e.x, y: e.y })}
|
||||
onclick={() => goto(`${AppRoute.ALBUMS}/${album.id}`)}
|
||||
{oncontextmenu}
|
||||
>
|
||||
<td class="text-md text-ellipsis text-left w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%] items-center">
|
||||
{album.albumName}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,13 @@
|
|||
} from '$lib/utils/album-utils';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let groupedAlbums: AlbumGroup[];
|
||||
export let albumGroupOption: string = AlbumGroupBy.None;
|
||||
export let onShowContextMenu: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined =
|
||||
undefined;
|
||||
interface Props {
|
||||
groupedAlbums: AlbumGroup[];
|
||||
albumGroupOption?: string;
|
||||
onShowContextMenu?: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined;
|
||||
}
|
||||
|
||||
let { groupedAlbums, albumGroupOption = AlbumGroupBy.None, onShowContextMenu }: Props = $props();
|
||||
</script>
|
||||
|
||||
<table class="mt-2 w-full text-left">
|
||||
|
|
@ -46,7 +49,7 @@
|
|||
>
|
||||
<tr
|
||||
class="flex w-full place-items-center p-2 md:pl-5 md:pr-5 md:pt-3 md:pb-3"
|
||||
on:click={() => toggleAlbumGroupCollapsing(albumGroup.id)}
|
||||
onclick={() => toggleAlbumGroupCollapsing(albumGroup.id)}
|
||||
aria-expanded={!isCollapsed}
|
||||
>
|
||||
<td class="text-md text-left -mb-1">
|
||||
|
|
|
|||
|
|
@ -18,15 +18,19 @@
|
|||
import { t } from 'svelte-i18n';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let onClose: () => void;
|
||||
export let onRemove: (userId: string) => void;
|
||||
export let onRefreshAlbum: () => void;
|
||||
interface Props {
|
||||
album: AlbumResponseDto;
|
||||
onClose: () => void;
|
||||
onRemove: (userId: string) => void;
|
||||
onRefreshAlbum: () => void;
|
||||
}
|
||||
|
||||
let currentUser: UserResponseDto;
|
||||
let selectedRemoveUser: UserResponseDto | null = null;
|
||||
let { album, onClose, onRemove, onRefreshAlbum }: Props = $props();
|
||||
|
||||
$: isOwned = currentUser?.id == album.ownerId;
|
||||
let currentUser: UserResponseDto | undefined = $state();
|
||||
let selectedRemoveUser: UserResponseDto | null = $state(null);
|
||||
|
||||
let isOwned = $derived(currentUser?.id == album.ownerId);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
|
|
@ -123,7 +127,7 @@
|
|||
{:else if user.id == currentUser?.id}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (selectedRemoveUser = user)}
|
||||
onclick={() => (selectedRemoveUser = user)}
|
||||
class="text-sm font-medium text-immich-primary transition-colors hover:text-immich-primary/75 dark:text-immich-dark-primary"
|
||||
>{$t('leave')}</button
|
||||
>
|
||||
|
|
|
|||
|
|
@ -18,13 +18,17 @@
|
|||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let onClose: () => void;
|
||||
export let onSelect: (selectedUsers: AlbumUserAddDto[]) => void;
|
||||
export let onShare: () => void;
|
||||
interface Props {
|
||||
album: AlbumResponseDto;
|
||||
onClose: () => void;
|
||||
onSelect: (selectedUsers: AlbumUserAddDto[]) => void;
|
||||
onShare: () => void;
|
||||
}
|
||||
|
||||
let users: UserResponseDto[] = [];
|
||||
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = {};
|
||||
let { album, onClose, onSelect, onShare }: Props = $props();
|
||||
|
||||
let users: UserResponseDto[] = $state([]);
|
||||
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = $state({});
|
||||
|
||||
const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [
|
||||
{ title: $t('role_editor'), value: AlbumUserRole.Editor, icon: mdiPencil },
|
||||
|
|
@ -32,7 +36,7 @@
|
|||
{ title: $t('remove_user'), value: 'none' },
|
||||
];
|
||||
|
||||
let sharedLinks: SharedLinkResponseDto[] = [];
|
||||
let sharedLinks: SharedLinkResponseDto[] = $state([]);
|
||||
onMount(async () => {
|
||||
await getSharedLinks();
|
||||
const data = await searchUsers();
|
||||
|
|
@ -121,11 +125,7 @@
|
|||
{#each users as user}
|
||||
{#if !Object.keys(selectedUsers).includes(user.id)}
|
||||
<div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => handleToggle(user)}
|
||||
class="flex w-full place-items-center gap-4 p-4"
|
||||
>
|
||||
<button type="button" onclick={() => handleToggle(user)} class="flex w-full place-items-center gap-4 p-4">
|
||||
<UserAvatar {user} size="md" />
|
||||
<div class="text-left flex-grow">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
|
|
@ -150,7 +150,7 @@
|
|||
fullwidth
|
||||
rounded="full"
|
||||
disabled={Object.keys(selectedUsers).length === 0}
|
||||
on:click={() =>
|
||||
onclick={() =>
|
||||
onSelect(Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })))}
|
||||
>{$t('add')}</Button
|
||||
>
|
||||
|
|
@ -163,7 +163,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="flex flex-col place-content-center place-items-center gap-2 hover:cursor-pointer"
|
||||
on:click={onShare}
|
||||
onclick={onShare}
|
||||
>
|
||||
<Icon path={mdiLink} size={24} />
|
||||
<p class="text-sm">{$t('create_link')}</p>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue