chore(web): migration svelte 5 syntax (#13883)
This commit is contained in:
@@ -1,13 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import UploadCover from '$lib/components/shared-components/drag-and-drop-upload-overlay.svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import type { Snippet } from 'svelte';
|
||||
interface Props {
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
let { isViewing: showAssetViewer, setAsset, gridScrollTarget } = assetViewingStore;
|
||||
|
||||
// $page.data.asset is loaded by route specific +page.ts loaders if that
|
||||
// route contains the assetId path.
|
||||
$: {
|
||||
run(() => {
|
||||
if ($page.data.asset) {
|
||||
setAsset($page.data.asset);
|
||||
} else {
|
||||
@@ -15,11 +23,11 @@
|
||||
}
|
||||
const asset = $page.url.searchParams.get('at');
|
||||
$gridScrollTarget = { at: asset };
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class:display-none={$showAssetViewer}>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<UploadCover />
|
||||
|
||||
|
||||
@@ -10,16 +10,22 @@
|
||||
import SearchBar from '$lib/components/elements/search-bar.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let searchQuery = '';
|
||||
let albumGroups: string[] = [];
|
||||
let { data }: Props = $props();
|
||||
|
||||
let searchQuery = $state('');
|
||||
let albumGroups: string[] = $state([]);
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
<div class="flex place-items-center gap-2" slot="buttons">
|
||||
<AlbumsControls {albumGroups} bind:searchQuery />
|
||||
</div>
|
||||
{#snippet buttons()}
|
||||
<div class="flex place-items-center gap-2">
|
||||
<AlbumsControls {albumGroups} bind:searchQuery />
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="xl:hidden">
|
||||
<div class="w-fit h-14 dark:text-immich-dark-fg py-2">
|
||||
@@ -43,6 +49,8 @@
|
||||
{searchQuery}
|
||||
bind:albumGroupIds={albumGroups}
|
||||
>
|
||||
<EmptyPlaceholder slot="empty" text={$t('no_albums_message')} onClick={() => createAlbumAndRedirect()} />
|
||||
{#snippet empty()}
|
||||
<EmptyPlaceholder text={$t('no_albums_message')} onClick={() => createAlbumAndRedirect()} />
|
||||
{/snippet}
|
||||
</Albums>
|
||||
</UserPageLayout>
|
||||
|
||||
+119
-117
@@ -32,7 +32,7 @@
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { AppRoute, AlbumPageViewMode } from '$lib/constants';
|
||||
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
|
||||
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
@@ -87,69 +87,33 @@
|
||||
import { confirmAlbumDelete } from '$lib/utils/album-utils';
|
||||
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data = $bindable() }: Props = $props();
|
||||
|
||||
let { isViewing: showAssetViewer, setAsset, gridScrollTarget } = assetViewingStore;
|
||||
let { slideshowState, slideshowNavigation } = slideshowStore;
|
||||
|
||||
let oldAt: AssetGridRouteSearchParams | null | undefined;
|
||||
let oldAt: AssetGridRouteSearchParams | null | undefined = $state();
|
||||
|
||||
$: album = data.album;
|
||||
$: albumId = album.id;
|
||||
$: albumKey = `${albumId}_${albumOrder}`;
|
||||
let backUrl: string = $state(AppRoute.ALBUMS);
|
||||
let viewMode = $state(AlbumPageViewMode.VIEW);
|
||||
let isCreatingSharedAlbum = $state(false);
|
||||
let isShowActivity = $state(false);
|
||||
let isLiked: ActivityResponseDto | null = $state(null);
|
||||
let reactions: ActivityResponseDto[] = $state([]);
|
||||
let globalWidth: number = $state(0);
|
||||
let assetGridWidth: number = $derived(isShowActivity ? globalWidth - (globalWidth < 768 ? 360 : 460) : globalWidth);
|
||||
let albumOrder: AssetOrder | undefined = $state(data.album.order);
|
||||
|
||||
$: {
|
||||
if (!album.isActivityEnabled && $numberOfComments === 0) {
|
||||
isShowActivity = false;
|
||||
}
|
||||
}
|
||||
|
||||
enum ViewMode {
|
||||
LINK_SHARING = 'link-sharing',
|
||||
SELECT_USERS = 'select-users',
|
||||
SELECT_THUMBNAIL = 'select-thumbnail',
|
||||
SELECT_ASSETS = 'select-assets',
|
||||
VIEW_USERS = 'view-users',
|
||||
VIEW = 'view',
|
||||
OPTIONS = 'options',
|
||||
}
|
||||
|
||||
let backUrl: string = AppRoute.ALBUMS;
|
||||
let viewMode = ViewMode.VIEW;
|
||||
let isCreatingSharedAlbum = false;
|
||||
let isShowActivity = false;
|
||||
let isLiked: ActivityResponseDto | null = null;
|
||||
let reactions: ActivityResponseDto[] = [];
|
||||
let globalWidth: number;
|
||||
let assetGridWidth: number;
|
||||
let albumOrder: AssetOrder | undefined = data.album.order;
|
||||
|
||||
$: assetStore = new AssetStore({ albumId, order: albumOrder });
|
||||
const assetInteractionStore = createAssetInteractionStore();
|
||||
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
|
||||
|
||||
$: timelineStore = new AssetStore({ isArchived: false, withPartners: true }, albumId);
|
||||
const timelineInteractionStore = createAssetInteractionStore();
|
||||
const { selectedAssets: timelineSelected } = timelineInteractionStore;
|
||||
|
||||
$: isOwned = $user.id == album.ownerId;
|
||||
$: isAllUserOwned = [...$selectedAssets].every((asset) => asset.ownerId === $user.id);
|
||||
$: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite);
|
||||
$: isAllArchived = [...$selectedAssets].every((asset) => asset.isArchived);
|
||||
$: {
|
||||
assetGridWidth = isShowActivity ? globalWidth - (globalWidth < 768 ? 360 : 460) : globalWidth;
|
||||
}
|
||||
$: showActivityStatus =
|
||||
album.albumUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0);
|
||||
|
||||
// svelte-ignore reactive_declaration_non_reactive_property
|
||||
$: isEditor =
|
||||
album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor ||
|
||||
album.ownerId === $user.id;
|
||||
|
||||
// svelte-ignore reactive_declaration_non_reactive_property
|
||||
$: albumHasViewers = album.albumUsers.some(({ role }) => role === AlbumUserRole.Viewer);
|
||||
|
||||
afterNavigate(({ from }) => {
|
||||
let url: string | undefined = from?.url?.pathname;
|
||||
|
||||
@@ -171,12 +135,15 @@
|
||||
|
||||
const handleToggleEnableActivity = async () => {
|
||||
try {
|
||||
await updateAlbumInfo({
|
||||
const updateAlbum = await updateAlbumInfo({
|
||||
id: album.id,
|
||||
updateAlbumDto: {
|
||||
isActivityEnabled: !album.isActivityEnabled,
|
||||
},
|
||||
});
|
||||
|
||||
album = { ...album, isActivityEnabled: updateAlbum.isActivityEnabled };
|
||||
|
||||
await refreshAlbum();
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
@@ -236,11 +203,6 @@
|
||||
isShowActivity = !isShowActivity;
|
||||
};
|
||||
|
||||
$: if (album.albumUsers.length > 0) {
|
||||
handlePromiseError(getFavorite());
|
||||
handlePromiseError(getNumberOfComments());
|
||||
}
|
||||
|
||||
const handleStartSlideshow = async () => {
|
||||
const asset =
|
||||
$slideshowNavigation === SlideshowNavigation.Shuffle ? await assetStore.getRandomAsset() : assetStore.assets[0];
|
||||
@@ -251,21 +213,21 @@
|
||||
};
|
||||
|
||||
const handleEscape = async () => {
|
||||
if (viewMode === ViewMode.SELECT_USERS) {
|
||||
viewMode = ViewMode.VIEW;
|
||||
if (viewMode === AlbumPageViewMode.SELECT_USERS) {
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewMode === ViewMode.SELECT_ASSETS) {
|
||||
if (viewMode === AlbumPageViewMode.SELECT_ASSETS) {
|
||||
await handleCloseSelectAssets();
|
||||
return;
|
||||
}
|
||||
if (viewMode === ViewMode.LINK_SHARING) {
|
||||
viewMode = ViewMode.VIEW;
|
||||
if (viewMode === AlbumPageViewMode.LINK_SHARING) {
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
return;
|
||||
}
|
||||
if (viewMode === ViewMode.OPTIONS) {
|
||||
viewMode = ViewMode.VIEW;
|
||||
if (viewMode === AlbumPageViewMode.OPTIONS) {
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
return;
|
||||
}
|
||||
if ($showAssetViewer) {
|
||||
@@ -280,7 +242,7 @@
|
||||
};
|
||||
|
||||
const refreshAlbum = async () => {
|
||||
data.album = await getAlbumInfo({ id: album.id, withoutAssets: true });
|
||||
album = await getAlbumInfo({ id: album.id, withoutAssets: true });
|
||||
};
|
||||
|
||||
const handleAddAssets = async () => {
|
||||
@@ -308,7 +270,7 @@
|
||||
};
|
||||
|
||||
const setModeToView = async () => {
|
||||
viewMode = ViewMode.VIEW;
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
assetStore.destroy();
|
||||
assetStore = new AssetStore({ albumId, order: albumOrder });
|
||||
timelineStore.destroy();
|
||||
@@ -341,13 +303,13 @@
|
||||
});
|
||||
await refreshAlbum();
|
||||
|
||||
viewMode = ViewMode.VIEW;
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.error_adding_users_to_album'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveUser = async (userId: string, nextViewMode: ViewMode) => {
|
||||
const handleRemoveUser = async (userId: string, nextViewMode: AlbumPageViewMode) => {
|
||||
if (userId == 'me' || userId === $user.id) {
|
||||
await goto(backUrl);
|
||||
return;
|
||||
@@ -357,7 +319,7 @@
|
||||
await refreshAlbum();
|
||||
|
||||
// Dynamically set the view mode based on the passed argument
|
||||
viewMode = album.albumUsers.length > 0 ? nextViewMode : ViewMode.VIEW;
|
||||
viewMode = album.albumUsers.length > 0 ? nextViewMode : AlbumPageViewMode.VIEW;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.error_deleting_shared_user'));
|
||||
}
|
||||
@@ -371,7 +333,7 @@
|
||||
const isConfirmed = await confirmAlbumDelete(album);
|
||||
|
||||
if (!isConfirmed) {
|
||||
viewMode = ViewMode.VIEW;
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -381,7 +343,7 @@
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_delete_album'));
|
||||
} finally {
|
||||
viewMode = ViewMode.VIEW;
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -391,11 +353,11 @@
|
||||
};
|
||||
|
||||
const handleUpdateThumbnail = async (assetId: string) => {
|
||||
if (viewMode !== ViewMode.SELECT_THUMBNAIL) {
|
||||
if (viewMode !== AlbumPageViewMode.SELECT_THUMBNAIL) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewMode = ViewMode.VIEW;
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
assetInteractionStore.clearMultiselect();
|
||||
|
||||
await updateThumbnail(assetId);
|
||||
@@ -432,6 +394,40 @@
|
||||
assetStore.destroy();
|
||||
timelineStore.destroy();
|
||||
});
|
||||
|
||||
let album = $state(data.album);
|
||||
let albumId = $derived(album.id);
|
||||
let albumKey = $derived(`${albumId}_${albumOrder}`);
|
||||
|
||||
$effect(() => {
|
||||
if (!album.isActivityEnabled && $numberOfComments === 0) {
|
||||
isShowActivity = false;
|
||||
}
|
||||
});
|
||||
|
||||
let assetStore = $derived(new AssetStore({ albumId, order: albumOrder }));
|
||||
let timelineStore = $derived(new AssetStore({ isArchived: false, withPartners: true }, albumId));
|
||||
|
||||
let isOwned = $derived($user.id == album.ownerId);
|
||||
let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id));
|
||||
let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite));
|
||||
let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived));
|
||||
|
||||
let showActivityStatus = $derived(
|
||||
album.albumUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0),
|
||||
);
|
||||
let isEditor = $derived(
|
||||
album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor ||
|
||||
album.ownerId === $user.id,
|
||||
);
|
||||
|
||||
let albumHasViewers = $derived(album.albumUsers.some(({ role }) => role === AlbumUserRole.Viewer));
|
||||
$effect(() => {
|
||||
if (album.albumUsers.length > 0) {
|
||||
handlePromiseError(getFavorite());
|
||||
handlePromiseError(getNumberOfComments());
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex overflow-hidden" bind:clientWidth={globalWidth}>
|
||||
@@ -475,14 +471,14 @@
|
||||
</ButtonContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{:else}
|
||||
{#if viewMode === ViewMode.VIEW}
|
||||
{#if viewMode === AlbumPageViewMode.VIEW}
|
||||
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(backUrl)}>
|
||||
<svelte:fragment slot="trailing">
|
||||
{#snippet trailing()}
|
||||
{#if isEditor}
|
||||
<CircleIconButton
|
||||
title={$t('add_photos')}
|
||||
on:click={async () => {
|
||||
viewMode = ViewMode.SELECT_ASSETS;
|
||||
onclick={async () => {
|
||||
viewMode = AlbumPageViewMode.SELECT_ASSETS;
|
||||
oldAt = { at: $gridScrollTarget?.at };
|
||||
await navigate(
|
||||
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: null } },
|
||||
@@ -496,23 +492,27 @@
|
||||
{#if isOwned}
|
||||
<CircleIconButton
|
||||
title={$t('share')}
|
||||
on:click={() => (viewMode = ViewMode.SELECT_USERS)}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
|
||||
icon={mdiShareVariantOutline}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if album.assetCount > 0}
|
||||
<CircleIconButton title={$t('slideshow')} on:click={handleStartSlideshow} icon={mdiPresentationPlay} />
|
||||
<CircleIconButton title={$t('download')} on:click={handleDownloadAlbum} icon={mdiFolderDownloadOutline} />
|
||||
<CircleIconButton title={$t('slideshow')} onclick={handleStartSlideshow} icon={mdiPresentationPlay} />
|
||||
<CircleIconButton title={$t('download')} onclick={handleDownloadAlbum} icon={mdiFolderDownloadOutline} />
|
||||
|
||||
{#if isOwned}
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('album_options')}>
|
||||
<MenuOption
|
||||
icon={mdiImageOutline}
|
||||
text={$t('select_album_cover')}
|
||||
onClick={() => (viewMode = ViewMode.SELECT_THUMBNAIL)}
|
||||
onClick={() => (viewMode = AlbumPageViewMode.SELECT_THUMBNAIL)}
|
||||
/>
|
||||
<MenuOption
|
||||
icon={mdiCogOutline}
|
||||
text={$t('options')}
|
||||
onClick={() => (viewMode = AlbumPageViewMode.OPTIONS)}
|
||||
/>
|
||||
<MenuOption icon={mdiCogOutline} text={$t('options')} onClick={() => (viewMode = ViewMode.OPTIONS)} />
|
||||
<MenuOption icon={mdiDeleteOutline} text={$t('delete_album')} onClick={() => handleRemoveAlbum()} />
|
||||
</ButtonContextMenu>
|
||||
{/if}
|
||||
@@ -523,18 +523,18 @@
|
||||
size="sm"
|
||||
rounded="lg"
|
||||
disabled={album.assetCount === 0}
|
||||
on:click={() => (viewMode = ViewMode.SELECT_USERS)}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
|
||||
>
|
||||
{$t('share')}
|
||||
</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.SELECT_ASSETS}
|
||||
{#if viewMode === AlbumPageViewMode.SELECT_ASSETS}
|
||||
<ControlAppBar onClose={handleCloseSelectAssets}>
|
||||
<svelte:fragment slot="leading">
|
||||
{#snippet leading()}
|
||||
<p class="text-lg dark:text-immich-dark-fg">
|
||||
{#if $timelineSelected.size === 0}
|
||||
{$t('add_to_album')}
|
||||
@@ -542,26 +542,28 @@
|
||||
{$t('selected_count', { values: { count: $timelineSelected.size } })}
|
||||
{/if}
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
|
||||
<svelte:fragment slot="trailing">
|
||||
{#snippet trailing()}
|
||||
<button
|
||||
type="button"
|
||||
on:click={handleSelectFromComputer}
|
||||
onclick={handleSelectFromComputer}
|
||||
class="rounded-lg px-6 py-2 text-sm font-medium text-immich-primary transition-all hover:bg-immich-primary/10 dark:text-immich-dark-primary dark:hover:bg-immich-dark-primary/25"
|
||||
>
|
||||
{$t('select_from_computer')}
|
||||
</button>
|
||||
<Button size="sm" rounded="lg" disabled={$timelineSelected.size === 0} on:click={handleAddAssets}
|
||||
<Button size="sm" rounded="lg" disabled={$timelineSelected.size === 0} onclick={handleAddAssets}
|
||||
>{$t('done')}</Button
|
||||
>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.SELECT_THUMBNAIL}
|
||||
<ControlAppBar onClose={() => (viewMode = ViewMode.VIEW)}>
|
||||
<svelte:fragment slot="leading">{$t('select_album_cover')}</svelte:fragment>
|
||||
{#if viewMode === AlbumPageViewMode.SELECT_THUMBNAIL}
|
||||
<ControlAppBar onClose={() => (viewMode = AlbumPageViewMode.VIEW)}>
|
||||
{#snippet leading()}
|
||||
{$t('select_album_cover')}
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -572,7 +574,7 @@
|
||||
>
|
||||
<!-- Use key because AssetGrid can't deal with changing stores -->
|
||||
{#key albumKey}
|
||||
{#if viewMode === ViewMode.SELECT_ASSETS}
|
||||
{#if viewMode === AlbumPageViewMode.SELECT_ASSETS}
|
||||
<AssetGrid
|
||||
enableRouting={false}
|
||||
assetStore={timelineStore}
|
||||
@@ -586,13 +588,13 @@
|
||||
{assetStore}
|
||||
{assetInteractionStore}
|
||||
isShared={album.albumUsers.length > 0}
|
||||
isSelectionMode={viewMode === ViewMode.SELECT_THUMBNAIL}
|
||||
singleSelect={viewMode === ViewMode.SELECT_THUMBNAIL}
|
||||
isSelectionMode={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL}
|
||||
singleSelect={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL}
|
||||
showArchiveIcon
|
||||
onSelect={({ id }) => handleUpdateThumbnail(id)}
|
||||
onEscape={handleEscape}
|
||||
>
|
||||
{#if viewMode !== ViewMode.SELECT_THUMBNAIL}
|
||||
{#if viewMode !== AlbumPageViewMode.SELECT_THUMBNAIL}
|
||||
<!-- ALBUM TITLE -->
|
||||
<section class="pt-8 md:pt-24">
|
||||
<AlbumTitle
|
||||
@@ -616,18 +618,18 @@
|
||||
color="gray"
|
||||
size="20"
|
||||
icon={mdiLink}
|
||||
on:click={() => (viewMode = ViewMode.LINK_SHARING)}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.LINK_SHARING)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- owner -->
|
||||
<button type="button" on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
|
||||
<button type="button" onclick={() => (viewMode = AlbumPageViewMode.VIEW_USERS)}>
|
||||
<UserAvatar user={album.owner} size="md" />
|
||||
</button>
|
||||
|
||||
<!-- users with write access (collaborators) -->
|
||||
{#each album.albumUsers.filter(({ role }) => role === AlbumUserRole.Editor) as { user } (user.id)}
|
||||
<button type="button" on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
|
||||
<button type="button" onclick={() => (viewMode = AlbumPageViewMode.VIEW_USERS)}>
|
||||
<UserAvatar {user} size="md" />
|
||||
</button>
|
||||
{/each}
|
||||
@@ -639,7 +641,7 @@
|
||||
color="gray"
|
||||
size="20"
|
||||
icon={mdiDotsVertical}
|
||||
on:click={() => (viewMode = ViewMode.VIEW_USERS)}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.VIEW_USERS)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -648,7 +650,7 @@
|
||||
color="gray"
|
||||
size="20"
|
||||
icon={mdiPlus}
|
||||
on:click={() => (viewMode = ViewMode.SELECT_USERS)}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
|
||||
title={$t('add_more_users')}
|
||||
/>
|
||||
{/if}
|
||||
@@ -665,7 +667,7 @@
|
||||
<p class="text-xs dark:text-immich-dark-fg">{$t('add_photos').toUpperCase()}</p>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.SELECT_ASSETS)}
|
||||
class="mt-5 flex w-full place-items-center gap-6 rounded-md border bg-immich-bg px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none dark:bg-immich-dark-gray dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
|
||||
>
|
||||
<span class="text-text-immich-primary dark:text-immich-dark-primary"
|
||||
@@ -717,29 +719,29 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if viewMode === ViewMode.SELECT_USERS}
|
||||
{#if viewMode === AlbumPageViewMode.SELECT_USERS}
|
||||
<UserSelectionModal
|
||||
{album}
|
||||
onSelect={handleAddUsers}
|
||||
onShare={() => (viewMode = ViewMode.LINK_SHARING)}
|
||||
onClose={() => (viewMode = ViewMode.VIEW)}
|
||||
onShare={() => (viewMode = AlbumPageViewMode.LINK_SHARING)}
|
||||
onClose={() => (viewMode = AlbumPageViewMode.VIEW)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.LINK_SHARING}
|
||||
<CreateSharedLinkModal albumId={album.id} onClose={() => (viewMode = ViewMode.VIEW)} />
|
||||
{#if viewMode === AlbumPageViewMode.LINK_SHARING}
|
||||
<CreateSharedLinkModal albumId={album.id} onClose={() => (viewMode = AlbumPageViewMode.VIEW)} />
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.VIEW_USERS}
|
||||
{#if viewMode === AlbumPageViewMode.VIEW_USERS}
|
||||
<ShareInfoModal
|
||||
onClose={() => (viewMode = ViewMode.VIEW)}
|
||||
onClose={() => (viewMode = AlbumPageViewMode.VIEW)}
|
||||
{album}
|
||||
onRemove={(userId) => handleRemoveUser(userId, ViewMode.VIEW_USERS)}
|
||||
onRemove={(userId) => handleRemoveUser(userId, AlbumPageViewMode.VIEW_USERS)}
|
||||
onRefreshAlbum={refreshAlbum}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.OPTIONS && $user}
|
||||
{#if viewMode === AlbumPageViewMode.OPTIONS && $user}
|
||||
<AlbumOptions
|
||||
{album}
|
||||
order={albumOrder}
|
||||
@@ -748,11 +750,11 @@
|
||||
albumOrder = order;
|
||||
await setModeToView();
|
||||
}}
|
||||
onRemove={(userId) => handleRemoveUser(userId, ViewMode.OPTIONS)}
|
||||
onRemove={(userId) => handleRemoveUser(userId, AlbumPageViewMode.OPTIONS)}
|
||||
onRefreshAlbum={refreshAlbum}
|
||||
onClose={() => (viewMode = ViewMode.VIEW)}
|
||||
onClose={() => (viewMode = AlbumPageViewMode.VIEW)}
|
||||
onToggleEnabledActivity={handleToggleEnableActivity}
|
||||
onShowSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)}
|
||||
onShowSelectSharedUser={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -19,13 +19,17 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const assetStore = new AssetStore({ isArchived: true });
|
||||
const assetInteractionStore = createAssetInteractionStore();
|
||||
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
|
||||
|
||||
$: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite);
|
||||
let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite));
|
||||
|
||||
onDestroy(() => {
|
||||
assetStore.destroy();
|
||||
@@ -51,6 +55,8 @@
|
||||
|
||||
<UserPageLayout hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}>
|
||||
<AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} removeAction={AssetAction.UNARCHIVE}>
|
||||
<EmptyPlaceholder text={$t('no_archived_assets_message')} slot="empty" />
|
||||
{#snippet empty()}
|
||||
<EmptyPlaceholder text={$t('no_archived_assets_message')} />
|
||||
{/snippet}
|
||||
</AssetGrid>
|
||||
</UserPageLayout>
|
||||
|
||||
@@ -11,8 +11,12 @@
|
||||
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||
import SupporterBadge from '$lib/components/shared-components/side-bar/supporter-badge.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
let showLicenseActivated = false;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
let showLicenseActivated = $state(false);
|
||||
const { isPurchased } = purchaseStore;
|
||||
</script>
|
||||
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
enum Field {
|
||||
CITY = 'exifInfo.city',
|
||||
@@ -23,9 +27,10 @@
|
||||
return targetField?.items || [];
|
||||
};
|
||||
|
||||
$: places = getFieldItems(data.items, Field.CITY);
|
||||
$: people = data.response.people;
|
||||
$: hasPeople = data.response.total > 0;
|
||||
let places = $derived(getFieldItems(data.items, Field.CITY));
|
||||
let people = $state(data.response.people);
|
||||
|
||||
let hasPeople = $derived(data.response.total > 0);
|
||||
|
||||
onMount(() => {
|
||||
return websocketEvents.on('on_person_thumbnail', (personId: string) => {
|
||||
@@ -51,13 +56,21 @@
|
||||
draggable="false">{$t('view_all')}</a
|
||||
>
|
||||
</div>
|
||||
<SingleGridRow class="grid md:grid-auto-fill-28 grid-auto-fill-20 gap-x-4" let:itemCount>
|
||||
{#each people.slice(0, itemCount) as person (person.id)}
|
||||
<a href="{AppRoute.PEOPLE}/{person.id}" class="text-center">
|
||||
<ImageThumbnail circle shadow url={getPeopleThumbnailUrl(person)} altText={person.name} widthStyle="100%" />
|
||||
<p class="mt-2 text-ellipsis text-sm font-medium dark:text-white">{person.name}</p>
|
||||
</a>
|
||||
{/each}
|
||||
<SingleGridRow class="grid md:grid-auto-fill-28 grid-auto-fill-20 gap-x-4">
|
||||
{#snippet children({ itemCount })}
|
||||
{#each people.slice(0, itemCount) as person (person.id)}
|
||||
<a href="{AppRoute.PEOPLE}/{person.id}" class="text-center">
|
||||
<ImageThumbnail
|
||||
circle
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(person)}
|
||||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
/>
|
||||
<p class="mt-2 text-ellipsis text-sm font-medium dark:text-white">{person.name}</p>
|
||||
</a>
|
||||
{/each}
|
||||
{/snippet}
|
||||
</SingleGridRow>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -72,23 +85,29 @@
|
||||
draggable="false">{$t('view_all')}</a
|
||||
>
|
||||
</div>
|
||||
<SingleGridRow class="grid md:grid-auto-fill-36 grid-auto-fill-28 gap-x-4" let:itemCount>
|
||||
{#each places.slice(0, itemCount) as item (item.data.id)}
|
||||
<a class="relative" href="{AppRoute.SEARCH}?{getMetadataSearchQuery({ city: item.value })}" draggable="false">
|
||||
<div class="flex justify-center overflow-hidden rounded-xl brightness-75 filter">
|
||||
<img
|
||||
src={getAssetThumbnailUrl({ id: item.data.id, size: AssetMediaSize.Thumbnail })}
|
||||
alt={item.value}
|
||||
class="object-cover aspect-square w-full"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="w-100 absolute bottom-2 w-full text-ellipsis px-1 text-center text-sm font-medium capitalize text-white backdrop-blur-[1px] hover:cursor-pointer"
|
||||
<SingleGridRow class="grid md:grid-auto-fill-36 grid-auto-fill-28 gap-x-4">
|
||||
{#snippet children({ itemCount })}
|
||||
{#each places.slice(0, itemCount) as item (item.data.id)}
|
||||
<a
|
||||
class="relative"
|
||||
href="{AppRoute.SEARCH}?{getMetadataSearchQuery({ city: item.value })}"
|
||||
draggable="false"
|
||||
>
|
||||
{item.value}
|
||||
</span>
|
||||
</a>
|
||||
{/each}
|
||||
<div class="flex justify-center overflow-hidden rounded-xl brightness-75 filter">
|
||||
<img
|
||||
src={getAssetThumbnailUrl({ id: item.data.id, size: AssetMediaSize.Thumbnail })}
|
||||
alt={item.value}
|
||||
class="object-cover aspect-square w-full"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="w-100 absolute bottom-2 w-full text-ellipsis px-1 text-center text-sm font-medium capitalize text-white backdrop-blur-[1px] hover:cursor-pointer"
|
||||
>
|
||||
{item.value}
|
||||
</span>
|
||||
</a>
|
||||
{/each}
|
||||
{/snippet}
|
||||
</SingleGridRow>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -21,13 +21,17 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const assetStore = new AssetStore({ isFavorite: true });
|
||||
const assetInteractionStore = createAssetInteractionStore();
|
||||
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
|
||||
|
||||
$: isAllArchive = [...$selectedAssets].every((asset) => asset.isArchived);
|
||||
let isAllArchive = $derived([...$selectedAssets].every((asset) => asset.isArchived));
|
||||
|
||||
onDestroy(() => {
|
||||
assetStore.destroy();
|
||||
@@ -56,6 +60,8 @@
|
||||
|
||||
<UserPageLayout hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}>
|
||||
<AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} removeAction={AssetAction.UNFAVORITE}>
|
||||
<EmptyPlaceholder text={$t('no_favorites_message')} slot="empty" />
|
||||
{#snippet empty()}
|
||||
<EmptyPlaceholder text={$t('no_favorites_message')} />
|
||||
{/snippet}
|
||||
</AssetGrid>
|
||||
</UserPageLayout>
|
||||
|
||||
@@ -18,15 +18,19 @@
|
||||
import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
|
||||
import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
const viewport: Viewport = { width: 0, height: 0 };
|
||||
let { data }: Props = $props();
|
||||
|
||||
$: pathSegments = data.path ? data.path.split('/') : [];
|
||||
$: tree = buildTree($foldersStore?.uniquePaths || []);
|
||||
$: currentPath = $page.url.searchParams.get(QueryParameter.PATH) || '';
|
||||
$: currentTreeItems = currentPath ? data.currentFolders : Object.keys(tree);
|
||||
let selectedAssets: Set<AssetResponseDto> = $state(new Set());
|
||||
const viewport: Viewport = $state({ width: 0, height: 0 });
|
||||
|
||||
let pathSegments = $derived(data.path ? data.path.split('/') : []);
|
||||
let tree = $derived(buildTree($foldersStore?.uniquePaths || []));
|
||||
let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
|
||||
let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree));
|
||||
|
||||
onMount(async () => {
|
||||
await foldersStore.fetchUniquePaths();
|
||||
@@ -48,20 +52,22 @@
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
<SideBarSection slot="sidebar">
|
||||
<SkipLink target={`#${headerId}`} text={$t('skip_to_folders')} />
|
||||
<section>
|
||||
<div class="text-xs pl-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div>
|
||||
<div class="h-full">
|
||||
<TreeItems
|
||||
icons={{ default: mdiFolderOutline, active: mdiFolder }}
|
||||
items={tree}
|
||||
active={currentPath}
|
||||
{getLink}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</SideBarSection>
|
||||
{#snippet sidebar()}
|
||||
<SideBarSection>
|
||||
<SkipLink target={`#${headerId}`} text={$t('skip_to_folders')} />
|
||||
<section>
|
||||
<div class="text-xs pl-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div>
|
||||
<div class="h-full">
|
||||
<TreeItems
|
||||
icons={{ default: mdiFolderOutline, active: mdiFolder }}
|
||||
items={tree}
|
||||
active={currentPath}
|
||||
{getLink}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</SideBarSection>
|
||||
{/snippet}
|
||||
|
||||
<Breadcrumbs {pathSegments} icon={mdiFolderHome} title={$t('folders')} {getLink} />
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import MapSettingsModal from '$lib/components/map-page/map-settings-modal.svelte';
|
||||
@@ -17,15 +19,19 @@
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
let { isViewing: showAssetViewer, asset: viewingAsset, setAssetId } = assetViewingStore;
|
||||
|
||||
let abortController: AbortController;
|
||||
let mapMarkers: MapMarkerResponseDto[] = [];
|
||||
let viewingAssets: string[] = [];
|
||||
let mapMarkers: MapMarkerResponseDto[] = $state([]);
|
||||
let viewingAssets: string[] = $state([]);
|
||||
let viewingAssetCursor = 0;
|
||||
let showSettingsModal = false;
|
||||
let showSettingsModal = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
mapMarkers = await loadMapMarkers();
|
||||
@@ -36,9 +42,11 @@
|
||||
assetViewingStore.showAssetViewer(false);
|
||||
});
|
||||
|
||||
$: if (!$featureFlags.map) {
|
||||
handlePromiseError(goto(AppRoute.PHOTOS));
|
||||
}
|
||||
run(() => {
|
||||
if (!$featureFlags.map) {
|
||||
handlePromiseError(goto(AppRoute.PHOTOS));
|
||||
}
|
||||
});
|
||||
const omit = (obj: MapSettings, key: string) => {
|
||||
return Object.fromEntries(Object.entries(obj).filter(([k]) => k !== key));
|
||||
};
|
||||
|
||||
+7
-3
@@ -15,7 +15,11 @@
|
||||
import { mdiPlus, mdiArrowLeft } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const assetStore = new AssetStore({ userId: data.partner.id, isArchived: false, withStacked: true });
|
||||
const assetInteractionStore = createAssetInteractionStore();
|
||||
@@ -39,11 +43,11 @@
|
||||
</AssetSelectControlBar>
|
||||
{:else}
|
||||
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(AppRoute.SHARING)}>
|
||||
<svelte:fragment slot="leading">
|
||||
{#snippet leading()}
|
||||
<p class="whitespace-nowrap text-immich-fg dark:text-immich-dark-fg">
|
||||
{data.partner.name}'s photos
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
<AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} />
|
||||
|
||||
@@ -38,34 +38,35 @@
|
||||
import { fly } from 'svelte/transition';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
$: people = data.people.people;
|
||||
$: visiblePeople = people.filter((people) => !people.isHidden);
|
||||
$: countVisiblePeople = searchName ? searchedPeopleLocal.length : data.people.total - data.people.hidden;
|
||||
$: showPeople = searchName ? searchedPeopleLocal : visiblePeople;
|
||||
|
||||
let selectHidden = false;
|
||||
let searchName = '';
|
||||
let showChangeNameModal = false;
|
||||
let showSetBirthDateModal = false;
|
||||
let showMergeModal = false;
|
||||
let personName = '';
|
||||
let nextPage = data.people.hasNextPage ? 2 : null;
|
||||
let personMerge1: PersonResponseDto;
|
||||
let personMerge2: PersonResponseDto;
|
||||
let potentialMergePeople: PersonResponseDto[] = [];
|
||||
let edittingPerson: PersonResponseDto | null = null;
|
||||
let searchedPeopleLocal: PersonResponseDto[] = [];
|
||||
let handleSearchPeople: (force?: boolean, name?: string) => Promise<void>;
|
||||
let changeNameInputEl: HTMLInputElement | null;
|
||||
let innerHeight: number;
|
||||
let { data }: Props = $props();
|
||||
|
||||
let selectHidden = $state(false);
|
||||
let searchName = $state('');
|
||||
let showChangeNameModal = $state(false);
|
||||
let showSetBirthDateModal = $state(false);
|
||||
let showMergeModal = $state(false);
|
||||
let personName = $state('');
|
||||
let nextPage = $state(data.people.hasNextPage ? 2 : null);
|
||||
let personMerge1 = $state<PersonResponseDto>();
|
||||
let personMerge2 = $state<PersonResponseDto>();
|
||||
let potentialMergePeople: PersonResponseDto[] = $state([]);
|
||||
let edittingPerson: PersonResponseDto | null = $state(null);
|
||||
let searchedPeopleLocal: PersonResponseDto[] = $state([]);
|
||||
// let handleSearchPeople: (force?: boolean, name?: string) => Promise<void> = $state();
|
||||
let changeNameInputEl = $state<HTMLInputElement>();
|
||||
let innerHeight = $state(0);
|
||||
let searchPeopleElement = $state<ReturnType<typeof SearchPeople>>();
|
||||
onMount(() => {
|
||||
const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE);
|
||||
if (getSearchedPeople) {
|
||||
searchName = getSearchedPeople;
|
||||
handlePromiseError(handleSearchPeople(true, searchName));
|
||||
if (searchPeopleElement) {
|
||||
handlePromiseError(searchPeopleElement.searchPeople(true, searchName));
|
||||
}
|
||||
}
|
||||
return websocketEvents.on('on_person_thumbnail', (personId: string) => {
|
||||
for (const person of people) {
|
||||
@@ -198,7 +199,9 @@
|
||||
);
|
||||
};
|
||||
|
||||
const submitNameChange = async () => {
|
||||
const submitNameChange = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
potentialMergePeople = [];
|
||||
showChangeNameModal = false;
|
||||
if (!edittingPerson || personName === edittingPerson.name) {
|
||||
@@ -225,9 +228,9 @@
|
||||
potentialMergePeople = people
|
||||
.filter(
|
||||
(person: PersonResponseDto) =>
|
||||
personMerge2.name.toLowerCase() === person.name.toLowerCase() &&
|
||||
personMerge2?.name.toLowerCase() === person.name.toLowerCase() &&
|
||||
person.id !== personMerge2.id &&
|
||||
person.id !== personMerge1.id &&
|
||||
person.id !== personMerge1?.id &&
|
||||
!person.isHidden,
|
||||
)
|
||||
.slice(0, 3);
|
||||
@@ -293,11 +296,26 @@
|
||||
const onResetSearchBar = async () => {
|
||||
await clearQueryParam(QueryParameter.SEARCHED_PEOPLE, $page.url);
|
||||
};
|
||||
|
||||
let people = $state(data.people.people);
|
||||
$effect(() => {
|
||||
people = data.people.people;
|
||||
});
|
||||
let visiblePeople = $derived(people.filter((people) => !people.isHidden));
|
||||
let countVisiblePeople = $derived(searchName ? searchedPeopleLocal.length : data.people.total - data.people.hidden);
|
||||
let showPeople = $derived(searchName ? searchedPeopleLocal : visiblePeople);
|
||||
|
||||
// const submitNameChange = (event: Event) => {
|
||||
// event.preventDefault();
|
||||
// if (searchPeopleElement) {
|
||||
// handlePromiseError(searchPeopleElement.searchPeople(true, searchName));
|
||||
// }
|
||||
// };
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight />
|
||||
|
||||
{#if showMergeModal}
|
||||
{#if showMergeModal && personMerge1 && personMerge2}
|
||||
<MergeSuggestionModal
|
||||
{personMerge1}
|
||||
{personMerge2}
|
||||
@@ -312,23 +330,23 @@
|
||||
title={$t('people')}
|
||||
description={countVisiblePeople === 0 && !searchName ? undefined : `(${countVisiblePeople.toLocaleString($locale)})`}
|
||||
>
|
||||
<svelte:fragment slot="buttons">
|
||||
{#snippet buttons()}
|
||||
{#if people.length > 0}
|
||||
<div class="flex gap-2 items-center justify-center">
|
||||
<div class="hidden sm:block">
|
||||
<div class="w-40 lg:w-80 h-10">
|
||||
<SearchPeople
|
||||
bind:this={searchPeopleElement}
|
||||
type="searchBar"
|
||||
placeholder={$t('search_people')}
|
||||
onReset={onResetSearchBar}
|
||||
onSearch={handleSearch}
|
||||
bind:searchName
|
||||
bind:searchedPeopleLocal
|
||||
bind:handleSearch={handleSearchPeople}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<LinkButton on:click={() => (selectHidden = !selectHidden)}>
|
||||
<LinkButton onclick={() => (selectHidden = !selectHidden)}>
|
||||
<div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
|
||||
<Icon path={mdiEyeOutline} size="18" />
|
||||
<p class="ml-2">{$t('show_and_hide_people')}</p>
|
||||
@@ -336,24 +354,20 @@
|
||||
</LinkButton>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
|
||||
{#if countVisiblePeople > 0 && (!searchName || searchedPeopleLocal.length > 0)}
|
||||
<PeopleInfiniteScroll
|
||||
people={showPeople}
|
||||
hasNextPage={!!nextPage && !searchName}
|
||||
{loadNextPage}
|
||||
let:person
|
||||
let:index
|
||||
>
|
||||
<PeopleCard
|
||||
{person}
|
||||
preload={index < 20}
|
||||
onChangeName={() => handleChangeName(person)}
|
||||
onSetBirthDate={() => handleSetBirthDate(person)}
|
||||
onMergePeople={() => handleMergePeople(person)}
|
||||
onHidePerson={() => handleHidePerson(person)}
|
||||
/>
|
||||
<PeopleInfiniteScroll people={showPeople} hasNextPage={!!nextPage && !searchName} {loadNextPage}>
|
||||
{#snippet children({ person, index })}
|
||||
<PeopleCard
|
||||
{person}
|
||||
preload={index < 20}
|
||||
onChangeName={() => handleChangeName(person)}
|
||||
onSetBirthDate={() => handleSetBirthDate(person)}
|
||||
onMergePeople={() => handleMergePeople(person)}
|
||||
onHidePerson={() => handleHidePerson(person)}
|
||||
/>
|
||||
{/snippet}
|
||||
</PeopleInfiniteScroll>
|
||||
{:else}
|
||||
<div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
|
||||
@@ -368,7 +382,7 @@
|
||||
|
||||
{#if showChangeNameModal}
|
||||
<FullScreenModal title={$t('change_name')} onClose={() => (showChangeNameModal = false)}>
|
||||
<form on:submit|preventDefault={submitNameChange} autocomplete="off" id="change-name-form">
|
||||
<form onsubmit={submitNameChange} autocomplete="off" id="change-name-form">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="name">{$t('name')}</label>
|
||||
<input
|
||||
@@ -381,16 +395,17 @@
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<svelte:fragment slot="sticky-bottom">
|
||||
|
||||
{#snippet stickyBottom()}
|
||||
<Button
|
||||
color="gray"
|
||||
fullwidth
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
showChangeNameModal = false;
|
||||
}}>{$t('cancel')}</Button
|
||||
>
|
||||
<Button type="submit" fullwidth form="change-name-form">{$t('ok')}</Button>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
|
||||
|
||||
+70
-75
@@ -25,7 +25,7 @@
|
||||
NotificationType,
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||
import { AppRoute, PersonPageViewMode, QueryParameter } from '$lib/constants';
|
||||
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { AssetStore } from '$lib/stores/assets.store';
|
||||
@@ -58,47 +58,33 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let numberOfAssets = data.statistics.assets;
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
enum ViewMode {
|
||||
VIEW_ASSETS = 'view-assets',
|
||||
SELECT_PERSON = 'select-person',
|
||||
MERGE_PEOPLE = 'merge-people',
|
||||
SUGGEST_MERGE = 'suggest-merge',
|
||||
BIRTH_DATE = 'birth-date',
|
||||
UNASSIGN_ASSETS = 'unassign-faces',
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data = $bindable() }: Props = $props();
|
||||
|
||||
let numberOfAssets = $state(data.statistics.assets);
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
let assetStore = new AssetStore({
|
||||
isArchived: false,
|
||||
personId: data.person.id,
|
||||
});
|
||||
|
||||
$: person = data.person;
|
||||
$: thumbnailData = getPeopleThumbnailUrl(person);
|
||||
$: if (person) {
|
||||
handlePromiseError(updateAssetCount());
|
||||
handlePromiseError(assetStore.updateOptions({ personId: person.id }));
|
||||
}
|
||||
|
||||
const assetInteractionStore = createAssetInteractionStore();
|
||||
const { selectedAssets, isMultiSelectState } = assetInteractionStore;
|
||||
|
||||
let viewMode: ViewMode = ViewMode.VIEW_ASSETS;
|
||||
let isEditingName = false;
|
||||
let previousRoute: string = AppRoute.EXPLORE;
|
||||
let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS);
|
||||
let isEditingName = $state(false);
|
||||
let previousRoute: string = $state(AppRoute.EXPLORE);
|
||||
let people: PersonResponseDto[] = [];
|
||||
let personMerge1: PersonResponseDto;
|
||||
let personMerge2: PersonResponseDto;
|
||||
let potentialMergePeople: PersonResponseDto[] = [];
|
||||
|
||||
let refreshAssetGrid = false;
|
||||
let personMerge1: PersonResponseDto | undefined = $state();
|
||||
let personMerge2: PersonResponseDto | undefined = $state();
|
||||
let potentialMergePeople: PersonResponseDto[] = $state([]);
|
||||
|
||||
let personName = '';
|
||||
let suggestedPeople: PersonResponseDto[] = [];
|
||||
let suggestedPeople: PersonResponseDto[] = $state([]);
|
||||
|
||||
/**
|
||||
* Save the word used to search people name: for example,
|
||||
@@ -107,11 +93,8 @@
|
||||
* However, it needs to make a new api request if searching 'r' returns 20 names (arbitrary value, the limit sent back by the server).
|
||||
* or if the new search word starts with another word / letter
|
||||
**/
|
||||
let isSearchingPeople = false;
|
||||
let suggestionContainer: HTMLDivElement;
|
||||
|
||||
$: isAllArchive = [...$selectedAssets].every((asset) => asset.isArchived);
|
||||
$: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite);
|
||||
let isSearchingPeople = $state(false);
|
||||
let suggestionContainer: HTMLElement | undefined = $state();
|
||||
|
||||
onMount(() => {
|
||||
const action = $page.url.searchParams.get(QueryParameter.ACTION);
|
||||
@@ -120,7 +103,7 @@
|
||||
previousRoute = getPreviousRoute;
|
||||
}
|
||||
if (action == 'merge') {
|
||||
viewMode = ViewMode.MERGE_PEOPLE;
|
||||
viewMode = PersonPageViewMode.MERGE_PEOPLE;
|
||||
}
|
||||
|
||||
return websocketEvents.on('on_person_thumbnail', (personId: string) => {
|
||||
@@ -131,7 +114,7 @@
|
||||
});
|
||||
|
||||
const handleEscape = async () => {
|
||||
if ($showAssetViewer || viewMode === ViewMode.SUGGEST_MERGE) {
|
||||
if ($showAssetViewer || viewMode === PersonPageViewMode.SUGGEST_MERGE) {
|
||||
return;
|
||||
}
|
||||
if ($isMultiSelectState) {
|
||||
@@ -162,11 +145,11 @@
|
||||
const handleUnmerge = () => {
|
||||
$assetStore.removeAssets([...$selectedAssets].map((a) => a.id));
|
||||
assetInteractionStore.clearMultiselect();
|
||||
viewMode = ViewMode.VIEW_ASSETS;
|
||||
viewMode = PersonPageViewMode.VIEW_ASSETS;
|
||||
};
|
||||
|
||||
const handleReassignAssets = () => {
|
||||
viewMode = ViewMode.UNASSIGN_ASSETS;
|
||||
viewMode = PersonPageViewMode.UNASSIGN_ASSETS;
|
||||
};
|
||||
|
||||
const toggleHidePerson = async () => {
|
||||
@@ -191,13 +174,11 @@
|
||||
await updateAssetCount();
|
||||
await handleGoBack();
|
||||
|
||||
data.person = person;
|
||||
|
||||
refreshAssetGrid = !refreshAssetGrid;
|
||||
data = { ...data, person };
|
||||
};
|
||||
|
||||
const handleSelectFeaturePhoto = async (asset: AssetResponseDto) => {
|
||||
if (viewMode !== ViewMode.SELECT_PERSON) {
|
||||
if (viewMode !== PersonPageViewMode.SELECT_PERSON) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -209,12 +190,12 @@
|
||||
|
||||
assetInteractionStore.clearMultiselect();
|
||||
|
||||
viewMode = ViewMode.VIEW_ASSETS;
|
||||
viewMode = PersonPageViewMode.VIEW_ASSETS;
|
||||
};
|
||||
|
||||
const handleMergeSamePerson = async (response: [PersonResponseDto, PersonResponseDto]) => {
|
||||
const [personToMerge, personToBeMergedIn] = response;
|
||||
viewMode = ViewMode.VIEW_ASSETS;
|
||||
viewMode = PersonPageViewMode.VIEW_ASSETS;
|
||||
isEditingName = false;
|
||||
try {
|
||||
await mergePerson({
|
||||
@@ -228,7 +209,6 @@
|
||||
people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id);
|
||||
if (personToBeMergedIn.name != personName && person.id === personToBeMergedIn.id) {
|
||||
await updateAssetCount();
|
||||
refreshAssetGrid = !refreshAssetGrid;
|
||||
return;
|
||||
}
|
||||
await goto(`${AppRoute.PEOPLE}/${personToBeMergedIn.id}`, { replaceState: true });
|
||||
@@ -243,11 +223,11 @@
|
||||
personName = person.name;
|
||||
personMerge1 = person;
|
||||
personMerge2 = person2;
|
||||
viewMode = ViewMode.SUGGEST_MERGE;
|
||||
viewMode = PersonPageViewMode.SUGGEST_MERGE;
|
||||
};
|
||||
|
||||
const changeName = async () => {
|
||||
viewMode = ViewMode.VIEW_ASSETS;
|
||||
viewMode = PersonPageViewMode.VIEW_ASSETS;
|
||||
person.name = personName;
|
||||
try {
|
||||
isEditingName = false;
|
||||
@@ -264,7 +244,7 @@
|
||||
};
|
||||
|
||||
const handleCancelEditName = () => {
|
||||
if (viewMode === ViewMode.SUGGEST_MERGE) {
|
||||
if (viewMode === PersonPageViewMode.SUGGEST_MERGE) {
|
||||
return;
|
||||
}
|
||||
isSearchingPeople = false;
|
||||
@@ -295,13 +275,13 @@
|
||||
potentialMergePeople = result
|
||||
.filter(
|
||||
(person: PersonResponseDto) =>
|
||||
personMerge2.name.toLowerCase() === person.name.toLowerCase() &&
|
||||
personMerge2?.name.toLowerCase() === person.name.toLowerCase() &&
|
||||
person.id !== personMerge2.id &&
|
||||
person.id !== personMerge1.id &&
|
||||
person.id !== personMerge1?.id &&
|
||||
!person.isHidden,
|
||||
)
|
||||
.slice(0, 3);
|
||||
viewMode = ViewMode.SUGGEST_MERGE;
|
||||
viewMode = PersonPageViewMode.SUGGEST_MERGE;
|
||||
return;
|
||||
}
|
||||
await changeName();
|
||||
@@ -309,7 +289,7 @@
|
||||
|
||||
const handleSetBirthDate = async (birthDate: string) => {
|
||||
try {
|
||||
viewMode = ViewMode.VIEW_ASSETS;
|
||||
viewMode = PersonPageViewMode.VIEW_ASSETS;
|
||||
person.birthDate = birthDate;
|
||||
|
||||
const updatedPerson = await updatePerson({
|
||||
@@ -331,7 +311,7 @@
|
||||
};
|
||||
|
||||
const handleGoBack = async () => {
|
||||
viewMode = ViewMode.VIEW_ASSETS;
|
||||
viewMode = PersonPageViewMode.VIEW_ASSETS;
|
||||
if ($page.url.searchParams.has(QueryParameter.ACTION)) {
|
||||
$page.url.searchParams.delete(QueryParameter.ACTION);
|
||||
await goto($page.url);
|
||||
@@ -341,37 +321,50 @@
|
||||
onDestroy(() => {
|
||||
assetStore.destroy();
|
||||
});
|
||||
let person = $derived(data.person);
|
||||
|
||||
let thumbnailData = $derived(getPeopleThumbnailUrl(person));
|
||||
|
||||
$effect(() => {
|
||||
if (person) {
|
||||
handlePromiseError(updateAssetCount());
|
||||
handlePromiseError(assetStore.updateOptions({ personId: person.id }));
|
||||
}
|
||||
});
|
||||
|
||||
let isAllArchive = $derived([...$selectedAssets].every((asset) => asset.isArchived));
|
||||
let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite));
|
||||
</script>
|
||||
|
||||
{#if viewMode === ViewMode.UNASSIGN_ASSETS}
|
||||
{#if viewMode === PersonPageViewMode.UNASSIGN_ASSETS}
|
||||
<UnMergeFaceSelector
|
||||
assetIds={[...$selectedAssets].map((a) => a.id)}
|
||||
personAssets={person}
|
||||
onClose={() => (viewMode = ViewMode.VIEW_ASSETS)}
|
||||
onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)}
|
||||
onConfirm={handleUnmerge}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.SUGGEST_MERGE}
|
||||
{#if viewMode === PersonPageViewMode.SUGGEST_MERGE && personMerge1 && personMerge2}
|
||||
<MergeSuggestionModal
|
||||
{personMerge1}
|
||||
{personMerge2}
|
||||
{potentialMergePeople}
|
||||
onClose={() => (viewMode = ViewMode.VIEW_ASSETS)}
|
||||
onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)}
|
||||
onReject={changeName}
|
||||
onConfirm={handleMergeSamePerson}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.BIRTH_DATE}
|
||||
{#if viewMode === PersonPageViewMode.BIRTH_DATE}
|
||||
<SetBirthDateModal
|
||||
birthDate={person.birthDate ?? ''}
|
||||
onClose={() => (viewMode = ViewMode.VIEW_ASSETS)}
|
||||
onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)}
|
||||
onUpdate={handleSetBirthDate}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.MERGE_PEOPLE}
|
||||
{#if viewMode === PersonPageViewMode.MERGE_PEOPLE}
|
||||
<MergeFaceSelector {person} onBack={handleGoBack} onMerge={handleMerge} />
|
||||
{/if}
|
||||
|
||||
@@ -399,14 +392,14 @@
|
||||
</ButtonContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{:else}
|
||||
{#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE}
|
||||
{#if viewMode === PersonPageViewMode.VIEW_ASSETS || viewMode === PersonPageViewMode.SUGGEST_MERGE || viewMode === PersonPageViewMode.BIRTH_DATE}
|
||||
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(previousRoute)}>
|
||||
<svelte:fragment slot="trailing">
|
||||
{#snippet trailing()}
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<MenuOption
|
||||
text={$t('select_featured_photo')}
|
||||
icon={mdiAccountBoxOutline}
|
||||
onClick={() => (viewMode = ViewMode.SELECT_PERSON)}
|
||||
onClick={() => (viewMode = PersonPageViewMode.SELECT_PERSON)}
|
||||
/>
|
||||
<MenuOption
|
||||
text={person.isHidden ? $t('unhide_person') : $t('hide_person')}
|
||||
@@ -416,21 +409,23 @@
|
||||
<MenuOption
|
||||
text={$t('set_date_of_birth')}
|
||||
icon={mdiCalendarEditOutline}
|
||||
onClick={() => (viewMode = ViewMode.BIRTH_DATE)}
|
||||
onClick={() => (viewMode = PersonPageViewMode.BIRTH_DATE)}
|
||||
/>
|
||||
<MenuOption
|
||||
text={$t('merge_people')}
|
||||
icon={mdiAccountMultipleCheckOutline}
|
||||
onClick={() => (viewMode = ViewMode.MERGE_PEOPLE)}
|
||||
onClick={() => (viewMode = PersonPageViewMode.MERGE_PEOPLE)}
|
||||
/>
|
||||
</ButtonContextMenu>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.SELECT_PERSON}
|
||||
<ControlAppBar onClose={() => (viewMode = ViewMode.VIEW_ASSETS)}>
|
||||
<svelte:fragment slot="leading">{$t('select_featured_photo')}</svelte:fragment>
|
||||
{#if viewMode === PersonPageViewMode.SELECT_PERSON}
|
||||
<ControlAppBar onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)}>
|
||||
{#snippet leading()}
|
||||
{$t('select_featured_photo')}
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -442,12 +437,12 @@
|
||||
enableRouting={true}
|
||||
{assetStore}
|
||||
{assetInteractionStore}
|
||||
isSelectionMode={viewMode === ViewMode.SELECT_PERSON}
|
||||
singleSelect={viewMode === ViewMode.SELECT_PERSON}
|
||||
isSelectionMode={viewMode === PersonPageViewMode.SELECT_PERSON}
|
||||
singleSelect={viewMode === PersonPageViewMode.SELECT_PERSON}
|
||||
onSelect={handleSelectFeaturePhoto}
|
||||
onEscape={handleEscape}
|
||||
>
|
||||
{#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE}
|
||||
{#if viewMode === PersonPageViewMode.VIEW_ASSETS || viewMode === PersonPageViewMode.SUGGEST_MERGE || viewMode === PersonPageViewMode.BIRTH_DATE}
|
||||
<!-- Person information block -->
|
||||
<div
|
||||
class="relative w-fit p-4 sm:px-6"
|
||||
@@ -473,7 +468,7 @@
|
||||
type="button"
|
||||
class="flex items-center justify-center"
|
||||
title={$t('edit_name')}
|
||||
on:click={() => (isEditingName = true)}
|
||||
onclick={() => (isEditingName = true)}
|
||||
>
|
||||
<ImageThumbnail
|
||||
circle
|
||||
@@ -510,11 +505,11 @@
|
||||
{#each suggestedPeople as person, index (person.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full border-t border-gray-400 dark:border-immich-dark-gray h-14 place-items-center bg-gray-200 p-2 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-[#232932] focus:bg-gray-300 focus:dark:bg-[#232932] {index ===
|
||||
class="flex w-full border border-gray-200 dark:border-immich-dark-gray h-14 place-items-center bg-gray-100 p-2 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-[#232932] focus:bg-gray-300 focus:dark:bg-[#232932] {index ===
|
||||
suggestedPeople.length - 1
|
||||
? 'rounded-b-lg border-b'
|
||||
: ''}"
|
||||
on:click={() => handleSuggestPeople(person)}
|
||||
onclick={() => handleSuggestPeople(person)}
|
||||
>
|
||||
<ImageThumbnail
|
||||
circle
|
||||
|
||||
@@ -35,13 +35,12 @@
|
||||
const assetInteractionStore = createAssetInteractionStore();
|
||||
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
|
||||
|
||||
let isAllFavorite: boolean;
|
||||
let isAllOwned: boolean;
|
||||
let isAssetStackSelected: boolean;
|
||||
let isLinkActionAvailable: boolean;
|
||||
let isAllFavorite = $state(false);
|
||||
let isAllOwned = $state(false);
|
||||
let isAssetStackSelected = $state(false);
|
||||
let isLinkActionAvailable = $state(false);
|
||||
|
||||
// svelte-ignore reactive_declaration_non_reactive_property
|
||||
$: {
|
||||
$effect(() => {
|
||||
const selection = [...$selectedAssets];
|
||||
isAllOwned = selection.every((asset) => asset.ownerId === $user.id);
|
||||
isAllFavorite = selection.every((asset) => asset.isFavorite);
|
||||
@@ -52,7 +51,7 @@
|
||||
selection.some((asset) => asset.type === AssetTypeEnum.Image) &&
|
||||
selection.some((asset) => asset.type === AssetTypeEnum.Image);
|
||||
isLinkActionAvailable = isAllOwned && (isLivePhoto || isLivePhotoCandidate);
|
||||
}
|
||||
});
|
||||
|
||||
const handleEscape = () => {
|
||||
if ($showAssetViewer) {
|
||||
@@ -134,6 +133,8 @@
|
||||
{#if $preferences.memories.enabled}
|
||||
<MemoryLane />
|
||||
{/if}
|
||||
<EmptyPlaceholder text={$t('no_assets_message')} onClick={() => openFileUploadDialog()} slot="empty" />
|
||||
{#snippet empty()}
|
||||
<EmptyPlaceholder text={$t('no_assets_message')} onClick={() => openFileUploadDialog()} />
|
||||
{/snippet}
|
||||
</AssetGrid>
|
||||
</UserPageLayout>
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
type AssetWithCity = AssetResponseDto & {
|
||||
exifInfo: {
|
||||
@@ -17,10 +21,10 @@
|
||||
};
|
||||
};
|
||||
|
||||
$: places = data.items.filter((item): item is AssetWithCity => !!item.exifInfo?.city);
|
||||
$: hasPlaces = places.length > 0;
|
||||
let places = $derived(data.items.filter((item): item is AssetWithCity => !!item.exifInfo?.city));
|
||||
let hasPlaces = $derived(places.length > 0);
|
||||
|
||||
let innerHeight: number;
|
||||
let innerHeight: number = $state(0);
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight />
|
||||
|
||||
@@ -40,24 +40,40 @@
|
||||
import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
|
||||
import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { afterUpdate, tick } from 'svelte';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
|
||||
|
||||
const MAX_ASSET_COUNT = 5000;
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
const viewport: Viewport = { width: 0, height: 0 };
|
||||
const viewport: Viewport = $state({ width: 0, height: 0 });
|
||||
|
||||
// The GalleryViewer pushes it's own history state, which causes weird
|
||||
// behavior for history.back(). To prevent that we store the previous page
|
||||
// manually and navigate back to that.
|
||||
let previousRoute = AppRoute.EXPLORE as string;
|
||||
let previousRoute = $state(AppRoute.EXPLORE as string);
|
||||
|
||||
let nextPage: number | null = 1;
|
||||
let searchResultAlbums: AlbumResponseDto[] = [];
|
||||
let searchResultAssets: AssetResponseDto[] = [];
|
||||
let isLoading = true;
|
||||
let scrollY = 0;
|
||||
let searchResultAlbums: AlbumResponseDto[] = $state([]);
|
||||
let searchResultAssets: AssetResponseDto[] = $state([]);
|
||||
let isLoading = $state(true);
|
||||
let scrollY = $state(0);
|
||||
let scrollYHistory = 0;
|
||||
let selectedAssets: Set<AssetResponseDto> = $state(new Set());
|
||||
|
||||
type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query'>;
|
||||
|
||||
let isMultiSelectionMode = $derived(selectedAssets.size > 0);
|
||||
let isAllArchived = $derived([...selectedAssets].every((asset) => asset.isArchived));
|
||||
let isAllFavorite = $derived([...selectedAssets].every((asset) => asset.isFavorite));
|
||||
let searchQuery = $derived($page.url.searchParams.get(QueryParameter.QUERY));
|
||||
|
||||
onMount(() => {
|
||||
if (terms && $featureFlags.loaded) {
|
||||
handlePromiseError(onSearchQueryUpdate());
|
||||
}
|
||||
});
|
||||
|
||||
let terms = $derived(searchQuery ? JSON.parse(searchQuery) : {});
|
||||
|
||||
const onEscape = () => {
|
||||
if ($showAssetViewer) {
|
||||
@@ -74,8 +90,7 @@
|
||||
$preventRaceConditionSearchBar = false;
|
||||
};
|
||||
|
||||
// save and restore scroll position
|
||||
afterUpdate(() => {
|
||||
$effect(() => {
|
||||
if (scrollY) {
|
||||
scrollYHistory = scrollY;
|
||||
}
|
||||
@@ -105,11 +120,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
$: isAllArchived = [...selectedAssets].every((asset) => asset.isArchived);
|
||||
$: isAllFavorite = [...selectedAssets].every((asset) => asset.isFavorite);
|
||||
|
||||
const onAssetDelete = (assetIds: string[]) => {
|
||||
const assetIdSet = new Set(assetIds);
|
||||
searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id));
|
||||
@@ -118,16 +128,6 @@
|
||||
selectedAssets = new Set(searchResultAssets);
|
||||
};
|
||||
|
||||
type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query'>;
|
||||
|
||||
$: searchQuery = $page.url.searchParams.get(QueryParameter.QUERY);
|
||||
let terms: SearchTerms;
|
||||
$: terms = searchQuery ? JSON.parse(searchQuery) : {};
|
||||
|
||||
$: if (terms && $featureFlags.loaded) {
|
||||
handlePromiseError(onSearchQueryUpdate());
|
||||
}
|
||||
|
||||
async function onSearchQueryUpdate() {
|
||||
nextPage = 1;
|
||||
searchResultAssets = [];
|
||||
@@ -234,7 +234,7 @@
|
||||
<div class="fixed z-[100] top-0 left-0 w-full">
|
||||
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
|
||||
<CreateSharedLink />
|
||||
<CircleIconButton title={$t('select_all')} icon={mdiSelectAll} on:click={handleSelectAll} />
|
||||
<CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} />
|
||||
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
|
||||
<AddToAlbum {onAddToAlbum} />
|
||||
<AddToAlbum shared {onAddToAlbum} />
|
||||
@@ -256,45 +256,52 @@
|
||||
<div class="fixed z-[100] top-0 left-0 w-full">
|
||||
<ControlAppBar onClose={() => goto(previousRoute)} backIcon={mdiArrowLeft}>
|
||||
<div class="w-full flex-1 pl-4">
|
||||
<SearchBar grayTheme={false} value={terms.query ?? ''} searchQuery={terms} />
|
||||
<SearchBar
|
||||
grayTheme={false}
|
||||
value={terms?.query ?? ''}
|
||||
searchQuery={terms}
|
||||
onSearch={() => handlePromiseError(onSearchQueryUpdate())}
|
||||
/>
|
||||
</div>
|
||||
</ControlAppBar>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="search-chips"
|
||||
class="mt-24 text-center w-full flex gap-5 place-content-center place-items-center flex-wrap px-24"
|
||||
>
|
||||
{#each getObjectKeys(terms) as key (key)}
|
||||
{@const value = terms[key]}
|
||||
<div class="flex place-content-center place-items-center text-xs">
|
||||
<div
|
||||
class="bg-immich-primary py-2 px-4 text-white dark:text-black dark:bg-immich-dark-primary
|
||||
{#if terms}
|
||||
<section
|
||||
id="search-chips"
|
||||
class="mt-24 text-center w-full flex gap-5 place-content-center place-items-center flex-wrap px-24"
|
||||
>
|
||||
{#each getObjectKeys(terms) as key (key)}
|
||||
{@const value = terms[key]}
|
||||
<div class="flex place-content-center place-items-center text-xs">
|
||||
<div
|
||||
class="bg-immich-primary py-2 px-4 text-white dark:text-black dark:bg-immich-dark-primary
|
||||
{value === true ? 'rounded-full' : 'rounded-tl-full rounded-bl-full'}"
|
||||
>
|
||||
{getHumanReadableSearchKey(key)}
|
||||
</div>
|
||||
|
||||
{#if value !== true}
|
||||
<div class="bg-gray-300 py-2 px-4 dark:bg-gray-800 dark:text-white rounded-tr-full rounded-br-full">
|
||||
{#if (key === 'takenAfter' || key === 'takenBefore') && typeof value === 'string'}
|
||||
{getHumanReadableDate(value)}
|
||||
{:else if key === 'personIds' && Array.isArray(value)}
|
||||
{#await getPersonName(value) then personName}
|
||||
{personName}
|
||||
{/await}
|
||||
{:else if value === null || value === ''}
|
||||
{$t('unknown')}
|
||||
{:else}
|
||||
{value}
|
||||
{/if}
|
||||
>
|
||||
{getHumanReadableSearchKey(key as keyof SearchTerms)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
{#if value !== true}
|
||||
<div class="bg-gray-300 py-2 px-4 dark:bg-gray-800 dark:text-white rounded-tr-full rounded-br-full">
|
||||
{#if (key === 'takenAfter' || key === 'takenBefore') && typeof value === 'string'}
|
||||
{getHumanReadableDate(value)}
|
||||
{:else if key === 'personIds' && Array.isArray(value)}
|
||||
{#await getPersonName(value) then personName}
|
||||
{personName}
|
||||
{/await}
|
||||
{:else if value === null || value === ''}
|
||||
{$t('unknown')}
|
||||
{:else}
|
||||
{value}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section
|
||||
class="relative mb-12 bg-immich-bg dark:bg-immich-dark-bg m-4"
|
||||
|
||||
@@ -15,21 +15,24 @@
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
let { gridScrollTarget } = assetViewingStore;
|
||||
let { sharedLink, passwordRequired, sharedLinkKey: key, meta } = data;
|
||||
let { title, description } = meta;
|
||||
let isOwned = $user ? $user.id === sharedLink?.userId : false;
|
||||
let password = '';
|
||||
let innerWidth: number;
|
||||
let { sharedLink, passwordRequired, sharedLinkKey: key, meta } = $state(data);
|
||||
let { title, description } = $state(meta);
|
||||
let isOwned = $derived($user ? $user.id === sharedLink?.userId : false);
|
||||
let password = $state('');
|
||||
let innerWidth: number = $state(0);
|
||||
|
||||
const handlePasswordSubmit = async () => {
|
||||
try {
|
||||
sharedLink = await getMySharedLink({ password, key });
|
||||
setSharedLink(sharedLink);
|
||||
passwordRequired = false;
|
||||
isOwned = $user ? $user.id === sharedLink.userId : false;
|
||||
title = (sharedLink.album ? sharedLink.album.albumName : $t('public_share')) + ' - Immich';
|
||||
description =
|
||||
sharedLink.description ||
|
||||
@@ -43,6 +46,11 @@
|
||||
handleError(error, $t('errors.unable_to_get_shared_link'));
|
||||
}
|
||||
};
|
||||
|
||||
const onsubmit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
await handlePasswordSubmit();
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth />
|
||||
@@ -54,13 +62,13 @@
|
||||
{#if passwordRequired}
|
||||
<header>
|
||||
<ControlAppBar showBackButton={false}>
|
||||
<svelte:fragment slot="leading">
|
||||
{#snippet leading()}
|
||||
<ImmichLogoSmallLink width={innerWidth} />
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
|
||||
<svelte:fragment slot="trailing">
|
||||
{#snippet trailing()}
|
||||
<ThemeButton />
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
</header>
|
||||
<main
|
||||
@@ -72,7 +80,7 @@
|
||||
{$t('sharing_enter_password')}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<form novalidate autocomplete="off" on:submit|preventDefault={handlePasswordSubmit}>
|
||||
<form novalidate autocomplete="off" {onsubmit}>
|
||||
<input type="password" class="immich-form-input mr-2" placeholder={$t('password')} bind:value={password} />
|
||||
<Button type="submit">{$t('submit')}</Button>
|
||||
</form>
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
import Albums from '$lib/components/album-page/albums-list.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const settings: AlbumViewSettings = {
|
||||
view: AlbumViewMode.Cover,
|
||||
@@ -34,21 +38,23 @@
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
<div class="flex" slot="buttons">
|
||||
<LinkButton on:click={() => createAlbumAndRedirect()}>
|
||||
<div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
|
||||
<Icon path={mdiPlusBoxOutline} size="18" class="shrink-0" />
|
||||
<span class="leading-none max-sm:text-xs">{$t('create_album')}</span>
|
||||
</div>
|
||||
</LinkButton>
|
||||
{#snippet buttons()}
|
||||
<div class="flex">
|
||||
<LinkButton onclick={() => createAlbumAndRedirect()}>
|
||||
<div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
|
||||
<Icon path={mdiPlusBoxOutline} size="18" class="shrink-0" />
|
||||
<span class="leading-none max-sm:text-xs">{$t('create_album')}</span>
|
||||
</div>
|
||||
</LinkButton>
|
||||
|
||||
<LinkButton href={AppRoute.SHARED_LINKS}>
|
||||
<div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
|
||||
<Icon path={mdiLink} size="18" class="shrink-0" />
|
||||
<span class="leading-none max-sm:text-xs">{$t('shared_links')}</span>
|
||||
</div>
|
||||
</LinkButton>
|
||||
</div>
|
||||
<LinkButton href={AppRoute.SHARED_LINKS}>
|
||||
<div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
|
||||
<Icon path={mdiLink} size="18" class="shrink-0" />
|
||||
<span class="leading-none max-sm:text-xs">{$t('shared_links')}</span>
|
||||
</div>
|
||||
</LinkButton>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#if data.partners.length > 0}
|
||||
@@ -89,7 +95,9 @@
|
||||
<!-- Shared Album List -->
|
||||
<Albums sharedAlbums={data.sharedAlbums} userSettings={settings} showOwner>
|
||||
<!-- Empty List -->
|
||||
<EmptyPlaceholder slot="empty" text={$t('no_shared_albums_message')} src={empty2Url} />
|
||||
{#snippet empty()}
|
||||
<EmptyPlaceholder text={$t('no_shared_albums_message')} src={empty2Url} />
|
||||
{/snippet}
|
||||
</Albums>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let sharedLinks: SharedLinkResponseDto[] = [];
|
||||
let editSharedLink: SharedLinkResponseDto | null = null;
|
||||
let sharedLinks: SharedLinkResponseDto[] = $state([]);
|
||||
let editSharedLink: SharedLinkResponseDto | null = $state(null);
|
||||
|
||||
const refresh = async () => {
|
||||
sharedLinks = await getAllSharedLinks();
|
||||
@@ -53,7 +53,9 @@
|
||||
</script>
|
||||
|
||||
<ControlAppBar backIcon={mdiArrowLeft} onClose={() => goto(AppRoute.SHARING)}>
|
||||
<svelte:fragment slot="leading">{$t('shared_links')}</svelte:fragment>
|
||||
{#snippet leading()}
|
||||
{$t('shared_links')}
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
|
||||
<section class="mt-[120px] flex flex-col pb-[120px] container max-w-screen-lg mx-auto px-3">
|
||||
|
||||
@@ -11,13 +11,11 @@
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import SettingInputField, {
|
||||
SettingInputFieldType,
|
||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
|
||||
import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
|
||||
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
|
||||
import { AppRoute, AssetAction, QueryParameter } from '$lib/constants';
|
||||
import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants';
|
||||
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
|
||||
import { AssetStore } from '$lib/stores/assets.store';
|
||||
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
|
||||
@@ -29,10 +27,14 @@
|
||||
import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
|
||||
import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
$: pathSegments = data.path ? data.path.split('/') : [];
|
||||
$: currentPath = $page.url.searchParams.get(QueryParameter.PATH) || '';
|
||||
let { data }: Props = $props();
|
||||
|
||||
let pathSegments = $derived(data.path ? data.path.split('/') : []);
|
||||
let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
|
||||
|
||||
const assetInteractionStore = createAssetInteractionStore();
|
||||
|
||||
@@ -42,14 +44,19 @@
|
||||
|
||||
const assetStore = new AssetStore({});
|
||||
|
||||
$: tags = data.tags;
|
||||
$: tagsMap = buildMap(tags);
|
||||
$: tag = currentPath ? tagsMap[currentPath] : null;
|
||||
$: tagId = tag?.id;
|
||||
$: tree = buildTree(tags.map((tag) => tag.value));
|
||||
$: {
|
||||
let tags = $state<TagResponseDto[]>([]);
|
||||
$effect(() => {
|
||||
tags = data.tags;
|
||||
});
|
||||
|
||||
let tagsMap = $derived(buildMap(tags));
|
||||
let tag = $derived(currentPath ? tagsMap[currentPath] : null);
|
||||
let tagId = $derived(tag?.id);
|
||||
let tree = $derived(buildTree(tags.map((tag) => tag.value)));
|
||||
|
||||
$effect.pre(() => {
|
||||
void assetStore.updateOptions({ tagId });
|
||||
}
|
||||
});
|
||||
|
||||
const handleNavigation = async (tag: string) => {
|
||||
await navigateToView(normalizeTreePath(`${data.path || ''}/${tag}`));
|
||||
@@ -67,15 +74,15 @@
|
||||
|
||||
const navigateToView = (path: string) => goto(getLink(path));
|
||||
|
||||
let isNewOpen = false;
|
||||
let newTagValue = '';
|
||||
let isNewOpen = $state(false);
|
||||
let newTagValue = $state('');
|
||||
const handleCreate = () => {
|
||||
newTagValue = tag ? tag.value + '/' : '';
|
||||
isNewOpen = true;
|
||||
};
|
||||
|
||||
let isEditOpen = false;
|
||||
let newTagColor = '';
|
||||
let isEditOpen = $state(false);
|
||||
let newTagColor = $state('');
|
||||
const handleEdit = () => {
|
||||
newTagColor = tag?.color ?? '';
|
||||
isEditOpen = true;
|
||||
@@ -135,49 +142,66 @@
|
||||
const parentPath = pathSegments.slice(0, -1).join('/');
|
||||
await navigateToView(parentPath);
|
||||
};
|
||||
|
||||
const onsubmit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
await handleSubmit();
|
||||
};
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title} scrollbar={false}>
|
||||
<SideBarSection slot="sidebar">
|
||||
<SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} />
|
||||
{#snippet sidebar()}
|
||||
<SideBarSection>
|
||||
<SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} />
|
||||
<section>
|
||||
<div class="text-xs pl-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div>
|
||||
<div class="h-full">
|
||||
<TreeItems
|
||||
icons={{ default: mdiTag, active: mdiTag }}
|
||||
items={tree}
|
||||
active={currentPath}
|
||||
{getLink}
|
||||
{getColor}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</SideBarSection>
|
||||
{/snippet}
|
||||
|
||||
{#snippet buttons()}
|
||||
<section>
|
||||
<div class="text-xs pl-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div>
|
||||
<div class="h-full">
|
||||
<TreeItems icons={{ default: mdiTag, active: mdiTag }} items={tree} active={currentPath} {getLink} {getColor} />
|
||||
</div>
|
||||
<LinkButton onclick={handleCreate}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiPlus} size="18" />
|
||||
<p class="hidden md:block">{$t('create_tag')}</p>
|
||||
</div>
|
||||
</LinkButton>
|
||||
|
||||
{#if pathSegments.length > 0 && tag}
|
||||
<LinkButton onclick={handleEdit}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiPencil} size="18" />
|
||||
<p class="hidden md:block">{$t('edit_tag')}</p>
|
||||
</div>
|
||||
</LinkButton>
|
||||
<LinkButton onclick={handleDelete}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiTrashCanOutline} size="18" />
|
||||
<p class="hidden md:block">{$t('delete_tag')}</p>
|
||||
</div>
|
||||
</LinkButton>
|
||||
{/if}
|
||||
</section>
|
||||
</SideBarSection>
|
||||
|
||||
<section slot="buttons">
|
||||
<LinkButton on:click={handleCreate}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiPlus} size="18" />
|
||||
<p class="hidden md:block">{$t('create_tag')}</p>
|
||||
</div>
|
||||
</LinkButton>
|
||||
|
||||
{#if pathSegments.length > 0 && tag}
|
||||
<LinkButton on:click={handleEdit}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiPencil} size="18" />
|
||||
<p class="hidden md:block">{$t('edit_tag')}</p>
|
||||
</div>
|
||||
</LinkButton>
|
||||
<LinkButton on:click={handleDelete}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiTrashCanOutline} size="18" />
|
||||
<p class="hidden md:block">{$t('delete_tag')}</p>
|
||||
</div>
|
||||
</LinkButton>
|
||||
{/if}
|
||||
</section>
|
||||
{/snippet}
|
||||
|
||||
<Breadcrumbs {pathSegments} icon={mdiTagMultiple} title={$t('tags')} {getLink} />
|
||||
|
||||
<section class="mt-2 h-full">
|
||||
{#if tag}
|
||||
<AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} removeAction={AssetAction.UNARCHIVE}>
|
||||
<TreeItemThumbnails items={data.children} icon={mdiTag} onClick={handleNavigation} slot="empty" />
|
||||
{#snippet empty()}
|
||||
<TreeItemThumbnails items={data.children} icon={mdiTag} onClick={handleNavigation} />
|
||||
{/snippet}
|
||||
</AssetGrid>
|
||||
{:else}
|
||||
<TreeItemThumbnails items={Object.keys(tree)} icon={mdiTag} onClick={handleNavigation} />
|
||||
@@ -193,7 +217,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form on:submit|preventDefault={handleSubmit} autocomplete="off" id="create-tag-form">
|
||||
<form {onsubmit} autocomplete="off" id="create-tag-form">
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
@@ -204,16 +228,17 @@
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<svelte:fragment slot="sticky-bottom">
|
||||
<Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button>
|
||||
|
||||
{#snippet stickyBottom()}
|
||||
<Button color="gray" fullwidth onclick={() => handleCancel()}>{$t('cancel')}</Button>
|
||||
<Button type="submit" fullwidth form="create-tag-form">{$t('create')}</Button>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
|
||||
{#if isEditOpen}
|
||||
<FullScreenModal title={$t('edit_tag')} icon={mdiTag} onClose={handleCancel}>
|
||||
<form on:submit|preventDefault={handleSubmit} autocomplete="off" id="edit-tag-form">
|
||||
<form {onsubmit} autocomplete="off" id="edit-tag-form">
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.COLOR}
|
||||
@@ -222,9 +247,10 @@
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<svelte:fragment slot="sticky-bottom">
|
||||
<Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button>
|
||||
|
||||
{#snippet stickyBottom()}
|
||||
<Button color="gray" fullwidth onclick={() => handleCancel()}>{$t('cancel')}</Button>
|
||||
<Button type="submit" fullwidth form="edit-tag-form">{$t('save')}</Button>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
|
||||
@@ -27,7 +27,11 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
if (!$featureFlags.trash) {
|
||||
handlePromiseError(goto(AppRoute.PHOTOS));
|
||||
@@ -99,26 +103,30 @@
|
||||
|
||||
{#if $featureFlags.loaded && $featureFlags.trash}
|
||||
<UserPageLayout hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}>
|
||||
<div class="flex place-items-center gap-2" slot="buttons">
|
||||
<LinkButton on:click={handleRestoreTrash} disabled={$isMultiSelectState}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiHistory} size="18" />
|
||||
{$t('restore_all')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
<LinkButton on:click={() => handleEmptyTrash()} disabled={$isMultiSelectState}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiDeleteForeverOutline} size="18" />
|
||||
{$t('empty_trash')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
</div>
|
||||
{#snippet buttons()}
|
||||
<div class="flex place-items-center gap-2">
|
||||
<LinkButton onclick={handleRestoreTrash} disabled={$isMultiSelectState}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiHistory} size="18" />
|
||||
{$t('restore_all')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
<LinkButton onclick={() => handleEmptyTrash()} disabled={$isMultiSelectState}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiDeleteForeverOutline} size="18" />
|
||||
{$t('empty_trash')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<AssetGrid enableRouting={true} {assetStore} {assetInteractionStore}>
|
||||
<p class="font-medium text-gray-500/60 dark:text-gray-300/60 p-4">
|
||||
{$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}
|
||||
</p>
|
||||
<EmptyPlaceholder text={$t('trash_no_results_message')} src={empty3Url} slot="empty" />
|
||||
{#snippet empty()}
|
||||
<EmptyPlaceholder text={$t('trash_no_results_message')} src={empty3Url} />
|
||||
{/snippet}
|
||||
</AssetGrid>
|
||||
</UserPageLayout>
|
||||
{/if}
|
||||
|
||||
@@ -7,18 +7,22 @@
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let data: PageData;
|
||||
export let isShowKeyboardShortcut = false;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
isShowKeyboardShortcut?: boolean;
|
||||
}
|
||||
|
||||
let { data, isShowKeyboardShortcut = $bindable(false) }: Props = $props();
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
<svelte:fragment slot="buttons">
|
||||
{#snippet buttons()}
|
||||
<CircleIconButton
|
||||
icon={mdiKeyboard}
|
||||
title={$t('show_keyboard_shortcuts')}
|
||||
on:click={() => (isShowKeyboardShortcut = !isShowKeyboardShortcut)}
|
||||
onclick={() => (isShowKeyboardShortcut = !isShowKeyboardShortcut)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
<section class="mx-4 flex place-content-center">
|
||||
<div class="w-full max-w-3xl">
|
||||
<UserSettingsList keys={data.keys} sessions={data.sessions} />
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
import type { PageData } from './$types';
|
||||
import UtilitiesMenu from '$lib/components/utilities-page/utilities-menu.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
|
||||
+42
-38
@@ -22,8 +22,12 @@
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
|
||||
export let data: PageData;
|
||||
export let isShowKeyboardShortcut = false;
|
||||
interface Props {
|
||||
data: PageData;
|
||||
isShowKeyboardShortcut?: boolean;
|
||||
}
|
||||
|
||||
let { data = $bindable(), isShowKeyboardShortcut = $bindable(false) }: Props = $props();
|
||||
|
||||
interface Shortcuts {
|
||||
general: ExplainedShortcut[];
|
||||
@@ -46,8 +50,8 @@
|
||||
],
|
||||
};
|
||||
|
||||
$: hasDuplicates = data.duplicates.length > 0;
|
||||
|
||||
let duplicates = $state(data.duplicates);
|
||||
let hasDuplicates = $derived(duplicates.length > 0);
|
||||
const withConfirmation = async (callback: () => Promise<void>, prompt?: string, confirmText?: string) => {
|
||||
if (prompt && confirmText) {
|
||||
const isConfirmed = await dialogController.show({ prompt, confirmText });
|
||||
@@ -82,7 +86,7 @@
|
||||
await deleteAssets({ assetBulkDeleteDto: { ids: trashIds, force: !$featureFlags.trash } });
|
||||
await updateAssets({ assetBulkUpdateDto: { ids: duplicateAssetIds, duplicateId: null } });
|
||||
|
||||
data.duplicates = data.duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
|
||||
duplicates = duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
|
||||
|
||||
deletedNotification(trashIds.length);
|
||||
},
|
||||
@@ -95,14 +99,12 @@
|
||||
await stackAssets(assets, false);
|
||||
const duplicateAssetIds = assets.map((asset) => asset.id);
|
||||
await updateAssets({ assetBulkUpdateDto: { ids: duplicateAssetIds, duplicateId: null } });
|
||||
data.duplicates = data.duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
|
||||
duplicates = duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
|
||||
};
|
||||
|
||||
const handleDeduplicateAll = async () => {
|
||||
const idsToKeep = data.duplicates
|
||||
.map((group) => suggestDuplicateByFileSize(group.assets))
|
||||
.map((asset) => asset?.id);
|
||||
const idsToDelete = data.duplicates.flatMap((group, i) =>
|
||||
const idsToKeep = duplicates.map((group) => suggestDuplicateByFileSize(group.assets)).map((asset) => asset?.id);
|
||||
const idsToDelete = duplicates.flatMap((group, i) =>
|
||||
group.assets.map((asset) => asset.id).filter((asset) => asset !== idsToKeep[i]),
|
||||
);
|
||||
|
||||
@@ -125,7 +127,7 @@
|
||||
},
|
||||
});
|
||||
|
||||
data.duplicates = [];
|
||||
duplicates = [];
|
||||
|
||||
deletedNotification(idsToDelete.length);
|
||||
},
|
||||
@@ -135,12 +137,12 @@
|
||||
};
|
||||
|
||||
const handleKeepAll = async () => {
|
||||
const ids = data.duplicates.flatMap((group) => group.assets.map((asset) => asset.id));
|
||||
const ids = duplicates.flatMap((group) => group.assets.map((asset) => asset.id));
|
||||
return withConfirmation(
|
||||
async () => {
|
||||
await updateAssets({ assetBulkUpdateDto: { ids, duplicateId: null } });
|
||||
|
||||
data.duplicates = [];
|
||||
duplicates = [];
|
||||
|
||||
notificationController.show({
|
||||
message: $t('resolved_all_duplicates'),
|
||||
@@ -153,38 +155,40 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title + ` (${data.duplicates.length.toLocaleString($locale)})`} scrollbar={true}>
|
||||
<div class="flex place-items-center gap-2" slot="buttons">
|
||||
<LinkButton on:click={() => handleDeduplicateAll()} disabled={!hasDuplicates}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiTrashCanOutline} size="18" />
|
||||
{$t('deduplicate_all')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
<LinkButton on:click={() => handleKeepAll()} disabled={!hasDuplicates}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiCheckOutline} size="18" />
|
||||
{$t('keep_all')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
<CircleIconButton
|
||||
icon={mdiKeyboard}
|
||||
title={$t('show_keyboard_shortcuts')}
|
||||
on:click={() => (isShowKeyboardShortcut = !isShowKeyboardShortcut)}
|
||||
/>
|
||||
</div>
|
||||
<UserPageLayout title={data.meta.title + ` (${duplicates.length.toLocaleString($locale)})`} scrollbar={true}>
|
||||
{#snippet buttons()}
|
||||
<div class="flex place-items-center gap-2">
|
||||
<LinkButton onclick={() => handleDeduplicateAll()} disabled={!hasDuplicates}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiTrashCanOutline} size="18" />
|
||||
{$t('deduplicate_all')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
<LinkButton onclick={() => handleKeepAll()} disabled={!hasDuplicates}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiCheckOutline} size="18" />
|
||||
{$t('keep_all')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
<CircleIconButton
|
||||
icon={mdiKeyboard}
|
||||
title={$t('show_keyboard_shortcuts')}
|
||||
onclick={() => (isShowKeyboardShortcut = !isShowKeyboardShortcut)}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="mt-4">
|
||||
{#if data.duplicates && data.duplicates.length > 0}
|
||||
{#if duplicates && duplicates.length > 0}
|
||||
<div class="mb-4 text-sm dark:text-white">
|
||||
<p>{$t('duplicates_description')}</p>
|
||||
</div>
|
||||
{#key data.duplicates[0].duplicateId}
|
||||
{#key duplicates[0].duplicateId}
|
||||
<DuplicatesCompareControl
|
||||
assets={data.duplicates[0].assets}
|
||||
assets={duplicates[0].assets}
|
||||
onResolve={(duplicateAssetIds, trashIds) =>
|
||||
handleResolve(data.duplicates[0].duplicateId, duplicateAssetIds, trashIds)}
|
||||
onStack={(assets) => handleStack(data.duplicates[0].duplicateId, assets)}
|
||||
handleResolve(duplicates[0].duplicateId, duplicateAssetIds, trashIds)}
|
||||
onStack={(assets) => handleStack(duplicates[0].duplicateId, assets)}
|
||||
/>
|
||||
{/key}
|
||||
{:else}
|
||||
|
||||
Reference in New Issue
Block a user