Merge branch 'main' into feature/readonly-sharing
# Conflicts: # e2e/src/utils.ts # mobile/openapi/.openapi-generator/FILES # mobile/openapi/README.md # mobile/openapi/lib/api.dart # mobile/openapi/lib/api_client.dart
This commit is contained in:
Generated
+21
-3
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.101.0",
|
||||
"version": "1.102.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-web",
|
||||
"version": "1.101.0",
|
||||
"version": "1.102.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.7.1",
|
||||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
|
||||
"@photo-sphere-viewer/video-plugin": "^5.7.2",
|
||||
"@zoom-image/svelte": "^0.2.6",
|
||||
"buffer": "^6.0.3",
|
||||
"copy-image-clipboard": "^2.1.2",
|
||||
@@ -63,7 +65,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.101.0",
|
||||
"version": "1.102.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
@@ -1590,6 +1592,22 @@
|
||||
"three": "^0.161.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@photo-sphere-viewer/equirectangular-video-adapter": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.7.2.tgz",
|
||||
"integrity": "sha512-cAaot52nPqa2p77Xp1humRvuxRIa8cqbZ/XRhA8kBToFLT1Ugh9YBcDD7pM/358JtAjicUbLpT7Ioap9iEigxQ==",
|
||||
"peerDependencies": {
|
||||
"@photo-sphere-viewer/core": "5.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@photo-sphere-viewer/video-plugin": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.7.2.tgz",
|
||||
"integrity": "sha512-vrPV9RCr4HsYiORkto1unDPeUkbN2kbyogvNUoLiQ78M4xkPOqoKxtfxCxTYoM+7gECwNL9VTF81+okck498qA==",
|
||||
"peerDependencies": {
|
||||
"@photo-sphere-viewer/core": "5.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
"version": "1.0.0-next.24",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz",
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.101.0",
|
||||
"version": "1.102.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"scripts": {
|
||||
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||
@@ -61,6 +61,8 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.7.1",
|
||||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
|
||||
"@photo-sphere-viewer/video-plugin": "^5.7.2",
|
||||
"@zoom-image/svelte": "^0.2.6",
|
||||
"buffer": "^6.0.3",
|
||||
"copy-image-clipboard": "^2.1.2",
|
||||
|
||||
@@ -101,6 +101,16 @@
|
||||
isEdited={config.image.colorspace !== savedConfig.image.colorspace}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
id="prefer-embedded"
|
||||
title="PREFER EMBEDDED PREVIEW"
|
||||
subtitle="Use embedded previews in RAW photos as the input to image processing when available. This can produce more accurate colors for some images, but the quality of the preview is camera-dependent and the image may have more compression artifacts."
|
||||
checked={config.image.extractEmbedded}
|
||||
on:toggle={() => (config.image.extractEmbedded = !config.image.extractEmbedded)}
|
||||
isEdited={config.image.extractEmbedded !== savedConfig.image.extractEmbedded}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ml-4">
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
mdiFolderDownloadOutline,
|
||||
mdiHeart,
|
||||
mdiHeartOutline,
|
||||
mdiHistory,
|
||||
mdiImageAlbum,
|
||||
mdiImageMinusOutline,
|
||||
mdiImageOutline,
|
||||
@@ -52,6 +53,7 @@
|
||||
|
||||
type MenuItemEvent =
|
||||
| 'addToAlbum'
|
||||
| 'restoreAsset'
|
||||
| 'addToSharedAlbum'
|
||||
| 'asProfileImage'
|
||||
| 'setAsAlbumCover'
|
||||
@@ -70,6 +72,7 @@
|
||||
delete: void;
|
||||
toggleArchive: void;
|
||||
addToAlbum: void;
|
||||
restoreAsset: void;
|
||||
addToSharedAlbum: void;
|
||||
asProfileImage: void;
|
||||
setAsAlbumCover: void;
|
||||
@@ -208,12 +211,16 @@
|
||||
{#if showDownloadButton}
|
||||
<MenuOption icon={mdiFolderDownloadOutline} on:click={() => onMenuClick('download')} text="Download" />
|
||||
{/if}
|
||||
<MenuOption icon={mdiImageAlbum} on:click={() => onMenuClick('addToAlbum')} text="Add to album" />
|
||||
<MenuOption
|
||||
icon={mdiShareVariantOutline}
|
||||
on:click={() => onMenuClick('addToSharedAlbum')}
|
||||
text="Add to shared album"
|
||||
/>
|
||||
{#if asset.isTrashed}
|
||||
<MenuOption icon={mdiHistory} on:click={() => onMenuClick('restoreAsset')} text="Restore" />
|
||||
{:else}
|
||||
<MenuOption icon={mdiImageAlbum} on:click={() => onMenuClick('addToAlbum')} text="Add to album" />
|
||||
<MenuOption
|
||||
icon={mdiShareVariantOutline}
|
||||
on:click={() => onMenuClick('addToSharedAlbum')}
|
||||
text="Add to shared album"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isOwner}
|
||||
{#if hasStackChildren}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
getActivityStatistics,
|
||||
getAllAlbums,
|
||||
runAssetJobs,
|
||||
restoreAssets,
|
||||
updateAsset,
|
||||
updateAlbumInfo,
|
||||
type ActivityResponseDto,
|
||||
@@ -49,7 +50,7 @@
|
||||
import PanoramaViewer from './panorama-viewer.svelte';
|
||||
import PhotoViewer from './photo-viewer.svelte';
|
||||
import SlideshowBar from './slideshow-bar.svelte';
|
||||
import VideoViewer from './video-viewer.svelte';
|
||||
import VideoViewer from './video-wrapper-viewer.svelte';
|
||||
|
||||
export let assetStore: AssetStore | null = null;
|
||||
export let asset: AssetResponseDto;
|
||||
@@ -403,6 +404,22 @@
|
||||
await handleGetAllAlbums();
|
||||
};
|
||||
|
||||
const handleRestoreAsset = async () => {
|
||||
try {
|
||||
await restoreAssets({ bulkIdsDto: { ids: [asset.id] } });
|
||||
asset.isTrashed = false;
|
||||
|
||||
dispatch('action', { type: AssetAction.RESTORE, asset });
|
||||
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: `Restored asset`,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Error restoring asset');
|
||||
}
|
||||
};
|
||||
|
||||
const toggleArchive = async () => {
|
||||
try {
|
||||
const data = await updateAsset({
|
||||
@@ -556,6 +573,7 @@
|
||||
on:delete={() => trashOrDelete()}
|
||||
on:favorite={toggleFavorite}
|
||||
on:addToAlbum={() => openAlbumPicker(false)}
|
||||
on:restoreAsset={() => handleRestoreAsset()}
|
||||
on:addToSharedAlbum={() => openAlbumPicker(true)}
|
||||
on:playMotionPhoto={() => (shouldPlayMotionPhoto = true)}
|
||||
on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)}
|
||||
@@ -604,6 +622,7 @@
|
||||
{:else}
|
||||
<VideoViewer
|
||||
assetId={previewStackedAsset.id}
|
||||
projectionType={previewStackedAsset.exifInfo?.projectionType}
|
||||
on:close={closeViewer}
|
||||
on:onVideoEnded={() => navigateAsset()}
|
||||
on:onVideoStarted={handleVideoStarted}
|
||||
@@ -624,6 +643,7 @@
|
||||
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
|
||||
<VideoViewer
|
||||
assetId={asset.livePhotoVideoId}
|
||||
projectionType={asset.exifInfo?.projectionType}
|
||||
on:close={closeViewer}
|
||||
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
|
||||
/>
|
||||
@@ -637,6 +657,7 @@
|
||||
{:else}
|
||||
<VideoViewer
|
||||
assetId={asset.id}
|
||||
projectionType={asset.exifInfo?.projectionType}
|
||||
on:close={closeViewer}
|
||||
on:onVideoEnded={() => navigateAsset()}
|
||||
on:onVideoStarted={handleVideoStarted}
|
||||
|
||||
@@ -1,22 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { serveFile, type AssetResponseDto } from '@immich/sdk';
|
||||
import { serveFile, type AssetResponseDto, AssetTypeEnum } from '@immich/sdk';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { getKey } from '$lib/utils';
|
||||
export let asset: AssetResponseDto;
|
||||
import type { AdapterConstructor, PluginConstructor } from '@photo-sphere-viewer/core';
|
||||
export let asset: Pick<AssetResponseDto, 'id' | 'type'>;
|
||||
|
||||
const photoSphereConfigs =
|
||||
asset.type === AssetTypeEnum.Video
|
||||
? ([
|
||||
import('@photo-sphere-viewer/equirectangular-video-adapter').then(
|
||||
({ EquirectangularVideoAdapter }) => EquirectangularVideoAdapter,
|
||||
),
|
||||
import('@photo-sphere-viewer/video-plugin').then(({ VideoPlugin }) => [VideoPlugin]),
|
||||
true,
|
||||
import('@photo-sphere-viewer/video-plugin/index.css'),
|
||||
] as [PromiseLike<AdapterConstructor>, Promise<PluginConstructor[]>, true, unknown])
|
||||
: ([undefined, [], false] as [undefined, [], false]);
|
||||
|
||||
const loadAssetData = async () => {
|
||||
const data = await serveFile({ id: asset.id, isWeb: false, isThumb: false, key: getKey() });
|
||||
return URL.createObjectURL(data);
|
||||
const url = URL.createObjectURL(data);
|
||||
if (asset.type === AssetTypeEnum.Video) {
|
||||
return { source: url };
|
||||
}
|
||||
return url;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
|
||||
<!-- the photo sphere viewer is quite large, so lazy load it in parallel with loading the data -->
|
||||
{#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte')])}
|
||||
{#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte'), ...photoSphereConfigs])}
|
||||
<LoadingSpinner />
|
||||
{:then [data, module]}
|
||||
<svelte:component this={module.default} panorama={data} />
|
||||
{:then [data, module, adapter, plugins, navbar]}
|
||||
<svelte:component this={module.default} panorama={data} plugins={plugins ?? undefined} {navbar} {adapter} />
|
||||
{:catch}
|
||||
Failed to load asset
|
||||
{/await}
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { Viewer } from '@photo-sphere-viewer/core';
|
||||
import {
|
||||
Viewer,
|
||||
EquirectangularAdapter,
|
||||
type PluginConstructor,
|
||||
type AdapterConstructor,
|
||||
} from '@photo-sphere-viewer/core';
|
||||
import '@photo-sphere-viewer/core/index.css';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
export let panorama: string;
|
||||
export let panorama: string | { source: string };
|
||||
export let adapter: AdapterConstructor | [AdapterConstructor, unknown] = EquirectangularAdapter;
|
||||
export let plugins: (PluginConstructor | [PluginConstructor, unknown])[] = [];
|
||||
export let navbar = false;
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let viewer: Viewer;
|
||||
|
||||
onMount(() => {
|
||||
viewer = new Viewer({
|
||||
adapter,
|
||||
plugins,
|
||||
container,
|
||||
panorama,
|
||||
navbar: false,
|
||||
touchmoveTwoFingers: true,
|
||||
mousewheelCtrlKey: false,
|
||||
navbar,
|
||||
maxFov: 180,
|
||||
fisheye: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
import { slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { SlideshowLook, slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
|
||||
const { slideshowState, slideshowLook } = slideshowStore;
|
||||
|
||||
@@ -150,15 +150,24 @@
|
||||
<div
|
||||
bind:this={element}
|
||||
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
|
||||
class="flex h-full select-none place-content-center place-items-center"
|
||||
class="relative h-full select-none"
|
||||
>
|
||||
{#if !imageLoaded}
|
||||
<LoadingSpinner />
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{:else}
|
||||
<div bind:this={imgElement} class="h-full w-full">
|
||||
<div bind:this={imgElement} class="h-full w-full" transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}>
|
||||
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
|
||||
<img
|
||||
src={assetData}
|
||||
alt={getAltText(asset)}
|
||||
class="absolute top-0 left-0 -z-10 object-cover h-full w-full blur-lg"
|
||||
draggable="false"
|
||||
/>
|
||||
{/if}
|
||||
<img
|
||||
bind:this={$photoViewer}
|
||||
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
|
||||
src={assetData}
|
||||
alt={getAltText(asset)}
|
||||
class="h-full w-full {$slideshowState === SlideshowState.None
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { AssetTypeEnum } from '@immich/sdk';
|
||||
import { ProjectionType } from '$lib/constants';
|
||||
import VideoNativeViewer from '$lib/components/asset-viewer/video-native-viewer.svelte';
|
||||
import PanoramaViewer from '$lib/components/asset-viewer/panorama-viewer.svelte';
|
||||
|
||||
export let assetId: string;
|
||||
export let projectionType: string | null | undefined;
|
||||
</script>
|
||||
|
||||
{#if projectionType === ProjectionType.EQUIRECTANGULAR}
|
||||
<PanoramaViewer asset={{ id: assetId, type: AssetTypeEnum.Video }} />
|
||||
{:else}
|
||||
<VideoNativeViewer {assetId} on:onVideoEnded on:onVideoStarted />
|
||||
{/if}
|
||||
@@ -169,6 +169,7 @@
|
||||
switch (action) {
|
||||
case removeAction:
|
||||
case AssetAction.TRASH:
|
||||
case AssetAction.RESTORE:
|
||||
case AssetAction.DELETE: {
|
||||
// find the next asset to show or close the viewer
|
||||
(await handleNext()) || (await handlePrevious()) || handleClose();
|
||||
|
||||
@@ -4,7 +4,14 @@
|
||||
SettingInputFieldType,
|
||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { mdiArrowDownThin, mdiArrowUpThin, mdiFitToPageOutline, mdiFitToScreenOutline, mdiShuffle } from '@mdi/js';
|
||||
import {
|
||||
mdiArrowDownThin,
|
||||
mdiArrowUpThin,
|
||||
mdiFitToPageOutline,
|
||||
mdiFitToScreenOutline,
|
||||
mdiPanorama,
|
||||
mdiShuffle,
|
||||
} from '@mdi/js';
|
||||
import { SlideshowLook, SlideshowNavigation, slideshowStore } from '../stores/slideshow.store';
|
||||
import Button from './elements/buttons/button.svelte';
|
||||
import type { RenderedOption } from './elements/dropdown.svelte';
|
||||
@@ -23,6 +30,7 @@
|
||||
const lookOptions: Record<SlideshowLook, RenderedOption> = {
|
||||
[SlideshowLook.Contain]: { icon: mdiFitToScreenOutline, title: 'Contain' },
|
||||
[SlideshowLook.Cover]: { icon: mdiFitToPageOutline, title: 'Cover' },
|
||||
[SlideshowLook.BlurredBackground]: { icon: mdiPanorama, title: 'Blurred background' },
|
||||
};
|
||||
|
||||
const handleToggle = <Type extends SlideshowNavigation | SlideshowLook>(
|
||||
|
||||
@@ -5,7 +5,7 @@ export enum AssetAction {
|
||||
UNFAVORITE = 'unfavorite',
|
||||
TRASH = 'trash',
|
||||
DELETE = 'delete',
|
||||
// RESTORE = 'restore',
|
||||
RESTORE = 'restore',
|
||||
ADD = 'add',
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,13 @@ export enum SlideshowNavigation {
|
||||
export enum SlideshowLook {
|
||||
Contain = 'contain',
|
||||
Cover = 'cover',
|
||||
BlurredBackground = 'blurred-background',
|
||||
}
|
||||
|
||||
export const slideshowLookCssMapping: Record<SlideshowLook, string> = {
|
||||
[SlideshowLook.Contain]: 'object-contain',
|
||||
[SlideshowLook.Cover]: 'object-cover',
|
||||
[SlideshowLook.BlurredBackground]: 'object-contain',
|
||||
};
|
||||
|
||||
function createSlideshowStore() {
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
|
||||
const handleSearch = async (force: boolean) => {
|
||||
$page.url.searchParams.set(QueryParameter.SEARCHED_PEOPLE, searchName);
|
||||
await goto($page.url);
|
||||
await goto($page.url, { keepFocus: true });
|
||||
await handleSearchPeople(force);
|
||||
};
|
||||
|
||||
|
||||
@@ -39,8 +39,12 @@
|
||||
try {
|
||||
await emptyTrash();
|
||||
|
||||
const deletedAssetIds = assetStore.assets.map((a) => a.id);
|
||||
const numberOfAssets = deletedAssetIds.length;
|
||||
assetStore.removeAssets(deletedAssetIds);
|
||||
|
||||
notificationController.show({
|
||||
message: `Empty trash initiated. Refresh the page to see the changes`,
|
||||
message: `Permanently deleted ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`,
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -52,8 +56,12 @@
|
||||
try {
|
||||
await restoreTrash();
|
||||
|
||||
const restoredAssetIds = assetStore.assets.map((a) => a.id);
|
||||
const numberOfAssets = restoredAssetIds.length;
|
||||
assetStore.removeAssets(restoredAssetIds);
|
||||
|
||||
notificationController.show({
|
||||
message: `Restore trash initiated. Refresh the page to see the changes`,
|
||||
message: `Restored ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`,
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import OnboadingStorageTemplate from '$lib/components/onboarding-page/onboarding-storage-template.svelte';
|
||||
import OnboardingTheme from '$lib/components/onboarding-page/onboarding-theme.svelte';
|
||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||
import { setAdminOnboarding } from '@immich/sdk';
|
||||
import { updateAdminOnboarding } from '@immich/sdk';
|
||||
|
||||
let index = 0;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
const handleDoneClicked = async () => {
|
||||
if (index >= onboardingSteps.length - 1) {
|
||||
await setAdminOnboarding();
|
||||
await updateAdminOnboarding({ adminOnboardingUpdateDto: { isOnboarded: true } });
|
||||
await goto(AppRoute.PHOTOS);
|
||||
} else {
|
||||
index++;
|
||||
|
||||
Reference in New Issue
Block a user