refactor: album queries (#32668)
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import MapModal from '$lib/modals/MapModal.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getAlbumMapMarkersQuery } from '$lib/queries/album.query';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
import { getAlbumMapMarkers, type AlbumResponseDto, type MapMarkerResponseDto } from '@immich/sdk';
|
||||
import { type AlbumResponseDto } from '@immich/sdk';
|
||||
import { IconButton, modalManager } from '@immich/ui';
|
||||
import { mdiMapOutline } from '@mdi/js';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
@@ -15,17 +15,12 @@
|
||||
}
|
||||
|
||||
let { album }: Props = $props();
|
||||
let cancelable: AbortController;
|
||||
|
||||
let returnToMap = $state(false);
|
||||
let mapMarkers: MapMarkerResponseDto[] = $state([]);
|
||||
|
||||
onMount(async () => {
|
||||
mapMarkers = await loadMapMarkers();
|
||||
});
|
||||
const query = createQuery(() => getAlbumMapMarkersQuery(album.id));
|
||||
const mapMarkers = $derived(query.data ?? []);
|
||||
|
||||
onDestroy(() => {
|
||||
cancelable?.abort();
|
||||
assetViewerManager.showAssetViewer(false);
|
||||
});
|
||||
|
||||
@@ -38,18 +33,6 @@
|
||||
void onClick();
|
||||
});
|
||||
|
||||
const loadMapMarkers = async () => {
|
||||
cancelable?.abort();
|
||||
cancelable = new AbortController();
|
||||
|
||||
try {
|
||||
return await getAlbumMapMarkers({ ...authManager.params, id: album.id }, { signal: cancelable.signal });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.something_went_wrong'));
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const onClick = async () => {
|
||||
const assetIds = await modalManager.show(MapModal, { mapMarkers });
|
||||
if (assetIds) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import AlbumCover from '$lib/components/album-page/AlbumCover.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { getAlbumInfo } from '@immich/sdk';
|
||||
import { getAlbumQuery } from '$lib/queries/album.query';
|
||||
import { queryClient } from '$lib/queries/client';
|
||||
import { IconButton, Text, LoadingSpinner } from '@immich/ui';
|
||||
import { mdiTrashCanOutline } from '@mdi/js';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
@@ -12,12 +13,18 @@
|
||||
}
|
||||
|
||||
let { albumId, onDelete }: Props = $props();
|
||||
|
||||
const query = createQuery(
|
||||
() => getAlbumQuery(albumId),
|
||||
() => queryClient,
|
||||
);
|
||||
const album = $derived(query.data);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#await getAlbumInfo({ ...authManager.params, id: albumId })}
|
||||
{#if query.isPending}
|
||||
<LoadingSpinner />
|
||||
{:then album}
|
||||
{:else if album}
|
||||
<div class="flex gap-2">
|
||||
<AlbumCover {album} class="size-24" />
|
||||
<p
|
||||
@@ -46,7 +53,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:catch}
|
||||
{:else}
|
||||
<div class="flex justify-between gap-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<Text>{$t('unknown')}</Text>
|
||||
@@ -63,5 +70,5 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import AlbumCardGroup from '$lib/components/album-page/AlbumCardGroup.svelte';
|
||||
import AlbumsTable from '$lib/components/album-page/AlbumsTable.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { handleDeleteAlbum, isAlbumOwner } from '$lib/services/album.service';
|
||||
import {
|
||||
@@ -32,8 +31,8 @@
|
||||
}
|
||||
|
||||
let {
|
||||
ownedAlbums = $bindable([]),
|
||||
sharedAlbums = $bindable([]),
|
||||
ownedAlbums = [],
|
||||
sharedAlbums = [],
|
||||
searchQuery = '',
|
||||
userSettings,
|
||||
allowEdit = false,
|
||||
@@ -166,29 +165,8 @@
|
||||
const albumsToRemove = ownedAlbums.filter((album) => album.assetCount === 0 && !album.albumName);
|
||||
await Promise.allSettled(albumsToRemove.map((album) => handleDeleteAlbum(album, { prompt: false, notify: false })));
|
||||
};
|
||||
|
||||
const findAndUpdate = (albums: AlbumResponseDto[], album: AlbumResponseDto) => {
|
||||
const target = albums.find(({ id }) => id === album.id);
|
||||
if (target) {
|
||||
Object.assign(target, album);
|
||||
}
|
||||
|
||||
return albums;
|
||||
};
|
||||
|
||||
const onAlbumUpdate = (album: AlbumResponseDto) => {
|
||||
ownedAlbums = findAndUpdate(ownedAlbums, album);
|
||||
sharedAlbums = findAndUpdate(sharedAlbums, album);
|
||||
};
|
||||
|
||||
const onAlbumDelete = (album: AlbumResponseDto) => {
|
||||
ownedAlbums = ownedAlbums.filter(({ id }) => id !== album.id);
|
||||
sharedAlbums = sharedAlbums.filter(({ id }) => id !== album.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<OnEvents {onAlbumUpdate} {onAlbumDelete} />
|
||||
|
||||
{#if albums.length > 0}
|
||||
{#if userSettings.view === AlbumViewMode.Cover}
|
||||
<!-- Album Cards -->
|
||||
|
||||
@@ -13,26 +13,20 @@
|
||||
import { getAssetMediaUrl } from '$lib/utils';
|
||||
import { delay, getDimensions } from '$lib/utils/asset-utils';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getParentPath } from '$lib/utils/tree-utils';
|
||||
import {
|
||||
AssetMediaSize,
|
||||
getAllAlbums,
|
||||
getAssetInfo,
|
||||
type AlbumResponseDto,
|
||||
type AssetResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { AssetMediaSize, getAssetInfo, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { Icon, IconButton, Link, LoadingSpinner, Text } from '@immich/ui';
|
||||
import { mdiCamera, mdiCameraIris, mdiClose, mdiImageOutline, mdiInformationOutline } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { slide } from 'svelte/transition';
|
||||
import PersonSidePanel from '../faces-page/PersonSidePanel.svelte';
|
||||
import OnEvents from '../OnEvents.svelte';
|
||||
import UserAvatar from '../shared-components/UserAvatar.svelte';
|
||||
import AlbumListItemDetails from './AlbumListItemDetails.svelte';
|
||||
import DetailPanelPeople from '$lib/components/asset-viewer/DetailPanelPeople.svelte';
|
||||
import { faceManager } from '$lib/stores/face.svelte';
|
||||
import { getAlbumsByAssetQuery } from '$lib/queries/album.query';
|
||||
import { createPreloadedQuery } from '$lib/queries/client';
|
||||
import { getPublicConfigQuery } from '$lib/queries/config-public.query';
|
||||
|
||||
@@ -60,20 +54,11 @@
|
||||
let previousId: string | undefined = $state();
|
||||
let previousRoute = $derived(currentAlbum?.id ? Route.viewAlbum(currentAlbum) : Route.photos());
|
||||
|
||||
const refreshAlbums = async () => {
|
||||
if (authManager.isSharedLink) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return await getAllAlbums({ assetId: asset.id });
|
||||
} catch (error) {
|
||||
handleError(error, 'Error getting asset album membership');
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
let albums = $derived(refreshAlbums());
|
||||
const albumsQuery = createQuery(() => ({
|
||||
...getAlbumsByAssetQuery(asset.id),
|
||||
enabled: !authManager.isSharedLink,
|
||||
}));
|
||||
const albums = $derived(albumsQuery.data ?? []);
|
||||
|
||||
$effect(() => {
|
||||
if (!previousId) {
|
||||
@@ -116,8 +101,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<OnEvents onAlbumAddAssets={() => (albums = refreshAlbums())} />
|
||||
|
||||
{#if !assetViewerManager.isEditFacesPanelOpen}
|
||||
<section class="relative p-2">
|
||||
<div class="flex place-items-center gap-2">
|
||||
@@ -345,39 +328,37 @@
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#await albums then albums}
|
||||
{#if albums.length > 0}
|
||||
<section class="p-6 dark:text-immich-dark-fg">
|
||||
<div class="pb-4">
|
||||
<Text size="small" color="muted">{$t('appears_in')}</Text>
|
||||
</div>
|
||||
{#each albums as album (album.id)}
|
||||
<a href={Route.viewAlbum(album)}>
|
||||
<div class="flex items-center gap-4 pt-2 hover:cursor-pointer">
|
||||
<div>
|
||||
<img
|
||||
alt={album.albumName}
|
||||
class="size-12.5 rounded-sm object-cover"
|
||||
src={album.albumThumbnailAssetId &&
|
||||
getAssetMediaUrl({ id: album.albumThumbnailAssetId, size: AssetMediaSize.Preview })}
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
{#if albums.length > 0}
|
||||
<section class="p-6 dark:text-immich-dark-fg">
|
||||
<div class="pb-4">
|
||||
<Text size="small" color="muted">{$t('appears_in')}</Text>
|
||||
</div>
|
||||
{#each albums as album (album.id)}
|
||||
<a href={Route.viewAlbum(album)}>
|
||||
<div class="flex items-center gap-4 pt-2 hover:cursor-pointer">
|
||||
<div>
|
||||
<img
|
||||
alt={album.albumName}
|
||||
class="size-12.5 rounded-sm object-cover"
|
||||
src={album.albumThumbnailAssetId &&
|
||||
getAssetMediaUrl({ id: album.albumThumbnailAssetId, size: AssetMediaSize.Preview })}
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-auto">
|
||||
<p class="dark:text-immich-dark-primary">{album.albumName}</p>
|
||||
<div class="flex flex-col gap-0 text-sm">
|
||||
<div>
|
||||
<AlbumListItemDetails {album} />
|
||||
</div>
|
||||
<div class="my-auto">
|
||||
<p class="dark:text-immich-dark-primary">{album.albumName}</p>
|
||||
<div class="flex flex-col gap-0 text-sm">
|
||||
<div>
|
||||
<AlbumListItemDetails {album} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if authManager.authenticated && authManager.preferences.tags.enabled}
|
||||
<section class="relative px-2 pb-12 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { tick } from 'svelte';
|
||||
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||
import RecentAlbums from '$lib/components/shared-components/side-bar/RecentAlbums.svelte';
|
||||
import { albumFactory } from '@test-data/factories/album-factory';
|
||||
@@ -16,13 +15,8 @@ describe('RecentAlbums component', () => {
|
||||
sdkMock.getAllAlbums.mockResolvedValueOnce([...albums]);
|
||||
render(RecentAlbums);
|
||||
|
||||
const links = await screen.findAllByRole('link');
|
||||
expect(sdkMock.getAllAlbums).toHaveBeenCalledOnce();
|
||||
|
||||
// wtf
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
const links = screen.getAllByRole('link');
|
||||
expect(links).toHaveLength(3);
|
||||
// @ts-expect-error pending a type fix: https://github.com/testing-library/jest-dom/pull/742/changes
|
||||
expect(links[0]).toHaveAttribute('href', `/albums/${albums[2].id}`);
|
||||
|
||||
@@ -1,28 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { getRecentAlbumsQuery } from '$lib/queries/album.query';
|
||||
import { queryClient } from '$lib/queries/client';
|
||||
import { Route } from '$lib/route';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { getAssetMediaUrl } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getAllAlbums } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
|
||||
let albums = $state(userInteraction.recentAlbums);
|
||||
|
||||
const refreshAlbums = async () => {
|
||||
try {
|
||||
const allAlbums = await getAllAlbums({});
|
||||
albums = allAlbums.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()).slice(0, 3);
|
||||
userInteraction.recentAlbums = albums;
|
||||
} catch (error) {
|
||||
handleError(error, $t('failed_to_load_assets'));
|
||||
}
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
if (!userInteraction.recentAlbums) {
|
||||
void refreshAlbums();
|
||||
}
|
||||
});
|
||||
const query = createQuery(getRecentAlbumsQuery, () => queryClient);
|
||||
const albums = $derived(query.data ?? []);
|
||||
</script>
|
||||
|
||||
{#each albums as album (album.id)}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type {
|
||||
AlbumResponseDto,
|
||||
AlbumUserRole,
|
||||
ApiKeyResponseDto,
|
||||
AssetResponseDto,
|
||||
IntegrityReport,
|
||||
@@ -47,12 +45,6 @@ export type Events = {
|
||||
|
||||
AlbumAddAssets: [{ assetIds: string[]; albumIds: string[] }];
|
||||
AlbumRemoveAssets: [{ assetIds: string[]; albumIds: string[] }];
|
||||
AlbumCreate: [AlbumResponseDto];
|
||||
AlbumUpdate: [AlbumResponseDto];
|
||||
AlbumDelete: [AlbumResponseDto];
|
||||
AlbumShare: [];
|
||||
AlbumUserUpdate: [{ albumId: string; userId: string; role: AlbumUserRole }];
|
||||
AlbumUserDelete: [{ albumId: string; userId: string }];
|
||||
|
||||
PersonUpdate: [PersonResponseDto];
|
||||
PersonShare: [{ personId: string }];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import AlbumCover from '$lib/components/album-page/AlbumCover.svelte';
|
||||
import { handleUpdateAlbum } from '$lib/services/album.service';
|
||||
import { updateAlbumInfoMutation } from '$lib/queries/album.query';
|
||||
import { executeMutation } from '$lib/queries/client';
|
||||
import { type AlbumResponseDto } from '@immich/sdk';
|
||||
import { Field, FormModal, Input, Textarea } from '@immich/ui';
|
||||
import { mdiRenameOutline } from '@mdi/js';
|
||||
@@ -17,10 +18,11 @@
|
||||
let description = $state(album.description);
|
||||
|
||||
const onSubmit = async () => {
|
||||
const success = await handleUpdateAlbum(album, { albumName, description: description || null });
|
||||
if (success) {
|
||||
onClose();
|
||||
}
|
||||
await executeMutation(
|
||||
updateAlbumInfoMutation,
|
||||
[{ id: album.id, updateAlbumDto: { albumName, description: description || null } }],
|
||||
{ onSuccess: onClose },
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
<script lang="ts">
|
||||
import AlbumSharedLink from '$lib/components/album-page/AlbumSharedLink.svelte';
|
||||
import HeaderActionButton from '$lib/components/HeaderActionButton.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import UserAvatar from '$lib/components/shared-components/UserAvatar.svelte';
|
||||
import { getAlbumActions, handleRemoveUserFromAlbum, handleUpdateUserAlbumRole } from '$lib/services/album.service';
|
||||
import { getAlbumQuery, updateAlbumInfoMutation } from '$lib/queries/album.query';
|
||||
import { executeMutation, queryClient } from '$lib/queries/client';
|
||||
import { getAllSharedLinksQuery } from '$lib/queries/shared-link.query';
|
||||
import {
|
||||
getAlbumActions,
|
||||
handleRemoveUserFromAlbum,
|
||||
handleUpdateAlbum,
|
||||
handleUpdateUserAlbumRole,
|
||||
} from '$lib/services/album.service';
|
||||
import { AlbumUserRole, AssetOrder, getAlbumInfo, type AlbumResponseDto, type UserResponseDto } from '@immich/sdk';
|
||||
AlbumUserRole,
|
||||
AssetOrder,
|
||||
type AlbumResponseDto,
|
||||
type UpdateAlbumDto,
|
||||
type UserResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { Field, HStack, Modal, ModalBody, Select, Stack, Switch, Text, type SelectOption } from '@immich/ui';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { queryClient } from '$lib/queries/client';
|
||||
import { getAllSharedLinksQuery } from '$lib/queries/shared-link.query';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
@@ -22,7 +23,13 @@
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
let { album, readOnly = false, onClose }: Props = $props();
|
||||
let { album: initialAlbum, readOnly = false, onClose }: Props = $props();
|
||||
|
||||
const query = createQuery(
|
||||
() => getAlbumQuery(initialAlbum.id),
|
||||
() => queryClient,
|
||||
);
|
||||
const album = $derived(query.data ?? initialAlbum);
|
||||
|
||||
const handleRoleSelect = async (user: UserResponseDto, role: AlbumUserRole | 'none') => {
|
||||
if (role === 'none') {
|
||||
@@ -33,26 +40,18 @@
|
||||
await handleUpdateUserAlbumRole({ albumId: album.id, userId: user.id, role });
|
||||
};
|
||||
|
||||
const refreshAlbum = async () => {
|
||||
album = await getAlbumInfo({ id: album.id });
|
||||
};
|
||||
|
||||
const onAlbumUserDelete = async ({ userId }: { userId: string }) => {
|
||||
album.albumUsers = album.albumUsers.filter(({ user: { id } }) => id !== userId);
|
||||
await refreshAlbum();
|
||||
};
|
||||
|
||||
const { AddUsers, CreateSharedLink } = $derived(getAlbumActions($t, album));
|
||||
|
||||
const query = createQuery(
|
||||
const updateAlbum = (updateAlbumDto: UpdateAlbumDto) =>
|
||||
executeMutation(updateAlbumInfoMutation, [{ id: album.id, updateAlbumDto }]);
|
||||
|
||||
const sharedLinksQuery = createQuery(
|
||||
() => getAllSharedLinksQuery({ albumId: album.id }),
|
||||
() => queryClient,
|
||||
);
|
||||
const sharedLinks = $derived(query.data ?? []);
|
||||
const sharedLinks = $derived(sharedLinksQuery.data ?? []);
|
||||
</script>
|
||||
|
||||
<OnEvents {onAlbumUserDelete} onAlbumShare={refreshAlbum} onAlbumUpdate={(newAlbum) => (album = newAlbum)} />
|
||||
|
||||
<Modal title={readOnly ? $t('album') : $t('options')} {onClose} size="small">
|
||||
<ModalBody>
|
||||
<Stack gap={6}>
|
||||
@@ -67,14 +66,14 @@
|
||||
{ label: $t('newest_first'), value: AssetOrder.Desc },
|
||||
{ label: $t('oldest_first'), value: AssetOrder.Asc },
|
||||
]}
|
||||
onChange={(value) => handleUpdateAlbum(album, { order: value })}
|
||||
onChange={(value) => updateAlbum({ order: value })}
|
||||
/>
|
||||
</Field>
|
||||
{/if}
|
||||
<Field label={$t('comments_and_likes')} description={$t('let_others_respond')} disabled={readOnly}>
|
||||
<Switch
|
||||
checked={album.isActivityEnabled}
|
||||
onCheckedChange={(checked) => handleUpdateAlbum(album, { isActivityEnabled: checked })}
|
||||
onCheckedChange={(checked) => updateAlbum({ isActivityEnabled: checked })}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
@@ -5,19 +5,25 @@
|
||||
AlbumModalRowType,
|
||||
isSelectableRowType,
|
||||
} from '$lib/components/shared-components/album-selection/album-selection-utils';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { createAlbumMutation, getAllAlbumsQuery, getRecentAlbumsQuery } from '$lib/queries/album.query';
|
||||
import { executeMutation, queryClient } from '$lib/queries/client';
|
||||
import { albumViewSettings } from '$lib/stores/preferences.store';
|
||||
import { createAlbum, getAllAlbums, type AlbumResponseDto } from '@immich/sdk';
|
||||
import { type AlbumResponseDto } from '@immich/sdk';
|
||||
import { Button, Icon, Modal, ModalBody, ModalFooter, Text } from '@immich/ui';
|
||||
import { mdiKeyboardReturn } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { t } from 'svelte-i18n';
|
||||
import AlbumListItem from '../components/asset-viewer/AlbumListItem.svelte';
|
||||
import NewAlbumListItem from '../components/shared-components/album-selection/NewAlbumListItem.svelte';
|
||||
|
||||
let albums: AlbumResponseDto[] = $state([]);
|
||||
let recentAlbums: AlbumResponseDto[] = $state([]);
|
||||
let loading = $state(true);
|
||||
const queries = {
|
||||
albums: createQuery(getAllAlbumsQuery, () => queryClient),
|
||||
recentAlbums: createQuery(getRecentAlbumsQuery, () => queryClient),
|
||||
};
|
||||
|
||||
const albums = $derived(queries.albums.data ?? []);
|
||||
const recentAlbums = $derived(queries.recentAlbums.data ?? []);
|
||||
const loading = $derived(queries.albums.isPending);
|
||||
let search = $state('');
|
||||
let selectedRowIndex: number = $state(-1);
|
||||
|
||||
@@ -28,12 +34,6 @@
|
||||
|
||||
let { onClose, selectedItemsCount }: Props = $props();
|
||||
|
||||
onMount(async () => {
|
||||
albums = await getAllAlbums({});
|
||||
recentAlbums = [...albums].sort((a, b) => (new Date(a.updatedAt) > new Date(b.updatedAt) ? -1 : 1)).slice(0, 3);
|
||||
loading = false;
|
||||
});
|
||||
|
||||
const multiSelectedAlbumIds: string[] = $state([]);
|
||||
const multiSelectActive = $derived(multiSelectedAlbumIds.length > 0);
|
||||
|
||||
@@ -43,11 +43,10 @@
|
||||
);
|
||||
const selectableRowCount = $derived(albumModalRows.filter((row) => isSelectableRowType(row.type)).length);
|
||||
|
||||
const onNewAlbum = async (name: string) => {
|
||||
const album = await createAlbum({ createAlbumDto: { albumName: name } });
|
||||
eventManager.emit('AlbumCreate', album);
|
||||
onClose([album]);
|
||||
};
|
||||
const onNewAlbum = (name: string) =>
|
||||
executeMutation(createAlbumMutation, [{ createAlbumDto: { albumName: name } }], {
|
||||
onSuccess: (album) => onClose([album]),
|
||||
});
|
||||
|
||||
const handleAlbumClick = (album?: AlbumResponseDto) => {
|
||||
if (multiSelectActive) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte';
|
||||
import { addAssetsToAlbums } from '$lib/services/album.service';
|
||||
import { addAssetsToAlbumsMutation } from '$lib/queries/album.query';
|
||||
import { executeMutation } from '$lib/queries/client';
|
||||
import { type AlbumResponseDto } from '@immich/sdk';
|
||||
|
||||
type Props = {
|
||||
@@ -17,10 +18,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await addAssetsToAlbums(albumIds, assetIds, { notify: true });
|
||||
if (success) {
|
||||
onClose();
|
||||
}
|
||||
await executeMutation(addAssetsToAlbumsMutation, { albumIds, assetIds }, { onSuccess: onClose });
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,8 +1,224 @@
|
||||
import { getAlbumInfo } from '@immich/sdk';
|
||||
import { queryOptions } from '@tanstack/svelte-query';
|
||||
import {
|
||||
addAssetsToAlbum,
|
||||
addAssetsToAlbums,
|
||||
addUsersToAlbum,
|
||||
AlbumUserRole,
|
||||
BulkIdErrorReason,
|
||||
createAlbum,
|
||||
deleteAlbum,
|
||||
getAlbumInfo,
|
||||
getAlbumMapMarkers,
|
||||
getAlbumStatistics,
|
||||
getAllAlbums,
|
||||
removeAssetFromAlbum,
|
||||
removeUserFromAlbum,
|
||||
updateAlbumInfo,
|
||||
updateAlbumUser,
|
||||
type AlbumResponseDto,
|
||||
type AlbumsAddAssetsResponseDto,
|
||||
type BulkIdResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { toastManager } from '@immich/ui';
|
||||
import { mutationOptions, queryOptions, type MutationMeta } from '@tanstack/svelte-query';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { asMutation, queryClient } from '$lib/queries/client';
|
||||
import { Route } from '$lib/route';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
|
||||
type AlbumsFilter = { isOwned?: boolean; isShared?: boolean };
|
||||
|
||||
export const albumsQueryKey = ['albums'];
|
||||
|
||||
const isOwned = (album: AlbumResponseDto) =>
|
||||
authManager.authenticated &&
|
||||
album.albumUsers.some(({ role, user }) => role === AlbumUserRole.Owner && user.id === authManager.user.id);
|
||||
|
||||
const matchesFilter = (filter: AlbumsFilter) => (album: AlbumResponseDto) =>
|
||||
(filter.isOwned === undefined || isOwned(album) === filter.isOwned) &&
|
||||
(filter.isShared === undefined || album.shared === filter.isShared);
|
||||
|
||||
const findOrFail = (albums: AlbumResponseDto[], id: string) => {
|
||||
const album = albums.find((album) => album.id === id);
|
||||
if (!album) {
|
||||
throw new Error(`Album ${id} not found`);
|
||||
}
|
||||
return album;
|
||||
};
|
||||
|
||||
export const getAllAlbumsQuery = (filter: AlbumsFilter = {}) =>
|
||||
queryOptions({
|
||||
queryKey: albumsQueryKey,
|
||||
queryFn: ({ signal }) => getAllAlbums({}, { signal }),
|
||||
select: (albums) => albums.filter(matchesFilter(filter)),
|
||||
});
|
||||
|
||||
export const getRecentAlbumsQuery = () =>
|
||||
queryOptions({
|
||||
...getAllAlbumsQuery(),
|
||||
select: (albums) =>
|
||||
[...albums].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()).slice(0, 3),
|
||||
});
|
||||
|
||||
export const getAlbumQuery = (id: string) =>
|
||||
queryOptions({
|
||||
...getAllAlbumsQuery(),
|
||||
select: (albums) => findOrFail(albums, id),
|
||||
});
|
||||
|
||||
export const getAlbumsByAssetQuery = (assetId: string) =>
|
||||
queryOptions({
|
||||
queryKey: [...albumsQueryKey, 'asset', assetId],
|
||||
queryFn: ({ signal }) => getAllAlbums({ assetId }, { signal }),
|
||||
});
|
||||
|
||||
export const getAlbumStatisticsQuery = () =>
|
||||
queryOptions({
|
||||
queryKey: [...albumsQueryKey, 'statistics'],
|
||||
queryFn: ({ signal }) => getAlbumStatistics({ signal }),
|
||||
});
|
||||
|
||||
export const getAlbumInfoQuery = (id: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['album', id],
|
||||
queryFn: ({ signal }) => getAlbumInfo({ id }, { signal }),
|
||||
queryFn: ({ signal }) => getAlbumInfo({ ...authManager.params, id }, { signal }),
|
||||
});
|
||||
|
||||
export const getAlbumMapMarkersQuery = (id: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['album', id, 'map-markers'],
|
||||
queryFn: ({ signal }) => getAlbumMapMarkers({ ...authManager.params, id }, { signal }),
|
||||
});
|
||||
|
||||
export const invalidateAlbums = () => queryClient.invalidateQueries({ queryKey: albumsQueryKey });
|
||||
|
||||
export const invalidateAlbum = (id: string) =>
|
||||
queryClient.invalidateQueries({ queryKey: getAlbumInfoQuery(id).queryKey, exact: true });
|
||||
|
||||
const onAlbumChange = async (album: AlbumResponseDto) => {
|
||||
queryClient.setQueryData(getAlbumInfoQuery(album.id).queryKey, album);
|
||||
await Promise.all([invalidateAlbum(album.id), invalidateAlbums()]);
|
||||
};
|
||||
|
||||
const onAlbumAssetsChange = (ids: string[]) =>
|
||||
Promise.all([...ids.map((id) => invalidateAlbum(id)), invalidateAlbums()]);
|
||||
|
||||
export const createAlbumMutation = () =>
|
||||
asMutation(createAlbum, {
|
||||
onSuccess: onAlbumChange,
|
||||
meta: { error: { toast: 'errors.failed_to_create_album' } },
|
||||
});
|
||||
|
||||
const defaultMeta: MutationMeta = {
|
||||
success: { toast: 'album_info_updated' },
|
||||
error: { toast: 'errors.unable_to_update_album_info' },
|
||||
};
|
||||
|
||||
export const updateAlbumInfoMutation = (meta?: MutationMeta) =>
|
||||
asMutation(updateAlbumInfo, { onSuccess: onAlbumChange, meta: meta ?? defaultMeta });
|
||||
|
||||
export const deleteAlbumMutation = (meta?: MutationMeta) =>
|
||||
asMutation(deleteAlbum, {
|
||||
onSuccess: (_, [{ id }]) => {
|
||||
queryClient.removeQueries({ queryKey: ['album', id] });
|
||||
return invalidateAlbums();
|
||||
},
|
||||
meta: meta ?? { error: { toast: 'errors.unable_to_delete_album' } },
|
||||
});
|
||||
|
||||
const notifyAddToAlbum = ($t: MessageFormatter, albumId: string, assetIds: string[], results: BulkIdResponseDto[]) => {
|
||||
const successCount = results.filter(({ success }) => success).length;
|
||||
const duplicateCount = results.filter(({ error }) => error === 'duplicate').length;
|
||||
let description: string | undefined;
|
||||
|
||||
if (duplicateCount === assetIds.length) {
|
||||
description = $t('assets_were_part_of_album_count', { values: { count: duplicateCount } });
|
||||
} else if (successCount === assetIds.length) {
|
||||
description = $t('assets_added_to_album_count', { values: { count: successCount } });
|
||||
} else if (successCount > 0) {
|
||||
description = $t('assets_added_to_album_partial_count', { values: { successCount, totalCount: assetIds.length } });
|
||||
}
|
||||
|
||||
const button = { label: $t('view_album'), onclick: () => goto(Route.viewAlbum({ id: albumId })) };
|
||||
if (description) {
|
||||
toastManager.primary({ description, button }, { timeout: 5000 });
|
||||
return;
|
||||
}
|
||||
|
||||
toastManager.danger(
|
||||
{ description: $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } }), button },
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
};
|
||||
|
||||
const notifyAddToAlbums = (
|
||||
$t: MessageFormatter,
|
||||
albumIds: string[],
|
||||
assetIds: string[],
|
||||
results: AlbumsAddAssetsResponseDto,
|
||||
) => {
|
||||
if (results.error === BulkIdErrorReason.Duplicate) {
|
||||
toastManager.info($t('assets_were_part_of_albums_count', { values: { count: assetIds.length } }));
|
||||
} else if (results.error) {
|
||||
toastManager.warning($t('assets_cannot_be_added_to_albums', { values: { count: assetIds.length } }));
|
||||
} else {
|
||||
toastManager.primary(
|
||||
$t('assets_added_to_albums_count', {
|
||||
values: { albumTotal: albumIds.length, assetTotal: assetIds.length },
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const addAssetsToAlbumsMutation = ({ notify = true }: { notify?: boolean } = {}) =>
|
||||
mutationOptions({
|
||||
mutationFn: ({
|
||||
albumIds,
|
||||
assetIds,
|
||||
}: {
|
||||
albumIds: string[];
|
||||
assetIds: string[];
|
||||
}): Promise<BulkIdResponseDto[] | AlbumsAddAssetsResponseDto> =>
|
||||
albumIds.length === 1
|
||||
? addAssetsToAlbum({ ...authManager.params, id: albumIds[0], bulkIdsDto: { ids: assetIds } })
|
||||
: addAssetsToAlbums({ ...authManager.params, albumsAddAssetsDto: { albumIds, assetIds } }),
|
||||
onSuccess: async (results, { albumIds, assetIds }) => {
|
||||
eventManager.emit('AlbumAddAssets', { assetIds, albumIds });
|
||||
if (notify) {
|
||||
const $t = await getFormatter();
|
||||
if (Array.isArray(results)) {
|
||||
notifyAddToAlbum($t, albumIds[0], assetIds, results);
|
||||
} else {
|
||||
notifyAddToAlbums($t, albumIds, assetIds, results);
|
||||
}
|
||||
}
|
||||
await onAlbumAssetsChange(albumIds);
|
||||
},
|
||||
meta: { error: { toast: 'errors.error_adding_assets_to_album' } },
|
||||
});
|
||||
|
||||
export const removeAssetFromAlbumMutation = () =>
|
||||
asMutation(removeAssetFromAlbum, {
|
||||
onSuccess: (_, [{ id }]) => onAlbumAssetsChange([id]),
|
||||
meta: { error: { toast: 'errors.error_removing_assets_from_album' } },
|
||||
});
|
||||
|
||||
export const addUsersToAlbumMutation = () =>
|
||||
asMutation(addUsersToAlbum, {
|
||||
onSuccess: onAlbumChange,
|
||||
meta: { error: { toast: 'errors.error_adding_users_to_album' } },
|
||||
});
|
||||
|
||||
export const updateAlbumUserMutation = () =>
|
||||
asMutation(updateAlbumUser, {
|
||||
onSuccess: (_, [{ id }]) => onAlbumAssetsChange([id]),
|
||||
meta: { error: { toast: 'errors.unable_to_change_album_user_role' } },
|
||||
});
|
||||
|
||||
export const removeUserFromAlbumMutation = () =>
|
||||
asMutation(removeUserFromAlbum, {
|
||||
onSuccess: (_, [{ id }]) => onAlbumAssetsChange([id]),
|
||||
meta: { error: { toast: 'errors.unable_to_remove_album_users' } },
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type SharedLinkResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { queryOptions } from '@tanstack/svelte-query';
|
||||
import { getAlbumInfoQuery } from '$lib/queries/album.query';
|
||||
import { invalidateAlbum, invalidateAlbums } from '$lib/queries/album.query';
|
||||
import { asMutation, queryClient } from '$lib/queries/client';
|
||||
|
||||
const findOrFail = (sharedLinks: SharedLinkResponseDto[], id: string) => {
|
||||
@@ -31,15 +31,19 @@ export const getSharedLinkQuery = (id: string) =>
|
||||
select: (sharedLinks) => findOrFail(sharedLinks, id),
|
||||
});
|
||||
|
||||
const invalidateAlbum = (albumId?: string | null) =>
|
||||
albumId ? queryClient.invalidateQueries({ queryKey: getAlbumInfoQuery(albumId).queryKey }) : undefined;
|
||||
const onAlbumSharingChange = async (albumId?: string | null) => {
|
||||
await invalidateAlbums();
|
||||
if (albumId) {
|
||||
await invalidateAlbum(albumId);
|
||||
}
|
||||
};
|
||||
|
||||
export const createSharedLinkMutation = () =>
|
||||
asMutation(createSharedLink, {
|
||||
onSuccess: async (_, [{ sharedLinkCreateDto }]) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: getAllSharedLinksQuery().queryKey, exact: true }),
|
||||
invalidateAlbum(sharedLinkCreateDto.albumId),
|
||||
onAlbumSharingChange(sharedLinkCreateDto.albumId),
|
||||
]);
|
||||
},
|
||||
meta: { error: { toast: 'errors.failed_to_create_shared_link' } },
|
||||
@@ -60,7 +64,7 @@ export const removeSharedLinkMutation = () =>
|
||||
queryClient.setQueryData(getAllSharedLinksQuery().queryKey, (sharedLinks) =>
|
||||
sharedLinks?.filter((sharedLink) => sharedLink.id !== id),
|
||||
);
|
||||
await invalidateAlbum(albumId);
|
||||
await onAlbumSharingChange(albumId);
|
||||
},
|
||||
meta: { success: { toast: 'deleted_shared_link' }, error: { toast: 'errors.unable_to_delete_shared_link' } },
|
||||
});
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
import {
|
||||
addAssetsToAlbum as addToAlbum,
|
||||
addAssetsToAlbums as addToAlbums,
|
||||
addUsersToAlbum,
|
||||
AlbumUserRole,
|
||||
BulkIdErrorReason,
|
||||
deleteAlbum,
|
||||
removeUserFromAlbum,
|
||||
updateAlbumInfo,
|
||||
updateAlbumUser,
|
||||
type AlbumResponseDto,
|
||||
type AlbumsAddAssetsResponseDto,
|
||||
type AssetResponseDto,
|
||||
type BulkIdResponseDto,
|
||||
type UpdateAlbumDto,
|
||||
type UserResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||
import { AlbumUserRole, type AlbumResponseDto, type UserResponseDto } from '@immich/sdk';
|
||||
import { modalManager, type ActionItem } from '@immich/ui';
|
||||
import {
|
||||
mdiDownload,
|
||||
mdiImageOutline,
|
||||
@@ -31,19 +15,26 @@ import {
|
||||
} from '@mdi/js';
|
||||
import { type MessageFormatter } from 'svelte-i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import AddUsersModal from '$lib/modals/AddUsersModal.svelte';
|
||||
import AlbumEditModal from '$lib/modals/AlbumEditModal.svelte';
|
||||
import AlbumOptionsModal from '$lib/modals/AlbumOptionsModal.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import {
|
||||
addAssetsToAlbumsMutation,
|
||||
addUsersToAlbumMutation,
|
||||
deleteAlbumMutation,
|
||||
removeUserFromAlbumMutation,
|
||||
updateAlbumInfoMutation,
|
||||
updateAlbumUserMutation,
|
||||
} from '$lib/queries/album.query';
|
||||
import { executeMutation } from '$lib/queries/client';
|
||||
import { Route } from '$lib/route';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { createAlbumAndRedirect } from '$lib/utils/album-utils';
|
||||
import { downloadArchive } from '$lib/utils/asset-utils';
|
||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
|
||||
export const isAlbumOwner = (album: AlbumResponseDto) => album.albumUsers[0].user.id === authManager.user.id;
|
||||
@@ -129,7 +120,7 @@ export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto) =
|
||||
return { AddUsers, CreateSharedLink, Delete, Download, Edit, Leave, Options, Share };
|
||||
};
|
||||
|
||||
export const getAlbumAssetActions = ($t: MessageFormatter, album: AlbumResponseDto, asset: AssetResponseDto) => {
|
||||
export const getAlbumAssetActions = ($t: MessageFormatter, album: AlbumResponseDto, asset: { id: string }) => {
|
||||
const SetCover: ActionItem = {
|
||||
title: $t('set_as_album_cover'),
|
||||
icon: mdiImageOutline,
|
||||
@@ -146,11 +137,7 @@ export const getAlbumAssetsActions = ($t: MessageFormatter, album: AlbumResponse
|
||||
icon: mdiPlusBoxOutline,
|
||||
$if: () => assets.length > 0,
|
||||
onAction: () =>
|
||||
addAssetsToAlbums(
|
||||
[album.id],
|
||||
assets.map(({ id }) => id),
|
||||
{ notify: true },
|
||||
).then(() => undefined),
|
||||
executeMutation(addAssetsToAlbumsMutation, { albumIds: [album.id], assetIds: assets.map(({ id }) => id) }),
|
||||
};
|
||||
|
||||
const Upload: ActionItem = {
|
||||
@@ -163,78 +150,13 @@ export const getAlbumAssetsActions = ($t: MessageFormatter, album: AlbumResponse
|
||||
return { AddAssets, Upload };
|
||||
};
|
||||
|
||||
export const addAssetsToAlbums = async (albumIds: string[], assetIds: string[], { notify }: { notify: boolean }) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
if (albumIds.length === 1) {
|
||||
const albumId = albumIds[0];
|
||||
const results = await addToAlbum({ ...authManager.params, id: albumId, bulkIdsDto: { ids: assetIds } });
|
||||
if (notify) {
|
||||
notifyAddToAlbum($t, albumId, assetIds, results);
|
||||
}
|
||||
}
|
||||
|
||||
if (albumIds.length > 1) {
|
||||
const results = await addToAlbums({ ...authManager.params, albumsAddAssetsDto: { albumIds, assetIds } });
|
||||
if (notify) {
|
||||
notifyAddToAlbums($t, albumIds, assetIds, results);
|
||||
}
|
||||
}
|
||||
|
||||
eventManager.emit('AlbumAddAssets', { assetIds, albumIds });
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.error_adding_assets_to_album'));
|
||||
return false;
|
||||
const leaveAlbumPage = (album: AlbumResponseDto) => {
|
||||
if (page.params.albumId === album.id) {
|
||||
void goto(Route.albums());
|
||||
}
|
||||
};
|
||||
|
||||
const notifyAddToAlbum = ($t: MessageFormatter, albumId: string, assetIds: string[], results: BulkIdResponseDto[]) => {
|
||||
const successCount = results.filter(({ success }) => success).length;
|
||||
const duplicateCount = results.filter(({ error }) => error === 'duplicate').length;
|
||||
let description: string | undefined;
|
||||
|
||||
if (duplicateCount === assetIds.length) {
|
||||
description = $t('assets_were_part_of_album_count', { values: { count: duplicateCount } });
|
||||
} else if (successCount === assetIds.length) {
|
||||
description = $t('assets_added_to_album_count', { values: { count: successCount } });
|
||||
} else if (successCount > 0) {
|
||||
description = $t('assets_added_to_album_partial_count', { values: { successCount, totalCount: assetIds.length } });
|
||||
}
|
||||
|
||||
const button = { label: $t('view_album'), onclick: () => goto(Route.viewAlbum({ id: albumId })) };
|
||||
if (description) {
|
||||
toastManager.primary({ description, button }, { timeout: 5000 });
|
||||
return;
|
||||
}
|
||||
|
||||
toastManager.danger(
|
||||
{ description: $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } }), button },
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
};
|
||||
|
||||
const notifyAddToAlbums = (
|
||||
$t: MessageFormatter,
|
||||
albumIds: string[],
|
||||
assetIds: string[],
|
||||
results: AlbumsAddAssetsResponseDto,
|
||||
) => {
|
||||
if (results.error === BulkIdErrorReason.Duplicate) {
|
||||
toastManager.info($t('assets_were_part_of_albums_count', { values: { count: assetIds.length } }));
|
||||
} else if (results.error) {
|
||||
toastManager.warning($t('assets_cannot_be_added_to_albums', { values: { count: assetIds.length } }));
|
||||
} else {
|
||||
toastManager.primary(
|
||||
$t('assets_added_to_albums_count', {
|
||||
values: { albumTotal: albumIds.length, assetTotal: assetIds.length },
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleUpdateUserAlbumRole = async ({
|
||||
export const handleUpdateUserAlbumRole = ({
|
||||
albumId,
|
||||
userId,
|
||||
role,
|
||||
@@ -242,28 +164,14 @@ export const handleUpdateUserAlbumRole = async ({
|
||||
albumId: string;
|
||||
userId: string;
|
||||
role: AlbumUserRole;
|
||||
}) => {
|
||||
const $t = await getFormatter();
|
||||
}) => executeMutation(updateAlbumUserMutation, [{ id: albumId, userId, updateAlbumUserDto: { role } }]);
|
||||
|
||||
try {
|
||||
await updateAlbumUser({ id: albumId, userId, updateAlbumUserDto: { role } });
|
||||
eventManager.emit('AlbumUserUpdate', { albumId, userId, role });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_change_album_user_role'));
|
||||
}
|
||||
};
|
||||
|
||||
export const handleAddUsersToAlbum = async (album: AlbumResponseDto, users: UserResponseDto[]) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
await addUsersToAlbum({ id: album.id, addUsersDto: { albumUsers: users.map(({ id }) => ({ userId: id })) } });
|
||||
eventManager.emit('AlbumShare');
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.error_adding_users_to_album'));
|
||||
}
|
||||
};
|
||||
export const handleAddUsersToAlbum = (album: AlbumResponseDto, users: UserResponseDto[], onSuccess?: () => void) =>
|
||||
executeMutation(
|
||||
addUsersToAlbumMutation,
|
||||
[{ id: album.id, addUsersDto: { albumUsers: users.map(({ id }) => ({ userId: id })) } }],
|
||||
{ onSuccess },
|
||||
);
|
||||
|
||||
export const handleRemoveUserFromAlbum = async (album: AlbumResponseDto, albumUser: UserResponseDto) => {
|
||||
const $t = await getFormatter();
|
||||
@@ -278,12 +186,7 @@ export const handleRemoveUserFromAlbum = async (album: AlbumResponseDto, albumUs
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await removeUserFromAlbum({ id: album.id, userId: albumUser.id });
|
||||
eventManager.emit('AlbumUserDelete', { albumId: album.id, userId: albumUser.id });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_remove_album_users'));
|
||||
}
|
||||
await executeMutation(removeUserFromAlbumMutation, [{ id: album.id, userId: albumUser.id }]);
|
||||
};
|
||||
|
||||
export const handleLeaveAlbum = async (album: AlbumResponseDto) => {
|
||||
@@ -299,49 +202,20 @@ export const handleLeaveAlbum = async (album: AlbumResponseDto) => {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await removeUserFromAlbum({ id: album.id, userId: 'me' });
|
||||
userInteraction.recentAlbums = undefined;
|
||||
eventManager.emit('AlbumDelete', album);
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_remove_album_users'));
|
||||
}
|
||||
await executeMutation(removeUserFromAlbumMutation, [{ id: album.id, userId: 'me' }], {
|
||||
onSuccess: () => leaveAlbumPage(album),
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateThumbnail = async (album: AlbumResponseDto, assetId: string) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
const response = await updateAlbumInfo({
|
||||
id: album.id,
|
||||
updateAlbumDto: {
|
||||
albumThumbnailAssetId: assetId,
|
||||
},
|
||||
});
|
||||
eventManager.emit('AlbumUpdate', response);
|
||||
toastManager.primary($t('album_cover_updated'));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_update_album_cover'));
|
||||
}
|
||||
};
|
||||
|
||||
export const handleUpdateAlbum = async ({ id }: { id: string }, dto: UpdateAlbumDto) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
const response = await updateAlbumInfo({ id, updateAlbumDto: dto });
|
||||
eventManager.emit('AlbumUpdate', response);
|
||||
toastManager.primary({
|
||||
description: $t('album_info_updated'),
|
||||
button: { label: $t('view_album'), onclick: () => goto(Route.viewAlbum({ id })) },
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_update_album_info'));
|
||||
}
|
||||
};
|
||||
const handleUpdateThumbnail = (album: AlbumResponseDto, assetId: string) =>
|
||||
executeMutation(
|
||||
() =>
|
||||
updateAlbumInfoMutation({
|
||||
success: { toast: 'album_cover_updated' },
|
||||
error: { toast: 'errors.unable_to_update_album_cover' },
|
||||
}),
|
||||
[{ id: album.id, updateAlbumDto: { albumThumbnailAssetId: assetId } }],
|
||||
);
|
||||
|
||||
export const handleDeleteAlbum = async (album: AlbumResponseDto, options?: { prompt?: boolean; notify?: boolean }) => {
|
||||
const $t = await getFormatter();
|
||||
@@ -355,21 +229,18 @@ export const handleDeleteAlbum = async (album: AlbumResponseDto, options?: { pro
|
||||
const description = $t('album_delete_confirmation_description');
|
||||
const success = await modalManager.showDialog({ prompt: `${confirmation} ${description}` });
|
||||
if (!success) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteAlbum({ id: album.id });
|
||||
eventManager.emit('AlbumDelete', album);
|
||||
if (notify) {
|
||||
toastManager.primary();
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_delete_album'), { notify });
|
||||
return false;
|
||||
}
|
||||
await executeMutation(
|
||||
() =>
|
||||
deleteAlbumMutation(
|
||||
notify ? { success: { toast: true }, error: { toast: 'errors.unable_to_delete_album' } } : {},
|
||||
),
|
||||
[{ id: album.id }],
|
||||
{ onSuccess: () => leaveAlbumPage(album) },
|
||||
);
|
||||
};
|
||||
|
||||
export const handleDownloadAlbum = async (album: AlbumResponseDto) => {
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
AssetVisibility,
|
||||
bulkTagAssets,
|
||||
getAssetInfo,
|
||||
removeAssetFromAlbum,
|
||||
runAssetJobs,
|
||||
updateAsset,
|
||||
type AlbumResponseDto,
|
||||
@@ -52,7 +51,8 @@ import AssetAddToAlbumModal from '$lib/modals/AssetAddToAlbumModal.svelte';
|
||||
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
|
||||
import ProfileImageCropperModal from '$lib/modals/ProfileImageCropperModal.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { readPreloadedQuery } from '$lib/queries/client';
|
||||
import { removeAssetFromAlbumMutation } from '$lib/queries/album.query';
|
||||
import { executeMutation, readPreloadedQuery } from '$lib/queries/client';
|
||||
import { getUserConfigQuery } from '$lib/queries/config-user.query';
|
||||
import { Route } from '$lib/route';
|
||||
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
@@ -518,19 +518,13 @@ const handleBulkRemoveAssetsFromAlbum = async (assetIds: string[], album: AlbumR
|
||||
const handleRemoveAssetsFromAlbum = async (assetIds: string[], album: AlbumResponseDto) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
const results = await removeAssetFromAlbum({
|
||||
id: album.id,
|
||||
bulkIdsDto: { ids: assetIds },
|
||||
});
|
||||
|
||||
const count = results.filter(({ success }) => success).length;
|
||||
|
||||
toastManager.primary($t('assets_removed_count', { values: { count } }));
|
||||
eventManager.emit('AlbumRemoveAssets', { assetIds, albumIds: [album.id] });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.error_removing_assets_from_album'));
|
||||
}
|
||||
await executeMutation(removeAssetFromAlbumMutation, [{ id: album.id, bulkIdsDto: { ids: assetIds } }], {
|
||||
onSuccess: (results) => {
|
||||
const count = results.filter(({ success }) => success).length;
|
||||
toastManager.primary($t('assets_removed_count', { values: { count } }));
|
||||
eventManager.emit('AlbumRemoveAssets', { assetIds, albumIds: [album.id] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getAssetJobMessage = ($t: MessageFormatter, job: AssetJobName) => {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { AlbumResponseDto } from '@immich/sdk';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
|
||||
interface UserInteractions {
|
||||
recentAlbums?: AlbumResponseDto[];
|
||||
}
|
||||
|
||||
const defaultUserInteraction: UserInteractions = {
|
||||
recentAlbums: undefined,
|
||||
};
|
||||
|
||||
export const userInteraction = $state<UserInteractions>(defaultUserInteraction);
|
||||
|
||||
const resetRecentAlbums = () => {
|
||||
userInteraction.recentAlbums = undefined;
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
Object.assign(userInteraction, defaultUserInteraction);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/no-top-level-side-effects
|
||||
eventManager.on({
|
||||
AlbumCreate: () => resetRecentAlbums(),
|
||||
AlbumUpdate: () => resetRecentAlbums(),
|
||||
AlbumDelete: () => resetRecentAlbums(),
|
||||
AuthLogout: () => reset(),
|
||||
});
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { AlbumResponseDto } from '@immich/sdk';
|
||||
import * as sdk from '@immich/sdk';
|
||||
import { orderBy } from 'lodash-es';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { createAlbumMutation } from '$lib/queries/album.query';
|
||||
import { executeMutation } from '$lib/queries/client';
|
||||
import { Route } from '$lib/route';
|
||||
import {
|
||||
AlbumFilter,
|
||||
@@ -15,35 +14,16 @@ import {
|
||||
locale,
|
||||
type AlbumViewSettings,
|
||||
} from '$lib/stores/preferences.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
|
||||
/**
|
||||
* -------------------------
|
||||
* Albums General Management
|
||||
* -------------------------
|
||||
*/
|
||||
export const createAlbum = async (name?: string, assetIds?: string[]) => {
|
||||
try {
|
||||
const newAlbum: AlbumResponseDto = await sdk.createAlbum({
|
||||
createAlbumDto: {
|
||||
albumName: name ?? '',
|
||||
assetIds,
|
||||
},
|
||||
});
|
||||
eventManager.emit('AlbumCreate', newAlbum);
|
||||
return newAlbum;
|
||||
} catch (error) {
|
||||
const $t = get(t);
|
||||
handleError(error, $t('errors.failed_to_create_album'));
|
||||
}
|
||||
};
|
||||
|
||||
export const createAlbumAndRedirect = async (name?: string, assetIds?: string[]) => {
|
||||
const newAlbum = await createAlbum(name, assetIds);
|
||||
if (newAlbum) {
|
||||
await goto(Route.viewAlbum(newAlbum));
|
||||
}
|
||||
};
|
||||
export const createAlbumAndRedirect = (name?: string, assetIds?: string[]) =>
|
||||
executeMutation(createAlbumMutation, [{ createAlbumDto: { albumName: name ?? '', assetIds } }], {
|
||||
onSuccess: (album) => void goto(Route.viewAlbum(album)),
|
||||
});
|
||||
|
||||
/**
|
||||
* -------------
|
||||
|
||||
@@ -12,7 +12,8 @@ import { t } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { uploadManager } from '$lib/managers/upload-manager.svelte';
|
||||
import { addAssetsToAlbums } from '$lib/services/album.service';
|
||||
import { addAssetsToAlbumsMutation } from '$lib/queries/album.query';
|
||||
import { executeMutation } from '$lib/queries/client';
|
||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||
import { UploadState } from '$lib/types';
|
||||
import { uploadRequest } from '$lib/utils';
|
||||
@@ -221,7 +222,10 @@ async function fileUploader({
|
||||
|
||||
if (albumId && !authManager.isSharedLink) {
|
||||
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_adding_to_album') });
|
||||
await addAssetsToAlbums([albumId], [responseData.id], { notify: false });
|
||||
await executeMutation(() => addAssetsToAlbumsMutation({ notify: false }), {
|
||||
albumIds: [albumId],
|
||||
assetIds: [responseData.id],
|
||||
});
|
||||
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_added_to_album') });
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/EmptyPlaceholder.svelte';
|
||||
import GroupTab from '$lib/elements/GroupTab.svelte';
|
||||
import SearchBar from '$lib/elements/SearchBar.svelte';
|
||||
import { getAllAlbumsQuery } from '$lib/queries/album.query';
|
||||
import { createPreloadedQuery } from '$lib/queries/client';
|
||||
import { Route } from '$lib/route';
|
||||
import { AlbumFilter, albumViewSettings } from '$lib/stores/preferences.store';
|
||||
import { createAlbumAndRedirect } from '$lib/utils/album-utils';
|
||||
@@ -18,6 +20,11 @@
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const queries = {
|
||||
ownedAlbums: createPreloadedQuery(() => getAllAlbumsQuery({ isOwned: true })),
|
||||
sharedAlbums: createPreloadedQuery(() => getAllAlbumsQuery({ isShared: true })),
|
||||
};
|
||||
|
||||
let searchQuery = $state('');
|
||||
let albumGroups: string[] = $state([]);
|
||||
</script>
|
||||
@@ -44,8 +51,8 @@
|
||||
</div>
|
||||
|
||||
<Albums
|
||||
ownedAlbums={data.albums}
|
||||
sharedAlbums={data.sharedAlbums}
|
||||
ownedAlbums={queries.ownedAlbums.data}
|
||||
sharedAlbums={queries.sharedAlbums.data}
|
||||
userSettings={$albumViewSettings}
|
||||
allowEdit
|
||||
{searchQuery}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { getAllAlbums } from '@immich/sdk';
|
||||
import { getAllAlbumsQuery } from '$lib/queries/album.query';
|
||||
import { queryClient } from '$lib/queries/client';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url }) => {
|
||||
await authenticate(url);
|
||||
const sharedAlbums = await getAllAlbums({ isShared: true });
|
||||
const albums = await getAllAlbums({ isOwned: true });
|
||||
await queryClient.query(getAllAlbumsQuery());
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
albums,
|
||||
sharedAlbums,
|
||||
meta: {
|
||||
title: $t('albums'),
|
||||
},
|
||||
|
||||
+32
-91
@@ -27,27 +27,18 @@
|
||||
import { assetMultiSelectManager, AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { getAlbumInfoQuery } from '$lib/queries/album.query';
|
||||
import { createPreloadedQuery, queryClient } from '$lib/queries/client';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { Route } from '$lib/route';
|
||||
import {
|
||||
getAlbumActions,
|
||||
getAlbumAssetsActions,
|
||||
handleDeleteAlbum,
|
||||
isAlbumEditor,
|
||||
isAlbumOwner,
|
||||
} from '$lib/services/album.service';
|
||||
import { getAlbumActions, getAlbumAssetsActions, isAlbumEditor, isAlbumOwner } from '$lib/services/album.service';
|
||||
import { getGlobalActions } from '$lib/services/app.service';
|
||||
import { getAssetBulkActions } from '$lib/services/asset.service';
|
||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { isAlbumsRoute, navigate, type AssetGridRouteSearchParams } from '$lib/utils/navigation';
|
||||
import { AlbumUserRole, AssetVisibility, updateAlbumInfo, type AlbumResponseDto } from '@immich/sdk';
|
||||
import { ActionButton, CommandPaletteDefaultProvider, Icon, IconButton, toastManager } from '@immich/ui';
|
||||
import { AlbumUserRole, AssetVisibility } from '@immich/sdk';
|
||||
import { ActionButton, CommandPaletteDefaultProvider, Icon, IconButton } from '@immich/ui';
|
||||
import {
|
||||
mdiAccountEye,
|
||||
mdiAccountEyeOutline,
|
||||
@@ -68,12 +59,23 @@
|
||||
import AlbumTitle from './AlbumTitle.svelte';
|
||||
import ActionMenuItem from '$lib/components/ActionMenuItem.svelte';
|
||||
import { delay } from '$lib/utils/asset-utils';
|
||||
import {
|
||||
deleteAlbumMutation,
|
||||
getAlbumInfoQuery,
|
||||
invalidateAlbum,
|
||||
updateAlbumInfoMutation,
|
||||
} from '$lib/queries/album.query';
|
||||
import { createPreloadedQuery, executeMutation } from '$lib/queries/client';
|
||||
import { getUserConfigQuery } from '$lib/queries/config-user.query';
|
||||
|
||||
const query = createPreloadedQuery(getUserConfigQuery);
|
||||
const config = $derived(query.data);
|
||||
let { params }: PageProps = $props();
|
||||
|
||||
const { params }: PageProps = $props();
|
||||
const queries = {
|
||||
config: createPreloadedQuery(getUserConfigQuery),
|
||||
album: createPreloadedQuery(() => getAlbumInfoQuery(params.albumId)),
|
||||
};
|
||||
const config = $derived(queries.config.data);
|
||||
const album = $derived(queries.album.data);
|
||||
let { slideshowState, slideshowNavigation } = slideshowStore;
|
||||
let oldAt: AssetGridRouteSearchParams | null | undefined = $state();
|
||||
let viewMode: AlbumPageViewMode = $state(AlbumPageViewMode.VIEW);
|
||||
@@ -122,16 +124,7 @@
|
||||
await goto(Route.albums());
|
||||
};
|
||||
|
||||
const refreshAlbum = async (updated?: AlbumResponseDto) => {
|
||||
if (updated) {
|
||||
if (updated.id !== album.id) {
|
||||
return;
|
||||
}
|
||||
queryClient.setQueryData(getAlbumInfoQuery(album.id).queryKey, updated);
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: getAlbumInfoQuery(album.id).queryKey });
|
||||
};
|
||||
const refreshAlbum = () => invalidateAlbum(album.id);
|
||||
|
||||
const setModeToView = async () => {
|
||||
timelineManager.suspendTransitions = true;
|
||||
@@ -191,29 +184,22 @@
|
||||
await updateThumbnail(firstAsset.id);
|
||||
};
|
||||
|
||||
const updateThumbnail = async (assetId: string) => {
|
||||
try {
|
||||
const response = await updateAlbumInfo({
|
||||
id: album.id,
|
||||
updateAlbumDto: {
|
||||
albumThumbnailAssetId: assetId,
|
||||
},
|
||||
});
|
||||
eventManager.emit('AlbumUpdate', response);
|
||||
toastManager.primary($t('album_cover_updated'));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_update_album_cover'));
|
||||
}
|
||||
};
|
||||
const updateThumbnail = (assetId: string) =>
|
||||
executeMutation(
|
||||
() =>
|
||||
updateAlbumInfoMutation({
|
||||
success: { toast: 'album_cover_updated' },
|
||||
error: { toast: 'errors.unable_to_update_album_cover' },
|
||||
}),
|
||||
[{ id: album.id, updateAlbumDto: { albumThumbnailAssetId: assetId } }],
|
||||
);
|
||||
|
||||
onNavigate(async ({ to }) => {
|
||||
if (!isAlbumsRoute(to?.route.id) && album.assetCount === 0 && !album.albumName) {
|
||||
await handleDeleteAlbum(album, { notify: false, prompt: false });
|
||||
await executeMutation(() => deleteAlbumMutation({}), [{ id: album.id }]);
|
||||
}
|
||||
});
|
||||
|
||||
const albumQuery = createPreloadedQuery(() => getAlbumInfoQuery(params.albumId));
|
||||
let album = $derived(albumQuery.data);
|
||||
const albumId = $derived(album.id);
|
||||
|
||||
const albumHasViewers = $derived(album.albumUsers.some(({ role }) => role === AlbumUserRole.Viewer));
|
||||
@@ -274,42 +260,15 @@
|
||||
viewMode === AlbumPageViewMode.SELECT_ASSETS ? timelineMultiSelectManager : assetMultiSelectManager,
|
||||
);
|
||||
|
||||
const onAlbumDelete = async ({ id }: AlbumResponseDto) => {
|
||||
if (id !== album.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
await goto(Route.albums());
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
};
|
||||
|
||||
const onAlbumAddAssets = async ({ albumIds, assetIds }: { albumIds: string[]; assetIds: string[] }) => {
|
||||
const onAlbumAddAssets = async ({ albumIds }: { albumIds: string[]; assetIds: string[] }) => {
|
||||
if (!albumIds.includes(album.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
album = { ...album, assetCount: album.assetCount + assetIds.length };
|
||||
|
||||
timelineMultiSelectManager.clear();
|
||||
await setModeToView();
|
||||
};
|
||||
|
||||
const onAlbumShare = async () => {
|
||||
await refreshAlbum();
|
||||
await setModeToView();
|
||||
};
|
||||
|
||||
const onAlbumUserUpdate = ({ albumId, userId, role }: { albumId: string; userId: string; role: AlbumUserRole }) => {
|
||||
if (albumId !== album.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const albumUsers = album.albumUsers.map((albumUser) =>
|
||||
albumUser.user.id === userId ? { ...albumUser, role } : albumUser,
|
||||
);
|
||||
album = { ...album, albumUsers };
|
||||
};
|
||||
|
||||
const { Cast } = $derived(getGlobalActions($t));
|
||||
const Actions = $derived(getAlbumActions($t, album));
|
||||
const { AddAssets, Upload } = $derived(getAlbumAssetsActions($t, album, timelineMultiSelectManager.assets));
|
||||
@@ -323,16 +282,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<OnEvents
|
||||
{onAlbumDelete}
|
||||
{onAlbumAddAssets}
|
||||
{onAlbumRemoveAssets}
|
||||
{onAlbumShare}
|
||||
{onAlbumUserUpdate}
|
||||
onAlbumUserDelete={() => refreshAlbum()}
|
||||
onAlbumUpdate={(album) => refreshAlbum(album)}
|
||||
onAssetsDelete={() => refreshAlbum()}
|
||||
/>
|
||||
<OnEvents {onAlbumAddAssets} {onAlbumRemoveAssets} onAssetsDelete={() => refreshAlbum()} />
|
||||
<CommandPaletteDefaultProvider name={$t('album')} actions={[AddAssets, Upload, Close, ...Object.values(Actions)]} />
|
||||
|
||||
<div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: Route.albums() }}>
|
||||
@@ -357,12 +307,7 @@
|
||||
{#if viewMode !== AlbumPageViewMode.SELECT_THUMBNAIL}
|
||||
<!-- ALBUM TITLE -->
|
||||
<section class="pt-8 md:pt-24">
|
||||
<AlbumTitle
|
||||
id={album.id}
|
||||
albumName={album.albumName}
|
||||
{isEditor}
|
||||
onUpdate={(albumName) => (album = { ...album, albumName })}
|
||||
/>
|
||||
<AlbumTitle id={album.id} albumName={album.albumName} {isEditor} />
|
||||
|
||||
{#if album.assetCount > 0}
|
||||
<AlbumSummary {album} />
|
||||
@@ -406,11 +351,7 @@
|
||||
<ActionButton action={Actions.Share} />
|
||||
</div>
|
||||
{/if}
|
||||
<AlbumDescription
|
||||
id={album.id}
|
||||
{isEditor}
|
||||
bind:description={() => album.description, (description) => (album = { ...album, description })}
|
||||
/>
|
||||
<AlbumDescription id={album.id} {isEditor} description={album.description} />
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
|
||||
+11
-18
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updateAlbumInfo } from '@immich/sdk';
|
||||
import { updateAlbumInfoMutation } from '$lib/queries/album.query';
|
||||
import { executeMutation } from '$lib/queries/client';
|
||||
import { Textarea } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fromAction } from 'svelte/attachments';
|
||||
@@ -13,26 +12,20 @@
|
||||
isEditor: boolean;
|
||||
}
|
||||
|
||||
let { id, description = $bindable(), isEditor }: Props = $props();
|
||||
let { id, description, isEditor }: Props = $props();
|
||||
|
||||
const handleFocusOut = async () => {
|
||||
try {
|
||||
const response = await updateAlbumInfo({
|
||||
id,
|
||||
updateAlbumDto: {
|
||||
description: description || null,
|
||||
},
|
||||
});
|
||||
eventManager.emit('AlbumUpdate', response);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_save_album'));
|
||||
}
|
||||
};
|
||||
let value = $derived(description);
|
||||
|
||||
const handleFocusOut = () =>
|
||||
executeMutation(
|
||||
() => updateAlbumInfoMutation({ error: { toast: 'errors.unable_to_save_album' } }),
|
||||
[{ id, updateAlbumDto: { description: value || null } }],
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if isEditor}
|
||||
<Textarea
|
||||
bind:value={description}
|
||||
bind:value
|
||||
variant="ghost"
|
||||
onfocusout={handleFocusOut}
|
||||
placeholder={$t('add_a_description')}
|
||||
|
||||
+7
-13
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updateAlbumInfo } from '@immich/sdk';
|
||||
import { updateAlbumInfoMutation } from '$lib/queries/album.query';
|
||||
import { executeMutation } from '$lib/queries/client';
|
||||
import { Textarea } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fromAction } from 'svelte/attachments';
|
||||
@@ -11,10 +10,9 @@
|
||||
id: string;
|
||||
albumName: string;
|
||||
isEditor: boolean;
|
||||
onUpdate: (albumName: string) => void;
|
||||
};
|
||||
|
||||
let { id, albumName = $bindable(), isEditor, onUpdate }: Props = $props();
|
||||
let { id, albumName, isEditor }: Props = $props();
|
||||
|
||||
let newAlbumName = $derived(albumName);
|
||||
|
||||
@@ -25,14 +23,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await updateAlbumInfo({ id, updateAlbumDto: { albumName: newAlbumName } });
|
||||
({ albumName } = response);
|
||||
eventManager.emit('AlbumUpdate', response);
|
||||
onUpdate(albumName);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_save_album'));
|
||||
}
|
||||
await executeMutation(
|
||||
() => updateAlbumInfoMutation({ error: { toast: 'errors.unable_to_save_album' } }),
|
||||
[{ id, updateAlbumDto: { albumName: newAlbumName } }],
|
||||
);
|
||||
};
|
||||
|
||||
const textClasses = 'text-2xl lg:text-6xl text-primary';
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/EmptyPlaceholder.svelte';
|
||||
import UserAvatar from '$lib/components/shared-components/UserAvatar.svelte';
|
||||
import { getAllAlbumsQuery } from '$lib/queries/album.query';
|
||||
import { createPreloadedQuery } from '$lib/queries/client';
|
||||
import { Route } from '$lib/route';
|
||||
import { getAlbumsActions } from '$lib/services/album.service';
|
||||
import { getSharedLinksActions } from '$lib/services/shared-link.service';
|
||||
@@ -24,6 +26,9 @@
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const query = createPreloadedQuery(() => getAllAlbumsQuery({ isShared: true }));
|
||||
const sharedAlbums = $derived(query.data);
|
||||
|
||||
const settings: AlbumViewSettings = {
|
||||
view: AlbumViewMode.Cover,
|
||||
filter: AlbumFilter.Shared,
|
||||
@@ -76,7 +81,7 @@
|
||||
|
||||
<div>
|
||||
<!-- Shared Album List -->
|
||||
<Albums sharedAlbums={data.sharedAlbums} userSettings={settings} showOwner>
|
||||
<Albums {sharedAlbums} userSettings={settings} showOwner>
|
||||
<!-- Empty List -->
|
||||
{#snippet empty()}
|
||||
<EmptyPlaceholder text={$t('no_shared_albums_message')} src={empty2Url} class="mx-auto mt-10" />
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { PartnerDirection, getAllAlbums, getPartners } from '@immich/sdk';
|
||||
import { PartnerDirection, getPartners } from '@immich/sdk';
|
||||
import { getAllAlbumsQuery } from '$lib/queries/album.query';
|
||||
import { queryClient } from '$lib/queries/client';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url }) => {
|
||||
await authenticate(url);
|
||||
const sharedAlbums = await getAllAlbums({ isShared: true });
|
||||
await queryClient.query(getAllAlbumsQuery({ isShared: true }));
|
||||
const partners = await getPartners({ direction: PartnerDirection.SharedWith });
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
sharedAlbums,
|
||||
partners,
|
||||
meta: {
|
||||
title: $t('sharing'),
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
import { getHeatmapRange } from '$lib';
|
||||
import CalendarHeatmap from '$lib/components/CalendarHeatmap.svelte';
|
||||
import Skeleton from '$lib/elements/Skeleton.svelte';
|
||||
import { getAlbumStatisticsQuery } from '$lib/queries/album.query';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import {
|
||||
AssetVisibility,
|
||||
CalendarHeatmapType,
|
||||
getAlbumStatistics,
|
||||
getAssetStatistics,
|
||||
getMyCalendarHeatmap,
|
||||
type AlbumStatisticsResponseDto,
|
||||
type AssetStatsResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { Heading, Table, TableBody, TableCell, TableHeader, TableHeading, TableRow } from '@immich/ui';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
@@ -42,19 +43,15 @@
|
||||
total: 0,
|
||||
});
|
||||
|
||||
let albumStats: AlbumStatisticsResponseDto = $state({
|
||||
owned: 0,
|
||||
shared: 0,
|
||||
notShared: 0,
|
||||
});
|
||||
const query = createQuery(getAlbumStatisticsQuery);
|
||||
const albumStats: AlbumStatisticsResponseDto = $derived(query.data ?? { owned: 0, shared: 0, notShared: 0 });
|
||||
|
||||
const getUsage = async () => {
|
||||
[timelineStats, favoriteStats, archiveStats, trashStats, albumStats] = await Promise.all([
|
||||
[timelineStats, favoriteStats, archiveStats, trashStats] = await Promise.all([
|
||||
getAssetStatistics({ visibility: AssetVisibility.Timeline }),
|
||||
getAssetStatistics({ isFavorite: true }),
|
||||
getAssetStatistics({ visibility: AssetVisibility.Archive }),
|
||||
getAssetStatistics({ isTrashed: true }),
|
||||
getAlbumStatistics(),
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
+14
-11
@@ -4,9 +4,11 @@
|
||||
import { getAllMetadataItems, type DifferingMetadataFields } from '$lib/utils/duplicate-utils';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { getAllAlbums, type AssetResponseDto } from '@immich/sdk';
|
||||
import { getAlbumsByAssetQuery } from '$lib/queries/album.query';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { mdiBookmarkOutline, mdiHeart, mdiImageMultipleOutline, mdiMagnifyPlus } from '@mdi/js';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { t } from 'svelte-i18n';
|
||||
import InfoRow from './InfoRow.svelte';
|
||||
|
||||
@@ -30,6 +32,9 @@
|
||||
initialVisibleCount = 5,
|
||||
}: Props = $props();
|
||||
|
||||
const query = createQuery(() => getAlbumsByAssetQuery(asset.id));
|
||||
const albums = $derived(query.data);
|
||||
|
||||
const listFormat = $derived(new Intl.ListFormat($lang));
|
||||
const isFromExternalLibrary = $derived(!!asset.libraryId);
|
||||
|
||||
@@ -114,17 +119,15 @@
|
||||
|
||||
<!-- Albums always shown -->
|
||||
<InfoRow icon={mdiBookmarkOutline} title={$t('albums')}>
|
||||
{#await getAllAlbums({ assetId: asset.id })}
|
||||
{#if !albums}
|
||||
{$t('scanning_for_album')}
|
||||
{:then albums}
|
||||
{#if albums.length === 1}
|
||||
{albums[0].albumName}
|
||||
{:else}
|
||||
<span title={listFormat.format(albums.map(({ albumName }) => albumName))}>
|
||||
{$t('in_albums', { values: { count: albums.length } })}
|
||||
</span>
|
||||
{/if}
|
||||
{/await}
|
||||
{:else if albums.length === 1}
|
||||
{albums[0].albumName}
|
||||
{:else}
|
||||
<span title={listFormat.format(albums.map(({ albumName }) => albumName))}>
|
||||
{$t('in_albums', { values: { count: albums.length } })}
|
||||
</span>
|
||||
{/if}
|
||||
</InfoRow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,11 @@
|
||||
<script module lang="ts">
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { getAlbumInfo, getTagById } from '@immich/sdk';
|
||||
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const albumNameCache = new Map<string, Promise<string>>();
|
||||
const getAlbumName = (id: string): Promise<string> => {
|
||||
let albumName = albumNameCache.get(id);
|
||||
if (!albumName) {
|
||||
albumName = getAlbumInfo({ ...authManager.params, id })
|
||||
.then((album) => album.albumName)
|
||||
.catch(() => id);
|
||||
albumNameCache.set(id, albumName);
|
||||
}
|
||||
return albumName;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { pluginManager } from '$lib/managers/plugin-manager.svelte';
|
||||
import type { JSONSchemaProperty } from '$lib/types';
|
||||
import { type WorkflowStepDto } from '@immich/sdk';
|
||||
import { Badge, Card, CardBody, CardDescription, CardHeader, CardTitle, Icon, IconButton } from '@immich/ui';
|
||||
import { getTagById } from '@immich/sdk';
|
||||
import { getAlbumQuery } from '$lib/queries/album.query';
|
||||
import { queryClient } from '$lib/queries/client';
|
||||
import {
|
||||
mdiAutoFix,
|
||||
mdiDragVertical,
|
||||
@@ -47,6 +32,12 @@
|
||||
|
||||
let { step, index, onEdit, onDelete, onInsertBefore, onDragOver, onDrop, onDragEnd, onDragStart }: Props = $props();
|
||||
|
||||
const getAlbumName = (id: string): Promise<string> =>
|
||||
queryClient
|
||||
.query(getAlbumQuery(id))
|
||||
.then((album) => album.albumName)
|
||||
.catch(() => id);
|
||||
|
||||
const method = $derived(pluginManager.getMethod(step.method));
|
||||
const isFilter = $derived(method?.uiHints?.includes('Filter') ?? false);
|
||||
const schema = $derived(method?.schema as JSONSchemaProperty | undefined);
|
||||
|
||||
Reference in New Issue
Block a user