mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge remote-tracking branch 'origin/main' into feat/yucca-integration
This commit is contained in:
@@ -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);
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 }))
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user