mirror of
https://github.com/KevinMidboe/immich.git
synced 2026-04-25 16:23:46 +00:00
@@ -3,21 +3,21 @@ import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { api, user } }) => {
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: albums } = await api.albumApi.getAllAlbums();
|
||||
try {
|
||||
const { data: albums } = await api.albumApi.getAllAlbums();
|
||||
|
||||
return {
|
||||
user: user,
|
||||
albums: albums,
|
||||
meta: {
|
||||
title: 'Albums'
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
return {
|
||||
user: user,
|
||||
albums: albums,
|
||||
meta: {
|
||||
title: 'Albums',
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,171 +1,163 @@
|
||||
<script lang="ts">
|
||||
import { albumViewSettings } from '$lib/stores/preferences.store';
|
||||
import AlbumCard from '$lib/components/album-page/album-card.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
|
||||
import { useAlbums } from './albums.bloc';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import Dropdown from '$lib/components/elements/dropdown.svelte';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import type { AlbumResponseDto } from '@api';
|
||||
import { albumViewSettings } from '$lib/stores/preferences.store';
|
||||
import AlbumCard from '$lib/components/album-page/album-card.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
|
||||
import { useAlbums } from './albums.bloc';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import Dropdown from '$lib/components/elements/dropdown.svelte';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import type { AlbumResponseDto } from '@api';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
|
||||
const sortByOptions = ['Most recent photo', 'Last modified', 'Album title'];
|
||||
const sortByOptions = ['Most recent photo', 'Last modified', 'Album title'];
|
||||
|
||||
const {
|
||||
albums: unsortedAlbums,
|
||||
isShowContextMenu,
|
||||
contextMenuPosition,
|
||||
contextMenuTargetAlbum,
|
||||
createAlbum,
|
||||
deleteAlbum,
|
||||
showAlbumContextMenu,
|
||||
closeAlbumContextMenu
|
||||
} = useAlbums({ albums: data.albums });
|
||||
const {
|
||||
albums: unsortedAlbums,
|
||||
isShowContextMenu,
|
||||
contextMenuPosition,
|
||||
contextMenuTargetAlbum,
|
||||
createAlbum,
|
||||
deleteAlbum,
|
||||
showAlbumContextMenu,
|
||||
closeAlbumContextMenu,
|
||||
} = useAlbums({ albums: data.albums });
|
||||
|
||||
let albums = unsortedAlbums;
|
||||
let albumToDelete: AlbumResponseDto | null;
|
||||
let albums = unsortedAlbums;
|
||||
let albumToDelete: AlbumResponseDto | null;
|
||||
|
||||
const setAlbumToDelete = () => {
|
||||
albumToDelete = $contextMenuTargetAlbum ?? null;
|
||||
closeAlbumContextMenu();
|
||||
};
|
||||
const setAlbumToDelete = () => {
|
||||
albumToDelete = $contextMenuTargetAlbum ?? null;
|
||||
closeAlbumContextMenu();
|
||||
};
|
||||
|
||||
const deleteSelectedAlbum = async () => {
|
||||
if (!albumToDelete) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteAlbum(albumToDelete);
|
||||
} catch {
|
||||
notificationController.show({
|
||||
message: 'Error deleting album',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
} finally {
|
||||
albumToDelete = null;
|
||||
}
|
||||
};
|
||||
const deleteSelectedAlbum = async () => {
|
||||
if (!albumToDelete) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteAlbum(albumToDelete);
|
||||
} catch {
|
||||
notificationController.show({
|
||||
message: 'Error deleting album',
|
||||
type: NotificationType.Error,
|
||||
});
|
||||
} finally {
|
||||
albumToDelete = null;
|
||||
}
|
||||
};
|
||||
|
||||
const sortByDate = (a: string, b: string) => {
|
||||
const aDate = new Date(a);
|
||||
const bDate = new Date(b);
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
};
|
||||
const sortByDate = (a: string, b: string) => {
|
||||
const aDate = new Date(a);
|
||||
const bDate = new Date(b);
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
};
|
||||
|
||||
$: {
|
||||
const { sortBy } = $albumViewSettings;
|
||||
if (sortBy === 'Most recent photo') {
|
||||
$albums = $unsortedAlbums.sort((a, b) =>
|
||||
a.lastModifiedAssetTimestamp && b.lastModifiedAssetTimestamp
|
||||
? sortByDate(a.lastModifiedAssetTimestamp, b.lastModifiedAssetTimestamp)
|
||||
: sortByDate(a.updatedAt, b.updatedAt)
|
||||
);
|
||||
} else if (sortBy === 'Last modified') {
|
||||
$albums = $unsortedAlbums.sort((a, b) => sortByDate(a.updatedAt, b.updatedAt));
|
||||
} else if (sortBy === 'Album title') {
|
||||
$albums = $unsortedAlbums.sort((a, b) => a.albumName.localeCompare(b.albumName));
|
||||
}
|
||||
}
|
||||
$: {
|
||||
const { sortBy } = $albumViewSettings;
|
||||
if (sortBy === 'Most recent photo') {
|
||||
$albums = $unsortedAlbums.sort((a, b) =>
|
||||
a.lastModifiedAssetTimestamp && b.lastModifiedAssetTimestamp
|
||||
? sortByDate(a.lastModifiedAssetTimestamp, b.lastModifiedAssetTimestamp)
|
||||
: sortByDate(a.updatedAt, b.updatedAt),
|
||||
);
|
||||
} else if (sortBy === 'Last modified') {
|
||||
$albums = $unsortedAlbums.sort((a, b) => sortByDate(a.updatedAt, b.updatedAt));
|
||||
} else if (sortBy === 'Album title') {
|
||||
$albums = $unsortedAlbums.sort((a, b) => a.albumName.localeCompare(b.albumName));
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateAlbum = async () => {
|
||||
const newAlbum = await createAlbum();
|
||||
if (newAlbum) {
|
||||
goto('/albums/' + newAlbum.id);
|
||||
}
|
||||
};
|
||||
const handleCreateAlbum = async () => {
|
||||
const newAlbum = await createAlbum();
|
||||
if (newAlbum) {
|
||||
goto('/albums/' + newAlbum.id);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
removeAlbumsIfEmpty();
|
||||
});
|
||||
onMount(() => {
|
||||
removeAlbumsIfEmpty();
|
||||
});
|
||||
|
||||
const removeAlbumsIfEmpty = async () => {
|
||||
try {
|
||||
for (const album of $albums) {
|
||||
if (album.assetCount == 0 && album.albumName == 'Untitled') {
|
||||
await deleteAlbum(album);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
const removeAlbumsIfEmpty = async () => {
|
||||
try {
|
||||
for (const album of $albums) {
|
||||
if (album.assetCount == 0 && album.albumName == 'Untitled') {
|
||||
await deleteAlbum(album);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<UserPageLayout user={data.user} title={data.meta.title}>
|
||||
<div class="flex place-items-center gap-2" slot="buttons">
|
||||
<LinkButton on:click={handleCreateAlbum}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<PlusBoxOutline size="18" />
|
||||
Create album
|
||||
</div>
|
||||
</LinkButton>
|
||||
<div class="flex place-items-center gap-2" slot="buttons">
|
||||
<LinkButton on:click={handleCreateAlbum}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<PlusBoxOutline size="18" />
|
||||
Create album
|
||||
</div>
|
||||
</LinkButton>
|
||||
|
||||
<Dropdown options={sortByOptions} bind:value={$albumViewSettings.sortBy} />
|
||||
</div>
|
||||
<Dropdown options={sortByOptions} bind:value={$albumViewSettings.sortBy} />
|
||||
</div>
|
||||
|
||||
<!-- Album Card -->
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))]">
|
||||
{#each $albums as album (album.id)}
|
||||
<a
|
||||
data-sveltekit-preload-data="hover"
|
||||
href={`albums/${album.id}`}
|
||||
animate:flip={{ duration: 200 }}
|
||||
>
|
||||
<AlbumCard
|
||||
{album}
|
||||
on:showalbumcontextmenu={(e) => showAlbumContextMenu(e.detail, album)}
|
||||
user={data.user}
|
||||
/>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- Album Card -->
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))]">
|
||||
{#each $albums as album (album.id)}
|
||||
<a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}>
|
||||
<AlbumCard {album} on:showalbumcontextmenu={(e) => showAlbumContextMenu(e.detail, album)} user={data.user} />
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Empty Message -->
|
||||
{#if $albums.length === 0}
|
||||
<EmptyPlaceholder
|
||||
text="Create an album to organize your photos and videos"
|
||||
actionHandler={handleCreateAlbum}
|
||||
alt="Empty albums"
|
||||
/>
|
||||
{/if}
|
||||
<!-- Empty Message -->
|
||||
{#if $albums.length === 0}
|
||||
<EmptyPlaceholder
|
||||
text="Create an album to organize your photos and videos"
|
||||
actionHandler={handleCreateAlbum}
|
||||
alt="Empty albums"
|
||||
/>
|
||||
{/if}
|
||||
</UserPageLayout>
|
||||
|
||||
<!-- Context Menu -->
|
||||
{#if $isShowContextMenu}
|
||||
<ContextMenu {...$contextMenuPosition} on:outclick={closeAlbumContextMenu}>
|
||||
<MenuOption on:click={() => setAlbumToDelete()}>
|
||||
<span class="flex place-items-center place-content-center gap-2">
|
||||
<DeleteOutline size="18" />
|
||||
<p>Delete album</p>
|
||||
</span>
|
||||
</MenuOption>
|
||||
</ContextMenu>
|
||||
<ContextMenu {...$contextMenuPosition} on:outclick={closeAlbumContextMenu}>
|
||||
<MenuOption on:click={() => setAlbumToDelete()}>
|
||||
<span class="flex place-items-center place-content-center gap-2">
|
||||
<DeleteOutline size="18" />
|
||||
<p>Delete album</p>
|
||||
</span>
|
||||
</MenuOption>
|
||||
</ContextMenu>
|
||||
{/if}
|
||||
|
||||
{#if albumToDelete}
|
||||
<ConfirmDialogue
|
||||
title="Delete Album"
|
||||
confirmText="Delete"
|
||||
on:confirm={deleteSelectedAlbum}
|
||||
on:cancel={() => (albumToDelete = null)}
|
||||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<p>Are you sure you want to delete the album <b>{albumToDelete.albumName}</b>?</p>
|
||||
<p>If this album is shared, other users will not be able to access it anymore.</p>
|
||||
</svelte:fragment>
|
||||
</ConfirmDialogue>
|
||||
<ConfirmDialogue
|
||||
title="Delete Album"
|
||||
confirmText="Delete"
|
||||
on:confirm={deleteSelectedAlbum}
|
||||
on:cancel={() => (albumToDelete = null)}
|
||||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<p>Are you sure you want to delete the album <b>{albumToDelete.albumName}</b>?</p>
|
||||
<p>If this album is shared, other users will not be able to access it anymore.</p>
|
||||
</svelte:fragment>
|
||||
</ConfirmDialogue>
|
||||
{/if}
|
||||
|
||||
@@ -3,21 +3,21 @@ import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params, locals: { api, user } }) => {
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
const albumId = params['albumId'];
|
||||
const albumId = params['albumId'];
|
||||
|
||||
try {
|
||||
const { data: album } = await api.albumApi.getAlbumInfo({ id: albumId });
|
||||
return {
|
||||
album,
|
||||
meta: {
|
||||
title: album.albumName
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
throw redirect(302, AppRoute.ALBUMS);
|
||||
}
|
||||
try {
|
||||
const { data: album } = await api.albumApi.getAlbumInfo({ id: albumId });
|
||||
return {
|
||||
album,
|
||||
meta: {
|
||||
title: album.albumName,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
throw redirect(302, AppRoute.ALBUMS);
|
||||
}
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<div class="immich-scrollbar">
|
||||
<AlbumViewer album={data.album} />
|
||||
<AlbumViewer album={data.album} />
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
export const prerender = false;
|
||||
import type { PageLoad } from './$types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
export const prerender = false;
|
||||
|
||||
export const load: PageLoad = async ({ params, parent }) => {
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
const albumId = params['albumId'];
|
||||
const albumId = params['albumId'];
|
||||
|
||||
if (albumId) {
|
||||
throw redirect(302, `${AppRoute.ALBUMS}/${albumId}`);
|
||||
} else {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
if (albumId) {
|
||||
throw redirect(302, `${AppRoute.ALBUMS}/${albumId}`);
|
||||
} else {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,142 +1,137 @@
|
||||
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||||
import { useAlbums } from '../albums.bloc';
|
||||
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
|
||||
import { api, CreateAlbumDto } from '@api';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||
import { albumFactory } from '@test-data';
|
||||
import { get } from 'svelte/store';
|
||||
import { useAlbums } from '../albums.bloc';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
const apiMock: jest.MockedObject<typeof api> = api as jest.MockedObject<typeof api>;
|
||||
|
||||
describe('Albums BLoC', () => {
|
||||
let sut: ReturnType<typeof useAlbums>;
|
||||
const _albums = albumFactory.buildList(5);
|
||||
let sut: ReturnType<typeof useAlbums>;
|
||||
const _albums = albumFactory.buildList(5);
|
||||
|
||||
beforeEach(() => {
|
||||
sut = useAlbums({ albums: [..._albums] });
|
||||
});
|
||||
beforeEach(() => {
|
||||
sut = useAlbums({ albums: [..._albums] });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
const notifications = get(notificationController.notificationList);
|
||||
afterEach(() => {
|
||||
const notifications = get(notificationController.notificationList);
|
||||
|
||||
notifications.forEach((notification) =>
|
||||
notificationController.removeNotificationById(notification.id)
|
||||
);
|
||||
});
|
||||
notifications.forEach((notification) => notificationController.removeNotificationById(notification.id));
|
||||
});
|
||||
|
||||
it('inits with provided albums', () => {
|
||||
const albums = get(sut.albums);
|
||||
expect(albums.length).toEqual(5);
|
||||
expect(albums).toEqual(_albums);
|
||||
});
|
||||
it('inits with provided albums', () => {
|
||||
const albums = get(sut.albums);
|
||||
expect(albums.length).toEqual(5);
|
||||
expect(albums).toEqual(_albums);
|
||||
});
|
||||
|
||||
it('loads albums from the server', async () => {
|
||||
// TODO: this method currently deletes albums with no assets and albumName === 'Untitled' which might not be the best approach
|
||||
const loadedAlbums = [..._albums, albumFactory.build({ id: 'new_loaded_uuid' })];
|
||||
it('loads albums from the server', async () => {
|
||||
// TODO: this method currently deletes albums with no assets and albumName === 'Untitled' which might not be the best approach
|
||||
const loadedAlbums = [..._albums, albumFactory.build({ id: 'new_loaded_uuid' })];
|
||||
|
||||
apiMock.albumApi.getAllAlbums.mockResolvedValueOnce({
|
||||
data: loadedAlbums,
|
||||
config: {},
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: ''
|
||||
});
|
||||
apiMock.albumApi.getAllAlbums.mockResolvedValueOnce({
|
||||
data: loadedAlbums,
|
||||
config: {},
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: '',
|
||||
});
|
||||
|
||||
await sut.loadAlbums();
|
||||
const albums = get(sut.albums);
|
||||
await sut.loadAlbums();
|
||||
const albums = get(sut.albums);
|
||||
|
||||
expect(apiMock.albumApi.getAllAlbums).toHaveBeenCalledTimes(1);
|
||||
expect(albums).toEqual(loadedAlbums);
|
||||
});
|
||||
expect(apiMock.albumApi.getAllAlbums).toHaveBeenCalledTimes(1);
|
||||
expect(albums).toEqual(loadedAlbums);
|
||||
});
|
||||
|
||||
it('shows error message when it fails loading albums', async () => {
|
||||
apiMock.albumApi.getAllAlbums.mockRejectedValueOnce({}); // TODO: implement APIProblem interface in the server
|
||||
it('shows error message when it fails loading albums', async () => {
|
||||
apiMock.albumApi.getAllAlbums.mockRejectedValueOnce({}); // TODO: implement APIProblem interface in the server
|
||||
|
||||
expect(get(notificationController.notificationList)).toHaveLength(0);
|
||||
await sut.loadAlbums();
|
||||
const albums = get(sut.albums);
|
||||
const notifications = get(notificationController.notificationList);
|
||||
expect(get(notificationController.notificationList)).toHaveLength(0);
|
||||
await sut.loadAlbums();
|
||||
const albums = get(sut.albums);
|
||||
const notifications = get(notificationController.notificationList);
|
||||
|
||||
expect(apiMock.albumApi.getAllAlbums).toHaveBeenCalledTimes(1);
|
||||
expect(albums).toEqual(_albums);
|
||||
expect(notifications).toHaveLength(1);
|
||||
expect(notifications[0].type).toEqual(NotificationType.Error);
|
||||
});
|
||||
expect(apiMock.albumApi.getAllAlbums).toHaveBeenCalledTimes(1);
|
||||
expect(albums).toEqual(_albums);
|
||||
expect(notifications).toHaveLength(1);
|
||||
expect(notifications[0].type).toEqual(NotificationType.Error);
|
||||
});
|
||||
|
||||
it('creates a new album', async () => {
|
||||
// TODO: we probably shouldn't hardcode the album name "untitled" here and let the user input the album name before creating it
|
||||
const payload: CreateAlbumDto = {
|
||||
albumName: 'Untitled'
|
||||
};
|
||||
it('creates a new album', async () => {
|
||||
// TODO: we probably shouldn't hardcode the album name "untitled" here and let the user input the album name before creating it
|
||||
const payload: CreateAlbumDto = {
|
||||
albumName: 'Untitled',
|
||||
};
|
||||
|
||||
const returnedAlbum = albumFactory.build();
|
||||
const returnedAlbum = albumFactory.build();
|
||||
|
||||
apiMock.albumApi.createAlbum.mockResolvedValueOnce({
|
||||
data: returnedAlbum,
|
||||
config: {},
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: ''
|
||||
});
|
||||
apiMock.albumApi.createAlbum.mockResolvedValueOnce({
|
||||
data: returnedAlbum,
|
||||
config: {},
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: '',
|
||||
});
|
||||
|
||||
const newAlbum = await sut.createAlbum();
|
||||
const newAlbum = await sut.createAlbum();
|
||||
|
||||
expect(apiMock.albumApi.createAlbum).toHaveBeenCalledTimes(1);
|
||||
expect(apiMock.albumApi.createAlbum).toHaveBeenCalledWith({ createAlbumDto: payload });
|
||||
expect(newAlbum).toEqual(returnedAlbum);
|
||||
});
|
||||
expect(apiMock.albumApi.createAlbum).toHaveBeenCalledTimes(1);
|
||||
expect(apiMock.albumApi.createAlbum).toHaveBeenCalledWith({ createAlbumDto: payload });
|
||||
expect(newAlbum).toEqual(returnedAlbum);
|
||||
});
|
||||
|
||||
it('shows error message when it fails creating an album', async () => {
|
||||
apiMock.albumApi.createAlbum.mockRejectedValueOnce({});
|
||||
it('shows error message when it fails creating an album', async () => {
|
||||
apiMock.albumApi.createAlbum.mockRejectedValueOnce({});
|
||||
|
||||
const newAlbum = await sut.createAlbum();
|
||||
const notifications = get(notificationController.notificationList);
|
||||
const newAlbum = await sut.createAlbum();
|
||||
const notifications = get(notificationController.notificationList);
|
||||
|
||||
expect(apiMock.albumApi.createAlbum).toHaveBeenCalledTimes(1);
|
||||
expect(newAlbum).not.toBeDefined();
|
||||
expect(notifications).toHaveLength(1);
|
||||
expect(notifications[0].type).toEqual(NotificationType.Error);
|
||||
});
|
||||
expect(apiMock.albumApi.createAlbum).toHaveBeenCalledTimes(1);
|
||||
expect(newAlbum).not.toBeDefined();
|
||||
expect(notifications).toHaveLength(1);
|
||||
expect(notifications[0].type).toEqual(NotificationType.Error);
|
||||
});
|
||||
|
||||
it('selects an album and deletes it', async () => {
|
||||
apiMock.albumApi.deleteAlbum.mockResolvedValueOnce({
|
||||
data: undefined,
|
||||
config: {},
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: ''
|
||||
});
|
||||
it('selects an album and deletes it', async () => {
|
||||
apiMock.albumApi.deleteAlbum.mockResolvedValueOnce({
|
||||
data: undefined,
|
||||
config: {},
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: '',
|
||||
});
|
||||
|
||||
const albumToDelete = get(sut.albums)[2]; // delete third album
|
||||
const albumToDeleteId = albumToDelete.id;
|
||||
const contextMenuCoords = { x: 100, y: 150 };
|
||||
const albumToDelete = get(sut.albums)[2]; // delete third album
|
||||
const albumToDeleteId = albumToDelete.id;
|
||||
const contextMenuCoords = { x: 100, y: 150 };
|
||||
|
||||
expect(get(sut.isShowContextMenu)).toBe(false);
|
||||
sut.showAlbumContextMenu(contextMenuCoords, albumToDelete);
|
||||
expect(get(sut.contextMenuPosition)).toEqual(contextMenuCoords);
|
||||
expect(get(sut.isShowContextMenu)).toBe(true);
|
||||
expect(get(sut.contextMenuTargetAlbum)).toEqual(albumToDelete);
|
||||
expect(get(sut.isShowContextMenu)).toBe(false);
|
||||
sut.showAlbumContextMenu(contextMenuCoords, albumToDelete);
|
||||
expect(get(sut.contextMenuPosition)).toEqual(contextMenuCoords);
|
||||
expect(get(sut.isShowContextMenu)).toBe(true);
|
||||
expect(get(sut.contextMenuTargetAlbum)).toEqual(albumToDelete);
|
||||
|
||||
await sut.deleteAlbum(albumToDelete);
|
||||
const updatedAlbums = get(sut.albums);
|
||||
await sut.deleteAlbum(albumToDelete);
|
||||
const updatedAlbums = get(sut.albums);
|
||||
|
||||
expect(apiMock.albumApi.deleteAlbum).toHaveBeenCalledTimes(1);
|
||||
expect(apiMock.albumApi.deleteAlbum).toHaveBeenCalledWith({ id: albumToDeleteId });
|
||||
expect(updatedAlbums).toHaveLength(4);
|
||||
expect(updatedAlbums).not.toContain(albumToDelete);
|
||||
});
|
||||
expect(apiMock.albumApi.deleteAlbum).toHaveBeenCalledTimes(1);
|
||||
expect(apiMock.albumApi.deleteAlbum).toHaveBeenCalledWith({ id: albumToDeleteId });
|
||||
expect(updatedAlbums).toHaveLength(4);
|
||||
expect(updatedAlbums).not.toContain(albumToDelete);
|
||||
});
|
||||
|
||||
it('closes album context menu, deselecting album', () => {
|
||||
const albumToDelete = get(sut.albums)[2]; // delete third album
|
||||
sut.showAlbumContextMenu({ x: 100, y: 150 }, albumToDelete);
|
||||
it('closes album context menu, deselecting album', () => {
|
||||
const albumToDelete = get(sut.albums)[2]; // delete third album
|
||||
sut.showAlbumContextMenu({ x: 100, y: 150 }, albumToDelete);
|
||||
|
||||
expect(get(sut.isShowContextMenu)).toBe(true);
|
||||
expect(get(sut.isShowContextMenu)).toBe(true);
|
||||
|
||||
sut.closeAlbumContextMenu();
|
||||
expect(get(sut.isShowContextMenu)).toBe(false);
|
||||
});
|
||||
sut.closeAlbumContextMenu();
|
||||
expect(get(sut.isShowContextMenu)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,91 +1,88 @@
|
||||
import type { OnShowContextMenuDetail } from '$lib/components/album-page/album-card';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
|
||||
import { AlbumResponseDto, api } from '@api';
|
||||
import { derived, get, writable } from 'svelte/store';
|
||||
|
||||
type AlbumsProps = { albums: AlbumResponseDto[] };
|
||||
|
||||
export const useAlbums = (props: AlbumsProps) => {
|
||||
const albums = writable([...props.albums]);
|
||||
const contextMenuPosition = writable<OnShowContextMenuDetail>({ x: 0, y: 0 });
|
||||
const contextMenuTargetAlbum = writable<AlbumResponseDto | undefined>();
|
||||
const isShowContextMenu = derived(contextMenuTargetAlbum, ($selectedAlbum) => !!$selectedAlbum);
|
||||
const albums = writable([...props.albums]);
|
||||
const contextMenuPosition = writable<OnShowContextMenuDetail>({ x: 0, y: 0 });
|
||||
const contextMenuTargetAlbum = writable<AlbumResponseDto | undefined>();
|
||||
const isShowContextMenu = derived(contextMenuTargetAlbum, ($selectedAlbum) => !!$selectedAlbum);
|
||||
|
||||
async function loadAlbums(): Promise<void> {
|
||||
try {
|
||||
const { data } = await api.albumApi.getAllAlbums();
|
||||
albums.set(data);
|
||||
async function loadAlbums(): Promise<void> {
|
||||
try {
|
||||
const { data } = await api.albumApi.getAllAlbums();
|
||||
albums.set(data);
|
||||
|
||||
// Delete album that has no photos and is named 'Untitled'
|
||||
for (const album of data) {
|
||||
if (album.albumName === 'Untitled' && album.assetCount === 0) {
|
||||
setTimeout(async () => {
|
||||
await deleteAlbum(album);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
notificationController.show({
|
||||
message: 'Error loading albums',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
}
|
||||
// Delete album that has no photos and is named 'Untitled'
|
||||
for (const album of data) {
|
||||
if (album.albumName === 'Untitled' && album.assetCount === 0) {
|
||||
setTimeout(async () => {
|
||||
await deleteAlbum(album);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
notificationController.show({
|
||||
message: 'Error loading albums',
|
||||
type: NotificationType.Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function createAlbum(): Promise<AlbumResponseDto | undefined> {
|
||||
try {
|
||||
const { data: newAlbum } = await api.albumApi.createAlbum({
|
||||
createAlbumDto: {
|
||||
albumName: 'Untitled'
|
||||
}
|
||||
});
|
||||
async function createAlbum(): Promise<AlbumResponseDto | undefined> {
|
||||
try {
|
||||
const { data: newAlbum } = await api.albumApi.createAlbum({
|
||||
createAlbumDto: {
|
||||
albumName: 'Untitled',
|
||||
},
|
||||
});
|
||||
|
||||
return newAlbum;
|
||||
} catch {
|
||||
notificationController.show({
|
||||
message: 'Error creating album',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
}
|
||||
return newAlbum;
|
||||
} catch {
|
||||
notificationController.show({
|
||||
message: 'Error creating album',
|
||||
type: NotificationType.Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAlbum(albumToDelete: AlbumResponseDto): Promise<void> {
|
||||
await api.albumApi.deleteAlbum({ id: albumToDelete.id });
|
||||
albums.set(
|
||||
get(albums).filter(({ id }) => {
|
||||
return id !== albumToDelete.id;
|
||||
})
|
||||
);
|
||||
}
|
||||
async function deleteAlbum(albumToDelete: AlbumResponseDto): Promise<void> {
|
||||
await api.albumApi.deleteAlbum({ id: albumToDelete.id });
|
||||
albums.set(
|
||||
get(albums).filter(({ id }) => {
|
||||
return id !== albumToDelete.id;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function showAlbumContextMenu(
|
||||
contextMenuDetail: OnShowContextMenuDetail,
|
||||
album: AlbumResponseDto
|
||||
): Promise<void> {
|
||||
contextMenuTargetAlbum.set(album);
|
||||
async function showAlbumContextMenu(
|
||||
contextMenuDetail: OnShowContextMenuDetail,
|
||||
album: AlbumResponseDto,
|
||||
): Promise<void> {
|
||||
contextMenuTargetAlbum.set(album);
|
||||
|
||||
contextMenuPosition.set({
|
||||
x: contextMenuDetail.x,
|
||||
y: contextMenuDetail.y
|
||||
});
|
||||
}
|
||||
contextMenuPosition.set({
|
||||
x: contextMenuDetail.x,
|
||||
y: contextMenuDetail.y,
|
||||
});
|
||||
}
|
||||
|
||||
function closeAlbumContextMenu() {
|
||||
contextMenuTargetAlbum.set(undefined);
|
||||
}
|
||||
function closeAlbumContextMenu() {
|
||||
contextMenuTargetAlbum.set(undefined);
|
||||
}
|
||||
|
||||
return {
|
||||
albums,
|
||||
isShowContextMenu,
|
||||
contextMenuPosition,
|
||||
contextMenuTargetAlbum,
|
||||
loadAlbums,
|
||||
createAlbum,
|
||||
deleteAlbum,
|
||||
showAlbumContextMenu,
|
||||
closeAlbumContextMenu
|
||||
};
|
||||
return {
|
||||
albums,
|
||||
isShowContextMenu,
|
||||
contextMenuPosition,
|
||||
contextMenuTargetAlbum,
|
||||
loadAlbums,
|
||||
createAlbum,
|
||||
deleteAlbum,
|
||||
showAlbumContextMenu,
|
||||
closeAlbumContextMenu,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { user } }) => {
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Archive'
|
||||
}
|
||||
};
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Archive',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,81 +1,75 @@
|
||||
<script lang="ts">
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
||||
import { archivedAsset } from '$lib/stores/archived-asset.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, AssetResponseDto } from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
||||
import { archivedAsset } from '$lib/stores/archived-asset.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, AssetResponseDto } from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
$: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite);
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
$: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const { data: assets } = await api.assetApi.getAllAssets({
|
||||
isArchived: true,
|
||||
withoutThumbs: true
|
||||
});
|
||||
$archivedAsset = assets;
|
||||
} catch {
|
||||
handleError(Error, 'Unable to load archived assets');
|
||||
}
|
||||
});
|
||||
onMount(async () => {
|
||||
try {
|
||||
const { data: assets } = await api.assetApi.getAllAssets({
|
||||
isArchived: true,
|
||||
withoutThumbs: true,
|
||||
});
|
||||
$archivedAsset = assets;
|
||||
} catch {
|
||||
handleError(Error, 'Unable to load archived assets');
|
||||
}
|
||||
});
|
||||
|
||||
const onAssetDelete = (assetId: string) => {
|
||||
$archivedAsset = $archivedAsset.filter((a) => a.id !== assetId);
|
||||
};
|
||||
const handleSelectAll = () => {
|
||||
selectedAssets = new Set($archivedAsset);
|
||||
};
|
||||
const onAssetDelete = (assetId: string) => {
|
||||
$archivedAsset = $archivedAsset.filter((a) => a.id !== assetId);
|
||||
};
|
||||
const handleSelectAll = () => {
|
||||
selectedAssets = new Set($archivedAsset);
|
||||
};
|
||||
</script>
|
||||
|
||||
<UserPageLayout user={data.user} hideNavbar={isMultiSelectionMode} title={data.meta.title}>
|
||||
<!-- Empty Message -->
|
||||
{#if $archivedAsset.length === 0}
|
||||
<EmptyPlaceholder
|
||||
text="Archive photos and videos to hide them from your Photos view"
|
||||
alt="Empty archive"
|
||||
/>
|
||||
{/if}
|
||||
<!-- Empty Message -->
|
||||
{#if $archivedAsset.length === 0}
|
||||
<EmptyPlaceholder text="Archive photos and videos to hide them from your Photos view" alt="Empty archive" />
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="header">
|
||||
{#if isMultiSelectionMode}
|
||||
<AssetSelectControlBar
|
||||
assets={selectedAssets}
|
||||
clearSelect={() => (selectedAssets = new Set())}
|
||||
>
|
||||
<ArchiveAction unarchive onAssetArchive={(asset) => onAssetDelete(asset.id)} />
|
||||
<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
|
||||
<CreateSharedLink />
|
||||
<AssetSelectContextMenu icon={Plus} title="Add">
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</AssetSelectContextMenu>
|
||||
<DeleteAssets {onAssetDelete} />
|
||||
<AssetSelectContextMenu icon={DotsVertical} title="Add">
|
||||
<DownloadAction menuItem />
|
||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||
</AssetSelectContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="header">
|
||||
{#if isMultiSelectionMode}
|
||||
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
|
||||
<ArchiveAction unarchive onAssetArchive={(asset) => onAssetDelete(asset.id)} />
|
||||
<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
|
||||
<CreateSharedLink />
|
||||
<AssetSelectContextMenu icon={Plus} title="Add">
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</AssetSelectContextMenu>
|
||||
<DeleteAssets {onAssetDelete} />
|
||||
<AssetSelectContextMenu icon={DotsVertical} title="Add">
|
||||
<DownloadAction menuItem />
|
||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||
</AssetSelectContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<GalleryViewer assets={$archivedAsset} bind:selectedAssets viewFrom="archive-page" />
|
||||
<GalleryViewer assets={$archivedAsset} bind:selectedAssets viewFrom="archive-page" />
|
||||
</UserPageLayout>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
export const prerender = false;
|
||||
import type { PageLoad } from './$types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
export const prerender = false;
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
throw redirect(302, AppRoute.ARCHIVE);
|
||||
throw redirect(302, AppRoute.ARCHIVE);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
|
||||
export const load = (async ({ locals, parent }) => {
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
const { data: items } = await locals.api.searchApi.getExploreData();
|
||||
const { data: people } = await locals.api.personApi.getAllPeople();
|
||||
return {
|
||||
user,
|
||||
items,
|
||||
people,
|
||||
meta: {
|
||||
title: 'Explore'
|
||||
}
|
||||
};
|
||||
const { data: items } = await locals.api.searchApi.getExploreData();
|
||||
const { data: people } = await locals.api.personApi.getAllPeople();
|
||||
return {
|
||||
user,
|
||||
items,
|
||||
people,
|
||||
meta: {
|
||||
title: 'Explore',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,157 +1,157 @@
|
||||
<script lang="ts">
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { AssetTypeEnum, SearchExploreResponseDto, api } from '@api';
|
||||
import ClockOutline from 'svelte-material-icons/ClockOutline.svelte';
|
||||
import HeartMultipleOutline from 'svelte-material-icons/HeartMultipleOutline.svelte';
|
||||
import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
|
||||
import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { AssetTypeEnum, SearchExploreResponseDto, api } from '@api';
|
||||
import ClockOutline from 'svelte-material-icons/ClockOutline.svelte';
|
||||
import HeartMultipleOutline from 'svelte-material-icons/HeartMultipleOutline.svelte';
|
||||
import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
|
||||
import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
|
||||
enum Field {
|
||||
CITY = 'exifInfo.city',
|
||||
TAGS = 'smartInfo.tags',
|
||||
OBJECTS = 'smartInfo.objects'
|
||||
}
|
||||
enum Field {
|
||||
CITY = 'exifInfo.city',
|
||||
TAGS = 'smartInfo.tags',
|
||||
OBJECTS = 'smartInfo.objects',
|
||||
}
|
||||
|
||||
const MAX_ITEMS = 12;
|
||||
const MAX_ITEMS = 12;
|
||||
|
||||
const getFieldItems = (items: SearchExploreResponseDto[], field: Field) => {
|
||||
const targetField = items.find((item) => item.fieldName === field);
|
||||
return targetField?.items || [];
|
||||
};
|
||||
const getFieldItems = (items: SearchExploreResponseDto[], field: Field) => {
|
||||
const targetField = items.find((item) => item.fieldName === field);
|
||||
return targetField?.items || [];
|
||||
};
|
||||
|
||||
$: things = getFieldItems(data.items, Field.OBJECTS);
|
||||
$: places = getFieldItems(data.items, Field.CITY);
|
||||
$: people = data.people.slice(0, MAX_ITEMS);
|
||||
$: things = getFieldItems(data.items, Field.OBJECTS);
|
||||
$: places = getFieldItems(data.items, Field.CITY);
|
||||
$: people = data.people.slice(0, MAX_ITEMS);
|
||||
</script>
|
||||
|
||||
<UserPageLayout user={data.user} title={data.meta.title}>
|
||||
{#if people.length > 0}
|
||||
<div class="mb-6 mt-2">
|
||||
<div class="flex justify-between">
|
||||
<p class="mb-4 dark:text-immich-dark-fg font-medium">People</p>
|
||||
{#if data.people.length > MAX_ITEMS}
|
||||
<a
|
||||
href={AppRoute.PEOPLE}
|
||||
class="font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary dark:text-immich-dark-fg"
|
||||
draggable="false">View All</a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{#each people as person (person.id)}
|
||||
<a href="/people/{person.id}" class="w-24 text-center">
|
||||
<ImageThumbnail
|
||||
circle
|
||||
shadow
|
||||
url={api.getPeopleThumbnailUrl(person.id)}
|
||||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
/>
|
||||
<p class="font-medium mt-2 text-ellipsis text-sm dark:text-white">{person.name}</p>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if people.length > 0}
|
||||
<div class="mb-6 mt-2">
|
||||
<div class="flex justify-between">
|
||||
<p class="mb-4 dark:text-immich-dark-fg font-medium">People</p>
|
||||
{#if data.people.length > MAX_ITEMS}
|
||||
<a
|
||||
href={AppRoute.PEOPLE}
|
||||
class="font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary dark:text-immich-dark-fg"
|
||||
draggable="false">View All</a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{#each people as person (person.id)}
|
||||
<a href="/people/{person.id}" class="w-24 text-center">
|
||||
<ImageThumbnail
|
||||
circle
|
||||
shadow
|
||||
url={api.getPeopleThumbnailUrl(person.id)}
|
||||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
/>
|
||||
<p class="font-medium mt-2 text-ellipsis text-sm dark:text-white">{person.name}</p>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if places.length > 0}
|
||||
<div class="mb-6 mt-2">
|
||||
<div>
|
||||
<p class="mb-4 dark:text-immich-dark-fg font-medium">Places</p>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{#each places as item}
|
||||
<a class="relative" href="/search?{Field.CITY}={item.value}" draggable="false">
|
||||
<div
|
||||
class="filter brightness-75 rounded-xl overflow-hidden w-[calc((100vw-(72px+5rem))/2)] max-w-[156px] flex justify-center"
|
||||
>
|
||||
<Thumbnail thumbnailSize={156} asset={item.data} readonly />
|
||||
</div>
|
||||
<span
|
||||
class="capitalize absolute bottom-2 w-full text-center text-sm font-medium text-white text-ellipsis w-100 px-1 hover:cursor-pointer backdrop-blur-[1px]"
|
||||
>
|
||||
{item.value}
|
||||
</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if places.length > 0}
|
||||
<div class="mb-6 mt-2">
|
||||
<div>
|
||||
<p class="mb-4 dark:text-immich-dark-fg font-medium">Places</p>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{#each places as item}
|
||||
<a class="relative" href="/search?{Field.CITY}={item.value}" draggable="false">
|
||||
<div
|
||||
class="filter brightness-75 rounded-xl overflow-hidden w-[calc((100vw-(72px+5rem))/2)] max-w-[156px] flex justify-center"
|
||||
>
|
||||
<Thumbnail thumbnailSize={156} asset={item.data} readonly />
|
||||
</div>
|
||||
<span
|
||||
class="capitalize absolute bottom-2 w-full text-center text-sm font-medium text-white text-ellipsis w-100 px-1 hover:cursor-pointer backdrop-blur-[1px]"
|
||||
>
|
||||
{item.value}
|
||||
</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if things.length > 0}
|
||||
<div class="mb-6 mt-2">
|
||||
<div>
|
||||
<p class="mb-4 dark:text-immich-dark-fg font-medium">Things</p>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{#each things as item}
|
||||
<a class="relative" href="/search?{Field.OBJECTS}={item.value}" draggable="false">
|
||||
<div
|
||||
class="filter brightness-75 rounded-xl overflow-hidden w-[calc((100vw-(72px+5rem))/2)] max-w-[156px] justify-center flex"
|
||||
>
|
||||
<Thumbnail thumbnailSize={156} asset={item.data} readonly />
|
||||
</div>
|
||||
<span
|
||||
class="capitalize absolute bottom-2 w-full text-center text-sm font-medium text-white text-ellipsis w-100 px-1 hover:cursor-pointer backdrop-blur-[1px]"
|
||||
>
|
||||
{item.value}
|
||||
</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if things.length > 0}
|
||||
<div class="mb-6 mt-2">
|
||||
<div>
|
||||
<p class="mb-4 dark:text-immich-dark-fg font-medium">Things</p>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{#each things as item}
|
||||
<a class="relative" href="/search?{Field.OBJECTS}={item.value}" draggable="false">
|
||||
<div
|
||||
class="filter brightness-75 rounded-xl overflow-hidden w-[calc((100vw-(72px+5rem))/2)] max-w-[156px] justify-center flex"
|
||||
>
|
||||
<Thumbnail thumbnailSize={156} asset={item.data} readonly />
|
||||
</div>
|
||||
<span
|
||||
class="capitalize absolute bottom-2 w-full text-center text-sm font-medium text-white text-ellipsis w-100 px-1 hover:cursor-pointer backdrop-blur-[1px]"
|
||||
>
|
||||
{item.value}
|
||||
</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class="dark:border-immich-dark-gray mb-4" />
|
||||
<hr class="dark:border-immich-dark-gray mb-4" />
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-8">
|
||||
<div class="flex flex-col gap-6 dark:text-immich-dark-fg">
|
||||
<p class="text-sm">YOUR ACTIVITY</p>
|
||||
<div class="flex flex-col gap-4 dark:text-immich-dark-fg/80">
|
||||
<a
|
||||
href={AppRoute.FAVORITES}
|
||||
class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary content-center gap-2"
|
||||
draggable="false"
|
||||
>
|
||||
<HeartMultipleOutline size={24} />
|
||||
<span>Favorites</span>
|
||||
</a>
|
||||
<a
|
||||
href="/search?recent=true"
|
||||
class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary content-center gap-2"
|
||||
draggable="false"
|
||||
>
|
||||
<ClockOutline size={24} />
|
||||
<span>Recently added</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-6 dark:text-immich-dark-fg">
|
||||
<p class="text-sm">CATEGORIES</p>
|
||||
<div class="flex flex-col gap-4 dark:text-immich-dark-fg/80">
|
||||
<a
|
||||
href="/search?type={AssetTypeEnum.Video}"
|
||||
class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary items-center gap-2"
|
||||
>
|
||||
<PlayCircleOutline size={24} />
|
||||
<span>Videos</span>
|
||||
</a>
|
||||
<div>
|
||||
<a
|
||||
href="/search?motion=true"
|
||||
class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary items-center gap-2"
|
||||
>
|
||||
<MotionPlayOutline size={24} />
|
||||
<span>Motion photos</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-8">
|
||||
<div class="flex flex-col gap-6 dark:text-immich-dark-fg">
|
||||
<p class="text-sm">YOUR ACTIVITY</p>
|
||||
<div class="flex flex-col gap-4 dark:text-immich-dark-fg/80">
|
||||
<a
|
||||
href={AppRoute.FAVORITES}
|
||||
class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary content-center gap-2"
|
||||
draggable="false"
|
||||
>
|
||||
<HeartMultipleOutline size={24} />
|
||||
<span>Favorites</span>
|
||||
</a>
|
||||
<a
|
||||
href="/search?recent=true"
|
||||
class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary content-center gap-2"
|
||||
draggable="false"
|
||||
>
|
||||
<ClockOutline size={24} />
|
||||
<span>Recently added</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-6 dark:text-immich-dark-fg">
|
||||
<p class="text-sm">CATEGORIES</p>
|
||||
<div class="flex flex-col gap-4 dark:text-immich-dark-fg/80">
|
||||
<a
|
||||
href="/search?type={AssetTypeEnum.Video}"
|
||||
class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary items-center gap-2"
|
||||
>
|
||||
<PlayCircleOutline size={24} />
|
||||
<span>Videos</span>
|
||||
</a>
|
||||
<div>
|
||||
<a
|
||||
href="/search?motion=true"
|
||||
class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary items-center gap-2"
|
||||
>
|
||||
<MotionPlayOutline size={24} />
|
||||
<span>Motion photos</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UserPageLayout>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { user } }) => {
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Favorites'
|
||||
}
|
||||
};
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Favorites',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,82 +1,79 @@
|
||||
<script lang="ts">
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, AssetResponseDto } from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
import Error from '../../+error.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, AssetResponseDto } from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
import Error from '../../+error.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
||||
|
||||
let favorites: AssetResponseDto[] = [];
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
let favorites: AssetResponseDto[] = [];
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
$: isAllArchive = Array.from(selectedAssets).every((asset) => asset.isArchived);
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
$: isAllArchive = Array.from(selectedAssets).every((asset) => asset.isArchived);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const { data: assets } = await api.assetApi.getAllAssets({
|
||||
isFavorite: true,
|
||||
withoutThumbs: true
|
||||
});
|
||||
favorites = assets;
|
||||
} catch {
|
||||
handleError(Error, 'Unable to load favorites');
|
||||
}
|
||||
});
|
||||
onMount(async () => {
|
||||
try {
|
||||
const { data: assets } = await api.assetApi.getAllAssets({
|
||||
isFavorite: true,
|
||||
withoutThumbs: true,
|
||||
});
|
||||
favorites = assets;
|
||||
} catch {
|
||||
handleError(Error, 'Unable to load favorites');
|
||||
}
|
||||
});
|
||||
|
||||
const handleSelectAll = () => {
|
||||
selectedAssets = new Set(favorites);
|
||||
};
|
||||
const handleSelectAll = () => {
|
||||
selectedAssets = new Set(favorites);
|
||||
};
|
||||
|
||||
const onAssetDelete = (assetId: string) => {
|
||||
favorites = favorites.filter((a) => a.id !== assetId);
|
||||
};
|
||||
const onAssetDelete = (assetId: string) => {
|
||||
favorites = favorites.filter((a) => a.id !== assetId);
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Multiselection mode app bar -->
|
||||
{#if isMultiSelectionMode}
|
||||
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
|
||||
<FavoriteAction removeFavorite onAssetFavorite={(asset) => onAssetDelete(asset.id)} />
|
||||
<CreateSharedLink />
|
||||
<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
|
||||
<AssetSelectContextMenu icon={Plus} title="Add">
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</AssetSelectContextMenu>
|
||||
<DeleteAssets {onAssetDelete} />
|
||||
<AssetSelectContextMenu icon={DotsVertical} title="Menu">
|
||||
<DownloadAction menuItem />
|
||||
<ArchiveAction menuItem unarchive={isAllArchive} />
|
||||
</AssetSelectContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
|
||||
<FavoriteAction removeFavorite onAssetFavorite={(asset) => onAssetDelete(asset.id)} />
|
||||
<CreateSharedLink />
|
||||
<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
|
||||
<AssetSelectContextMenu icon={Plus} title="Add">
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</AssetSelectContextMenu>
|
||||
<DeleteAssets {onAssetDelete} />
|
||||
<AssetSelectContextMenu icon={DotsVertical} title="Menu">
|
||||
<DownloadAction menuItem />
|
||||
<ArchiveAction menuItem unarchive={isAllArchive} />
|
||||
</AssetSelectContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{/if}
|
||||
|
||||
<UserPageLayout user={data.user} hideNavbar={isMultiSelectionMode} title={data.meta.title}>
|
||||
<section>
|
||||
<!-- Empty Message -->
|
||||
{#if favorites.length === 0}
|
||||
<EmptyPlaceholder
|
||||
text="Add favorites to quickly find your best pictures and videos"
|
||||
alt="Empty favorites"
|
||||
/>
|
||||
{/if}
|
||||
<section>
|
||||
<!-- Empty Message -->
|
||||
{#if favorites.length === 0}
|
||||
<EmptyPlaceholder text="Add favorites to quickly find your best pictures and videos" alt="Empty favorites" />
|
||||
{/if}
|
||||
|
||||
<GalleryViewer assets={favorites} bind:selectedAssets viewFrom="favorites-page" />
|
||||
</section>
|
||||
<GalleryViewer assets={favorites} bind:selectedAssets viewFrom="favorites-page" />
|
||||
</section>
|
||||
</UserPageLayout>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
export const prerender = false;
|
||||
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
const { user } = await parent();
|
||||
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else {
|
||||
throw redirect(302, AppRoute.FAVORITES);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else {
|
||||
throw redirect(302, AppRoute.FAVORITES);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,14 +3,14 @@ import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { user } }) => {
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Map'
|
||||
}
|
||||
};
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Map',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,176 +1,172 @@
|
||||
<script lang="ts">
|
||||
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import MapSettingsModal from '$lib/components/map-page/map-settings-modal.svelte';
|
||||
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||
import {
|
||||
assetInteractionStore,
|
||||
isViewingAssetStoreState,
|
||||
viewingAssetStoreState
|
||||
} from '$lib/stores/asset-interaction.store';
|
||||
import { mapSettings } from '$lib/stores/preferences.store';
|
||||
import { MapMarkerResponseDto, api } from '@api';
|
||||
import { isEqual, omit } from 'lodash-es';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import Cog from 'svelte-material-icons/Cog.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import MapSettingsModal from '$lib/components/map-page/map-settings-modal.svelte';
|
||||
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||
import {
|
||||
assetInteractionStore,
|
||||
isViewingAssetStoreState,
|
||||
viewingAssetStoreState,
|
||||
} from '$lib/stores/asset-interaction.store';
|
||||
import { mapSettings } from '$lib/stores/preferences.store';
|
||||
import { MapMarkerResponseDto, api } from '@api';
|
||||
import { isEqual, omit } from 'lodash-es';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import Cog from 'svelte-material-icons/Cog.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
|
||||
let leaflet: typeof import('$lib/components/shared-components/leaflet');
|
||||
let mapMarkers: MapMarkerResponseDto[] = [];
|
||||
let abortController: AbortController;
|
||||
let viewingAssets: string[] = [];
|
||||
let viewingAssetCursor = 0;
|
||||
let showSettingsModal = false;
|
||||
let leaflet: typeof import('$lib/components/shared-components/leaflet');
|
||||
let mapMarkers: MapMarkerResponseDto[] = [];
|
||||
let abortController: AbortController;
|
||||
let viewingAssets: string[] = [];
|
||||
let viewingAssetCursor = 0;
|
||||
let showSettingsModal = false;
|
||||
|
||||
onMount(() => {
|
||||
loadMapMarkers().then((data) => (mapMarkers = data));
|
||||
import('$lib/components/shared-components/leaflet').then((data) => (leaflet = data));
|
||||
});
|
||||
onMount(() => {
|
||||
loadMapMarkers().then((data) => (mapMarkers = data));
|
||||
import('$lib/components/shared-components/leaflet').then((data) => (leaflet = data));
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
}
|
||||
assetInteractionStore.clearMultiselect();
|
||||
assetInteractionStore.setIsViewingAsset(false);
|
||||
});
|
||||
onDestroy(() => {
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
}
|
||||
assetInteractionStore.clearMultiselect();
|
||||
assetInteractionStore.setIsViewingAsset(false);
|
||||
});
|
||||
|
||||
async function loadMapMarkers() {
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
}
|
||||
abortController = new AbortController();
|
||||
async function loadMapMarkers() {
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
}
|
||||
abortController = new AbortController();
|
||||
|
||||
const { onlyFavorites } = $mapSettings;
|
||||
const { fileCreatedAfter, fileCreatedBefore } = getFileCreatedDates();
|
||||
const { onlyFavorites } = $mapSettings;
|
||||
const { fileCreatedAfter, fileCreatedBefore } = getFileCreatedDates();
|
||||
|
||||
const { data } = await api.assetApi.getMapMarkers(
|
||||
{
|
||||
isFavorite: onlyFavorites || undefined,
|
||||
fileCreatedAfter: fileCreatedAfter || undefined,
|
||||
fileCreatedBefore
|
||||
},
|
||||
{
|
||||
signal: abortController.signal
|
||||
}
|
||||
);
|
||||
return data;
|
||||
}
|
||||
const { data } = await api.assetApi.getMapMarkers(
|
||||
{
|
||||
isFavorite: onlyFavorites || undefined,
|
||||
fileCreatedAfter: fileCreatedAfter || undefined,
|
||||
fileCreatedBefore,
|
||||
},
|
||||
{
|
||||
signal: abortController.signal,
|
||||
},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
function getFileCreatedDates() {
|
||||
const { relativeDate, dateAfter, dateBefore } = $mapSettings;
|
||||
function getFileCreatedDates() {
|
||||
const { relativeDate, dateAfter, dateBefore } = $mapSettings;
|
||||
|
||||
if (relativeDate) {
|
||||
const duration = Duration.fromISO(relativeDate);
|
||||
return {
|
||||
fileCreatedAfter: duration.isValid ? DateTime.now().minus(duration).toISO() : undefined
|
||||
};
|
||||
}
|
||||
if (relativeDate) {
|
||||
const duration = Duration.fromISO(relativeDate);
|
||||
return {
|
||||
fileCreatedAfter: duration.isValid ? DateTime.now().minus(duration).toISO() : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
fileCreatedAfter: dateAfter ? new Date(dateAfter).toISOString() : undefined,
|
||||
fileCreatedBefore: dateBefore ? new Date(dateBefore).toISOString() : undefined
|
||||
};
|
||||
} catch {
|
||||
$mapSettings.dateAfter = '';
|
||||
$mapSettings.dateBefore = '';
|
||||
return {};
|
||||
}
|
||||
}
|
||||
try {
|
||||
return {
|
||||
fileCreatedAfter: dateAfter ? new Date(dateAfter).toISOString() : undefined,
|
||||
fileCreatedBefore: dateBefore ? new Date(dateBefore).toISOString() : undefined,
|
||||
};
|
||||
} catch {
|
||||
$mapSettings.dateAfter = '';
|
||||
$mapSettings.dateBefore = '';
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function onViewAssets(assetIds: string[], activeAssetIndex: number) {
|
||||
assetInteractionStore.setViewingAssetId(assetIds[activeAssetIndex]);
|
||||
viewingAssets = assetIds;
|
||||
viewingAssetCursor = activeAssetIndex;
|
||||
}
|
||||
function onViewAssets(assetIds: string[], activeAssetIndex: number) {
|
||||
assetInteractionStore.setViewingAssetId(assetIds[activeAssetIndex]);
|
||||
viewingAssets = assetIds;
|
||||
viewingAssetCursor = activeAssetIndex;
|
||||
}
|
||||
|
||||
function navigateNext() {
|
||||
if (viewingAssetCursor < viewingAssets.length - 1) {
|
||||
assetInteractionStore.setViewingAssetId(viewingAssets[++viewingAssetCursor]);
|
||||
}
|
||||
}
|
||||
function navigateNext() {
|
||||
if (viewingAssetCursor < viewingAssets.length - 1) {
|
||||
assetInteractionStore.setViewingAssetId(viewingAssets[++viewingAssetCursor]);
|
||||
}
|
||||
}
|
||||
|
||||
function navigatePrevious() {
|
||||
if (viewingAssetCursor > 0) {
|
||||
assetInteractionStore.setViewingAssetId(viewingAssets[--viewingAssetCursor]);
|
||||
}
|
||||
}
|
||||
function navigatePrevious() {
|
||||
if (viewingAssetCursor > 0) {
|
||||
assetInteractionStore.setViewingAssetId(viewingAssets[--viewingAssetCursor]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<UserPageLayout user={data.user} title={data.meta.title}>
|
||||
<div class="h-full w-full isolate">
|
||||
{#if leaflet}
|
||||
{@const { Map, TileLayer, AssetMarkerCluster, Control } = leaflet}
|
||||
<Map
|
||||
center={[30, 0]}
|
||||
zoom={3}
|
||||
allowDarkMode={$mapSettings.allowDarkMode}
|
||||
options={{
|
||||
maxBounds: [
|
||||
[-90, -180],
|
||||
[90, 180]
|
||||
],
|
||||
minZoom: 2.5
|
||||
}}
|
||||
>
|
||||
<TileLayer
|
||||
urlTemplate={'https://tile.openstreetmap.org/{z}/{x}/{y}.png'}
|
||||
options={{
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}}
|
||||
/>
|
||||
<AssetMarkerCluster
|
||||
markers={mapMarkers}
|
||||
on:view={({ detail }) => onViewAssets(detail.assetIds, detail.activeAssetIndex)}
|
||||
/>
|
||||
<Control>
|
||||
<button
|
||||
class="flex justify-center items-center bg-white text-black/70 w-8 h-8 font-bold rounded-sm border-2 border-black/20 hover:bg-gray-50 focus:bg-gray-50"
|
||||
title="Open map settings"
|
||||
on:click={() => (showSettingsModal = true)}
|
||||
>
|
||||
<Cog size="100%" class="p-1" />
|
||||
</button>
|
||||
</Control>
|
||||
</Map>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="h-full w-full isolate">
|
||||
{#if leaflet}
|
||||
{@const { Map, TileLayer, AssetMarkerCluster, Control } = leaflet}
|
||||
<Map
|
||||
center={[30, 0]}
|
||||
zoom={3}
|
||||
allowDarkMode={$mapSettings.allowDarkMode}
|
||||
options={{
|
||||
maxBounds: [
|
||||
[-90, -180],
|
||||
[90, 180],
|
||||
],
|
||||
minZoom: 2.5,
|
||||
}}
|
||||
>
|
||||
<TileLayer
|
||||
urlTemplate={'https://tile.openstreetmap.org/{z}/{x}/{y}.png'}
|
||||
options={{
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
}}
|
||||
/>
|
||||
<AssetMarkerCluster
|
||||
markers={mapMarkers}
|
||||
on:view={({ detail }) => onViewAssets(detail.assetIds, detail.activeAssetIndex)}
|
||||
/>
|
||||
<Control>
|
||||
<button
|
||||
class="flex justify-center items-center bg-white text-black/70 w-8 h-8 font-bold rounded-sm border-2 border-black/20 hover:bg-gray-50 focus:bg-gray-50"
|
||||
title="Open map settings"
|
||||
on:click={() => (showSettingsModal = true)}
|
||||
>
|
||||
<Cog size="100%" class="p-1" />
|
||||
</button>
|
||||
</Control>
|
||||
</Map>
|
||||
{/if}
|
||||
</div>
|
||||
</UserPageLayout>
|
||||
|
||||
<Portal target="body">
|
||||
{#if $isViewingAssetStoreState}
|
||||
<AssetViewer
|
||||
asset={$viewingAssetStoreState}
|
||||
showNavigation={viewingAssets.length > 1}
|
||||
on:navigate-next={navigateNext}
|
||||
on:navigate-previous={navigatePrevious}
|
||||
on:close={() => {
|
||||
assetInteractionStore.setIsViewingAsset(false);
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if $isViewingAssetStoreState}
|
||||
<AssetViewer
|
||||
asset={$viewingAssetStoreState}
|
||||
showNavigation={viewingAssets.length > 1}
|
||||
on:navigate-next={navigateNext}
|
||||
on:navigate-previous={navigatePrevious}
|
||||
on:close={() => {
|
||||
assetInteractionStore.setIsViewingAsset(false);
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</Portal>
|
||||
|
||||
{#if showSettingsModal}
|
||||
<MapSettingsModal
|
||||
settings={{ ...$mapSettings }}
|
||||
on:close={() => (showSettingsModal = false)}
|
||||
on:save={async ({ detail }) => {
|
||||
const shouldUpdate = !isEqual(
|
||||
omit(detail, 'allowDarkMode'),
|
||||
omit($mapSettings, 'allowDarkMode')
|
||||
);
|
||||
showSettingsModal = false;
|
||||
$mapSettings = detail;
|
||||
<MapSettingsModal
|
||||
settings={{ ...$mapSettings }}
|
||||
on:close={() => (showSettingsModal = false)}
|
||||
on:save={async ({ detail }) => {
|
||||
const shouldUpdate = !isEqual(omit(detail, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode'));
|
||||
showSettingsModal = false;
|
||||
$mapSettings = detail;
|
||||
|
||||
if (shouldUpdate) {
|
||||
mapMarkers = await loadMapMarkers();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
if (shouldUpdate) {
|
||||
mapMarkers = await loadMapMarkers();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { user } }) => {
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Memory'
|
||||
}
|
||||
};
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Memory',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import MemoryViewer from '$lib/components/memory-page/memory-viewer.svelte';
|
||||
import MemoryViewer from '$lib/components/memory-page/memory-viewer.svelte';
|
||||
</script>
|
||||
|
||||
<MemoryViewer />
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
export const prerender = false;
|
||||
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
const { user } = await parent();
|
||||
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else {
|
||||
throw redirect(302, AppRoute.MEMORY);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else {
|
||||
throw redirect(302, AppRoute.MEMORY);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
export const prerender = false;
|
||||
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
const { user } = await parent();
|
||||
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else {
|
||||
throw redirect(302, AppRoute.MEMORY);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else {
|
||||
throw redirect(302, AppRoute.MEMORY);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ params, parent, locals: { api } }) => {
|
||||
const { user } = await parent();
|
||||
const { user } = await parent();
|
||||
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
const { data: partner } = await api.userApi.getUserById({ userId: params['userId'] });
|
||||
const { data: partner } = await api.userApi.getUserById({ userId: params['userId'] });
|
||||
|
||||
return {
|
||||
user,
|
||||
partner,
|
||||
meta: {
|
||||
title: 'Partner'
|
||||
}
|
||||
};
|
||||
return {
|
||||
user,
|
||||
partner,
|
||||
meta: {
|
||||
title: 'Partner',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,62 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
||||
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import {
|
||||
assetInteractionStore,
|
||||
isMultiSelectStoreState,
|
||||
selectedAssets
|
||||
} from '$lib/stores/asset-interaction.store';
|
||||
import { onDestroy } from 'svelte';
|
||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
||||
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { assetInteractionStore, isMultiSelectStoreState, selectedAssets } from '$lib/stores/asset-interaction.store';
|
||||
import { onDestroy } from 'svelte';
|
||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
|
||||
onDestroy(() => {
|
||||
assetInteractionStore.clearMultiselect();
|
||||
});
|
||||
onDestroy(() => {
|
||||
assetInteractionStore.clearMultiselect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="grid h-screen pt-18 bg-immich-bg dark:bg-immich-dark-bg">
|
||||
{#if $isMultiSelectStoreState}
|
||||
<AssetSelectControlBar
|
||||
assets={$selectedAssets}
|
||||
clearSelect={assetInteractionStore.clearMultiselect}
|
||||
>
|
||||
<DownloadAction />
|
||||
</AssetSelectControlBar>
|
||||
<AssetSelectControlBar
|
||||
assets={$selectedAssets}
|
||||
clearSelect={assetInteractionStore.clearMultiselect}
|
||||
>
|
||||
<CreateSharedLink />
|
||||
<AssetSelectContextMenu icon={Plus} title="Add">
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</AssetSelectContextMenu>
|
||||
<DownloadAction />
|
||||
</AssetSelectControlBar>
|
||||
{:else}
|
||||
<ControlAppBar
|
||||
showBackButton
|
||||
backIcon={ArrowLeft}
|
||||
on:close-button-click={() => goto(AppRoute.SHARING)}
|
||||
>
|
||||
<svelte:fragment slot="leading">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
{data.partner.firstName}
|
||||
{data.partner.lastName}'s photos
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
<AssetGrid user={data.partner} />
|
||||
{#if $isMultiSelectStoreState}
|
||||
<AssetSelectControlBar assets={$selectedAssets} clearSelect={assetInteractionStore.clearMultiselect}>
|
||||
<DownloadAction />
|
||||
</AssetSelectControlBar>
|
||||
<AssetSelectControlBar assets={$selectedAssets} clearSelect={assetInteractionStore.clearMultiselect}>
|
||||
<CreateSharedLink />
|
||||
<AssetSelectContextMenu icon={Plus} title="Add">
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</AssetSelectContextMenu>
|
||||
<DownloadAction />
|
||||
</AssetSelectControlBar>
|
||||
{:else}
|
||||
<ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => goto(AppRoute.SHARING)}>
|
||||
<svelte:fragment slot="leading">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
{data.partner.firstName}
|
||||
{data.partner.lastName}'s photos
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
<AssetGrid user={data.partner} />
|
||||
</main>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
|
||||
export const load = (async ({ locals, parent }) => {
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
const { data: people } = await locals.api.personApi.getAllPeople();
|
||||
const { data: people } = await locals.api.personApi.getAllPeople();
|
||||
|
||||
return {
|
||||
user,
|
||||
people,
|
||||
meta: {
|
||||
title: 'People'
|
||||
}
|
||||
};
|
||||
return {
|
||||
user,
|
||||
people,
|
||||
meta: {
|
||||
title: 'People',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,48 +1,46 @@
|
||||
<script lang="ts">
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import { api } from '@api';
|
||||
import AccountOff from 'svelte-material-icons/AccountOff.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import { api } from '@api';
|
||||
import AccountOff from 'svelte-material-icons/AccountOff.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<UserPageLayout user={data.user} showUploadButton title="People">
|
||||
{#if data.people.length > 0}
|
||||
<div class="pl-4">
|
||||
<div class="flex flex-row flex-wrap gap-1">
|
||||
{#each data.people as person (person.id)}
|
||||
<div class="relative">
|
||||
<a href="/people/{person.id}" draggable="false">
|
||||
<div class="filter brightness-75 rounded-xl w-48">
|
||||
<ImageThumbnail
|
||||
shadow
|
||||
url={api.getPeopleThumbnailUrl(person.id)}
|
||||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
/>
|
||||
</div>
|
||||
{#if person.name}
|
||||
<span
|
||||
class="absolute bottom-2 w-full text-center font-medium text-white text-ellipsis w-100 px-1 hover:cursor-pointer backdrop-blur-[1px]"
|
||||
>
|
||||
{person.name}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex items-center place-content-center w-full min-h-[calc(66vh_-_11rem)] dark:text-white"
|
||||
>
|
||||
<div class="flex flex-col content-center items-center text-center">
|
||||
<AccountOff size="3.5em" />
|
||||
<p class="font-medium text-3xl mt-5">No people</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.people.length > 0}
|
||||
<div class="pl-4">
|
||||
<div class="flex flex-row flex-wrap gap-1">
|
||||
{#each data.people as person (person.id)}
|
||||
<div class="relative">
|
||||
<a href="/people/{person.id}" draggable="false">
|
||||
<div class="filter brightness-75 rounded-xl w-48">
|
||||
<ImageThumbnail
|
||||
shadow
|
||||
url={api.getPeopleThumbnailUrl(person.id)}
|
||||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
/>
|
||||
</div>
|
||||
{#if person.name}
|
||||
<span
|
||||
class="absolute bottom-2 w-full text-center font-medium text-white text-ellipsis w-100 px-1 hover:cursor-pointer backdrop-blur-[1px]"
|
||||
>
|
||||
{person.name}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center place-content-center w-full min-h-[calc(66vh_-_11rem)] dark:text-white">
|
||||
<div class="flex flex-col content-center items-center text-center">
|
||||
<AccountOff size="3.5em" />
|
||||
<p class="font-medium text-3xl mt-5">No people</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</UserPageLayout>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
|
||||
export const load = (async ({ locals, parent, params }) => {
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
const { data: person } = await locals.api.personApi.getPerson({ id: params.personId });
|
||||
const { data: assets } = await locals.api.personApi.getPersonAssets({ id: params.personId });
|
||||
const { data: person } = await locals.api.personApi.getPerson({ id: params.personId });
|
||||
const { data: assets } = await locals.api.personApi.getPersonAssets({ id: params.personId });
|
||||
|
||||
return {
|
||||
user,
|
||||
assets,
|
||||
person,
|
||||
meta: {
|
||||
title: person.name || 'Person'
|
||||
}
|
||||
};
|
||||
return {
|
||||
user,
|
||||
assets,
|
||||
person,
|
||||
meta: {
|
||||
title: person.name || 'Person',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,133 +1,118 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
|
||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AssetResponseDto, api } from '@api';
|
||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
|
||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AssetResponseDto, api } from '@api';
|
||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
|
||||
let isEditName = false;
|
||||
let previousRoute: string = AppRoute.EXPLORE;
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
$: isAllArchive = Array.from(selectedAssets).every((asset) => asset.isArchived);
|
||||
$: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite);
|
||||
let isEditName = false;
|
||||
let previousRoute: string = AppRoute.EXPLORE;
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
$: isAllArchive = Array.from(selectedAssets).every((asset) => asset.isArchived);
|
||||
$: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite);
|
||||
|
||||
afterNavigate(({ from }) => {
|
||||
// Prevent setting previousRoute to the current page.
|
||||
if (from && from.route.id !== $page.route.id) {
|
||||
previousRoute = from.url.href;
|
||||
}
|
||||
});
|
||||
afterNavigate(({ from }) => {
|
||||
// Prevent setting previousRoute to the current page.
|
||||
if (from && from.route.id !== $page.route.id) {
|
||||
previousRoute = from.url.href;
|
||||
}
|
||||
});
|
||||
|
||||
const handleNameChange = async (name: string) => {
|
||||
try {
|
||||
isEditName = false;
|
||||
data.person.name = name;
|
||||
await api.personApi.updatePerson({ id: data.person.id, personUpdateDto: { name } });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save name');
|
||||
}
|
||||
};
|
||||
const handleNameChange = async (name: string) => {
|
||||
try {
|
||||
isEditName = false;
|
||||
data.person.name = name;
|
||||
await api.personApi.updatePerson({ id: data.person.id, personUpdateDto: { name } });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save name');
|
||||
}
|
||||
};
|
||||
|
||||
const onAssetDelete = (assetId: string) => {
|
||||
data.assets = data.assets.filter((asset: AssetResponseDto) => asset.id !== assetId);
|
||||
};
|
||||
const handleSelectAll = () => {
|
||||
selectedAssets = new Set(data.assets);
|
||||
};
|
||||
const onAssetDelete = (assetId: string) => {
|
||||
data.assets = data.assets.filter((asset: AssetResponseDto) => asset.id !== assetId);
|
||||
};
|
||||
const handleSelectAll = () => {
|
||||
selectedAssets = new Set(data.assets);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if isMultiSelectionMode}
|
||||
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
|
||||
<CreateSharedLink />
|
||||
<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
|
||||
<AssetSelectContextMenu icon={Plus} title="Add">
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</AssetSelectContextMenu>
|
||||
<DeleteAssets {onAssetDelete} />
|
||||
<AssetSelectContextMenu icon={DotsVertical} title="Add">
|
||||
<DownloadAction menuItem filename="{data.person.name || 'immich'}.zip" />
|
||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||
<ArchiveAction
|
||||
menuItem
|
||||
unarchive={isAllArchive}
|
||||
onAssetArchive={(asset) => onAssetDelete(asset.id)}
|
||||
/>
|
||||
</AssetSelectContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
|
||||
<CreateSharedLink />
|
||||
<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
|
||||
<AssetSelectContextMenu icon={Plus} title="Add">
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</AssetSelectContextMenu>
|
||||
<DeleteAssets {onAssetDelete} />
|
||||
<AssetSelectContextMenu icon={DotsVertical} title="Add">
|
||||
<DownloadAction menuItem filename="{data.person.name || 'immich'}.zip" />
|
||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||
<ArchiveAction menuItem unarchive={isAllArchive} onAssetArchive={(asset) => onAssetDelete(asset.id)} />
|
||||
</AssetSelectContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{:else}
|
||||
<ControlAppBar
|
||||
showBackButton
|
||||
backIcon={ArrowLeft}
|
||||
on:close-button-click={() => goto(previousRoute)}
|
||||
/>
|
||||
<ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => goto(previousRoute)} />
|
||||
{/if}
|
||||
|
||||
<!-- Face information block -->
|
||||
<section class="pt-24 px-4 sm:px-6 flex place-items-center">
|
||||
{#if isEditName}
|
||||
<EditNameInput
|
||||
person={data.person}
|
||||
on:change={(event) => handleNameChange(event.detail)}
|
||||
on:cancel={() => (isEditName = false)}
|
||||
/>
|
||||
{:else}
|
||||
<ImageThumbnail
|
||||
circle
|
||||
shadow
|
||||
url={api.getPeopleThumbnailUrl(data.person.id)}
|
||||
altText={data.person.name}
|
||||
widthStyle="3.375rem"
|
||||
heightStyle="3.375rem"
|
||||
/>
|
||||
<button
|
||||
title="Edit name"
|
||||
class="px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
on:click={() => (isEditName = true)}
|
||||
>
|
||||
{#if data.person.name}
|
||||
<p class="font-medium py-2">{data.person.name}</p>
|
||||
{:else}
|
||||
<p class="font-medium w-fit">Add a name</p>
|
||||
<p class="text-sm text-gray-500 dark:text-immich-gray">
|
||||
Find them fast by name with search
|
||||
</p>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{#if isEditName}
|
||||
<EditNameInput
|
||||
person={data.person}
|
||||
on:change={(event) => handleNameChange(event.detail)}
|
||||
on:cancel={() => (isEditName = false)}
|
||||
/>
|
||||
{:else}
|
||||
<ImageThumbnail
|
||||
circle
|
||||
shadow
|
||||
url={api.getPeopleThumbnailUrl(data.person.id)}
|
||||
altText={data.person.name}
|
||||
widthStyle="3.375rem"
|
||||
heightStyle="3.375rem"
|
||||
/>
|
||||
<button
|
||||
title="Edit name"
|
||||
class="px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
on:click={() => (isEditName = true)}
|
||||
>
|
||||
{#if data.person.name}
|
||||
<p class="font-medium py-2">{data.person.name}</p>
|
||||
{:else}
|
||||
<p class="font-medium w-fit">Add a name</p>
|
||||
<p class="text-sm text-gray-500 dark:text-immich-gray">Find them fast by name with search</p>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- Gallery Block -->
|
||||
<section class="relative pt-8 sm:px-4 mb-12 bg-immich-bg dark:bg-immich-dark-bg">
|
||||
<section class="overflow-y-auto relative immich-scrollbar">
|
||||
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
|
||||
<GalleryViewer
|
||||
assets={data.assets}
|
||||
viewFrom="search-page"
|
||||
showArchiveIcon={true}
|
||||
bind:selectedAssets
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
<section class="overflow-y-auto relative immich-scrollbar">
|
||||
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
|
||||
<GalleryViewer assets={data.assets} viewFrom="search-page" showArchiveIcon={true} bind:selectedAssets />
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
export const prerender = false;
|
||||
import type { PageLoad } from './$types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
export const prerender = false;
|
||||
|
||||
export const load: PageLoad = async ({ params, parent }) => {
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
const personId = params['personId'];
|
||||
throw redirect(302, `${AppRoute.PEOPLE}/${personId}`);
|
||||
const personId = params['personId'];
|
||||
throw redirect(302, `${AppRoute.PEOPLE}/${personId}`);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { user } }) => {
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Photos'
|
||||
}
|
||||
};
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Photos',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,57 +1,50 @@
|
||||
<script lang="ts">
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
||||
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
||||
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import {
|
||||
assetInteractionStore,
|
||||
isMultiSelectStoreState,
|
||||
selectedAssets
|
||||
} from '$lib/stores/asset-interaction.store';
|
||||
import { assetStore } from '$lib/stores/assets.store';
|
||||
import { onDestroy } from 'svelte';
|
||||
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
||||
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
||||
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import { assetInteractionStore, isMultiSelectStoreState, selectedAssets } from '$lib/stores/asset-interaction.store';
|
||||
import { assetStore } from '$lib/stores/assets.store';
|
||||
import { onDestroy } from 'svelte';
|
||||
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
|
||||
onDestroy(() => {
|
||||
assetInteractionStore.clearMultiselect();
|
||||
});
|
||||
onDestroy(() => {
|
||||
assetInteractionStore.clearMultiselect();
|
||||
});
|
||||
|
||||
$: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
|
||||
$: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
|
||||
</script>
|
||||
|
||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectStoreState} showUploadButton>
|
||||
<svelte:fragment slot="header">
|
||||
{#if $isMultiSelectStoreState}
|
||||
<AssetSelectControlBar
|
||||
assets={$selectedAssets}
|
||||
clearSelect={assetInteractionStore.clearMultiselect}
|
||||
>
|
||||
<CreateSharedLink />
|
||||
<SelectAllAssets />
|
||||
<AssetSelectContextMenu icon={Plus} title="Add">
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</AssetSelectContextMenu>
|
||||
<DeleteAssets onAssetDelete={assetStore.removeAsset} />
|
||||
<AssetSelectContextMenu icon={DotsVertical} title="Menu">
|
||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||
<DownloadAction menuItem />
|
||||
<ArchiveAction menuItem onAssetArchive={(asset) => assetStore.removeAsset(asset.id)} />
|
||||
</AssetSelectContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="header">
|
||||
{#if $isMultiSelectStoreState}
|
||||
<AssetSelectControlBar assets={$selectedAssets} clearSelect={assetInteractionStore.clearMultiselect}>
|
||||
<CreateSharedLink />
|
||||
<SelectAllAssets />
|
||||
<AssetSelectContextMenu icon={Plus} title="Add">
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</AssetSelectContextMenu>
|
||||
<DeleteAssets onAssetDelete={assetStore.removeAsset} />
|
||||
<AssetSelectContextMenu icon={DotsVertical} title="Menu">
|
||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||
<DownloadAction menuItem />
|
||||
<ArchiveAction menuItem onAssetArchive={(asset) => assetStore.removeAsset(asset.id)} />
|
||||
</AssetSelectContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<AssetGrid slot="content" showMemoryLane />
|
||||
<AssetGrid slot="content" showMemoryLane />
|
||||
</UserPageLayout>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
export const prerender = false;
|
||||
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
const { user } = await parent();
|
||||
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
|
||||
export const load = (async ({ locals, parent, url }) => {
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
const term = url.searchParams.get('q') || url.searchParams.get('query') || undefined;
|
||||
const term = url.searchParams.get('q') || url.searchParams.get('query') || undefined;
|
||||
|
||||
const { data: results } = await locals.api.searchApi.search({}, { params: url.searchParams });
|
||||
const { data: results } = await locals.api.searchApi.search({}, { params: url.searchParams });
|
||||
|
||||
return {
|
||||
user,
|
||||
term,
|
||||
results,
|
||||
meta: {
|
||||
title: 'Search'
|
||||
}
|
||||
};
|
||||
return {
|
||||
user,
|
||||
term,
|
||||
results,
|
||||
meta: {
|
||||
title: 'Search',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,144 +1,132 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||
import type { AssetResponseDto } from '@api';
|
||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
|
||||
import ImageOffOutline from 'svelte-material-icons/ImageOffOutline.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import AlbumCard from '$lib/components/album-page/album-card.svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||
import type { AssetResponseDto } from '@api';
|
||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
|
||||
import ImageOffOutline from 'svelte-material-icons/ImageOffOutline.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import AlbumCard from '$lib/components/album-page/album-card.svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
|
||||
// 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;
|
||||
$: albums = data.results.albums.items;
|
||||
// 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;
|
||||
$: albums = data.results.albums.items;
|
||||
|
||||
afterNavigate(({ from }) => {
|
||||
// Prevent setting previousRoute to the current page.
|
||||
if (from && from.route.id !== $page.route.id) {
|
||||
previousRoute = from.url.href;
|
||||
}
|
||||
afterNavigate(({ from }) => {
|
||||
// Prevent setting previousRoute to the current page.
|
||||
if (from && from.route.id !== $page.route.id) {
|
||||
previousRoute = from.url.href;
|
||||
}
|
||||
|
||||
if (from?.route.id === '/(user)/albums/[albumId]') {
|
||||
previousRoute = AppRoute.EXPLORE;
|
||||
}
|
||||
});
|
||||
if (from?.route.id === '/(user)/albums/[albumId]') {
|
||||
previousRoute = AppRoute.EXPLORE;
|
||||
}
|
||||
});
|
||||
|
||||
$: term = (() => {
|
||||
let term = $page.url.searchParams.get('q') || data.term || '';
|
||||
const isMetadataSearch = $page.url.searchParams.get('clip') === 'false';
|
||||
if (isMetadataSearch && term !== '') {
|
||||
term = `m:${term}`;
|
||||
}
|
||||
return term;
|
||||
})();
|
||||
$: term = (() => {
|
||||
let term = $page.url.searchParams.get('q') || data.term || '';
|
||||
const isMetadataSearch = $page.url.searchParams.get('clip') === 'false';
|
||||
if (isMetadataSearch && term !== '') {
|
||||
term = `m:${term}`;
|
||||
}
|
||||
return term;
|
||||
})();
|
||||
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
$: isAllArchived = Array.from(selectedAssets).every((asset) => asset.isArchived);
|
||||
$: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite);
|
||||
$: searchResultAssets = data.results.assets.items;
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
$: isAllArchived = Array.from(selectedAssets).every((asset) => asset.isArchived);
|
||||
$: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite);
|
||||
$: searchResultAssets = data.results.assets.items;
|
||||
|
||||
const onAssetDelete = (assetId: string) => {
|
||||
searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => a.id !== assetId);
|
||||
};
|
||||
const handleSelectAll = () => {
|
||||
selectedAssets = new Set(searchResultAssets);
|
||||
};
|
||||
const onAssetDelete = (assetId: string) => {
|
||||
searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => a.id !== assetId);
|
||||
};
|
||||
const handleSelectAll = () => {
|
||||
selectedAssets = new Set(searchResultAssets);
|
||||
};
|
||||
</script>
|
||||
|
||||
<section>
|
||||
{#if isMultiSelectionMode}
|
||||
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
|
||||
<CreateSharedLink />
|
||||
<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
|
||||
<AssetSelectContextMenu icon={Plus} title="Add">
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</AssetSelectContextMenu>
|
||||
<DeleteAssets {onAssetDelete} />
|
||||
{#if isMultiSelectionMode}
|
||||
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
|
||||
<CreateSharedLink />
|
||||
<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
|
||||
<AssetSelectContextMenu icon={Plus} title="Add">
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</AssetSelectContextMenu>
|
||||
<DeleteAssets {onAssetDelete} />
|
||||
|
||||
<AssetSelectContextMenu icon={DotsVertical} title="Add">
|
||||
<DownloadAction menuItem />
|
||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||
<ArchiveAction menuItem unarchive={isAllArchived} />
|
||||
</AssetSelectContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{:else}
|
||||
<ControlAppBar on:close-button-click={() => goto(previousRoute)} backIcon={ArrowLeft}>
|
||||
<div class="w-full max-w-2xl flex-1 pl-4">
|
||||
<SearchBar grayTheme={false} value={term} />
|
||||
</div>
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
<AssetSelectContextMenu icon={DotsVertical} title="Add">
|
||||
<DownloadAction menuItem />
|
||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||
<ArchiveAction menuItem unarchive={isAllArchived} />
|
||||
</AssetSelectContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{:else}
|
||||
<ControlAppBar on:close-button-click={() => goto(previousRoute)} backIcon={ArrowLeft}>
|
||||
<div class="w-full max-w-2xl flex-1 pl-4">
|
||||
<SearchBar grayTheme={false} value={term} />
|
||||
</div>
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="relative pt-32 mb-12 bg-immich-bg dark:bg-immich-dark-bg">
|
||||
<section class="overflow-y-auto relative immich-scrollbar">
|
||||
{#if albums.length}
|
||||
<section>
|
||||
<div class="text-4xl font-medium text-black/70 dark:text-white/80 ml-6">ALBUMS</div>
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))]">
|
||||
{#each albums as album (album.id)}
|
||||
<a
|
||||
data-sveltekit-preload-data="hover"
|
||||
href={`albums/${album.id}`}
|
||||
animate:flip={{ duration: 200 }}
|
||||
>
|
||||
<AlbumCard
|
||||
{album}
|
||||
user={data.user}
|
||||
isSharingView={false}
|
||||
showItemCount={false}
|
||||
showContextMenu={false}
|
||||
/>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
<section class="overflow-y-auto relative immich-scrollbar">
|
||||
{#if albums.length}
|
||||
<section>
|
||||
<div class="text-4xl font-medium text-black/70 dark:text-white/80 ml-6">ALBUMS</div>
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))]">
|
||||
{#each albums as album (album.id)}
|
||||
<a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}>
|
||||
<AlbumCard {album} user={data.user} isSharingView={false} showItemCount={false} showContextMenu={false} />
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="m-6 text-4xl font-medium text-black/70 dark:text-white/80">PHOTOS & VIDEOS</div>
|
||||
</section>
|
||||
{/if}
|
||||
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
|
||||
{#if data.results?.assets?.items.length > 0}
|
||||
<div class="pl-4">
|
||||
<GalleryViewer
|
||||
assets={searchResultAssets}
|
||||
bind:selectedAssets
|
||||
viewFrom="search-page"
|
||||
showArchiveIcon={true}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex items-center place-content-center w-full min-h-[calc(66vh_-_11rem)] dark:text-white"
|
||||
>
|
||||
<div class="flex flex-col content-center items-center text-center">
|
||||
<ImageOffOutline size="3.5em" />
|
||||
<p class="font-medium text-3xl mt-5">No results</p>
|
||||
<p class="text-base font-normal">Try a synonym or more general keyword</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</section>
|
||||
<div class="m-6 text-4xl font-medium text-black/70 dark:text-white/80">PHOTOS & VIDEOS</div>
|
||||
</section>
|
||||
{/if}
|
||||
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
|
||||
{#if data.results?.assets?.items.length > 0}
|
||||
<div class="pl-4">
|
||||
<GalleryViewer
|
||||
assets={searchResultAssets}
|
||||
bind:selectedAssets
|
||||
viewFrom="search-page"
|
||||
showArchiveIcon={true}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center place-content-center w-full min-h-[calc(66vh_-_11rem)] dark:text-white">
|
||||
<div class="flex flex-col content-center items-center text-center">
|
||||
<ImageOffOutline size="3.5em" />
|
||||
<p class="font-medium text-3xl mt-5">No results</p>
|
||||
<p class="text-base font-normal">Try a synonym or more general keyword</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
export const prerender = false;
|
||||
import type { PageLoad } from './$types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
export const prerender = false;
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
throw redirect(302, AppRoute.SEARCH);
|
||||
throw redirect(302, AppRoute.SEARCH);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<svelte:head>
|
||||
<title>Opps! Error - Immich</title>
|
||||
<title>Opps! Error - Immich</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-screen h-screen flex place-items-center place-content-center">
|
||||
<div class="p-20 text-4xl dark:text-immich-dark-primary text-immich-primary">
|
||||
Page not found :/
|
||||
</div>
|
||||
<div class="p-20 text-4xl dark:text-immich-dark-primary text-immich-primary">Page not found :/</div>
|
||||
</section>
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { ThumbnailFormat, api as clientApi } from '@api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import featurePanelUrl from '$lib/assets/feature-panel.png';
|
||||
import { api as clientApi, ThumbnailFormat } from '@api';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params, locals: { api } }) => {
|
||||
const { key } = params;
|
||||
const { key } = params;
|
||||
|
||||
try {
|
||||
const { data: sharedLink } = await api.sharedLinkApi.getMySharedLink({ key });
|
||||
try {
|
||||
const { data: sharedLink } = await api.sharedLinkApi.getMySharedLink({ key });
|
||||
|
||||
const assetCount = sharedLink.assets.length;
|
||||
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
||||
const assetCount = sharedLink.assets.length;
|
||||
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
||||
|
||||
return {
|
||||
sharedLink,
|
||||
meta: {
|
||||
title: sharedLink.album ? sharedLink.album.albumName : 'Public Share',
|
||||
description: sharedLink.description || `${assetCount} shared photos & videos.`,
|
||||
imageUrl: assetId
|
||||
? clientApi.getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp, sharedLink.key)
|
||||
: featurePanelUrl
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
throw error(404, {
|
||||
message: 'Invalid shared link'
|
||||
});
|
||||
}
|
||||
return {
|
||||
sharedLink,
|
||||
meta: {
|
||||
title: sharedLink.album ? sharedLink.album.albumName : 'Public Share',
|
||||
description: sharedLink.description || `${assetCount} shared photos & videos.`,
|
||||
imageUrl: assetId
|
||||
? clientApi.getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp, sharedLink.key)
|
||||
: featurePanelUrl,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
throw error(404, {
|
||||
message: 'Invalid shared link',
|
||||
});
|
||||
}
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<script lang="ts">
|
||||
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
|
||||
import IndividualSharedViewer from '$lib/components/share-page/individual-shared-viewer.svelte';
|
||||
import { AlbumResponseDto, SharedLinkType } from '@api';
|
||||
import type { PageData } from './$types';
|
||||
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
|
||||
import IndividualSharedViewer from '$lib/components/share-page/individual-shared-viewer.svelte';
|
||||
import { AlbumResponseDto, SharedLinkType } from '@api';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
const { sharedLink } = data;
|
||||
export let data: PageData;
|
||||
const { sharedLink } = data;
|
||||
|
||||
let album: AlbumResponseDto | null = null;
|
||||
let isOwned = data.user ? data.user.id === sharedLink.userId : false;
|
||||
if (sharedLink.album) {
|
||||
album = { ...sharedLink.album, assets: sharedLink.assets };
|
||||
}
|
||||
let album: AlbumResponseDto | null = null;
|
||||
let isOwned = data.user ? data.user.id === sharedLink.userId : false;
|
||||
if (sharedLink.album) {
|
||||
album = { ...sharedLink.album, assets: sharedLink.assets };
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if sharedLink.type == SharedLinkType.Album && album}
|
||||
<div class="immich-scrollbar">
|
||||
<AlbumViewer {album} {sharedLink} />
|
||||
</div>
|
||||
<div class="immich-scrollbar">
|
||||
<AlbumViewer {album} {sharedLink} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if sharedLink.type == SharedLinkType.Individual}
|
||||
<div class="immich-scrollbar">
|
||||
<IndividualSharedViewer {sharedLink} {isOwned} />
|
||||
</div>
|
||||
<div class="immich-scrollbar">
|
||||
<IndividualSharedViewer {sharedLink} {isOwned} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -2,18 +2,18 @@ import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params, locals: { api } }) => {
|
||||
const { key, assetId } = params;
|
||||
const { data: asset } = await api.assetApi.getAssetById({ id: assetId, key });
|
||||
const { key, assetId } = params;
|
||||
const { data: asset } = await api.assetApi.getAssetById({ id: assetId, key });
|
||||
|
||||
if (!asset) {
|
||||
throw error(404, 'Asset not found');
|
||||
}
|
||||
if (!asset) {
|
||||
throw error(404, 'Asset not found');
|
||||
}
|
||||
|
||||
return {
|
||||
asset,
|
||||
key,
|
||||
meta: {
|
||||
title: 'Public Share'
|
||||
}
|
||||
};
|
||||
return {
|
||||
asset,
|
||||
key,
|
||||
meta: {
|
||||
title: 'Public Share',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
export let data: PageData;
|
||||
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
{#if data.asset && data.key}
|
||||
<AssetViewer
|
||||
asset={data.asset}
|
||||
publicSharedKey={data.key}
|
||||
on:navigate-previous={() => null}
|
||||
on:navigate-next={() => null}
|
||||
showNavigation={false}
|
||||
on:close={() => goto(`/share/${data.key}`)}
|
||||
/>
|
||||
<AssetViewer
|
||||
asset={data.asset}
|
||||
publicSharedKey={data.key}
|
||||
on:navigate-previous={() => null}
|
||||
on:navigate-next={() => null}
|
||||
showNavigation={false}
|
||||
on:close={() => goto(`/share/${data.key}`)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -3,24 +3,24 @@ import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { api, user } }) => {
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: sharedAlbums } = await api.albumApi.getAllAlbums({ shared: true });
|
||||
const { data: partners } = await api.partnerApi.getPartners({ direction: 'shared-with' });
|
||||
try {
|
||||
const { data: sharedAlbums } = await api.albumApi.getAllAlbums({ shared: true });
|
||||
const { data: partners } = await api.partnerApi.getPartners({ direction: 'shared-with' });
|
||||
|
||||
return {
|
||||
user,
|
||||
sharedAlbums,
|
||||
partners,
|
||||
meta: {
|
||||
title: 'Sharing'
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
return {
|
||||
user,
|
||||
sharedAlbums,
|
||||
partners,
|
||||
meta: {
|
||||
title: 'Sharing',
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,122 +1,118 @@
|
||||
<script lang="ts">
|
||||
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
|
||||
import Link from 'svelte-material-icons/Link.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { api } from '@api';
|
||||
import type { PageData } from './$types';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import empty2Url from '$lib/assets/empty-2.svg';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import AlbumCard from '$lib/components/album-page/album-card.svelte';
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
|
||||
import Link from 'svelte-material-icons/Link.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { api } from '@api';
|
||||
import type { PageData } from './$types';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import empty2Url from '$lib/assets/empty-2.svg';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import AlbumCard from '$lib/components/album-page/album-card.svelte';
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
|
||||
const createSharedAlbum = async () => {
|
||||
try {
|
||||
const { data: newAlbum } = await api.albumApi.createAlbum({
|
||||
createAlbumDto: {
|
||||
albumName: 'Untitled'
|
||||
}
|
||||
});
|
||||
const createSharedAlbum = async () => {
|
||||
try {
|
||||
const { data: newAlbum } = await api.albumApi.createAlbum({
|
||||
createAlbumDto: {
|
||||
albumName: 'Untitled',
|
||||
},
|
||||
});
|
||||
|
||||
goto('/albums/' + newAlbum.id);
|
||||
} catch (e) {
|
||||
notificationController.show({
|
||||
message: 'Error creating album, check console for more details',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
goto('/albums/' + newAlbum.id);
|
||||
} catch (e) {
|
||||
notificationController.show({
|
||||
message: 'Error creating album, check console for more details',
|
||||
type: NotificationType.Error,
|
||||
});
|
||||
|
||||
console.log('Error [createAlbum] ', e);
|
||||
}
|
||||
};
|
||||
console.log('Error [createAlbum] ', e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<UserPageLayout user={data.user} title={data.meta.title}>
|
||||
<div class="flex" slot="buttons">
|
||||
<LinkButton on:click={createSharedAlbum}>
|
||||
<div class="flex place-items-center gap-x-1 text-sm flex-wrap justify-center">
|
||||
<PlusBoxOutline size="18" class="shrink-0" />
|
||||
<span class="max-sm:text-xs leading-none">Create shared album</span>
|
||||
</div>
|
||||
</LinkButton>
|
||||
<div class="flex" slot="buttons">
|
||||
<LinkButton on:click={createSharedAlbum}>
|
||||
<div class="flex place-items-center gap-x-1 text-sm flex-wrap justify-center">
|
||||
<PlusBoxOutline size="18" class="shrink-0" />
|
||||
<span class="max-sm:text-xs leading-none">Create shared album</span>
|
||||
</div>
|
||||
</LinkButton>
|
||||
|
||||
<LinkButton on:click={() => goto(AppRoute.SHARED_LINKS)}>
|
||||
<div class="flex place-items-center gap-x-1 text-sm flex-wrap justify-center">
|
||||
<Link size="18" class="shrink-0" />
|
||||
<span class="max-sm:text-xs leading-none">Shared links</span>
|
||||
</div>
|
||||
</LinkButton>
|
||||
</div>
|
||||
<LinkButton on:click={() => goto(AppRoute.SHARED_LINKS)}>
|
||||
<div class="flex place-items-center gap-x-1 text-sm flex-wrap justify-center">
|
||||
<Link size="18" class="shrink-0" />
|
||||
<span class="max-sm:text-xs leading-none">Shared links</span>
|
||||
</div>
|
||||
</LinkButton>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#if data.partners.length > 0}
|
||||
<div class="mb-6 mt-2">
|
||||
<div>
|
||||
<p class="mb-4 dark:text-immich-dark-fg font-medium">Partners</p>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
{#if data.partners.length > 0}
|
||||
<div class="mb-6 mt-2">
|
||||
<div>
|
||||
<p class="mb-4 dark:text-immich-dark-fg font-medium">Partners</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{#each data.partners as partner (partner.id)}
|
||||
<a
|
||||
href="/partners/{partner.id}"
|
||||
class="flex rounded-lg gap-4 py-4 px-5 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all"
|
||||
>
|
||||
<UserAvatar user={partner} size="md" autoColor />
|
||||
<div class="text-left">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
{partner.firstName}
|
||||
{partner.lastName}
|
||||
</p>
|
||||
<p class="text-xs text-immich-fg/75 dark:text-immich-dark-fg/75">
|
||||
{partner.email}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{#each data.partners as partner (partner.id)}
|
||||
<a
|
||||
href="/partners/{partner.id}"
|
||||
class="flex rounded-lg gap-4 py-4 px-5 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all"
|
||||
>
|
||||
<UserAvatar user={partner} size="md" autoColor />
|
||||
<div class="text-left">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
{partner.firstName}
|
||||
{partner.lastName}
|
||||
</p>
|
||||
<p class="text-xs text-immich-fg/75 dark:text-immich-dark-fg/75">
|
||||
{partner.email}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="dark:border-immich-dark-gray mb-4" />
|
||||
{/if}
|
||||
<hr class="dark:border-immich-dark-gray mb-4" />
|
||||
{/if}
|
||||
|
||||
<div class="mb-6 mt-2">
|
||||
<div>
|
||||
<p class="mb-4 dark:text-immich-dark-fg font-medium">Albums</p>
|
||||
</div>
|
||||
<div class="mb-6 mt-2">
|
||||
<div>
|
||||
<p class="mb-4 dark:text-immich-dark-fg font-medium">Albums</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- Share Album List -->
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))]">
|
||||
{#each data.sharedAlbums as album (album.id)}
|
||||
<a
|
||||
data-sveltekit-preload-data="hover"
|
||||
href={`albums/${album.id}`}
|
||||
animate:flip={{ duration: 200 }}
|
||||
>
|
||||
<AlbumCard {album} user={data.user} isSharingView showContextMenu={false} />
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
<div>
|
||||
<!-- Share Album List -->
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))]">
|
||||
{#each data.sharedAlbums as album (album.id)}
|
||||
<a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}>
|
||||
<AlbumCard {album} user={data.user} isSharingView showContextMenu={false} />
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Empty List -->
|
||||
{#if data.sharedAlbums.length === 0}
|
||||
<div
|
||||
class="border dark:border-immich-dark-gray p-5 md:w-[500px] w-2/3 m-auto mt-10 bg-gray-50 dark:bg-immich-dark-gray rounded-3xl flex flex-col place-content-center place-items-center dark:text-immich-dark-fg"
|
||||
>
|
||||
<img src={empty2Url} alt="Empty shared album" width="500" draggable="false" />
|
||||
<p class="text-center text-immich-text-gray-500">
|
||||
Create a shared album to share photos and videos with people in your network
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Empty List -->
|
||||
{#if data.sharedAlbums.length === 0}
|
||||
<div
|
||||
class="border dark:border-immich-dark-gray p-5 md:w-[500px] w-2/3 m-auto mt-10 bg-gray-50 dark:bg-immich-dark-gray rounded-3xl flex flex-col place-content-center place-items-center dark:text-immich-dark-fg"
|
||||
>
|
||||
<img src={empty2Url} alt="Empty shared album" width="500" draggable="false" />
|
||||
<p class="text-center text-immich-text-gray-500">
|
||||
Create a shared album to share photos and videos with people in your network
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UserPageLayout>
|
||||
|
||||
@@ -3,14 +3,14 @@ import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { user } }) => {
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Shared Links'
|
||||
}
|
||||
};
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Shared Links',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,106 +1,104 @@
|
||||
<script lang="ts">
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||
import { api, SharedLinkResponseDto } from '@api';
|
||||
import { goto } from '$app/navigation';
|
||||
import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { onMount } from 'svelte';
|
||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||
import { api, SharedLinkResponseDto } from '@api';
|
||||
import { goto } from '$app/navigation';
|
||||
import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { onMount } from 'svelte';
|
||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
|
||||
let sharedLinks: SharedLinkResponseDto[] = [];
|
||||
let editSharedLink: SharedLinkResponseDto | null = null;
|
||||
let sharedLinks: SharedLinkResponseDto[] = [];
|
||||
let editSharedLink: SharedLinkResponseDto | null = null;
|
||||
|
||||
let deleteLinkId: string | null = null;
|
||||
let deleteLinkId: string | null = null;
|
||||
|
||||
const refresh = async () => {
|
||||
const { data } = await api.sharedLinkApi.getAllSharedLinks();
|
||||
sharedLinks = data;
|
||||
};
|
||||
const refresh = async () => {
|
||||
const { data } = await api.sharedLinkApi.getAllSharedLinks();
|
||||
sharedLinks = data;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
await refresh();
|
||||
});
|
||||
onMount(async () => {
|
||||
await refresh();
|
||||
});
|
||||
|
||||
const handleDeleteLink = async () => {
|
||||
if (!deleteLinkId) {
|
||||
return;
|
||||
}
|
||||
const handleDeleteLink = async () => {
|
||||
if (!deleteLinkId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.sharedLinkApi.removeSharedLink({ id: deleteLinkId });
|
||||
notificationController.show({ message: 'Deleted shared link', type: NotificationType.Info });
|
||||
deleteLinkId = null;
|
||||
refresh();
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to delete shared link');
|
||||
}
|
||||
};
|
||||
try {
|
||||
await api.sharedLinkApi.removeSharedLink({ id: deleteLinkId });
|
||||
notificationController.show({ message: 'Deleted shared link', type: NotificationType.Info });
|
||||
deleteLinkId = null;
|
||||
refresh();
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to delete shared link');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditDone = async () => {
|
||||
refresh();
|
||||
editSharedLink = null;
|
||||
};
|
||||
const handleEditDone = async () => {
|
||||
refresh();
|
||||
editSharedLink = null;
|
||||
};
|
||||
|
||||
const handleCopyLink = async (key: string) => {
|
||||
const link = `${window.location.origin}/share/${key}`;
|
||||
await navigator.clipboard.writeText(link);
|
||||
notificationController.show({
|
||||
message: 'Link copied to clipboard',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
};
|
||||
const handleCopyLink = async (key: string) => {
|
||||
const link = `${window.location.origin}/share/${key}`;
|
||||
await navigator.clipboard.writeText(link);
|
||||
notificationController.show({
|
||||
message: 'Link copied to clipboard',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<ControlAppBar backIcon={ArrowLeft} on:close-button-click={() => goto(AppRoute.SHARING)}>
|
||||
<svelte:fragment slot="leading">Shared links</svelte:fragment>
|
||||
<svelte:fragment slot="leading">Shared links</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
|
||||
<section class="flex flex-col pb-[120px] mt-[120px]">
|
||||
<div class="w-[50%] m-auto mb-4 dark:text-immich-gray">
|
||||
<p>Manage shared links</p>
|
||||
</div>
|
||||
{#if sharedLinks.length === 0}
|
||||
<div
|
||||
class="w-[50%] m-auto bg-gray-100 flex place-items-center place-content-center rounded-lg p-12"
|
||||
>
|
||||
<p>You don't have any shared links</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col w-[50%] m-auto">
|
||||
{#each sharedLinks as link (link.id)}
|
||||
<SharedLinkCard
|
||||
{link}
|
||||
on:delete={() => (deleteLinkId = link.id)}
|
||||
on:edit={() => (editSharedLink = link)}
|
||||
on:copy={() => handleCopyLink(link.key)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="w-[50%] m-auto mb-4 dark:text-immich-gray">
|
||||
<p>Manage shared links</p>
|
||||
</div>
|
||||
{#if sharedLinks.length === 0}
|
||||
<div class="w-[50%] m-auto bg-gray-100 flex place-items-center place-content-center rounded-lg p-12">
|
||||
<p>You don't have any shared links</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col w-[50%] m-auto">
|
||||
{#each sharedLinks as link (link.id)}
|
||||
<SharedLinkCard
|
||||
{link}
|
||||
on:delete={() => (deleteLinkId = link.id)}
|
||||
on:edit={() => (editSharedLink = link)}
|
||||
on:copy={() => handleCopyLink(link.key)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if editSharedLink}
|
||||
<CreateSharedLinkModal
|
||||
editingLink={editSharedLink}
|
||||
shareType={editSharedLink.type}
|
||||
album={editSharedLink.album}
|
||||
on:close={handleEditDone}
|
||||
/>
|
||||
<CreateSharedLinkModal
|
||||
editingLink={editSharedLink}
|
||||
shareType={editSharedLink.type}
|
||||
album={editSharedLink.album}
|
||||
on:close={handleEditDone}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if deleteLinkId}
|
||||
<ConfirmDialogue
|
||||
title="Delete Shared Link"
|
||||
prompt="Are you sure you want to delete this shared link?"
|
||||
confirmText="Delete"
|
||||
on:confirm={() => handleDeleteLink()}
|
||||
on:cancel={() => (deleteLinkId = null)}
|
||||
/>
|
||||
<ConfirmDialogue
|
||||
title="Delete Shared Link"
|
||||
prompt="Are you sure you want to delete this shared link?"
|
||||
confirmText="Delete"
|
||||
on:confirm={() => handleDeleteLink()}
|
||||
on:cancel={() => (deleteLinkId = null)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -3,14 +3,14 @@ import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { user } }) => {
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Settings'
|
||||
}
|
||||
};
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Settings',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script lang="ts">
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import UserSettingsList from '$lib/components/user-settings-page/user-settings-list.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import UserSettingsList from '$lib/components/user-settings-page/user-settings-list.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<UserPageLayout user={data.user} title={data.meta.title}>
|
||||
<section class="flex place-content-center mx-4">
|
||||
<div class="w-full max-w-3xl">
|
||||
<UserSettingsList user={data.user} />
|
||||
</div>
|
||||
</section>
|
||||
<section class="flex place-content-center mx-4">
|
||||
<div class="w-full max-w-3xl">
|
||||
<UserSettingsList user={data.user} />
|
||||
</div>
|
||||
</section>
|
||||
</UserPageLayout>
|
||||
|
||||
Reference in New Issue
Block a user