mirror of
https://github.com/KevinMidboe/immich.git
synced 2026-04-28 09:43:53 +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>
|
||||
|
||||
@@ -1,123 +1,119 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import Message from 'svelte-material-icons/Message.svelte';
|
||||
import PartyPopper from 'svelte-material-icons/PartyPopper.svelte';
|
||||
import CodeTags from 'svelte-material-icons/CodeTags.svelte';
|
||||
import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import Message from 'svelte-material-icons/Message.svelte';
|
||||
import PartyPopper from 'svelte-material-icons/PartyPopper.svelte';
|
||||
import CodeTags from 'svelte-material-icons/CodeTags.svelte';
|
||||
import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
|
||||
|
||||
const handleCopy = async () => {
|
||||
//
|
||||
const error = $page.error || null;
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
const handleCopy = async () => {
|
||||
//
|
||||
const error = $page.error || null;
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(`${error.message} - ${error.code}\n${error.stack}`);
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: 'Copied error to clipboard'
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to copy to clipboard');
|
||||
}
|
||||
};
|
||||
try {
|
||||
await navigator.clipboard.writeText(`${error.message} - ${error.code}\n${error.stack}`);
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: 'Copied error to clipboard',
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to copy to clipboard');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="h-screen w-screen">
|
||||
<section class="bg-immich-bg dark:bg-immich-dark-bg">
|
||||
<div class="flex border-b dark:border-b-immich-dark-gray place-items-center px-6 py-4">
|
||||
<a class="flex gap-2 place-items-center hover:cursor-pointer" href="/photos">
|
||||
<ImmichLogo height="35" width="35" />
|
||||
<h1 class="font-immich-title text-2xl text-immich-primary dark:text-immich-dark-primary">
|
||||
IMMICH
|
||||
</h1>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="bg-immich-bg dark:bg-immich-dark-bg">
|
||||
<div class="flex border-b dark:border-b-immich-dark-gray place-items-center px-6 py-4">
|
||||
<a class="flex gap-2 place-items-center hover:cursor-pointer" href="/photos">
|
||||
<ImmichLogo height="35" width="35" />
|
||||
<h1 class="font-immich-title text-2xl text-immich-primary dark:text-immich-dark-primary">IMMICH</h1>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div
|
||||
class="fixed top-0 w-full h-full bg-black/50 flex place-items-center place-content-center overflow-hidden"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray shadow-sm w-[500px] max-w-[95vw] rounded-3xl dark:text-immich-dark-fg"
|
||||
>
|
||||
<div>
|
||||
<div class="flex items-center justify-between gap-4 px-4 py-4">
|
||||
<h1 class="text-immich-primary dark:text-immich-dark-primary font-medium">
|
||||
🚨 Error - Something went wrong
|
||||
</h1>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => handleCopy()}
|
||||
class="transition-colors bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 dark:text-immich-dark-gray px-3 py-2 text-white rounded-full shadow-md text-sm"
|
||||
>
|
||||
<ContentCopy size={24} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fixed top-0 w-full h-full bg-black/50 flex place-items-center place-content-center overflow-hidden">
|
||||
<div>
|
||||
<div
|
||||
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray shadow-sm w-[500px] max-w-[95vw] rounded-3xl dark:text-immich-dark-fg"
|
||||
>
|
||||
<div>
|
||||
<div class="flex items-center justify-between gap-4 px-4 py-4">
|
||||
<h1 class="text-immich-primary dark:text-immich-dark-primary font-medium">
|
||||
🚨 Error - Something went wrong
|
||||
</h1>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => handleCopy()}
|
||||
class="transition-colors bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 dark:text-immich-dark-gray px-3 py-2 text-white rounded-full shadow-md text-sm"
|
||||
>
|
||||
<ContentCopy size={24} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<hr />
|
||||
|
||||
<div class="p-4 max-h-[75vh] min-h-[300px] overflow-y-auto immich-scrollbar pb-4 gap-4">
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<p class="text-red-500">{$page.error?.message} ({$page.error?.code})</p>
|
||||
{#if $page.error?.stack}
|
||||
<label for="stacktrace">Stacktrace</label>
|
||||
<pre id="stacktrace" class="text-xs">{$page.error?.stack || 'No stack'}</pre>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 max-h-[75vh] min-h-[300px] overflow-y-auto immich-scrollbar pb-4 gap-4">
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<p class="text-red-500">{$page.error?.message} ({$page.error?.code})</p>
|
||||
{#if $page.error?.stack}
|
||||
<label for="stacktrace">Stacktrace</label>
|
||||
<pre id="stacktrace" class="text-xs">{$page.error?.stack || 'No stack'}</pre>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<hr />
|
||||
|
||||
<div class="flex justify-around place-items-center place-content-center">
|
||||
<!-- href="https://github.com/immich-app/immich/issues/new" -->
|
||||
<a
|
||||
href="https://discord.com/invite/D8JsnBEuKb"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex justify-center grow basis-0 p-4"
|
||||
>
|
||||
<button class="flex flex-col gap-2 place-items-center place-content-center">
|
||||
<Message size={24} />
|
||||
<p class="text-sm">Get Help</p>
|
||||
</button>
|
||||
</a>
|
||||
<div class="flex justify-around place-items-center place-content-center">
|
||||
<!-- href="https://github.com/immich-app/immich/issues/new" -->
|
||||
<a
|
||||
href="https://discord.com/invite/D8JsnBEuKb"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex justify-center grow basis-0 p-4"
|
||||
>
|
||||
<button class="flex flex-col gap-2 place-items-center place-content-center">
|
||||
<Message size={24} />
|
||||
<p class="text-sm">Get Help</p>
|
||||
</button>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://github.com/immich-app/immich/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex justify-center grow basis-0 p-4"
|
||||
>
|
||||
<button class="flex flex-col gap-2 place-items-center place-content-center">
|
||||
<PartyPopper size={24} />
|
||||
<p class="text-sm">Read Changelog</p>
|
||||
</button>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/immich-app/immich/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex justify-center grow basis-0 p-4"
|
||||
>
|
||||
<button class="flex flex-col gap-2 place-items-center place-content-center">
|
||||
<PartyPopper size={24} />
|
||||
<p class="text-sm">Read Changelog</p>
|
||||
</button>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://immich.app/docs/guides/docker-help"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex justify-center grow basis-0 p-4"
|
||||
>
|
||||
<button class="flex flex-col gap-2 place-items-center place-content-center">
|
||||
<CodeTags size={24} />
|
||||
<p class="text-sm">Check Logs</p>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="https://immich.app/docs/guides/docker-help"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex justify-center grow basis-0 p-4"
|
||||
>
|
||||
<button class="flex flex-col gap-2 place-items-center place-content-center">
|
||||
<CodeTags size={24} />
|
||||
<p class="text-sm">Check Logs</p>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { api, user } }) => {
|
||||
const { data: serverVersion } = await api.serverInfoApi.getServerVersion();
|
||||
const { data: serverVersion } = await api.serverInfoApi.getServerVersion();
|
||||
|
||||
return { serverVersion, user };
|
||||
return { serverVersion, user };
|
||||
}) satisfies LayoutServerLoad;
|
||||
|
||||
@@ -1,86 +1,86 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import '../app.css';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||
import NavigationLoadingBar from '$lib/components/shared-components/navigation-loading-bar.svelte';
|
||||
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
|
||||
import UploadPanel from '$lib/components/shared-components/upload-panel.svelte';
|
||||
import NotificationList from '$lib/components/shared-components/notification/notification-list.svelte';
|
||||
import VersionAnnouncementBox from '$lib/components/shared-components/version-announcement-box.svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import { fileUploadHandler } from '$lib/utils/file-uploader';
|
||||
import UploadCover from '$lib/components/shared-components/drag-and-drop-upload-overlay.svelte';
|
||||
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
|
||||
import AppleHeader from '$lib/components/shared-components/apple-header.svelte';
|
||||
import FaviconHeader from '$lib/components/shared-components/favicon-header.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||
import NavigationLoadingBar from '$lib/components/shared-components/navigation-loading-bar.svelte';
|
||||
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
|
||||
import UploadPanel from '$lib/components/shared-components/upload-panel.svelte';
|
||||
import NotificationList from '$lib/components/shared-components/notification/notification-list.svelte';
|
||||
import VersionAnnouncementBox from '$lib/components/shared-components/version-announcement-box.svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import { fileUploadHandler } from '$lib/utils/file-uploader';
|
||||
import UploadCover from '$lib/components/shared-components/drag-and-drop-upload-overlay.svelte';
|
||||
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
|
||||
import AppleHeader from '$lib/components/shared-components/apple-header.svelte';
|
||||
import FaviconHeader from '$lib/components/shared-components/favicon-header.svelte';
|
||||
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
|
||||
let showNavigationLoadingBar = false;
|
||||
export let data: LayoutData;
|
||||
let albumId: string | undefined;
|
||||
let showNavigationLoadingBar = false;
|
||||
export let data: LayoutData;
|
||||
let albumId: string | undefined;
|
||||
|
||||
beforeNavigate(() => {
|
||||
showNavigationLoadingBar = true;
|
||||
});
|
||||
beforeNavigate(() => {
|
||||
showNavigationLoadingBar = true;
|
||||
});
|
||||
|
||||
afterNavigate(() => {
|
||||
showNavigationLoadingBar = false;
|
||||
});
|
||||
afterNavigate(() => {
|
||||
showNavigationLoadingBar = false;
|
||||
});
|
||||
|
||||
const dropHandler = async ({ dataTransfer }: DragEvent) => {
|
||||
const files = dataTransfer?.files;
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
const dropHandler = async ({ dataTransfer }: DragEvent) => {
|
||||
const files = dataTransfer?.files;
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filesArray: File[] = Array.from<File>(files);
|
||||
albumId = ($page.route.id === '/(user)/albums/[albumId]' || undefined) && $page.params.albumId;
|
||||
const filesArray: File[] = Array.from<File>(files);
|
||||
albumId = ($page.route.id === '/(user)/albums/[albumId]' || undefined) && $page.params.albumId;
|
||||
|
||||
const isShare = $page.route.id === '/(user)/share/[key]' || undefined;
|
||||
if (isShare) {
|
||||
dragAndDropFilesStore.set({ isDragging: true, files: filesArray });
|
||||
} else {
|
||||
await fileUploadHandler(filesArray, albumId);
|
||||
}
|
||||
};
|
||||
const isShare = $page.route.id === '/(user)/share/[key]' || undefined;
|
||||
if (isShare) {
|
||||
dragAndDropFilesStore.set({ isDragging: true, files: filesArray });
|
||||
} else {
|
||||
await fileUploadHandler(filesArray, albumId);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$page.data.meta?.title || 'Web'} - Immich</title>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" content="currentColor" />
|
||||
<FaviconHeader />
|
||||
<AppleHeader />
|
||||
<title>{$page.data.meta?.title || 'Web'} - Immich</title>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" content="currentColor" />
|
||||
<FaviconHeader />
|
||||
<AppleHeader />
|
||||
|
||||
{#if $page.data.meta}
|
||||
<meta name="description" content={$page.data.meta.description} />
|
||||
{#if $page.data.meta}
|
||||
<meta name="description" content={$page.data.meta.description} />
|
||||
|
||||
<!-- Facebook Meta Tags -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={$page.data.meta.title} />
|
||||
<meta property="og:description" content={$page.data.meta.description} />
|
||||
<meta property="og:image" content={$page.data.meta.imageUrl} />
|
||||
<!-- Facebook Meta Tags -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={$page.data.meta.title} />
|
||||
<meta property="og:description" content={$page.data.meta.description} />
|
||||
<meta property="og:image" content={$page.data.meta.imageUrl} />
|
||||
|
||||
<!-- Twitter Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={$page.data.meta.title} />
|
||||
<meta name="twitter:description" content={$page.data.meta.description} />
|
||||
<meta name="twitter:image" content={$page.data.meta.imageUrl} />
|
||||
{/if}
|
||||
<!-- Twitter Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={$page.data.meta.title} />
|
||||
<meta name="twitter:description" content={$page.data.meta.description} />
|
||||
<meta name="twitter:image" content={$page.data.meta.imageUrl} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<noscript
|
||||
class="h-screen w-screen absolute z-[1000] flex place-items-center place-content-center bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg"
|
||||
class="h-screen w-screen absolute z-[1000] flex place-items-center place-content-center bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg"
|
||||
>
|
||||
<FullscreenContainer title="Welcome to Immich">
|
||||
To use Immich, you must enable JavaScript or use a JavaScript compatible browser.
|
||||
</FullscreenContainer>
|
||||
<FullscreenContainer title="Welcome to Immich">
|
||||
To use Immich, you must enable JavaScript or use a JavaScript compatible browser.
|
||||
</FullscreenContainer>
|
||||
</noscript>
|
||||
|
||||
{#if showNavigationLoadingBar}
|
||||
<NavigationLoadingBar />
|
||||
<NavigationLoadingBar />
|
||||
{/if}
|
||||
|
||||
<slot {albumId} />
|
||||
@@ -90,9 +90,9 @@
|
||||
<NotificationList />
|
||||
|
||||
{#if data.user?.isAdmin}
|
||||
<VersionAnnouncementBox serverVersion={data.serverVersion} />
|
||||
<VersionAnnouncementBox serverVersion={data.serverVersion} />
|
||||
{/if}
|
||||
|
||||
{#if $page.route.id?.includes('(user)')}
|
||||
<UploadCover {dropHandler} />
|
||||
<UploadCover {dropHandler} />
|
||||
{/if}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
export const prerender = false;
|
||||
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
|
||||
export const load = (async ({ parent, locals: { api } }) => {
|
||||
const { user } = await parent();
|
||||
if (user) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
const { user } = await parent();
|
||||
if (user) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
|
||||
const { data } = await api.userApi.getUserCount({ admin: true });
|
||||
const { data } = await api.userApi.getUserCount({ admin: true });
|
||||
|
||||
if (data.userCount > 0) {
|
||||
// Redirect to login page if an admin is already registered.
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
if (data.userCount > 0) {
|
||||
// Redirect to login page if an admin is already registered.
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
return {
|
||||
meta: {
|
||||
title: 'Welcome 🎉',
|
||||
description: 'Immich Web Interface'
|
||||
}
|
||||
};
|
||||
return {
|
||||
meta: {
|
||||
title: 'Welcome 🎉',
|
||||
description: 'Immich Web Interface',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
|
||||
</script>
|
||||
|
||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||
<div class="flex flex-col place-items-center gap-8 text-center max-w-[350px]">
|
||||
<div class="flex place-items-center place-content-center">
|
||||
<ImmichLogo class="text-center" height="200" width="200" />
|
||||
</div>
|
||||
<h1
|
||||
class="text-4xl text-immich-primary dark:text-immich-dark-primary font-bold font-immich-title"
|
||||
>
|
||||
Welcome to IMMICH Web
|
||||
</h1>
|
||||
<Button size="lg" rounded="lg" on:click={() => goto('/auth/register')}>
|
||||
<span class="font-bold px-2">Getting Started</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col place-items-center gap-8 text-center max-w-[350px]">
|
||||
<div class="flex place-items-center place-content-center">
|
||||
<ImmichLogo class="text-center" height="200" width="200" />
|
||||
</div>
|
||||
<h1 class="text-4xl text-immich-primary dark:text-immich-dark-primary font-bold font-immich-title">
|
||||
Welcome to IMMICH Web
|
||||
</h1>
|
||||
<Button size="lg" rounded="lg" on:click={() => goto('/auth/register')}>
|
||||
<span class="font-bold px-2">Getting Started</span>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -3,9 +3,9 @@ import { json } from '@sveltejs/kit';
|
||||
const endpoint = process.env.IMMICH_API_URL_EXTERNAL || '/api';
|
||||
|
||||
export const GET = async () => {
|
||||
return json({
|
||||
api: {
|
||||
endpoint
|
||||
}
|
||||
});
|
||||
return json({
|
||||
api: {
|
||||
endpoint,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
<script lang="ts">
|
||||
// DO NOT include `import { page } from '$app/stores'` here, because this can
|
||||
// lead to pages not being unmounted, which then causes weird page nesting
|
||||
// and routing issues.
|
||||
//
|
||||
// This is an issue in SvelteKit caused by using the page store in layouts and
|
||||
// using transitions on pages: https://github.com/sveltejs/kit/issues/7405
|
||||
// DO NOT include `import { page } from '$app/stores'` here, because this can
|
||||
// lead to pages not being unmounted, which then causes weird page nesting
|
||||
// and routing issues.
|
||||
//
|
||||
// This is an issue in SvelteKit caused by using the page store in layouts and
|
||||
// using transitions on pages: https://github.com/sveltejs/kit/issues/7405
|
||||
|
||||
import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
|
||||
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
|
||||
import Sync from 'svelte-material-icons/Sync.svelte';
|
||||
import Cog from 'svelte-material-icons/Cog.svelte';
|
||||
import Server from 'svelte-material-icons/Server.svelte';
|
||||
import StatusBox from '$lib/components/shared-components/status-box.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '../../lib/constants';
|
||||
import type { LayoutData } from './$types';
|
||||
import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
|
||||
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
|
||||
import Sync from 'svelte-material-icons/Sync.svelte';
|
||||
import Cog from 'svelte-material-icons/Cog.svelte';
|
||||
import Server from 'svelte-material-icons/Server.svelte';
|
||||
import StatusBox from '$lib/components/shared-components/status-box.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '../../lib/constants';
|
||||
import type { LayoutData } from './$types';
|
||||
import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
|
||||
export let data: LayoutData;
|
||||
export let data: LayoutData;
|
||||
|
||||
// Circumvents the need to import the page store. Should be replaced by
|
||||
// `$page.data.meta.title` once issue #7405 of SvelteKit is resolved.
|
||||
const getPageTitle = (routeId: string | null) => {
|
||||
switch (routeId) {
|
||||
case AppRoute.ADMIN_USER_MANAGEMENT:
|
||||
return 'User Management';
|
||||
case AppRoute.ADMIN_SETTINGS:
|
||||
return 'System Settings';
|
||||
case AppRoute.ADMIN_JOBS:
|
||||
return 'Jobs';
|
||||
case AppRoute.ADMIN_STATS:
|
||||
return 'Server Stats';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
// Circumvents the need to import the page store. Should be replaced by
|
||||
// `$page.data.meta.title` once issue #7405 of SvelteKit is resolved.
|
||||
const getPageTitle = (routeId: string | null) => {
|
||||
switch (routeId) {
|
||||
case AppRoute.ADMIN_USER_MANAGEMENT:
|
||||
return 'User Management';
|
||||
case AppRoute.ADMIN_SETTINGS:
|
||||
return 'System Settings';
|
||||
case AppRoute.ADMIN_JOBS:
|
||||
return 'Jobs';
|
||||
case AppRoute.ADMIN_STATS:
|
||||
return 'Server Stats';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<UserPageLayout user={data.user} showUploadButton={false} title={getPageTitle(data.routeId)}>
|
||||
<SideBarSection slot="sidebar">
|
||||
<SideBarButton
|
||||
title="Users"
|
||||
logo={AccountMultipleOutline}
|
||||
isSelected={data.routeId === AppRoute.ADMIN_USER_MANAGEMENT}
|
||||
on:selected={() => goto(AppRoute.ADMIN_USER_MANAGEMENT)}
|
||||
/>
|
||||
<SideBarButton
|
||||
title="Jobs"
|
||||
logo={Sync}
|
||||
isSelected={data.routeId === AppRoute.ADMIN_JOBS}
|
||||
on:selected={() => goto(AppRoute.ADMIN_JOBS)}
|
||||
/>
|
||||
<SideBarButton
|
||||
title="Settings"
|
||||
logo={Cog}
|
||||
isSelected={data.routeId === AppRoute.ADMIN_SETTINGS}
|
||||
on:selected={() => goto(AppRoute.ADMIN_SETTINGS)}
|
||||
/>
|
||||
<SideBarButton
|
||||
title="Server Stats"
|
||||
logo={Server}
|
||||
isSelected={data.routeId === AppRoute.ADMIN_STATS}
|
||||
on:selected={() => goto(AppRoute.ADMIN_STATS)}
|
||||
/>
|
||||
<div class="mb-6 mt-auto">
|
||||
<StatusBox />
|
||||
</div>
|
||||
</SideBarSection>
|
||||
<SideBarSection slot="sidebar">
|
||||
<SideBarButton
|
||||
title="Users"
|
||||
logo={AccountMultipleOutline}
|
||||
isSelected={data.routeId === AppRoute.ADMIN_USER_MANAGEMENT}
|
||||
on:selected={() => goto(AppRoute.ADMIN_USER_MANAGEMENT)}
|
||||
/>
|
||||
<SideBarButton
|
||||
title="Jobs"
|
||||
logo={Sync}
|
||||
isSelected={data.routeId === AppRoute.ADMIN_JOBS}
|
||||
on:selected={() => goto(AppRoute.ADMIN_JOBS)}
|
||||
/>
|
||||
<SideBarButton
|
||||
title="Settings"
|
||||
logo={Cog}
|
||||
isSelected={data.routeId === AppRoute.ADMIN_SETTINGS}
|
||||
on:selected={() => goto(AppRoute.ADMIN_SETTINGS)}
|
||||
/>
|
||||
<SideBarButton
|
||||
title="Server Stats"
|
||||
logo={Server}
|
||||
isSelected={data.routeId === AppRoute.ADMIN_STATS}
|
||||
on:selected={() => goto(AppRoute.ADMIN_STATS)}
|
||||
/>
|
||||
<div class="mb-6 mt-auto">
|
||||
<StatusBox />
|
||||
</div>
|
||||
</SideBarSection>
|
||||
|
||||
<section id="setting-content" class="flex place-content-center mx-4">
|
||||
<section class="w-full sm:w-5/6 md:w-[800px] pt-5 pb-28">
|
||||
<slot />
|
||||
</section>
|
||||
</section>
|
||||
<section id="setting-content" class="flex place-content-center mx-4">
|
||||
<section class="w-full sm:w-5/6 md:w-[800px] pt-5 pb-28">
|
||||
<slot />
|
||||
</section>
|
||||
</section>
|
||||
</UserPageLayout>
|
||||
|
||||
@@ -3,12 +3,12 @@ import { redirect } from '@sveltejs/kit';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load = (async ({ parent, route }) => {
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else if (!user.isAdmin) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else if (!user.isAdmin) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
|
||||
return { routeId: route.id, user };
|
||||
return { routeId: route.id, user };
|
||||
}) satisfies LayoutLoad;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
const { user } = await parent();
|
||||
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else if (!user.isAdmin) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else if (!user.isAdmin) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
|
||||
throw redirect(302, AppRoute.ADMIN_USER_MANAGEMENT);
|
||||
throw redirect(302, AppRoute.ADMIN_USER_MANAGEMENT);
|
||||
};
|
||||
|
||||
@@ -3,23 +3,23 @@ import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { user, api } }) => {
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else if (!user.isAdmin) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else if (!user.isAdmin) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: jobs } = await api.jobApi.getAllJobsStatus();
|
||||
try {
|
||||
const { data: jobs } = await api.jobApi.getAllJobsStatus();
|
||||
|
||||
return {
|
||||
jobs,
|
||||
meta: {
|
||||
title: 'Job Status'
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('[jobs] > getAllJobsStatus', err);
|
||||
throw err;
|
||||
}
|
||||
return {
|
||||
jobs,
|
||||
meta: {
|
||||
title: 'Job Status',
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('[jobs] > getAllJobsStatus', err);
|
||||
throw err;
|
||||
}
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<script lang="ts">
|
||||
import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte';
|
||||
import { api } from '@api';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte';
|
||||
import { api } from '@api';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
let timer: NodeJS.Timer;
|
||||
export let data: PageData;
|
||||
let timer: NodeJS.Timer;
|
||||
|
||||
$: jobs = data.jobs;
|
||||
$: jobs = data.jobs;
|
||||
|
||||
const load = async () => {
|
||||
const { data } = await api.jobApi.getAllJobsStatus();
|
||||
jobs = data;
|
||||
};
|
||||
const load = async () => {
|
||||
const { data } = await api.jobApi.getAllJobsStatus();
|
||||
jobs = data;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
await load();
|
||||
timer = setInterval(load, 5_000);
|
||||
});
|
||||
onMount(async () => {
|
||||
await load();
|
||||
timer = setInterval(load, 5_000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(timer);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(timer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<JobsPanel {jobs} />
|
||||
|
||||
@@ -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 ({ parent, locals: { api } }) => {
|
||||
const { user } = await parent();
|
||||
const { user } = await parent();
|
||||
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else if (!user.isAdmin) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else if (!user.isAdmin) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
|
||||
const { data: stats } = await api.serverInfoApi.getStats();
|
||||
const { data: stats } = await api.serverInfoApi.getStats();
|
||||
|
||||
return {
|
||||
stats,
|
||||
meta: {
|
||||
title: 'Server Stats'
|
||||
}
|
||||
};
|
||||
return {
|
||||
stats,
|
||||
meta: {
|
||||
title: 'Server Stats',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { api } from '@api';
|
||||
import ServerStatsPanel from '$lib/components/admin-page/server-stats/server-stats-panel.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { api } from '@api';
|
||||
import ServerStatsPanel from '$lib/components/admin-page/server-stats/server-stats-panel.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
let setIntervalHandler: NodeJS.Timer;
|
||||
export let data: PageData;
|
||||
let setIntervalHandler: NodeJS.Timer;
|
||||
|
||||
onMount(async () => {
|
||||
setIntervalHandler = setInterval(async () => {
|
||||
const { data: stats } = await api.serverInfoApi.getStats();
|
||||
data.stats = stats;
|
||||
}, 5000);
|
||||
});
|
||||
onMount(async () => {
|
||||
setIntervalHandler = setInterval(async () => {
|
||||
const { data: stats } = await api.serverInfoApi.getStats();
|
||||
data.stats = stats;
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(setIntervalHandler);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(setIntervalHandler);
|
||||
});
|
||||
</script>
|
||||
|
||||
<ServerStatsPanel stats={data.stats} />
|
||||
|
||||
@@ -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: PageServerLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
const { user } = await parent();
|
||||
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else if (!user.isAdmin) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else if (!user.isAdmin) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'System Settings'
|
||||
}
|
||||
};
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'System Settings',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,59 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte';
|
||||
import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte';
|
||||
import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte';
|
||||
import PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte';
|
||||
import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
|
||||
import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import { api } from '@api';
|
||||
import type { PageData } from './$types';
|
||||
import { page } from '$app/stores';
|
||||
import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte';
|
||||
import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte';
|
||||
import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte';
|
||||
import PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte';
|
||||
import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
|
||||
import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import { api } from '@api';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
|
||||
const getConfig = async () => {
|
||||
const { data } = await api.systemConfigApi.getConfig();
|
||||
return data;
|
||||
};
|
||||
const getConfig = async () => {
|
||||
const { data } = await api.systemConfigApi.getConfig();
|
||||
return data;
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="">
|
||||
{#await getConfig()}
|
||||
<LoadingSpinner />
|
||||
{:then configs}
|
||||
<SettingAccordion
|
||||
title="FFmpeg Settings"
|
||||
subtitle="Manage the resolution and encoding information of the video files"
|
||||
>
|
||||
<FFmpegSettings ffmpegConfig={configs.ffmpeg} />
|
||||
</SettingAccordion>
|
||||
{#await getConfig()}
|
||||
<LoadingSpinner />
|
||||
{:then configs}
|
||||
<SettingAccordion
|
||||
title="FFmpeg Settings"
|
||||
subtitle="Manage the resolution and encoding information of the video files"
|
||||
>
|
||||
<FFmpegSettings ffmpegConfig={configs.ffmpeg} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
title="Job Settings"
|
||||
subtitle="Manage job concurrency"
|
||||
isOpen={$page.url.searchParams.get('open') === 'job-settings'}
|
||||
>
|
||||
<JobSettings jobConfig={configs.job} />
|
||||
</SettingAccordion>
|
||||
<SettingAccordion
|
||||
title="Job Settings"
|
||||
subtitle="Manage job concurrency"
|
||||
isOpen={$page.url.searchParams.get('open') === 'job-settings'}
|
||||
>
|
||||
<JobSettings jobConfig={configs.job} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
title="Password Authentication"
|
||||
subtitle="Manage login with password settings"
|
||||
>
|
||||
<PasswordLoginSettings passwordLoginConfig={configs.passwordLogin} />
|
||||
</SettingAccordion>
|
||||
<SettingAccordion title="Password Authentication" subtitle="Manage login with password settings">
|
||||
<PasswordLoginSettings passwordLoginConfig={configs.passwordLogin} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="OAuth Authentication" subtitle="Manage the login with OAuth settings">
|
||||
<OAuthSettings oauthConfig={configs.oauth} />
|
||||
</SettingAccordion>
|
||||
<SettingAccordion title="OAuth Authentication" subtitle="Manage the login with OAuth settings">
|
||||
<OAuthSettings oauthConfig={configs.oauth} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
title="Storage Template"
|
||||
subtitle="Manage the folder structure and file name of the upload asset"
|
||||
isOpen={$page.url.searchParams.get('open') === 'storage-template'}
|
||||
>
|
||||
<StorageTemplateSettings storageConfig={configs.storageTemplate} user={data.user} />
|
||||
</SettingAccordion>
|
||||
{/await}
|
||||
<SettingAccordion
|
||||
title="Storage Template"
|
||||
subtitle="Manage the folder structure and file name of the upload asset"
|
||||
isOpen={$page.url.searchParams.get('open') === 'storage-template'}
|
||||
>
|
||||
<StorageTemplateSettings storageConfig={configs.storageTemplate} user={data.user} />
|
||||
</SettingAccordion>
|
||||
{/await}
|
||||
</section>
|
||||
|
||||
@@ -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 ({ parent, locals: { api } }) => {
|
||||
const { user } = await parent();
|
||||
const { user } = await parent();
|
||||
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else if (!user.isAdmin) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else if (!user.isAdmin) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
|
||||
const { data: allUsers } = await api.userApi.getAllUsers({ isAll: false });
|
||||
const { data: allUsers } = await api.userApi.getAllUsers({ isAll: false });
|
||||
|
||||
return {
|
||||
user,
|
||||
allUsers,
|
||||
meta: {
|
||||
title: 'User Management'
|
||||
}
|
||||
};
|
||||
return {
|
||||
user,
|
||||
allUsers,
|
||||
meta: {
|
||||
title: 'User Management',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,299 +1,295 @@
|
||||
<script lang="ts">
|
||||
import { api, UserResponseDto } from '@api';
|
||||
import { api, UserResponseDto } from '@api';
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
|
||||
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||
import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte';
|
||||
import Check from 'svelte-material-icons/Check.svelte';
|
||||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
|
||||
import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte';
|
||||
import RestoreDialogue from '$lib/components/admin-page/restore-dialoge.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { onMount } from 'svelte';
|
||||
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
|
||||
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||
import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte';
|
||||
import Check from 'svelte-material-icons/Check.svelte';
|
||||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
|
||||
import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte';
|
||||
import RestoreDialogue from '$lib/components/admin-page/restore-dialoge.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
|
||||
let allUsers: UserResponseDto[] = [];
|
||||
let shouldShowEditUserForm = false;
|
||||
let shouldShowCreateUserForm = false;
|
||||
let shouldShowInfoPanel = false;
|
||||
let shouldShowDeleteConfirmDialog = false;
|
||||
let shouldShowRestoreDialog = false;
|
||||
let selectedUser: UserResponseDto;
|
||||
let allUsers: UserResponseDto[] = [];
|
||||
let shouldShowEditUserForm = false;
|
||||
let shouldShowCreateUserForm = false;
|
||||
let shouldShowInfoPanel = false;
|
||||
let shouldShowDeleteConfirmDialog = false;
|
||||
let shouldShowRestoreDialog = false;
|
||||
let selectedUser: UserResponseDto;
|
||||
|
||||
onMount(() => {
|
||||
allUsers = $page.data.allUsers;
|
||||
});
|
||||
onMount(() => {
|
||||
allUsers = $page.data.allUsers;
|
||||
});
|
||||
|
||||
const isDeleted = (user: UserResponseDto): boolean => {
|
||||
return user.deletedAt != null;
|
||||
};
|
||||
const isDeleted = (user: UserResponseDto): boolean => {
|
||||
return user.deletedAt != null;
|
||||
};
|
||||
|
||||
const deleteDateFormat: Intl.DateTimeFormatOptions = {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
};
|
||||
const deleteDateFormat: Intl.DateTimeFormatOptions = {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
};
|
||||
|
||||
const getDeleteDate = (user: UserResponseDto): string => {
|
||||
let deletedAt = new Date(user.deletedAt ? user.deletedAt : Date.now());
|
||||
deletedAt.setDate(deletedAt.getDate() + 7);
|
||||
return deletedAt.toLocaleString($locale, deleteDateFormat);
|
||||
};
|
||||
const getDeleteDate = (user: UserResponseDto): string => {
|
||||
let deletedAt = new Date(user.deletedAt ? user.deletedAt : Date.now());
|
||||
deletedAt.setDate(deletedAt.getDate() + 7);
|
||||
return deletedAt.toLocaleString($locale, deleteDateFormat);
|
||||
};
|
||||
|
||||
const onUserCreated = async () => {
|
||||
const getAllUsersRes = await api.userApi.getAllUsers({ isAll: false });
|
||||
allUsers = getAllUsersRes.data;
|
||||
shouldShowCreateUserForm = false;
|
||||
};
|
||||
const onUserCreated = async () => {
|
||||
const getAllUsersRes = await api.userApi.getAllUsers({ isAll: false });
|
||||
allUsers = getAllUsersRes.data;
|
||||
shouldShowCreateUserForm = false;
|
||||
};
|
||||
|
||||
const editUserHandler = async (user: UserResponseDto) => {
|
||||
selectedUser = user;
|
||||
shouldShowEditUserForm = true;
|
||||
};
|
||||
const editUserHandler = async (user: UserResponseDto) => {
|
||||
selectedUser = user;
|
||||
shouldShowEditUserForm = true;
|
||||
};
|
||||
|
||||
const onEditUserSuccess = async () => {
|
||||
const getAllUsersRes = await api.userApi.getAllUsers({ isAll: false });
|
||||
allUsers = getAllUsersRes.data;
|
||||
shouldShowEditUserForm = false;
|
||||
};
|
||||
const onEditUserSuccess = async () => {
|
||||
const getAllUsersRes = await api.userApi.getAllUsers({ isAll: false });
|
||||
allUsers = getAllUsersRes.data;
|
||||
shouldShowEditUserForm = false;
|
||||
};
|
||||
|
||||
const onEditPasswordSuccess = async () => {
|
||||
const getAllUsersRes = await api.userApi.getAllUsers({ isAll: false });
|
||||
allUsers = getAllUsersRes.data;
|
||||
shouldShowEditUserForm = false;
|
||||
shouldShowInfoPanel = true;
|
||||
};
|
||||
const onEditPasswordSuccess = async () => {
|
||||
const getAllUsersRes = await api.userApi.getAllUsers({ isAll: false });
|
||||
allUsers = getAllUsersRes.data;
|
||||
shouldShowEditUserForm = false;
|
||||
shouldShowInfoPanel = true;
|
||||
};
|
||||
|
||||
const deleteUserHandler = async (user: UserResponseDto) => {
|
||||
selectedUser = user;
|
||||
shouldShowDeleteConfirmDialog = true;
|
||||
};
|
||||
const deleteUserHandler = async (user: UserResponseDto) => {
|
||||
selectedUser = user;
|
||||
shouldShowDeleteConfirmDialog = true;
|
||||
};
|
||||
|
||||
const onUserDeleteSuccess = async () => {
|
||||
const getAllUsersRes = await api.userApi.getAllUsers({ isAll: false });
|
||||
allUsers = getAllUsersRes.data;
|
||||
shouldShowDeleteConfirmDialog = false;
|
||||
};
|
||||
const onUserDeleteSuccess = async () => {
|
||||
const getAllUsersRes = await api.userApi.getAllUsers({ isAll: false });
|
||||
allUsers = getAllUsersRes.data;
|
||||
shouldShowDeleteConfirmDialog = false;
|
||||
};
|
||||
|
||||
const onUserDeleteFail = async () => {
|
||||
const getAllUsersRes = await api.userApi.getAllUsers({ isAll: false });
|
||||
allUsers = getAllUsersRes.data;
|
||||
shouldShowDeleteConfirmDialog = false;
|
||||
};
|
||||
const onUserDeleteFail = async () => {
|
||||
const getAllUsersRes = await api.userApi.getAllUsers({ isAll: false });
|
||||
allUsers = getAllUsersRes.data;
|
||||
shouldShowDeleteConfirmDialog = false;
|
||||
};
|
||||
|
||||
const restoreUserHandler = async (user: UserResponseDto) => {
|
||||
selectedUser = user;
|
||||
shouldShowRestoreDialog = true;
|
||||
};
|
||||
const restoreUserHandler = async (user: UserResponseDto) => {
|
||||
selectedUser = user;
|
||||
shouldShowRestoreDialog = true;
|
||||
};
|
||||
|
||||
const onUserRestoreSuccess = async () => {
|
||||
const getAllUsersRes = await api.userApi.getAllUsers({ isAll: false });
|
||||
allUsers = getAllUsersRes.data;
|
||||
shouldShowRestoreDialog = false;
|
||||
};
|
||||
const onUserRestoreSuccess = async () => {
|
||||
const getAllUsersRes = await api.userApi.getAllUsers({ isAll: false });
|
||||
allUsers = getAllUsersRes.data;
|
||||
shouldShowRestoreDialog = false;
|
||||
};
|
||||
|
||||
const onUserRestoreFail = async () => {
|
||||
// show fail dialog
|
||||
const getAllUsersRes = await api.userApi.getAllUsers({ isAll: false });
|
||||
allUsers = getAllUsersRes.data;
|
||||
shouldShowRestoreDialog = false;
|
||||
};
|
||||
const onUserRestoreFail = async () => {
|
||||
// show fail dialog
|
||||
const getAllUsersRes = await api.userApi.getAllUsers({ isAll: false });
|
||||
allUsers = getAllUsersRes.data;
|
||||
shouldShowRestoreDialog = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<section>
|
||||
{#if shouldShowCreateUserForm}
|
||||
<FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}>
|
||||
<CreateUserForm on:user-created={onUserCreated} />
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
{#if shouldShowCreateUserForm}
|
||||
<FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}>
|
||||
<CreateUserForm on:user-created={onUserCreated} />
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
|
||||
{#if shouldShowEditUserForm}
|
||||
<FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
|
||||
<EditUserForm
|
||||
user={selectedUser}
|
||||
canResetPassword={selectedUser?.id !== data.user.id}
|
||||
on:edit-success={onEditUserSuccess}
|
||||
on:reset-password-success={onEditPasswordSuccess}
|
||||
/>
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
{#if shouldShowEditUserForm}
|
||||
<FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
|
||||
<EditUserForm
|
||||
user={selectedUser}
|
||||
canResetPassword={selectedUser?.id !== data.user.id}
|
||||
on:edit-success={onEditUserSuccess}
|
||||
on:reset-password-success={onEditPasswordSuccess}
|
||||
/>
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
|
||||
{#if shouldShowDeleteConfirmDialog}
|
||||
<DeleteConfirmDialog
|
||||
user={selectedUser}
|
||||
on:user-delete-success={onUserDeleteSuccess}
|
||||
on:user-delete-fail={onUserDeleteFail}
|
||||
on:cancel={() => (shouldShowDeleteConfirmDialog = false)}
|
||||
/>
|
||||
{/if}
|
||||
{#if shouldShowDeleteConfirmDialog}
|
||||
<DeleteConfirmDialog
|
||||
user={selectedUser}
|
||||
on:user-delete-success={onUserDeleteSuccess}
|
||||
on:user-delete-fail={onUserDeleteFail}
|
||||
on:cancel={() => (shouldShowDeleteConfirmDialog = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if shouldShowRestoreDialog}
|
||||
<RestoreDialogue
|
||||
user={selectedUser}
|
||||
on:user-restore-success={onUserRestoreSuccess}
|
||||
on:user-restore-fail={onUserRestoreFail}
|
||||
on:cancel={() => (shouldShowRestoreDialog = false)}
|
||||
/>
|
||||
{/if}
|
||||
{#if shouldShowRestoreDialog}
|
||||
<RestoreDialogue
|
||||
user={selectedUser}
|
||||
on:user-restore-success={onUserRestoreSuccess}
|
||||
on:user-restore-fail={onUserRestoreFail}
|
||||
on:cancel={() => (shouldShowRestoreDialog = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if shouldShowInfoPanel}
|
||||
<FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
|
||||
<div class="border bg-white shadow-sm w-[500px] max-w-[95vw] rounded-3xl p-8 text-sm">
|
||||
<h1 class="font-medium text-immich-primary text-lg mb-4">Password reset success</h1>
|
||||
{#if shouldShowInfoPanel}
|
||||
<FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
|
||||
<div class="border bg-white shadow-sm w-[500px] max-w-[95vw] rounded-3xl p-8 text-sm">
|
||||
<h1 class="font-medium text-immich-primary text-lg mb-4">Password reset success</h1>
|
||||
|
||||
<p>
|
||||
The user's password has been reset to the default <code
|
||||
class="font-bold bg-gray-200 px-2 py-1 rounded-md text-immich-primary">password</code
|
||||
>
|
||||
<br />
|
||||
Please inform the user, and they will need to change the password at the next log-on.
|
||||
</p>
|
||||
<p>
|
||||
The user's password has been reset to the default <code
|
||||
class="font-bold bg-gray-200 px-2 py-1 rounded-md text-immich-primary">password</code
|
||||
>
|
||||
<br />
|
||||
Please inform the user, and they will need to change the password at the next log-on.
|
||||
</p>
|
||||
|
||||
<div class="flex w-full mt-6">
|
||||
<Button fullwidth on:click={() => (shouldShowInfoPanel = false)}>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
<div class="flex w-full mt-6">
|
||||
<Button fullwidth on:click={() => (shouldShowInfoPanel = false)}>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
|
||||
<table class="text-left w-full my-5 sm:block hidden">
|
||||
<thead
|
||||
class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary w-full h-12 dark:bg-immich-dark-gray dark:text-immich-dark-primary dark:border-immich-dark-gray"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="text-center w-1/4 font-medium text-sm">Email</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">First name</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">Last name</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">Can import</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="overflow-y-auto rounded-md w-full max-h-[320px] block border dark:border-immich-dark-gray"
|
||||
>
|
||||
{#if allUsers}
|
||||
{#each allUsers as user, i}
|
||||
<tr
|
||||
class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-fg ${
|
||||
isDeleted(user)
|
||||
? 'bg-red-300 dark:bg-red-900'
|
||||
: i % 2 == 0
|
||||
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
|
||||
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.email}</td>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.firstName}</td>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">
|
||||
<div class="container flex flex-wrap mx-auto justify-center">
|
||||
{#if user.externalPath}
|
||||
<Check size="16" />
|
||||
{:else}
|
||||
<Close size="16" />
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">
|
||||
{#if !isDeleted(user)}
|
||||
<button
|
||||
on:click={() => editUserHandler(user)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
>
|
||||
<PencilOutline size="16" />
|
||||
</button>
|
||||
{#if user.id !== data.user.id}
|
||||
<button
|
||||
on:click={() => deleteUserHandler(user)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
>
|
||||
<TrashCanOutline size="16" />
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if isDeleted(user)}
|
||||
<button
|
||||
on:click={() => restoreUserHandler(user)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
title={`scheduled removal on ${getDeleteDate(user)}`}
|
||||
>
|
||||
<DeleteRestore size="16" />
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="text-left w-full my-5 sm:block hidden">
|
||||
<thead
|
||||
class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary w-full h-12 dark:bg-immich-dark-gray dark:text-immich-dark-primary dark:border-immich-dark-gray"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="text-center w-1/4 font-medium text-sm">Email</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">First name</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">Last name</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">Can import</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="overflow-y-auto rounded-md w-full max-h-[320px] block border dark:border-immich-dark-gray">
|
||||
{#if allUsers}
|
||||
{#each allUsers as user, i}
|
||||
<tr
|
||||
class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-fg ${
|
||||
isDeleted(user)
|
||||
? 'bg-red-300 dark:bg-red-900'
|
||||
: i % 2 == 0
|
||||
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
|
||||
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.email}</td>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.firstName}</td>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">
|
||||
<div class="container flex flex-wrap mx-auto justify-center">
|
||||
{#if user.externalPath}
|
||||
<Check size="16" />
|
||||
{:else}
|
||||
<Close size="16" />
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">
|
||||
{#if !isDeleted(user)}
|
||||
<button
|
||||
on:click={() => editUserHandler(user)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
>
|
||||
<PencilOutline size="16" />
|
||||
</button>
|
||||
{#if user.id !== data.user.id}
|
||||
<button
|
||||
on:click={() => deleteUserHandler(user)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
>
|
||||
<TrashCanOutline size="16" />
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if isDeleted(user)}
|
||||
<button
|
||||
on:click={() => restoreUserHandler(user)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
title={`scheduled removal on ${getDeleteDate(user)}`}
|
||||
>
|
||||
<DeleteRestore size="16" />
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="text-left w-full my-5 block sm:hidden">
|
||||
<thead
|
||||
class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary w-full h-12 dark:bg-immich-dark-gray dark:text-immich-dark-primary dark:border-immich-dark-gray"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="text-center w-1/2 font-medium text-sm flex justify-around">
|
||||
<span>Name</span>
|
||||
<span>Email</span>
|
||||
</th>
|
||||
<th class="text-center w-1/2 font-medium text-sm">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="overflow-y-auto rounded-md w-full max-h-[320px] block border dark:border-immich-dark-gray"
|
||||
>
|
||||
{#if allUsers}
|
||||
{#each allUsers as user, i}
|
||||
<tr
|
||||
class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-fg ${
|
||||
isDeleted(user)
|
||||
? 'bg-red-300 dark:bg-red-900'
|
||||
: i % 2 == 0
|
||||
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
|
||||
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
>
|
||||
<td class="text-sm px-4 w-2/3 text-ellipsis">
|
||||
<span>{user.firstName} {user.lastName}</span>
|
||||
<span>{user.email}</span>
|
||||
</td>
|
||||
<td class="text-sm px-4 w-1/3 text-ellipsis">
|
||||
{#if !isDeleted(user)}
|
||||
<button
|
||||
on:click={() => editUserHandler(user)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full sm:p-3 p-2 max-sm:mb-1 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
>
|
||||
<PencilOutline size="16" />
|
||||
</button>
|
||||
<button
|
||||
on:click={() => deleteUserHandler(user)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full sm:p-3 p-2 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
>
|
||||
<TrashCanOutline size="16" />
|
||||
</button>
|
||||
{/if}
|
||||
{#if isDeleted(user)}
|
||||
<button
|
||||
on:click={() => restoreUserHandler(user)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full sm:p-3 p-2 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
title={`scheduled removal on ${getDeleteDate(user)}`}
|
||||
>
|
||||
<DeleteRestore size="16" />
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="text-left w-full my-5 block sm:hidden">
|
||||
<thead
|
||||
class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary w-full h-12 dark:bg-immich-dark-gray dark:text-immich-dark-primary dark:border-immich-dark-gray"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="text-center w-1/2 font-medium text-sm flex justify-around">
|
||||
<span>Name</span>
|
||||
<span>Email</span>
|
||||
</th>
|
||||
<th class="text-center w-1/2 font-medium text-sm">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="overflow-y-auto rounded-md w-full max-h-[320px] block border dark:border-immich-dark-gray">
|
||||
{#if allUsers}
|
||||
{#each allUsers as user, i}
|
||||
<tr
|
||||
class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-fg ${
|
||||
isDeleted(user)
|
||||
? 'bg-red-300 dark:bg-red-900'
|
||||
: i % 2 == 0
|
||||
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
|
||||
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
>
|
||||
<td class="text-sm px-4 w-2/3 text-ellipsis">
|
||||
<span>{user.firstName} {user.lastName}</span>
|
||||
<span>{user.email}</span>
|
||||
</td>
|
||||
<td class="text-sm px-4 w-1/3 text-ellipsis">
|
||||
{#if !isDeleted(user)}
|
||||
<button
|
||||
on:click={() => editUserHandler(user)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full sm:p-3 p-2 max-sm:mb-1 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
>
|
||||
<PencilOutline size="16" />
|
||||
</button>
|
||||
<button
|
||||
on:click={() => deleteUserHandler(user)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full sm:p-3 p-2 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
>
|
||||
<TrashCanOutline size="16" />
|
||||
</button>
|
||||
{/if}
|
||||
{#if isDeleted(user)}
|
||||
<button
|
||||
on:click={() => restoreUserHandler(user)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full sm:p-3 p-2 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
title={`scheduled removal on ${getDeleteDate(user)}`}
|
||||
>
|
||||
<DeleteRestore size="16" />
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Button size="sm" on:click={() => (shouldShowCreateUserForm = true)}>Create user</Button>
|
||||
<Button size="sm" on:click={() => (shouldShowCreateUserForm = true)}>Create user</Button>
|
||||
</section>
|
||||
|
||||
@@ -3,16 +3,16 @@ import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { user } }) => {
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else if (!user.shouldChangePassword) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
if (!user) {
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
} else if (!user.shouldChangePassword) {
|
||||
throw redirect(302, AppRoute.PHOTOS);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Change Password'
|
||||
}
|
||||
};
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: 'Change Password',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import ChangePasswordForm from '$lib/components/forms/change-password-form.svelte';
|
||||
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import type { PageData } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
import ChangePasswordForm from '$lib/components/forms/change-password-form.svelte';
|
||||
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
|
||||
const onSuccessHandler = async () => {
|
||||
await fetch(AppRoute.AUTH_LOGOUT, { method: 'POST' });
|
||||
const onSuccessHandler = async () => {
|
||||
await fetch(AppRoute.AUTH_LOGOUT, { method: 'POST' });
|
||||
|
||||
goto(AppRoute.AUTH_LOGIN);
|
||||
};
|
||||
goto(AppRoute.AUTH_LOGIN);
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullscreenContainer title={data.meta.title}>
|
||||
<p slot="message">
|
||||
Hi {data.user.firstName}
|
||||
{data.user.lastName} ({data.user.email}),
|
||||
<br />
|
||||
<br />
|
||||
This is either the first time you are signing into the system or a request has been made to change
|
||||
your password. Please enter the new password below.
|
||||
</p>
|
||||
<p slot="message">
|
||||
Hi {data.user.firstName}
|
||||
{data.user.lastName} ({data.user.email}),
|
||||
<br />
|
||||
<br />
|
||||
This is either the first time you are signing into the system or a request has been made to change your password. Please
|
||||
enter the new password below.
|
||||
</p>
|
||||
|
||||
<ChangePasswordForm user={data.user} on:success={onSuccessHandler} />
|
||||
<ChangePasswordForm user={data.user} on:success={onSuccessHandler} />
|
||||
</FullscreenContainer>
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { OAuthConfigResponseDto } from '@api';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { api } }) => {
|
||||
const { data } = await api.userApi.getUserCount({ admin: true });
|
||||
if (data.userCount === 0) {
|
||||
// Admin not registered
|
||||
throw redirect(302, AppRoute.AUTH_REGISTER);
|
||||
}
|
||||
const { data } = await api.userApi.getUserCount({ admin: true });
|
||||
if (data.userCount === 0) {
|
||||
// Admin not registered
|
||||
throw redirect(302, AppRoute.AUTH_REGISTER);
|
||||
}
|
||||
|
||||
let authConfig: OAuthConfigResponseDto = {
|
||||
passwordLoginEnabled: true,
|
||||
enabled: false
|
||||
};
|
||||
let authConfig: OAuthConfigResponseDto = {
|
||||
passwordLoginEnabled: true,
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
try {
|
||||
// TODO: Figure out how to get correct redirect URI server-side.
|
||||
const { data } = await api.oauthApi.generateConfig({ oAuthConfigDto: { redirectUri: '/' } });
|
||||
data.url = undefined;
|
||||
try {
|
||||
// TODO: Figure out how to get correct redirect URI server-side.
|
||||
const { data } = await api.oauthApi.generateConfig({ oAuthConfigDto: { redirectUri: '/' } });
|
||||
data.url = undefined;
|
||||
|
||||
authConfig = data;
|
||||
} catch (err) {
|
||||
console.error('[ERROR] login/+page.server.ts:', err);
|
||||
}
|
||||
authConfig = data;
|
||||
} catch (err) {
|
||||
console.error('[ERROR] login/+page.server.ts:', err);
|
||||
}
|
||||
|
||||
return {
|
||||
authConfig,
|
||||
meta: {
|
||||
title: 'Login'
|
||||
}
|
||||
};
|
||||
return {
|
||||
authConfig,
|
||||
meta: {
|
||||
title: 'Login',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import LoginForm from '$lib/components/forms/login-form.svelte';
|
||||
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { loginPageMessage } from '$lib/constants';
|
||||
import type { PageData } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
import LoginForm from '$lib/components/forms/login-form.svelte';
|
||||
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { loginPageMessage } from '$lib/constants';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<FullscreenContainer title={data.meta.title} showMessage={!!loginPageMessage}>
|
||||
<p slot="message">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html loginPageMessage}
|
||||
</p>
|
||||
<p slot="message">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html loginPageMessage}
|
||||
</p>
|
||||
|
||||
<LoginForm
|
||||
authConfig={data.authConfig}
|
||||
on:success={() => goto(AppRoute.PHOTOS, { invalidateAll: true })}
|
||||
on:first-login={() => goto(AppRoute.AUTH_CHANGE_PASSWORD)}
|
||||
/>
|
||||
<LoginForm
|
||||
authConfig={data.authConfig}
|
||||
on:success={() => goto(AppRoute.PHOTOS, { invalidateAll: true })}
|
||||
on:first-login={() => goto(AppRoute.AUTH_CHANGE_PASSWORD)}
|
||||
/>
|
||||
</FullscreenContainer>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { api } from '@api';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export const POST = (async ({ cookies }) => {
|
||||
api.removeAccessToken();
|
||||
api.removeAccessToken();
|
||||
|
||||
cookies.delete('immich_auth_type', { path: '/' });
|
||||
cookies.delete('immich_access_token', { path: '/' });
|
||||
cookies.delete('immich_auth_type', { path: '/' });
|
||||
cookies.delete('immich_access_token', { path: '/' });
|
||||
|
||||
return json({ ok: true });
|
||||
return json({ ok: true });
|
||||
}) satisfies RequestHandler;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
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: { api } }) => {
|
||||
const { data } = await api.userApi.getUserCount({ admin: true });
|
||||
if (data.userCount != 0) {
|
||||
// Admin has been registered, redirect to login
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
const { data } = await api.userApi.getUserCount({ admin: true });
|
||||
if (data.userCount != 0) {
|
||||
// Admin has been registered, redirect to login
|
||||
throw redirect(302, AppRoute.AUTH_LOGIN);
|
||||
}
|
||||
|
||||
return {
|
||||
meta: {
|
||||
title: 'Admin Registration'
|
||||
}
|
||||
};
|
||||
return {
|
||||
meta: {
|
||||
title: 'Admin Registration',
|
||||
},
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import AdminRegistrationForm from '$lib/components/forms/admin-registration-form.svelte';
|
||||
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import AdminRegistrationForm from '$lib/components/forms/admin-registration-form.svelte';
|
||||
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<FullscreenContainer title={data.meta.title}>
|
||||
<p slot="message">
|
||||
Since you are the first user on the system, you will be assigned as the Admin and are
|
||||
responsible for administrative tasks, and additional users will be created by you.
|
||||
</p>
|
||||
<p slot="message">
|
||||
Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative
|
||||
tasks, and additional users will be created by you.
|
||||
</p>
|
||||
|
||||
<AdminRegistrationForm />
|
||||
<AdminRegistrationForm />
|
||||
</FullscreenContainer>
|
||||
|
||||
Reference in New Issue
Block a user