chore(web): prettier (#2821)

Co-authored-by: Thomas Way <thomas@6f.io>
This commit is contained in:
Jason Rasmussen
2023-07-01 00:50:47 -04:00
committed by GitHub
parent 7c2f7d6c51
commit f55b3add80
242 changed files with 12794 additions and 13426 deletions

View File

@@ -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;

View File

@@ -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}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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);
}
};

View File

@@ -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);
});
});

View File

@@ -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,
};
};

View File

@@ -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;

View File

@@ -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>

View File

@@ -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);
};

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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);
}
};

View File

@@ -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;

View File

@@ -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:
'&copy; <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: '&copy; <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}

View File

@@ -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;

View File

@@ -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 />

View File

@@ -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);
}
};

View File

@@ -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);
}
};

View File

@@ -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',
},
};
};

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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}`);
};

View File

@@ -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;

View File

@@ -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>

View File

@@ -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);
}
};

View File

@@ -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;

View File

@@ -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>

View File

@@ -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);
};

View File

@@ -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>

View File

@@ -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;

View File

@@ -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}

View File

@@ -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;

View File

@@ -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}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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}

View File

@@ -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;

View File

@@ -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>