Merge remote-tracking branch 'origin/main' into feat/yucca-integration

This commit is contained in:
izzy
2026-04-16 12:04:38 +01:00
523 changed files with 2711 additions and 54886 deletions
-29
View File
@@ -1,29 +0,0 @@
import { decodeBase64 } from '$lib/utils';
import { thumbHashToRGBA } from 'thumbhash';
/**
* Renders a thumbnail onto a canvas from a base64 encoded hash.
*/
export function thumbhash(canvas: HTMLCanvasElement, options: { base64ThumbHash: string }) {
render(canvas, options);
return {
update(newOptions: { base64ThumbHash: string }) {
render(canvas, newOptions);
},
};
}
const render = (canvas: HTMLCanvasElement, options: { base64ThumbHash: string }) => {
const ctx = canvas.getContext('2d');
if (!ctx) {
return;
}
const { w, h, rgba } = thumbHashToRGBA(decodeBase64(options.base64ThumbHash));
const pixels = ctx.createImageData(w, h);
canvas.width = w;
canvas.height = h;
pixels.data.set(rgba);
ctx.putImageData(pixels, 0, 0);
};
-6
View File
@@ -124,9 +124,6 @@ export const zoomImageAction = (node: HTMLElement, options?: { zoomTarget?: HTML
{ capture: true, signal },
);
if (options?.zoomTarget) {
options.zoomTarget.style.willChange = 'transform';
}
node.style.overflow = 'visible';
node.style.touchAction = 'none';
return {
@@ -138,9 +135,6 @@ export const zoomImageAction = (node: HTMLElement, options?: { zoomTarget?: HTML
},
destroy() {
controller.abort();
if (options?.zoomTarget) {
options.zoomTarget.style.willChange = '';
}
for (const unsubscribe of unsubscribes) {
unsubscribe();
}
+3 -3
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import { thumbhash } from '$lib/actions/thumbhash';
import AlphaBackground from '$lib/components/AlphaBackground.svelte';
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
import DelayedLoadingSpinner from '$lib/components/DelayedLoadingSpinner.svelte';
import ImageLayer from '$lib/components/ImageLayer.svelte';
import Thumbhash from '$lib/components/Thumbhash.svelte';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { getAssetUrls } from '$lib/utils';
import { AdaptiveImageLoader, type QualityList } from '$lib/utils/adaptive-image-loader.svelte';
@@ -148,7 +148,7 @@
});
</script>
<div class="relative h-full w-full overflow-hidden will-change-transform" bind:this={ref}>
<div class="relative h-full w-full overflow-hidden" bind:this={ref}>
{@render backdrop?.()}
<div
@@ -165,7 +165,7 @@
{#if show.thumbhash}
{#if asset.thumbhash}
<!-- Thumbhash / spinner layer -->
<canvas use:thumbhash={{ base64ThumbHash: asset.thumbhash }} class="h-full w-full absolute"></canvas>
<Thumbhash base64ThumbHash={asset.thumbhash} class="h-full w-full absolute" />
{:else if show.spinner}
<DelayedLoadingSpinner />
{/if}
+40
View File
@@ -0,0 +1,40 @@
<script lang="ts">
import { decodeBase64 } from '$lib/utils';
import { TUNABLES } from '$lib/utils/tunables';
import type { HTMLCanvasAttributes } from 'svelte/elements';
import { fade } from 'svelte/transition';
import { thumbHashToRGBA } from 'thumbhash';
type Props = HTMLCanvasAttributes & {
base64ThumbHash: string;
fadeOut?: boolean;
};
const { base64ThumbHash, fadeOut = false, class: className, ...restProps }: Props = $props();
const {
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
} = TUNABLES;
let canvas = $state<HTMLCanvasElement>();
$effect(() => {
const ctx = canvas?.getContext('2d');
if (!canvas || !ctx) {
return;
}
const { w, h, rgba } = thumbHashToRGBA(decodeBase64(base64ThumbHash));
canvas.width = w;
canvas.height = h;
const pixels = ctx.createImageData(w, h);
pixels.data.set(rgba);
ctx.putImageData(pixels, 0, 0);
});
</script>
<canvas
bind:this={canvas}
class={className}
out:fade={{ duration: fadeOut ? THUMBHASH_FADE_DURATION : 0 }}
{...restProps}
></canvas>
@@ -2,8 +2,9 @@
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import MapModal from '$lib/modals/MapModal.svelte';
import { handleError } from '$lib/utils/handle-error';
import { navigate } from '$lib/utils/navigation';
import { getAlbumInfo, type AlbumResponseDto, type MapMarkerResponseDto } from '@immich/sdk';
import { getAlbumMapMarkers, type AlbumResponseDto, type MapMarkerResponseDto } from '@immich/sdk';
import { IconButton, modalManager } from '@immich/ui';
import { mdiMapOutline } from '@mdi/js';
import { onDestroy, onMount } from 'svelte';
@@ -14,7 +15,7 @@
}
let { album }: Props = $props();
let abortController: AbortController;
let cancelable: AbortController;
let returnToMap = $state(false);
let mapMarkers: MapMarkerResponseDto[] = $state([]);
@@ -24,7 +25,7 @@
});
onDestroy(() => {
abortController?.abort();
cancelable?.abort();
assetViewerManager.showAssetViewer(false);
});
@@ -35,30 +36,17 @@
}
});
async function loadMapMarkers() {
if (abortController) {
abortController.abort();
const loadMapMarkers = async () => {
cancelable?.abort();
cancelable = new AbortController();
try {
return await getAlbumMapMarkers({ ...authManager.params, id: album.id }, { signal: cancelable.signal });
} catch (error) {
handleError(error, $t('errors.something_went_wrong'));
return [];
}
abortController = new AbortController();
let albumInfo: AlbumResponseDto = await getAlbumInfo({ id: album.id, withoutAssets: false, ...authManager.params });
let markers: MapMarkerResponseDto[] = [];
for (const asset of albumInfo.assets) {
if (asset.exifInfo?.latitude && asset.exifInfo?.longitude) {
markers.push({
id: asset.id,
lat: asset.exifInfo.latitude,
lon: asset.exifInfo.longitude,
city: asset.exifInfo?.city ?? null,
country: asset.exifInfo?.country ?? null,
state: asset.exifInfo?.state ?? null,
});
}
}
return markers;
}
};
const onClick = async () => {
const assetIds = await modalManager.show(MapModal, { mapMarkers });
@@ -91,7 +91,7 @@
const Close: ActionItem = $derived({
title: $t('go_back'),
icon: languageManager.rtl ? mdiArrowRight : mdiArrowLeft,
$if: () => !!onClose && !assetViewerManager.isFaceEditMode,
$if: () => !!onClose && !assetViewerManager.isFaceEditMode && !assetViewerManager.isEditFacesPanelOpen,
onAction: () => onClose?.(),
shortcuts: [{ key: 'Escape' }],
});
@@ -173,7 +173,7 @@
onDestroy(() => {
activityManager.reset();
assetViewerManager.closeEditor();
assetViewerManager.resetPanelState();
syncAssetViewerOpenClass(false);
preloadManager.destroy();
});
@@ -54,7 +54,7 @@
let { asset, currentAlbum = null }: Props = $props();
let showEditFaces = $state(false);
let showEditFaces = $derived(assetViewerManager.isEditFacesPanelOpen);
let isOwner = $derived(authManager.authenticated && authManager.user.id === asset.ownerId);
let people = $derived(asset.people || []);
let unassignedFaces = $derived(asset.unassignedFaces || []);
@@ -103,7 +103,7 @@
return;
}
showEditFaces = false;
assetViewerManager.closeEditFacesPanel();
previousId = asset.id;
});
@@ -119,7 +119,7 @@
const handleRefreshPeople = async () => {
asset = await getAssetInfo({ id: asset.id });
showEditFaces = false;
assetViewerManager.closeEditFacesPanel();
};
const getAssetFolderHref = (asset: AssetResponseDto) => {
@@ -214,7 +214,7 @@
shape="round"
color="secondary"
variant="ghost"
onclick={() => (showEditFaces = true)}
onclick={() => assetViewerManager.openEditFacesPanel()}
/>
{/if}
</div>
@@ -572,7 +572,7 @@
<PersonSidePanel
assetId={asset.id}
assetType={asset.type}
onClose={() => (showEditFaces = false)}
onClose={() => assetViewerManager.closeEditFacesPanel()}
onRefresh={handleRefreshPeople}
/>
{/if}
@@ -1,9 +1,9 @@
<script lang="ts">
import { shortcuts } from '$lib/actions/shortcut';
import { thumbhash } from '$lib/actions/thumbhash';
import { zoomImageAction } from '$lib/actions/zoom-image';
import AdaptiveImage from '$lib/components/AdaptiveImage.svelte';
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
import Thumbhash from '$lib/components/Thumbhash.svelte';
import OcrBoundingBox from '$lib/components/asset-viewer/ocr-bounding-box.svelte';
import AssetViewerEvents from '$lib/components/AssetViewerEvents.svelte';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
@@ -242,10 +242,7 @@
>
{#snippet backdrop()}
{#if blurredSlideshow}
<canvas
use:thumbhash={{ base64ThumbHash: asset.thumbhash! }}
class="absolute top-0 left-0 inset-s-0 h-dvh w-dvw"
></canvas>
<Thumbhash base64ThumbHash={asset.thumbhash!} class="absolute top-0 left-0 inset-s-0 h-dvh w-dvw" />
{/if}
{/snippet}
{#snippet overlays()}
@@ -1,5 +1,4 @@
<script lang="ts">
import { thumbhash } from '$lib/actions/thumbhash';
import { ProjectionType } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
@@ -10,7 +9,6 @@
import { moveFocus } from '$lib/utils/focus-util';
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
import { getAltText } from '$lib/utils/thumbnail-util';
import { TUNABLES } from '$lib/utils/tunables';
import { AssetMediaSize, AssetVisibility, type UserResponseDto } from '@immich/sdk';
import { Icon } from '@immich/ui';
import {
@@ -27,6 +25,7 @@
import { onMount } from 'svelte';
import type { ClassValue } from 'svelte/elements';
import { fade } from 'svelte/transition';
import Thumbhash from '$lib/components/Thumbhash.svelte';
import ImageThumbnail from './image-thumbnail.svelte';
import VideoThumbnail from './video-thumbnail.svelte';
interface Props {
@@ -75,10 +74,6 @@
dimmed = false,
}: Props = $props();
let {
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
} = TUNABLES;
let usingMobileDevice = $derived(mediaQueryManager.pointerCoarse);
let element: HTMLElement | undefined = $state();
let mouseOver = $state(false);
@@ -312,16 +307,14 @@
{/if}
{#if (!loaded || thumbError) && asset.thumbhash}
<canvas
use:thumbhash={{ base64ThumbHash: asset.thumbhash }}
<Thumbhash
base64ThumbHash={asset.thumbhash}
data-testid="thumbhash"
class="absolute top-0 object-cover group-focus-visible:rounded-lg"
style:width="{width}px"
style:height="{height}px"
class:rounded-xl={selected}
class={['absolute top-0 object-cover group-focus-visible:rounded-lg', { 'rounded-xl': selected }]}
style="width: {width}px; height: {height}px"
draggable="false"
out:fade={{ duration: THUMBHASH_FADE_DURATION }}
></canvas>
fadeOut
/>
{/if}
<!-- icon overlay -->
@@ -36,14 +36,18 @@
$effect(() => {
if (!enablePlayback) {
// Reset remaining time when playback is disabled.
remainingSeconds = durationInSeconds;
if (player) {
// Cancel video buffering.
player.src = '';
}
return;
}
if (!player) {
return;
}
const video = player;
return () => {
video.pause();
video.removeAttribute('src');
video.load();
};
});
const onMouseEnter = () => {
if (playbackOnIconHover) {
@@ -1,4 +1,5 @@
<script lang="ts">
import { shortcut } from '$lib/actions/shortcut';
import OnEvents from '$lib/components/OnEvents.svelte';
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
@@ -187,6 +188,19 @@
<OnEvents {onPersonThumbnailReady} />
<svelte:document
use:shortcut={{
shortcut: { key: 'Escape' },
onShortcut: () => {
if (showSelectedFaces) {
showSelectedFaces = false;
} else {
onClose();
}
},
}}
/>
<section
transition:fly={{ x: 360, duration: 100, easing: linear }}
class="absolute top-0 h-full w-90 overflow-x-hidden p-2 dark:text-immich-dark-fg bg-light"
@@ -14,12 +14,11 @@
import { fade } from 'svelte/transition';
import UserAvatar from '../user-avatar.svelte';
interface Props {
onLogout: () => void;
type Props = {
onClose?: () => void;
}
};
let { onLogout, onClose = () => {} }: Props = $props();
let { onClose }: Props = $props();
let info: ServerAboutResponseDto | undefined = $state();
@@ -48,7 +47,7 @@
size="tiny"
shape="round"
onclick={async () => {
onClose();
onClose?.();
await modalManager.show(AvatarEditModal);
}}
/>
@@ -99,7 +98,7 @@
<div class="mb-4 flex flex-col">
<Button
class="m-1 mx-4 rounded-none rounded-b-3xl bg-white p-3 dark:bg-immich-dark-primary/10"
onclick={onLogout}
href={Route.logout()}
leadingIcon={mdiLogout}
variant="ghost"
color="secondary">{$t('sign_out')}</Button
@@ -109,7 +108,7 @@
type="button"
class="text-center mt-4 underline text-xs text-primary"
onclick={async () => {
onClose();
onClose?.();
if (info) {
await modalManager.show(HelpAndFeedbackModal, { info });
}
@@ -178,10 +178,7 @@
</button>
{#if shouldShowAccountInfoPanel}
<AccountInfoPanel
onLogout={() => authManager.logout()}
onClose={() => (shouldShowAccountInfoPanel = false)}
/>
<AccountInfoPanel onClose={() => (shouldShowAccountInfoPanel = false)} />
{/if}
</div>
</section>
@@ -33,7 +33,7 @@
import DeviceList from './device-list.svelte';
import OAuthSettings from './oauth-settings.svelte';
import PartnerSettings from './partner-settings.svelte';
import UserAPIKeyList from './user-api-key-list.svelte';
import UserApiKeyList from './user-api-key-list.svelte';
import UserProfileSettings from './user-profile-settings.svelte';
interface Props {
@@ -72,7 +72,7 @@
</SettingAccordion>
<SettingAccordion icon={mdiApi} key="api-keys" title={$t('api_keys')} subtitle={$t('manage_your_api_keys')}>
<UserAPIKeyList bind:keys />
<UserApiKeyList bind:keys />
</SettingAccordion>
<SettingAccordion
@@ -44,6 +44,7 @@ class AssetViewerManager extends BaseEventManager<Events> {
isPlayingMotionPhoto = $state(false);
isShowEditor = $state(false);
#isFaceEditMode = $state(false);
#isEditFacesPanelOpen = $state(false);
#viewingAssetStoreState = $state<AssetResponseDto>();
#viewState = $state<boolean>(false);
gridScrollTarget = $state<AssetGridRouteSearchParams | null | undefined>();
@@ -72,6 +73,10 @@ class AssetViewerManager extends BaseEventManager<Events> {
return this.#isFaceEditMode;
}
get isEditFacesPanelOpen() {
return this.#isEditFacesPanelOpen;
}
get zoomState() {
return this.#zoomState;
}
@@ -186,6 +191,20 @@ class AssetViewerManager extends BaseEventManager<Events> {
this.#isFaceEditMode = false;
}
openEditFacesPanel() {
this.#isEditFacesPanelOpen = true;
}
closeEditFacesPanel() {
this.#isEditFacesPanelOpen = false;
}
resetPanelState() {
this.closeEditor();
this.closeFaceEditMode();
this.closeEditFacesPanel();
}
setAsset(asset: AssetResponseDto) {
this.#viewingAssetStoreState = asset;
this.#viewState = true;
+14 -12
View File
@@ -41,6 +41,12 @@ class AuthManager {
return this.#preferences;
}
constructor() {
eventManager.on({
SessionDelete: () => goto(Route.logout()),
});
}
async load() {
if (authManager.authenticated) {
return;
@@ -84,30 +90,26 @@ class AuthManager {
}
async logout() {
let redirectUri;
let redirectUri = Route.login();
try {
const response = await logout();
if (response.redirectUri) {
redirectUri = response.redirectUri;
}
} catch (error) {
console.log('Error logging out:', error);
} catch {
// noop
}
redirectUri = redirectUri ?? Route.login();
try {
if (redirectUri.startsWith('/')) {
await goto(redirectUri);
} else {
globalThis.location.href = redirectUri;
}
} finally {
if (redirectUri.startsWith('/')) {
this.isPurchased = false;
this.reset();
eventManager.emit('AuthLogout');
await goto(redirectUri);
} else {
globalThis.location.href = redirectUri;
}
}
@@ -74,6 +74,7 @@ export type Events = {
UserAdminDeleted: [{ id: string }];
SessionLocked: [];
SessionDelete: [];
SystemConfigUpdate: [SystemConfigDto];
+1 -1
View File
@@ -39,7 +39,7 @@
};
const refreshAlbum = async () => {
album = await getAlbumInfo({ id: album.id, withoutAssets: true });
album = await getAlbumInfo({ id: album.id });
};
const onAlbumUserDelete = async ({ userId }: { userId: string }) => {
@@ -55,12 +55,6 @@
}
});
$effect(() => {
if (searchWord === '') {
suggestedPlaces = [];
}
});
const handleConfirm = (confirmed?: boolean) => {
if (point && confirmed) {
geolocationManager.onSelected(point);
@@ -71,7 +65,7 @@
};
const getLocation = (name: string, admin1Name?: string, admin2Name?: string): string => {
return `${name}${admin1Name ? ', ' + admin1Name : ''}${admin2Name ? ', ' + admin2Name : ''}`;
return [name, admin1Name, admin2Name].filter(Boolean).join(', ');
};
const handleSearchPlaces = () => {
@@ -150,7 +144,7 @@
>
{#snippet prompt()}
<div class="flex flex-col w-full h-full gap-2">
<div class="relative w-64 sm:w-96 z-1">
<div class="relative w-64 sm:w-96 z-1" use:clickOutside={{ onOutclick: () => (hideSuggestion = true) }}>
{#if suggestionContainer}
<div use:listNavigation={suggestionContainer}>
<button type="button" class="w-full" onclick={() => (hideSuggestion = false)}>
@@ -167,22 +161,18 @@
{/if}
<div
class="absolute w-full"
class="absolute w-full bg-gray-200 dark:bg-gray-700 rounded-b-lg"
id="suggestion"
bind:this={suggestionContainer}
use:clickOutside={{ onOutclick: () => (hideSuggestion = true) }}
>
{#if !hideSuggestion}
{#each suggestedPlaces as place, index (place.latitude + place.longitude)}
{#each suggestedPlaces as place (place.latitude + place.longitude)}
<button
type="button"
class=" flex w-full border-t border-gray-400 dark:border-immich-dark-gray h-14 place-items-center bg-gray-200 p-2 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-[#232932] focus:bg-gray-300 focus:dark:bg-[#232932] {index ===
suggestedPlaces.length - 1
? 'rounded-b-lg border-b'
: ''}"
class="flex w-full border-t border-gray-400 dark:border-immich-dark-gray h-12 place-items-center px-5 hover:bg-gray-300 hover:dark:bg-[#232932] focus:bg-gray-300 focus:dark:bg-[#232932] last:rounded-b-lg last:border-b"
onclick={() => handleUseSuggested(place.latitude, place.longitude)}
>
<p class="ms-4 text-sm text-gray-700 dark:text-gray-100 truncate">
<p class="text-sm text-gray-700 dark:text-gray-100 truncate">
{getLocation(place.name, place.admin1name, place.admin2name)}
</p>
</button>
+1
View File
@@ -51,6 +51,7 @@ export const Docs = {
export const Route = {
// auth
login: (params?: { continue?: string; autoLaunch?: 0 | 1 }) => '/auth/login' + asQueryString(params),
logout: (params?: { continue?: string }) => '/auth/logout' + asQueryString(params),
register: () => '/auth/register',
changePassword: () => '/auth/change-password',
onboarding: (params?: { step?: string }) => '/auth/onboarding' + asQueryString(params),
+1 -1
View File
@@ -78,7 +78,7 @@ websocket
}
})
.on('on_new_release', (event) => eventManager.emit('ReleaseEvent', event))
.on('on_session_delete', () => authManager.logout())
.on('on_session_delete', () => eventManager.emit('SessionDelete'))
.on('on_user_delete', (id) => eventManager.emit('UserAdminDeleted', { id }))
.on('on_asset_update', (asset) => eventManager.emit('AssetUpdate', asset))
.on('on_person_thumbnail', (id) => eventManager.emit('PersonThumbnailReady', { id }))
+2 -3
View File
@@ -110,7 +110,7 @@ export const fileUploadHandler = async ({
const deviceAssetId = getDeviceAssetId(file);
uploadAssetsStore.addItem({ id: deviceAssetId, file, albumId });
promises.push(
uploadExecutionQueue.addTask(() => fileUploader({ assetFile: file, deviceAssetId, albumId, isLockedAssets })),
uploadExecutionQueue.addTask(() => fileUploader({ deviceAssetId, assetFile: file, albumId, isLockedAssets })),
);
} else {
toastManager.warning(get(t)('unsupported_file_type', { values: { file: file.name, type: file.type } }), {
@@ -132,6 +132,7 @@ type FileUploaderParams = {
albumId?: string;
replaceAssetId?: string;
isLockedAssets?: boolean;
// TODO rework the asset uploader and remove this
deviceAssetId: string;
};
@@ -151,8 +152,6 @@ async function fileUploader({
try {
const formData = new FormData();
for (const [key, value] of Object.entries({
deviceAssetId,
deviceId: 'WEB',
fileCreatedAt,
fileModifiedAt: new Date(assetFile.lastModified).toISOString(),
isFavorite: 'false',
+2
View File
@@ -1,3 +1,4 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { initLanguage } from '$lib/utils';
@@ -13,6 +14,7 @@ async function _init(fetch: Fetch) {
defaults.fetch = fetch;
await initLanguage();
await serverConfigManager.init();
await authManager.load();
if (!serverConfigManager.value.maintenanceMode) {
await featureFlagsManager.init();
@@ -133,7 +133,7 @@
};
const refreshAlbum = async () => {
album = await getAlbumInfo({ id: album.id, withoutAssets: true });
album = await getAlbumInfo({ id: album.id });
};
const setModeToView = async () => {
@@ -4,7 +4,7 @@ import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url);
const album = await getAlbumInfo({ id: params.albumId, withoutAssets: true });
const album = await getAlbumInfo({ id: params.albumId });
return {
album,
@@ -1,6 +1,8 @@
<script lang="ts">
import { goto } from '$app/navigation';
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { Route } from '$lib/route';
import { updateMyUser } from '@immich/sdk';
import { Alert, Button, Field, HelperText, PasswordInput, Stack, Text } from '@immich/ui';
import { t } from 'svelte-i18n';
@@ -23,7 +25,7 @@
}
await updateMyUser({ userUpdateMeDto: { password } });
await authManager.logout();
await goto(Route.logout());
};
</script>
+7 -1
View File
@@ -1,3 +1,4 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { Route } from '$lib/route';
import { getFormatter } from '$lib/utils/i18n';
@@ -7,6 +8,11 @@ import type { PageLoad } from './$types';
export const load = (async ({ parent, url }) => {
await parent();
const continueUrl = url.searchParams.get('continue') || Route.photos();
if (authManager.authenticated) {
redirect(307, continueUrl);
}
if (!serverConfigManager.value.isInitialized) {
// Admin not registered
redirect(307, Route.register());
@@ -17,6 +23,6 @@ export const load = (async ({ parent, url }) => {
meta: {
title: $t('login'),
},
continueUrl: url.searchParams.get('continue') || Route.photos(),
continueUrl,
};
}) satisfies PageLoad;
+12
View File
@@ -0,0 +1,12 @@
<script lang="ts">
import { authManager } from '$lib/managers/auth-manager.svelte';
import { LoadingSpinner } from '@immich/ui';
void authManager.logout();
</script>
<div class="h-screen w-screen overflow-hidden">
<div class="m-auto">
<LoadingSpinner />
</div>
</div>
@@ -8,7 +8,6 @@ export const albumFactory = Sync.makeFactory<AlbumResponseDto>({
description: '',
albumThumbnailAssetId: null,
assetCount: Sync.each((index) => index % 5),
assets: [],
createdAt: Sync.each(() => faker.date.past().toISOString()),
updatedAt: Sync.each(() => faker.date.past().toISOString()),
id: Sync.each(() => faker.string.uuid()),
@@ -7,9 +7,7 @@ import { Sync } from 'factory.ts';
export const assetFactory = Sync.makeFactory<AssetResponseDto>({
id: Sync.each(() => faker.string.uuid()),
createdAt: Sync.each(() => faker.date.past().toISOString()),
deviceAssetId: Sync.each(() => faker.string.uuid()),
ownerId: Sync.each(() => faker.string.uuid()),
deviceId: '',
libraryId: Sync.each(() => faker.string.uuid()),
type: Sync.each(() => faker.helpers.enumValue(AssetTypeEnum)),
originalPath: Sync.each(() => faker.system.filePath()),