From bccf2f60b2caa072e0bd7ab6f88b8d65a774a290 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 13 Dec 2024 10:59:14 -0600 Subject: [PATCH 01/42] fix(web): upload info panel covers timeline navigation bar (#14651) --- web/src/lib/components/shared-components/upload-panel.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/shared-components/upload-panel.svelte b/web/src/lib/components/shared-components/upload-panel.svelte index 2381b5a423..0eb7d1655c 100644 --- a/web/src/lib/components/shared-components/upload-panel.svelte +++ b/web/src/lib/components/shared-components/upload-panel.svelte @@ -48,7 +48,7 @@ } uploadAssetsStore.reset(); }} - class="fixed bottom-6 right-6 z-[10000]" + class="fixed bottom-6 right-16 z-[10000]" > {#if showDetail}
Date: Fri, 13 Dec 2024 18:13:38 +0100 Subject: [PATCH 02/42] fix(server): fixed email footer image stretched #14617 (#14671) --- server/src/emails/components/footer.template.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/server/src/emails/components/footer.template.tsx b/server/src/emails/components/footer.template.tsx index 7c41a7196d..c84246bf87 100644 --- a/server/src/emails/components/footer.template.tsx +++ b/server/src/emails/components/footer.template.tsx @@ -5,12 +5,14 @@ export const ImmichFooter = () => ( <> - - - +
+ + + +
-
+
Immich From b5022d80d6cdb4a27e33970522868d6ea58e3744 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sat, 14 Dec 2024 19:30:33 +0100 Subject: [PATCH 03/42] refactor(web): asset interaction (#14662) * refactor(web): asset interaction * feedback --- .../components/album-page/album-viewer.svelte | 19 ++-- .../memory-page/memory-viewer.svelte | 29 +++---- .../actions/select-all-assets.svelte | 10 +-- .../photos-page/asset-date-group.svelte | 26 +++--- .../components/photos-page/asset-grid.svelte | 77 ++++++++--------- .../individual-shared-viewer.svelte | 17 ++-- .../gallery-viewer/gallery-viewer.svelte | 62 +++++++------ web/src/lib/stores/asset-interaction.store.ts | 86 ------------------- .../stores/asset-interaction.svelte.spec.ts | 40 +++++++++ .../lib/stores/asset-interaction.svelte.ts | 66 ++++++++++++++ web/src/lib/utils/asset-utils.ts | 10 +-- .../[[assetId=id]]/+page.svelte | 77 +++++++++-------- .../[[assetId=id]]/+page.svelte | 22 ++--- .../[[assetId=id]]/+page.svelte | 26 +++--- .../[[assetId=id]]/+page.svelte | 6 +- .../[[assetId=id]]/+page.svelte | 12 ++- .../[[assetId=id]]/+page.svelte | 44 +++++----- .../(user)/photos/[[assetId=id]]/+page.svelte | 52 +++++------ .../[[assetId=id]]/+page.svelte | 33 ++++--- .../[[assetId=id]]/+page.svelte | 6 +- .../[[assetId=id]]/+page.svelte | 22 ++--- 21 files changed, 375 insertions(+), 367 deletions(-) delete mode 100644 web/src/lib/stores/asset-interaction.store.ts create mode 100644 web/src/lib/stores/asset-interaction.svelte.spec.ts create mode 100644 web/src/lib/stores/asset-interaction.svelte.ts diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 1dc43c5b61..02544e3e07 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -4,7 +4,6 @@ import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { AssetStore } from '$lib/stores/assets.store'; import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; @@ -20,6 +19,7 @@ import AlbumSummary from './album-summary.svelte'; import { t } from 'svelte-i18n'; import { onDestroy } from 'svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { sharedLink: SharedLinkResponseDto; @@ -34,8 +34,7 @@ let { isViewing: showAssetViewer } = assetViewingStore; const assetStore = new AssetStore({ albumId: album.id, order: album.order }); - const assetInteractionStore = createAssetInteractionStore(); - const { isMultiSelectState, selectedAssets } = assetInteractionStore; + const assetInteraction = new AssetInteraction(); dragAndDropFilesStore.subscribe((value) => { if (value.isDragging && value.files.length > 0) { @@ -52,8 +51,8 @@ use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: () => { - if (!$showAssetViewer && $isMultiSelectState) { - cancelMultiselect(assetInteractionStore); + if (!$showAssetViewer && assetInteraction.selectionActive) { + cancelMultiselect(assetInteraction); } }, }} @@ -61,13 +60,13 @@ />
- {#if $isMultiSelectState} + {#if assetInteraction.selectionActive} assetInteractionStore.clearMultiselect()} + assets={assetInteraction.selectedAssets} + clearSelect={() => assetInteraction.clearMultiselect()} > - + {#if sharedLink.allowDownload} {/if} @@ -102,7 +101,7 @@
- +

(0, { duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0), }); @@ -130,7 +129,7 @@ const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]); const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]); const handleEscape = async () => goto(AppRoute.PHOTOS); - const handleSelectAll = () => assetInteractionStore.selectAssets(current?.memory.assets || []); + const handleSelectAll = () => assetInteraction.selectAssets(current?.memory.assets || []); const handleAction = async (action: 'reset' | 'pause' | 'play') => { switch (action) { case 'play': { @@ -212,10 +211,6 @@ current = loadFromParams($memories, target); }); - let isMultiSelectionMode = $derived($selectedAssets.size > 0); - let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); - let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); - $effect(() => { handlePromiseError(handleProgress($progressBarController)); }); @@ -223,7 +218,6 @@ $effect(() => { handlePromiseError(handleAction(galleryInView ? 'pause' : 'play')); }); - let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id)); -{#if isMultiSelectionMode} +{#if assetInteraction.selectionActive}
- cancelMultiselect(assetInteractionStore)}> + cancelMultiselect(assetInteraction)} + > @@ -249,14 +246,14 @@ - + - - {#if $preferences.tags.enabled && isAllUserOwned} + + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {/if} @@ -490,7 +487,7 @@ onPrevious={handlePreviousAsset} assets={current.memory.assets} {viewport} - {assetInteractionStore} + {assetInteraction} />

diff --git a/web/src/lib/components/photos-page/actions/select-all-assets.svelte b/web/src/lib/components/photos-page/actions/select-all-assets.svelte index cc27f3ebbe..9e7c2b9163 100644 --- a/web/src/lib/components/photos-page/actions/select-all-assets.svelte +++ b/web/src/lib/components/photos-page/actions/select-all-assets.svelte @@ -1,24 +1,24 @@ diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index b2780cc1a0..586491ef47 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -2,7 +2,6 @@ import { intersectionObserver } from '$lib/actions/intersection-observer'; import Icon from '$lib/components/elements/icon.svelte'; import Skeleton from '$lib/components/photos-page/skeleton.svelte'; - import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { AssetBucket, type AssetStore, type Viewport } from '$lib/stores/assets.store'; import { navigate } from '$lib/utils/navigation'; import { findTotalOffset, type DateGroup, type ScrollTargetListener } from '$lib/utils/timeline-util'; @@ -13,6 +12,7 @@ import Thumbnail from '../assets/thumbnail/thumbnail.svelte'; import { TUNABLES } from '$lib/utils/tunables'; import { generateId } from '$lib/utils/generate-id'; + import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; export let element: HTMLElement | undefined = undefined; export let isSelectionMode = false; @@ -25,7 +25,7 @@ export let renderThumbsAtTopMargin: string | undefined = undefined; export let assetStore: AssetStore; export let bucket: AssetBucket; - export let assetInteractionStore: AssetInteractionStore; + export let assetInteraction: AssetInteraction; export let onScrollTarget: ScrollTargetListener | undefined = undefined; export let onAssetInGrid: ((asset: AssetResponseDto) => void) | undefined = undefined; @@ -43,13 +43,11 @@ /* TODO figure out a way to calculate this*/ const TITLE_HEIGHT = 51; - const { selectedGroup, selectedAssets, assetSelectionCandidates, isMultiSelectState } = assetInteractionStore; - let isMouseOverGroup = false; let hoveredDateGroup = ''; const onClick = (assets: AssetResponseDto[], groupTitle: string, asset: AssetResponseDto) => { - if (isSelectionMode || $isMultiSelectState) { + if (isSelectionMode || assetInteraction.selectionActive) { assetSelectHandler(asset, assets, groupTitle); return; } @@ -69,13 +67,15 @@ onSelectAssets(asset); // Check if all assets are selected in a group to toggle the group selection's icon - let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) => $selectedAssets.has(asset)).length; + let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) => + assetInteraction.selectedAssets.has(asset), + ).length; // if all assets are selected in a group, add the group to selected group if (selectedAssetsInGroupCount == assetsInDateGroup.length) { - assetInteractionStore.addGroupToMultiselectGroup(groupTitle); + assetInteraction.addGroupToMultiselectGroup(groupTitle); } else { - assetInteractionStore.removeGroupFromMultiselectGroup(groupTitle); + assetInteraction.removeGroupFromMultiselectGroup(groupTitle); } }; @@ -83,7 +83,7 @@ // Show multi select icon on hover on date group hoveredDateGroup = groupTitle; - if ($isMultiSelectState) { + if (assetInteraction.selectionActive) { onSelectAssetCandidates(asset); } }; @@ -151,14 +151,14 @@ class="flex z-[100] sticky top-[-1px] pt-[calc(1.75rem+1px)] pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm" style:width={dateGroup.geometry.containerWidth + 'px'} > - {#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroup.groupTitle))} + {#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dateGroup.groupTitle))}
handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)} on:keydown={() => handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)} > - {#if $selectedGroup.has(dateGroup.groupTitle)} + {#if assetInteraction.selectedGroup.has(dateGroup.groupTitle)} {:else} @@ -212,8 +212,8 @@ onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)} onSelect={(asset) => assetSelectHandler(asset, dateGroup.assets, dateGroup.groupTitle)} onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, asset)} - selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)} - selectionCandidate={$assetSelectionCandidates.has(asset)} + selected={assetInteraction.selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)} + selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)} disabled={$assetStore.albumAssets.has(asset.id)} thumbnailWidth={box.width} thumbnailHeight={box.height} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 5055cdcf4b..cc64c6f02b 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -3,7 +3,6 @@ import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut'; import type { Action } from '$lib/components/asset-viewer/actions/action'; import { AppRoute, AssetAction } from '$lib/constants'; - import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetBucket, AssetStore, type BucketListener, type ViewportXY } from '$lib/stores/assets.store'; import { locale, showDeleteModal } from '$lib/stores/preferences.store'; @@ -37,6 +36,7 @@ import type { UpdatePayload } from 'vite'; import { generateId } from '$lib/utils/generate-id'; import { isTimelineScrolling } from '$lib/stores/timeline.store'; + import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { isSelectionMode?: boolean; @@ -46,7 +46,7 @@ additionally, update the page location/url with the asset as the asset-grid is scrolled */ enableRouting: boolean; assetStore: AssetStore; - assetInteractionStore: AssetInteractionStore; + assetInteraction: AssetInteraction; removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.FAVORITE | AssetAction.UNFAVORITE | null; withStacked?: boolean; showArchiveIcon?: boolean; @@ -64,7 +64,7 @@ singleSelect = false, enableRouting, assetStore = $bindable(), - assetInteractionStore, + assetInteraction, removeAction = null, withStacked = false, showArchiveIcon = false, @@ -78,8 +78,6 @@ }: Props = $props(); let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget } = assetViewingStore; - const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } = - assetInteractionStore; const viewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 }); const safeViewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 }); @@ -437,11 +435,11 @@ (assetIds) => $assetStore.removeAssets(assetIds), idsSelectedAssets, ); - assetInteractionStore.clearMultiselect(); + assetInteraction.clearMultiselect(); }; const onDelete = () => { - const hasTrashedAsset = Array.from($selectedAssets).some((asset) => asset.isTrashed); + const hasTrashedAsset = assetInteraction.selectedAssetsArray.some((asset) => asset.isTrashed); if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) { isShowDeleteConfirmation = true; @@ -459,7 +457,7 @@ }; const onStackAssets = async () => { - const ids = await stackAssets(Array.from($selectedAssets)); + const ids = await stackAssets(assetInteraction.selectedAssetsArray); if (ids) { $assetStore.removeAssets(ids); onEscape(); @@ -467,7 +465,7 @@ }; const toggleArchive = async () => { - const ids = await archiveAssets(Array.from($selectedAssets), !isAllArchived); + const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived); if (ids) { $assetStore.removeAssets(ids); deselectAllAssets(); @@ -482,7 +480,7 @@ const handleSelectAsset = (asset: AssetResponseDto) => { if (!$assetStore.albumAssets.has(asset.id)) { - assetInteractionStore.selectAsset(asset); + assetInteraction.selectAsset(asset); } }; @@ -573,7 +571,7 @@ let shiftKeyIsDown = $state(false); const deselectAllAssets = () => { - cancelMultiselect(assetInteractionStore); + cancelMultiselect(assetInteraction); }; const onKeyDown = (event: KeyboardEvent) => { @@ -606,13 +604,13 @@ }; const handleGroupSelect = (group: string, assets: AssetResponseDto[]) => { - if ($selectedGroup.has(group)) { - assetInteractionStore.removeGroupFromMultiselectGroup(group); + if (assetInteraction.selectedGroup.has(group)) { + assetInteraction.removeGroupFromMultiselectGroup(group); for (const asset of assets) { - assetInteractionStore.removeAssetFromMultiselectGroup(asset); + assetInteraction.removeAssetFromMultiselectGroup(asset); } } else { - assetInteractionStore.addGroupToMultiselectGroup(group); + assetInteraction.addGroupToMultiselectGroup(group); for (const asset of assets) { handleSelectAsset(asset); } @@ -631,26 +629,26 @@ return; } - const rangeSelection = $assetSelectionCandidates.size > 0; - const deselect = $selectedAssets.has(asset); + const rangeSelection = assetInteraction.assetSelectionCandidates.size > 0; + const deselect = assetInteraction.selectedAssets.has(asset); // Select/deselect already loaded assets if (deselect) { - for (const candidate of $assetSelectionCandidates || []) { - assetInteractionStore.removeAssetFromMultiselectGroup(candidate); + for (const candidate of assetInteraction.assetSelectionCandidates) { + assetInteraction.removeAssetFromMultiselectGroup(candidate); } - assetInteractionStore.removeAssetFromMultiselectGroup(asset); + assetInteraction.removeAssetFromMultiselectGroup(asset); } else { - for (const candidate of $assetSelectionCandidates || []) { + for (const candidate of assetInteraction.assetSelectionCandidates) { handleSelectAsset(candidate); } handleSelectAsset(asset); } - assetInteractionStore.clearAssetSelectionCandidates(); + assetInteraction.clearAssetSelectionCandidates(); - if ($assetSelectionStart && rangeSelection) { - let startBucketIndex = $assetStore.getBucketIndexByAssetId($assetSelectionStart.id); + if (assetInteraction.assetSelectionStart && rangeSelection) { + let startBucketIndex = $assetStore.getBucketIndexByAssetId(assetInteraction.assetSelectionStart.id); let endBucketIndex = $assetStore.getBucketIndexByAssetId(asset.id); if (startBucketIndex === null || endBucketIndex === null) { @@ -667,7 +665,7 @@ await $assetStore.loadBucket(bucket.bucketDate); for (const asset of bucket.assets) { if (deselect) { - assetInteractionStore.removeAssetFromMultiselectGroup(asset); + assetInteraction.removeAssetFromMultiselectGroup(asset); } else { handleSelectAsset(asset); } @@ -682,16 +680,16 @@ const assetsGroupByDate = splitBucketIntoDateGroups(bucket, $locale); for (const dateGroup of assetsGroupByDate) { const dateGroupTitle = formatGroupTitle(dateGroup.date); - if (dateGroup.assets.every((a) => $selectedAssets.has(a))) { - assetInteractionStore.addGroupToMultiselectGroup(dateGroupTitle); + if (dateGroup.assets.every((a) => assetInteraction.selectedAssets.has(a))) { + assetInteraction.addGroupToMultiselectGroup(dateGroupTitle); } else { - assetInteractionStore.removeGroupFromMultiselectGroup(dateGroupTitle); + assetInteraction.removeGroupFromMultiselectGroup(dateGroupTitle); } } } } - assetInteractionStore.setAssetSelectionStart(deselect ? null : asset); + assetInteraction.setAssetSelectionStart(deselect ? null : asset); }; const selectAssetCandidates = (endAsset: AssetResponseDto) => { @@ -699,7 +697,7 @@ return; } - const startAsset = $assetSelectionStart; + const startAsset = assetInteraction.assetSelectionStart; if (!startAsset) { return; } @@ -711,11 +709,11 @@ [start, end] = [end, start]; } - assetInteractionStore.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1)); + assetInteraction.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1)); }; const onSelectStart = (e: Event) => { - if ($isMultiSelectState && shiftKeyIsDown) { + if (assetInteraction.selectionActive && shiftKeyIsDown) { e.preventDefault(); } }; @@ -724,12 +722,11 @@ }); let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); let isEmpty = $derived($assetStore.initialized && $assetStore.buckets.length === 0); - let idsSelectedAssets = $derived([...$selectedAssets].map(({ id }) => id)); - let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); + let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id)); $effect(() => { if (isEmpty) { - assetInteractionStore.clearMultiselect(); + assetInteraction.clearMultiselect(); } }); @@ -760,12 +757,12 @@ { shortcut: { key: 'Escape' }, onShortcut: onEscape }, { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) }, { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) }, - { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteractionStore) }, + { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteraction) }, { shortcut: { key: 'PageDown' }, preventDefault: false, onShortcut: focusElement }, { shortcut: { key: 'PageUp' }, preventDefault: false, onShortcut: focusElement }, ]; - if ($isMultiSelectState) { + if (assetInteraction.selectionActive) { shortcuts.push( { shortcut: { key: 'Delete' }, onShortcut: onDelete }, { shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete }, @@ -781,13 +778,13 @@ $effect(() => { if (!lastAssetMouseEvent) { - assetInteractionStore.clearAssetSelectionCandidates(); + assetInteraction.clearAssetSelectionCandidates(); } }); $effect(() => { if (!shiftKeyIsDown) { - assetInteractionStore.clearAssetSelectionCandidates(); + assetInteraction.clearAssetSelectionCandidates(); } }); @@ -889,7 +886,7 @@ {withStacked} {showArchiveIcon} {assetStore} - {assetInteractionStore} + {assetInteraction} {isSelectionMode} {singleSelect} {onScrollTarget} diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte index 5d625cef9d..ebc4b49001 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte @@ -15,11 +15,11 @@ import ControlAppBar from '../shared-components/control-app-bar.svelte'; import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; import { cancelMultiselect } from '$lib/utils/asset-utils'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; import type { Viewport } from '$lib/stores/assets.store'; import { t } from 'svelte-i18n'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { sharedLink: SharedLinkResponseDto; @@ -29,12 +29,10 @@ let { sharedLink = $bindable(), isOwned }: Props = $props(); const viewport: Viewport = $state({ width: 0, height: 0 }); - const assetInteractionStore = createAssetInteractionStore(); - const { selectedAssets } = assetInteractionStore; + const assetInteraction = new AssetInteraction(); let innerWidth: number = $state(0); let assets = $derived(sharedLink.assets); - let isMultiSelectionMode = $derived($selectedAssets.size > 0); dragAndDropFilesStore.subscribe((value) => { if (value.isDragging && value.files.length > 0) { @@ -73,15 +71,18 @@ }; const handleSelectAll = () => { - assetInteractionStore.selectAssets(assets); + assetInteraction.selectAssets(assets); };
- {#if isMultiSelectionMode} - cancelMultiselect(assetInteractionStore)}> + {#if assetInteraction.selectionActive} + cancelMultiselect(assetInteraction)} + > {#if sharedLink?.allowDownload} @@ -112,6 +113,6 @@ {/if}
- +
diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index eda340e7e2..8f8a067a90 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -5,7 +5,6 @@ import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; import { AppRoute, AssetAction } from '$lib/constants'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import type { Viewport } from '$lib/stores/assets.store'; import { showDeleteModal } from '$lib/stores/preferences.store'; import { deleteAssets } from '$lib/utils/actions'; @@ -22,10 +21,11 @@ import Portal from '../portal/portal.svelte'; import { handlePromiseError } from '$lib/utils'; import DeleteAssetDialog from '../../photos-page/delete-asset-dialog.svelte'; + import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { assets: AssetResponseDto[]; - assetInteractionStore: AssetInteractionStore; + assetInteraction: AssetInteraction; disableAssetSelect?: boolean; showArchiveIcon?: boolean; viewport: Viewport; @@ -38,7 +38,7 @@ let { assets = $bindable(), - assetInteractionStore = $bindable(), + assetInteraction, disableAssetSelect = false, showArchiveIcon = false, viewport, @@ -51,11 +51,8 @@ let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore; - const { assetSelectionCandidates, assetSelectionStart, selectedAssets, isMultiSelectState } = assetInteractionStore; - let showShortcuts = $state(false); let currentViewAssetIndex = 0; - let isMultiSelectionMode = $derived($selectedAssets.size > 0); let shiftKeyIsDown = $state(false); let lastAssetMouseEvent: AssetResponseDto | null = $state(null); @@ -66,11 +63,11 @@ }; const selectAllAssets = () => { - assetInteractionStore.selectAssets(assets); + assetInteraction.selectAssets(assets); }; const deselectAllAssets = () => { - cancelMultiselect(assetInteractionStore); + cancelMultiselect(assetInteraction); }; const onKeyDown = (event: KeyboardEvent) => { @@ -91,23 +88,23 @@ if (!asset) { return; } - const deselect = $selectedAssets.has(asset); + const deselect = assetInteraction.selectedAssets.has(asset); // Select/deselect already loaded assets if (deselect) { - for (const candidate of $assetSelectionCandidates || []) { - assetInteractionStore.removeAssetFromMultiselectGroup(candidate); + for (const candidate of assetInteraction.assetSelectionCandidates) { + assetInteraction.removeAssetFromMultiselectGroup(candidate); } - assetInteractionStore.removeAssetFromMultiselectGroup(asset); + assetInteraction.removeAssetFromMultiselectGroup(asset); } else { - for (const candidate of $assetSelectionCandidates || []) { - assetInteractionStore.selectAsset(candidate); + for (const candidate of assetInteraction.assetSelectionCandidates) { + assetInteraction.selectAsset(candidate); } - assetInteractionStore.selectAsset(asset); + assetInteraction.selectAsset(asset); } - assetInteractionStore.clearAssetSelectionCandidates(); - assetInteractionStore.setAssetSelectionStart(deselect ? null : asset); + assetInteraction.clearAssetSelectionCandidates(); + assetInteraction.setAssetSelectionStart(deselect ? null : asset); }; const handleSelectAssetCandidates = (asset: AssetResponseDto | null) => { @@ -122,7 +119,7 @@ return; } - const startAsset = $assetSelectionStart; + const startAsset = assetInteraction.assetSelectionStart; if (!startAsset) { return; } @@ -134,17 +131,17 @@ [start, end] = [end, start]; } - assetInteractionStore.setAssetSelectionCandidates(assets.slice(start, end + 1)); + assetInteraction.setAssetSelectionCandidates(assets.slice(start, end + 1)); }; const onSelectStart = (e: Event) => { - if ($isMultiSelectState && shiftKeyIsDown) { + if (assetInteraction.selectionActive && shiftKeyIsDown) { e.preventDefault(); } }; const onDelete = () => { - const hasTrashedAsset = Array.from($selectedAssets).some((asset) => asset.isTrashed); + const hasTrashedAsset = assetInteraction.selectedAssetsArray.some((asset) => asset.isTrashed); if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) { isShowDeleteConfirmation = true; @@ -168,11 +165,11 @@ (assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id))), idsSelectedAssets, ); - assetInteractionStore.clearMultiselect(); + assetInteraction.clearMultiselect(); }; const toggleArchive = async () => { - const ids = await archiveAssets(Array.from($selectedAssets), !isAllArchived); + const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived); if (ids) { assets.filter((asset) => !ids.includes(asset.id)); deselectAllAssets(); @@ -191,7 +188,7 @@ { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets() }, ]; - if ($isMultiSelectState) { + if (assetInteraction.selectionActive) { shortcuts.push( { shortcut: { key: 'Escape' }, onShortcut: deselectAllAssets }, { shortcut: { key: 'Delete' }, onShortcut: onDelete }, @@ -266,14 +263,13 @@ }; const assetMouseEventHandler = (asset: AssetResponseDto | null) => { - if ($isMultiSelectState) { + if (assetInteraction.selectionActive) { handleSelectAssetCandidates(asset); } }; let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); - let idsSelectedAssets = $derived([...$selectedAssets].map(({ id }) => id)); - let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); + let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id)); let geometry = $derived( (() => { @@ -297,13 +293,13 @@ $effect(() => { if (!lastAssetMouseEvent) { - assetInteractionStore.clearAssetSelectionCandidates(); + assetInteraction.clearAssetSelectionCandidates(); } }); $effect(() => { if (!shiftKeyIsDown) { - assetInteractionStore.clearAssetSelectionCandidates(); + assetInteraction.clearAssetSelectionCandidates(); } }); @@ -318,7 +314,7 @@ {#if isShowDeleteConfirmation} (isShowDeleteConfirmation = false)} onConfirm={() => handlePromiseError(trashOrDelete(true))} /> @@ -340,7 +336,7 @@ { - if (isMultiSelectionMode) { + if (assetInteraction.selectionActive) { handleSelectAssets(asset); return; } @@ -351,8 +347,8 @@ onIntersected={() => (i === Math.max(1, assets.length - 7) ? onIntersected?.() : void 0)} {showArchiveIcon} {asset} - selected={$selectedAssets.has(asset)} - selectionCandidate={$assetSelectionCandidates.has(asset)} + selected={assetInteraction.selectedAssets.has(asset)} + selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)} thumbnailWidth={geometry.boxes[i].width} thumbnailHeight={geometry.boxes[i].height} /> diff --git a/web/src/lib/stores/asset-interaction.store.ts b/web/src/lib/stores/asset-interaction.store.ts deleted file mode 100644 index f7db5382b0..0000000000 --- a/web/src/lib/stores/asset-interaction.store.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { AssetResponseDto } from '@immich/sdk'; -import { derived, readonly, writable } from 'svelte/store'; - -export type AssetInteractionStore = ReturnType; - -export function createAssetInteractionStore() { - const selectedAssets = writable(new Set()); - const selectedGroup = writable(new Set()); - const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0); - - // Candidates for the range selection. This set includes only loaded assets, so it improves highlight - // performance. From the user's perspective, range is highlighted almost immediately - const assetSelectionCandidates = writable(new Set()); - // The beginning of the selection range - const assetSelectionStart = writable(null); - - const selectAsset = (asset: AssetResponseDto) => { - selectedAssets.update(($selectedAssets) => $selectedAssets.add(asset)); - }; - - const selectAssets = (assets: AssetResponseDto[]) => { - selectedAssets.update(($selectedAssets) => { - for (const asset of assets) { - $selectedAssets.add(asset); - } - return $selectedAssets; - }); - }; - - const removeAssetFromMultiselectGroup = (asset: AssetResponseDto) => { - selectedAssets.update(($selectedAssets) => { - $selectedAssets.delete(asset); - return $selectedAssets; - }); - }; - - const addGroupToMultiselectGroup = (group: string) => { - selectedGroup.update(($selectedGroup) => $selectedGroup.add(group)); - }; - - const removeGroupFromMultiselectGroup = (group: string) => { - selectedGroup.update(($selectedGroup) => { - $selectedGroup.delete(group); - return $selectedGroup; - }); - }; - - const setAssetSelectionStart = (asset: AssetResponseDto | null) => { - assetSelectionStart.set(asset); - }; - - const setAssetSelectionCandidates = (assets: AssetResponseDto[]) => { - assetSelectionCandidates.set(new Set(assets)); - }; - - const clearAssetSelectionCandidates = () => { - assetSelectionCandidates.set(new Set()); - }; - - const clearMultiselect = () => { - // Multi-selection - selectedAssets.set(new Set()); - selectedGroup.set(new Set()); - - // Range selection - assetSelectionCandidates.set(new Set()); - assetSelectionStart.set(null); - }; - - return { - selectAsset, - selectAssets, - removeAssetFromMultiselectGroup, - addGroupToMultiselectGroup, - removeGroupFromMultiselectGroup, - setAssetSelectionCandidates, - clearAssetSelectionCandidates, - setAssetSelectionStart, - clearMultiselect, - isMultiSelectState: readonly(isMultiSelectStoreState), - selectedAssets: readonly(selectedAssets), - selectedGroup: readonly(selectedGroup), - assetSelectionCandidates: readonly(assetSelectionCandidates), - assetSelectionStart: readonly(assetSelectionStart), - }; -} diff --git a/web/src/lib/stores/asset-interaction.svelte.spec.ts b/web/src/lib/stores/asset-interaction.svelte.spec.ts new file mode 100644 index 0000000000..5d3043b37c --- /dev/null +++ b/web/src/lib/stores/asset-interaction.svelte.spec.ts @@ -0,0 +1,40 @@ +import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; +import { resetSavedUser, user } from '$lib/stores/user.store'; +import { assetFactory } from '@test-data/factories/asset-factory'; +import { userAdminFactory } from '@test-data/factories/user-factory'; + +describe('AssetInteraction', () => { + let assetInteraction: AssetInteraction; + + beforeEach(() => { + assetInteraction = new AssetInteraction(); + }); + + it('calculates derived values from selection', () => { + assetInteraction.selectAsset(assetFactory.build({ isFavorite: true, isArchived: true, isTrashed: true })); + assetInteraction.selectAsset(assetFactory.build({ isFavorite: true, isArchived: false, isTrashed: false })); + + expect(assetInteraction.selectionActive).toBe(true); + expect(assetInteraction.isAllTrashed).toBe(false); + expect(assetInteraction.isAllArchived).toBe(false); + expect(assetInteraction.isAllFavorite).toBe(true); + }); + + it('updates isAllUserOwned when the active user changes', () => { + const [user1, user2] = userAdminFactory.buildList(2); + assetInteraction.selectAsset(assetFactory.build({ ownerId: user1.id })); + + const cleanup = $effect.root(() => { + expect(assetInteraction.isAllUserOwned).toBe(false); + + user.set(user1); + expect(assetInteraction.isAllUserOwned).toBe(true); + + user.set(user2); + expect(assetInteraction.isAllUserOwned).toBe(false); + }); + + cleanup(); + resetSavedUser(); + }); +}); diff --git a/web/src/lib/stores/asset-interaction.svelte.ts b/web/src/lib/stores/asset-interaction.svelte.ts new file mode 100644 index 0000000000..4397c7f71f --- /dev/null +++ b/web/src/lib/stores/asset-interaction.svelte.ts @@ -0,0 +1,66 @@ +import { user } from '$lib/stores/user.store'; +import type { AssetResponseDto, UserAdminResponseDto } from '@immich/sdk'; +import { SvelteSet } from 'svelte/reactivity'; +import { fromStore } from 'svelte/store'; + +export class AssetInteraction { + readonly selectedAssets = new SvelteSet(); + readonly selectedGroup = new SvelteSet(); + assetSelectionCandidates = $state(new SvelteSet()); + assetSelectionStart = $state(null); + + selectionActive = $derived(this.selectedAssets.size > 0); + selectedAssetsArray = $derived([...this.selectedAssets]); + + private user = fromStore(user); + private userId = $derived(this.user.current?.id); + + isAllTrashed = $derived(this.selectedAssetsArray.every((asset) => asset.isTrashed)); + isAllArchived = $derived(this.selectedAssetsArray.every((asset) => asset.isArchived)); + isAllFavorite = $derived(this.selectedAssetsArray.every((asset) => asset.isFavorite)); + isAllUserOwned = $derived(this.selectedAssetsArray.every((asset) => asset.ownerId === this.userId)); + + selectAsset(asset: AssetResponseDto) { + this.selectedAssets.add(asset); + } + + selectAssets(assets: AssetResponseDto[]) { + for (const asset of assets) { + this.selectedAssets.add(asset); + } + } + + removeAssetFromMultiselectGroup(asset: AssetResponseDto) { + this.selectedAssets.delete(asset); + } + + addGroupToMultiselectGroup(group: string) { + this.selectedGroup.add(group); + } + + removeGroupFromMultiselectGroup(group: string) { + this.selectedGroup.delete(group); + } + + setAssetSelectionStart(asset: AssetResponseDto | null) { + this.assetSelectionStart = asset; + } + + setAssetSelectionCandidates(assets: AssetResponseDto[]) { + this.assetSelectionCandidates = new SvelteSet(assets); + } + + clearAssetSelectionCandidates() { + this.assetSelectionCandidates.clear(); + } + + clearMultiselect() { + // Multi-selection + this.selectedAssets.clear(); + this.selectedGroup.clear(); + + // Range selection + this.assetSelectionCandidates.clear(); + this.assetSelectionStart = null; + } +} diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 37041ecbc4..5b06a66597 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -2,7 +2,7 @@ import { goto } from '$app/navigation'; import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte'; import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification'; import { AppRoute } from '$lib/constants'; -import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; +import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; import { downloadManager } from '$lib/stores/download'; @@ -460,7 +460,7 @@ export const keepThisDeleteOthers = async (keepAsset: AssetResponseDto, stack: S } }; -export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => { +export const selectAllAssets = async (assetStore: AssetStore, assetInteraction: AssetInteraction) => { if (get(isSelectingAllAssets)) { // Selection is already ongoing return; @@ -474,7 +474,7 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt if (!get(isSelectingAllAssets)) { break; // Cancelled } - assetInteractionStore.selectAssets(bucket.assets); + assetInteraction.selectAssets(bucket.assets); // We use setTimeout to allow the UI to update. Otherwise, this may // cause a long delay between the start of 'select all' and the @@ -489,9 +489,9 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt } }; -export const cancelMultiselect = (assetInteractionStore: AssetInteractionStore) => { +export const cancelMultiselect = (assetInteraction: AssetInteraction) => { isSelectingAllAssets.set(false); - assetInteractionStore.clearMultiselect(); + assetInteraction.clearMultiselect(); }; export const toggleArchive = async (asset: AssetResponseDto) => { diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 5c63d8e1a3..0f6c62a5fa 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -35,7 +35,6 @@ import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import { AppRoute, AlbumPageViewMode } from '$lib/constants'; import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetStore } from '$lib/stores/assets.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; @@ -87,6 +86,7 @@ import { onDestroy } from 'svelte'; import { confirmAlbumDelete } from '$lib/utils/album-utils'; import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { data: PageData; @@ -107,11 +107,8 @@ let reactions: ActivityResponseDto[] = $state([]); let albumOrder: AssetOrder | undefined = $state(data.album.order); - const assetInteractionStore = createAssetInteractionStore(); - const { isMultiSelectState, selectedAssets } = assetInteractionStore; - - const timelineInteractionStore = createAssetInteractionStore(); - const { selectedAssets: timelineSelected } = timelineInteractionStore; + const assetInteraction = new AssetInteraction(); + const timelineInteraction = new AssetInteraction(); afterNavigate(({ from }) => { let url: string | undefined = from?.url?.pathname; @@ -234,8 +231,8 @@ if ($showAssetViewer) { return; } - if ($isMultiSelectState) { - cancelMultiselect(assetInteractionStore); + if (assetInteraction.selectionActive) { + cancelMultiselect(assetInteraction); return; } await goto(backUrl); @@ -245,9 +242,8 @@ const refreshAlbum = async () => { album = await getAlbumInfo({ id: album.id, withoutAssets: true }); }; - const handleAddAssets = async () => { - const assetIds = [...$timelineSelected].map((asset) => asset.id); + const assetIds = timelineInteraction.selectedAssetsArray.map((asset) => asset.id); try { const results = await addAssetsToAlbum({ @@ -263,7 +259,7 @@ await refreshAlbum(); - timelineInteractionStore.clearMultiselect(); + timelineInteraction.clearMultiselect(); await setModeToView(); } catch (error) { handleError(error, $t('errors.error_adding_assets_to_album')); @@ -284,13 +280,13 @@ }; const handleCloseSelectAssets = async () => { - timelineInteractionStore.clearMultiselect(); + timelineInteraction.clearMultiselect(); await setModeToView(); }; const handleSelectFromComputer = async () => { await openFileUploadDialog({ albumId: album.id }); - timelineInteractionStore.clearMultiselect(); + timelineInteraction.clearMultiselect(); await setModeToView(); }; @@ -359,16 +355,16 @@ } viewMode = AlbumPageViewMode.VIEW; - assetInteractionStore.clearMultiselect(); + assetInteraction.clearMultiselect(); await updateThumbnail(assetId); }; const updateThumbnailUsingCurrentSelection = async () => { - if ($selectedAssets.size === 1) { - const assetId = [...$selectedAssets][0].id; - assetInteractionStore.clearMultiselect(); - await updateThumbnail(assetId); + if (assetInteraction.selectedAssets.size === 1) { + const [firstAsset] = assetInteraction.selectedAssets; + assetInteraction.clearMultiselect(); + await updateThumbnail(firstAsset.id); } }; @@ -410,9 +406,6 @@ let timelineStore = $derived(new AssetStore({ isArchived: false, withPartners: true }, albumId)); let isOwned = $derived($user.id == album.ownerId); - let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id)); - let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); - let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); let showActivityStatus = $derived( album.albumUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0), @@ -433,40 +426,50 @@
- {#if $isMultiSelectState} - assetInteractionStore.clearMultiselect()}> + {#if assetInteraction.selectionActive} + assetInteraction.clearMultiselect()} + > - + - {#if isAllUserOwned} - assetStore.triggerUpdate()} /> + {#if assetInteraction.isAllUserOwned} + assetStore.triggerUpdate()} + /> {/if} - {#if isAllUserOwned} + {#if assetInteraction.isAllUserOwned} - {#if $selectedAssets.size === 1} + {#if assetInteraction.selectedAssets.size === 1} updateThumbnailUsingCurrentSelection()} /> {/if} - assetStore.triggerUpdate()} /> + assetStore.triggerUpdate()} + /> {/if} - {#if $preferences.tags.enabled && isAllUserOwned} + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {/if} - {#if isOwned || isAllUserOwned} + {#if isOwned || assetInteraction.isAllUserOwned} {/if} - {#if isAllUserOwned} + {#if assetInteraction.isAllUserOwned} {/if} @@ -540,10 +543,10 @@ {#snippet leading()}

- {#if $timelineSelected.size === 0} + {#if !timelineInteraction.selectionActive} {$t('add_to_album')} {:else} - {$t('selected_count', { values: { count: $timelineSelected.size } })} + {$t('selected_count', { values: { count: timelineInteraction.selectedAssets.size } })} {/if}

{/snippet} @@ -556,7 +559,7 @@ > {$t('select_from_computer')} - {/snippet} @@ -579,7 +582,7 @@ {:else} @@ -587,7 +590,7 @@ enableRouting={true} {album} {assetStore} - {assetInteractionStore} + {assetInteraction} isShared={album.albumUsers.length > 0} isSelectionMode={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL} singleSelect={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL} diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3402dff960..5301364ccb 100644 --- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -12,12 +12,12 @@ import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { AssetAction } from '$lib/constants'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { AssetStore } from '$lib/stores/assets.store'; import type { PageData } from './$types'; import { mdiPlus, mdiDotsVertical } from '@mdi/js'; import { t } from 'svelte-i18n'; import { onDestroy } from 'svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { data: PageData; @@ -26,26 +26,26 @@ let { data }: Props = $props(); const assetStore = new AssetStore({ isArchived: true }); - const assetInteractionStore = createAssetInteractionStore(); - const { isMultiSelectState, selectedAssets } = assetInteractionStore; - - let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); + const assetInteraction = new AssetInteraction(); onDestroy(() => { assetStore.destroy(); }); -{#if $isMultiSelectState} - assetInteractionStore.clearMultiselect()}> +{#if assetInteraction.selectionActive} + assetInteraction.clearMultiselect()} + > assetStore.removeAssets(assetIds)} /> - + - assetStore.triggerUpdate()} /> + assetStore.triggerUpdate()} /> assetStore.removeAssets(assetIds)} /> @@ -53,8 +53,8 @@ {/if} - - + + {#snippet empty()} {/snippet} diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index 6635eda6e9..33a03292cd 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -14,7 +14,6 @@ import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { AssetAction } from '$lib/constants'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { AssetStore } from '$lib/stores/assets.store'; import type { PageData } from './$types'; import { mdiDotsVertical, mdiPlus } from '@mdi/js'; @@ -22,6 +21,7 @@ import { onDestroy } from 'svelte'; import { preferences } from '$lib/stores/user.store'; import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { data: PageData; @@ -30,10 +30,7 @@ let { data }: Props = $props(); const assetStore = new AssetStore({ isFavorite: true }); - const assetInteractionStore = createAssetInteractionStore(); - const { isMultiSelectState, selectedAssets } = assetInteractionStore; - - let isAllArchive = $derived([...$selectedAssets].every((asset) => asset.isArchived)); + const assetInteraction = new AssetInteraction(); onDestroy(() => { assetStore.destroy(); @@ -41,11 +38,14 @@ -{#if $isMultiSelectState} - assetInteractionStore.clearMultiselect()}> +{#if assetInteraction.selectionActive} + assetInteraction.clearMultiselect()} + > assetStore.removeAssets(assetIds)} /> - + @@ -54,7 +54,11 @@ - assetStore.removeAssets(assetIds)} /> + assetStore.removeAssets(assetIds)} + /> {#if $preferences.tags.enabled} {/if} @@ -63,8 +67,8 @@ {/if} - - + + {#snippet empty()} {/snippet} diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index 065b28c674..5119905652 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -3,7 +3,6 @@ import { page } from '$app/stores'; import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte'; import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte'; import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; @@ -17,6 +16,7 @@ import type { PageData } from './$types'; import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte'; import SkipLink from '$lib/components/elements/buttons/skip-link.svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { data: PageData; @@ -31,7 +31,7 @@ let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || ''); let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree)); - const assetInteractionStore = createAssetInteractionStore(); + const assetInteraction = new AssetInteraction(); onMount(async () => { await foldersStore.fetchUniquePaths(); @@ -80,7 +80,7 @@
{ - assetInteractionStore.clearMultiselect(); assetStore.destroy(); });
- {#if $isMultiSelectState} - + {#if assetInteraction.selectionActive} + @@ -50,5 +48,5 @@ {/snippet} {/if} - +
diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 143a19dd5c..6788c678ed 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -27,7 +27,6 @@ notificationController, } from '$lib/components/shared-components/notification/notification'; import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetStore } from '$lib/stores/assets.store'; import { websocketEvents } from '$lib/stores/websocket'; @@ -58,8 +57,9 @@ import { listNavigation } from '$lib/actions/list-navigation'; import { t } from 'svelte-i18n'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; - import { preferences, user } from '$lib/stores/user.store'; + import { preferences } from '$lib/stores/user.store'; import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { data: PageData; @@ -78,8 +78,7 @@ handlePromiseError(assetStore.updateOptions(assetStoreOptions)); }); - const assetInteractionStore = createAssetInteractionStore(); - const { selectedAssets, isMultiSelectState } = assetInteractionStore; + const assetInteraction = new AssetInteraction(); let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS); let isEditingName = $state(false); @@ -123,8 +122,8 @@ if ($showAssetViewer || viewMode === PersonPageViewMode.SUGGEST_MERGE) { return; } - if ($isMultiSelectState) { - assetInteractionStore.clearMultiselect(); + if (assetInteraction.selectionActive) { + assetInteraction.clearMultiselect(); return; } else { await goto(previousRoute); @@ -149,8 +148,8 @@ }); const handleUnmerge = () => { - $assetStore.removeAssets([...$selectedAssets].map((a) => a.id)); - assetInteractionStore.clearMultiselect(); + $assetStore.removeAssets(assetInteraction.selectedAssetsArray.map((a) => a.id)); + assetInteraction.clearMultiselect(); viewMode = PersonPageViewMode.VIEW_ASSETS; }; @@ -194,7 +193,7 @@ handleError(error, $t('errors.unable_to_set_feature_photo')); } - assetInteractionStore.clearMultiselect(); + assetInteraction.clearMultiselect(); viewMode = PersonPageViewMode.VIEW_ASSETS; }; @@ -336,15 +335,11 @@ handlePromiseError(updateAssetCount()); } }); - - let isAllArchive = $derived([...$selectedAssets].every((asset) => asset.isArchived)); - let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); - let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id)); {#if viewMode === PersonPageViewMode.UNASSIGN_ASSETS} a.id)} + assetIds={assetInteraction.selectedAssetsArray.map((a) => a.id)} personAssets={person} onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)} onConfirm={handleUnmerge} @@ -375,15 +370,18 @@ {/if}
- {#if $isMultiSelectState} - assetInteractionStore.clearMultiselect()}> + {#if assetInteraction.selectionActive} + assetInteraction.clearMultiselect()} + > - + - assetStore.triggerUpdate()} /> + assetStore.triggerUpdate()} /> - $assetStore.removeAssets(assetIds)} /> - {#if $preferences.tags.enabled && isAllUserOwned} + $assetStore.removeAssets(assetIds)} + /> + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {/if} $assetStore.removeAssets(assetIds)} /> @@ -453,7 +455,7 @@ { - const selection = [...$selectedAssets]; - isAllOwned = selection.every((asset) => asset.ownerId === $user.id); - isAllFavorite = selection.every((asset) => asset.isFavorite); - isAssetStackSelected = selection.length === 1 && !!selection[0].stack; - const isLivePhoto = selection.length === 1 && !!selection[0].livePhotoVideoId; + let selectedAssets = $derived(assetInteraction.selectedAssetsArray); + let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack); + let isLinkActionAvailable = $derived.by(() => { + const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId; const isLivePhotoCandidate = - selection.length === 2 && - selection.some((asset) => asset.type === AssetTypeEnum.Image) && - selection.some((asset) => asset.type === AssetTypeEnum.Video); - isLinkActionAvailable = isAllOwned && (isLivePhoto || isLivePhotoCandidate); - }); + selectedAssets.length === 2 && + selectedAssets.some((asset) => asset.type === AssetTypeEnum.Image) && + selectedAssets.some((asset) => asset.type === AssetTypeEnum.Video); + return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate); + }); const handleEscape = () => { if ($showAssetViewer) { return; } - if ($isMultiSelectState) { - assetInteractionStore.clearMultiselect(); + if (assetInteraction.selectionActive) { + assetInteraction.clearMultiselect(); return; } }; @@ -78,22 +70,22 @@ }); -{#if $isMultiSelectState} +{#if assetInteraction.selectionActive} assetInteractionStore.clearMultiselect()} + assets={assetInteraction.selectedAssets} + clearSelect={() => assetInteraction.clearMultiselect()} > - + - assetStore.triggerUpdate()} /> + assetStore.triggerUpdate()} /> - {#if $selectedAssets.size > 1 || isAssetStackSelected} + {#if assetInteraction.selectedAssets.size > 1 || isAssetStackSelected} assetStore.removeAssets(assetIds)} @@ -103,7 +95,7 @@ {#if isLinkActionAvailable} @@ -121,11 +113,11 @@ {/if} - + ; - - let isMultiSelectionMode = $derived($selectedAssets.size > 0); - let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); - let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); let searchQuery = $derived($page.url.searchParams.get(QueryParameter.QUERY)); onMount(() => { @@ -86,8 +81,8 @@ return; } - if (isMultiSelectionMode) { - $selectedAssets = new Set(); + if (assetInteraction.selectionActive) { + assetInteraction.selectedAssets.clear(); return; } if (!$preventRaceConditionSearchBar) { @@ -131,7 +126,7 @@ searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id)); }; const handleSelectAll = () => { - assetInteractionStore.selectAssets(searchResultAssets); + assetInteraction.selectAssets(searchResultAssets); }; async function onSearchQueryUpdate() { @@ -231,29 +226,31 @@ function getObjectKeys(obj: T): (keyof T)[] { return Object.keys(obj) as (keyof T)[]; } - let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id));
- {#if isMultiSelectionMode} + {#if assetInteraction.selectionActive}
- cancelMultiselect(assetInteractionStore)}> + cancelMultiselect(assetInteraction)} + > - + - - {#if $preferences.tags.enabled && isAllUserOwned} + + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {/if} @@ -333,7 +330,7 @@ {#if searchResultAssets.length > 0} { return Object.fromEntries(tags.map((tag) => [tag.value, tag])); @@ -198,7 +198,7 @@
{#if tag} - + {#snippet empty()} {/snippet} diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index 8803ea38c8..7f97d3772b 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -15,7 +15,6 @@ notificationController, } from '$lib/components/shared-components/notification/notification'; import { AppRoute } from '$lib/constants'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { AssetStore } from '$lib/stores/assets.store'; import { featureFlags, serverConfig } from '$lib/stores/server-config.store'; import { handleError } from '$lib/utils/handle-error'; @@ -26,6 +25,7 @@ import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { t } from 'svelte-i18n'; import { onDestroy } from 'svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { data: PageData; @@ -39,8 +39,7 @@ const options = { isTrashed: true }; const assetStore = new AssetStore(options); - const assetInteractionStore = createAssetInteractionStore(); - const { isMultiSelectState, selectedAssets } = assetInteractionStore; + const assetInteraction = new AssetInteraction(); const handleEmptyTrash = async () => { const isConfirmed = await dialogController.show({ @@ -93,25 +92,28 @@ }); -{#if $isMultiSelectState} - assetInteractionStore.clearMultiselect()}> - +{#if assetInteraction.selectionActive} + assetInteraction.clearMultiselect()} + > + assetStore.removeAssets(assetIds)} /> assetStore.removeAssets(assetIds)} /> {/if} {#if $featureFlags.loaded && $featureFlags.trash} - + {#snippet buttons()}
- +
{$t('restore_all')}
- handleEmptyTrash()} disabled={$isMultiSelectState}> + handleEmptyTrash()} disabled={assetInteraction.selectionActive}>
{$t('empty_trash')} @@ -120,7 +122,7 @@
{/snippet} - +

{$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}

From cc111a1fcb1987ecba8dc025a1177de0f8f394cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 13:43:31 -0600 Subject: [PATCH 04/42] fix(deps): update dependency analyzer to v7 (#14673) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- mobile/immich_lint/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/immich_lint/pubspec.yaml b/mobile/immich_lint/pubspec.yaml index 9d1a3c26b3..5d871b03e6 100644 --- a/mobile/immich_lint/pubspec.yaml +++ b/mobile/immich_lint/pubspec.yaml @@ -5,7 +5,7 @@ environment: sdk: '>=3.0.0 <4.0.0' dependencies: - analyzer: ^6.8.0 + analyzer: ^7.0.0 analyzer_plugin: ^0.11.3 custom_lint_builder: ^0.6.4 glob: ^2.1.2 From dd9feeec45d272dbe2c2100e40e0f465d960a964 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 14 Dec 2024 12:53:15 -0700 Subject: [PATCH 05/42] chore(mobile): remove screen auto-dimming (#14699) --- .../pages/backup/backup_controller.page.dart | 191 +++++++----------- 1 file changed, 69 insertions(+), 122 deletions(-) diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart index d8baecf808..6783f7b54a 100644 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ b/mobile/lib/pages/backup/backup_controller.page.dart @@ -1,11 +1,9 @@ -import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -31,8 +29,6 @@ class BackupControllerPage extends HookConsumerWidget { BackUpState backupState = ref.watch(backupProvider); final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty; final didGetBackupInfo = useState(false); - final isScreenDarkened = useState(false); - final darkenScreenTimer = useRef(null); bool hasExclusiveAccess = backupState.backupProgress != BackUpProgressEnum.inBackground; @@ -43,25 +39,6 @@ class BackupControllerPage extends HookConsumerWidget { ? false : true; - void startScreenDarkenTimer() { - darkenScreenTimer.value = Timer(const Duration(seconds: 30), () { - isScreenDarkened.value = true; - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - }); - } - - void stopScreenDarkenTimer() { - darkenScreenTimer.value?.cancel(); - isScreenDarkened.value = false; - SystemChrome.setEnabledSystemUIMode( - SystemUiMode.manual, - overlays: [ - SystemUiOverlay.top, - SystemUiOverlay.bottom, - ], - ); - } - useEffect( () { // Update the background settings information just to make sure we @@ -77,8 +54,6 @@ class BackupControllerPage extends HookConsumerWidget { return () { WakelockPlus.disable(); - darkenScreenTimer.value?.cancel(); - isScreenDarkened.value = false; }; }, [], @@ -99,10 +74,8 @@ class BackupControllerPage extends HookConsumerWidget { useEffect( () { if (backupState.backupProgress == BackUpProgressEnum.inProgress) { - startScreenDarkenTimer(); WakelockPlus.enable(); } else { - stopScreenDarkenTimer(); WakelockPlus.disable(); } @@ -297,103 +270,77 @@ class BackupControllerPage extends HookConsumerWidget { ); } - return GestureDetector( - onTap: () { - if (isScreenDarkened.value) { - stopScreenDarkenTimer(); - } - if (backupState.backupProgress == BackUpProgressEnum.inProgress) { - startScreenDarkenTimer(); - } - }, - child: AnimatedOpacity( - opacity: isScreenDarkened.value ? 0.1 : 1.0, - duration: const Duration(seconds: 1), - child: Scaffold( - appBar: AppBar( - elevation: 0, - title: const Text( - "backup_controller_page_backup", - ).tr(), - leading: IconButton( - onPressed: () { - ref.watch(websocketProvider.notifier).listenUploadEvent(); - context.maybePop(true); - }, - splashRadius: 24, - icon: const Icon( - Icons.arrow_back_ios_rounded, - ), - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: IconButton( - onPressed: () => - context.pushRoute(const BackupOptionsRoute()), - splashRadius: 24, - icon: const Icon( - Icons.settings_outlined, - ), - ), - ), - ], - ), - body: Stack( - children: [ - Padding( - padding: - const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), - child: ListView( - // crossAxisAlignment: CrossAxisAlignment.start, - children: hasAnyAlbum - ? [ - buildFolderSelectionTile(), - BackupInfoCard( - title: "backup_controller_page_total".tr(), - subtitle: "backup_controller_page_total_sub".tr(), - info: ref - .watch(backupProvider) - .availableAlbums - .isEmpty - ? "..." - : "${backupState.allUniqueAssets.length}", - ), - BackupInfoCard( - title: "backup_controller_page_backup".tr(), - subtitle: "backup_controller_page_backup_sub".tr(), - info: ref - .watch(backupProvider) - .availableAlbums - .isEmpty - ? "..." - : "${backupState.selectedAlbumsBackupAssetsIds.length}", - ), - BackupInfoCard( - title: "backup_controller_page_remainder".tr(), - subtitle: - "backup_controller_page_remainder_sub".tr(), - info: ref - .watch(backupProvider) - .availableAlbums - .isEmpty - ? "..." - : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}", - ), - const Divider(), - const CurrentUploadingAssetInfoBox(), - if (!hasExclusiveAccess) buildBackgroundBackupInfo(), - buildBackupButton(), - ] - : [ - buildFolderSelectionTile(), - if (!didGetBackupInfo.value) buildLoadingIndicator(), - ], - ), - ), - ], + return Scaffold( + appBar: AppBar( + elevation: 0, + title: const Text( + "backup_controller_page_backup", + ).tr(), + leading: IconButton( + onPressed: () { + ref.watch(websocketProvider.notifier).listenUploadEvent(); + context.maybePop(true); + }, + splashRadius: 24, + icon: const Icon( + Icons.arrow_back_ios_rounded, ), ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: IconButton( + onPressed: () => context.pushRoute(const BackupOptionsRoute()), + splashRadius: 24, + icon: const Icon( + Icons.settings_outlined, + ), + ), + ), + ], + ), + body: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), + child: ListView( + // crossAxisAlignment: CrossAxisAlignment.start, + children: hasAnyAlbum + ? [ + buildFolderSelectionTile(), + BackupInfoCard( + title: "backup_controller_page_total".tr(), + subtitle: "backup_controller_page_total_sub".tr(), + info: ref.watch(backupProvider).availableAlbums.isEmpty + ? "..." + : "${backupState.allUniqueAssets.length}", + ), + BackupInfoCard( + title: "backup_controller_page_backup".tr(), + subtitle: "backup_controller_page_backup_sub".tr(), + info: ref.watch(backupProvider).availableAlbums.isEmpty + ? "..." + : "${backupState.selectedAlbumsBackupAssetsIds.length}", + ), + BackupInfoCard( + title: "backup_controller_page_remainder".tr(), + subtitle: "backup_controller_page_remainder_sub".tr(), + info: ref.watch(backupProvider).availableAlbums.isEmpty + ? "..." + : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}", + ), + const Divider(), + const CurrentUploadingAssetInfoBox(), + if (!hasExclusiveAccess) buildBackgroundBackupInfo(), + buildBackupButton(), + ] + : [ + buildFolderSelectionTile(), + if (!didGetBackupInfo.value) buildLoadingIndicator(), + ], + ), + ), + ], ), ); } From fe554c3a5bb0139d874ccd34cc947c7628543e5b Mon Sep 17 00:00:00 2001 From: Alex Sherwin Date: Sun, 15 Dec 2024 16:09:52 -0500 Subject: [PATCH 06/42] fix(mobile): set custom headers on external url (#14707) (#14708) --- mobile/lib/services/auth.service.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart index 0393470098..08741a15db 100644 --- a/mobile/lib/services/auth.service.dart +++ b/mobile/lib/services/auth.service.dart @@ -57,13 +57,18 @@ class AuthService { Future validateAuxilaryServerUrl(String url) async { final httpclient = HttpClient(); - final accessToken = _authRepository.getAccessToken(); bool isValid = false; try { final uri = Uri.parse('$url/users/me'); final request = await httpclient.getUrl(uri); - request.headers.add('x-immich-user-token', accessToken); + + // add auth token + any configured custom headers + final customHeaders = ApiService.getRequestHeaders(); + customHeaders.forEach((key, value) { + request.headers.add(key, value); + }); + final response = await request.close(); if (response.statusCode == 200) { isValid = true; From 6b0f9ec46cb5340932add131949c5952395aec7d Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Dec 2024 08:42:40 -0600 Subject: [PATCH 07/42] chore(mobile): post release tasks (#14656) --- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- mobile/ios/Runner/Info.plist | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 49ac6c4cff..613a8fdf10 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -403,7 +403,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 184; + CURRENT_PROJECT_VERSION = 185; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -546,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 184; + CURRENT_PROJECT_VERSION = 185; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -575,7 +575,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 184; + CURRENT_PROJECT_VERSION = 185; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 28d21e266e..2a74f88485 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.122.2 + 1.122.3 CFBundleSignature ???? CFBundleVersion - 184 + 185 FLTEnableImpeller ITSAppUsesNonExemptEncryption From 8945a5d862254bae244a52fa0edf04be5daf596c Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:45:01 +0100 Subject: [PATCH 08/42] fix: reduce the number of API requests when changing route (#14666) * fix: reduce the number of API requests when changing route * fix: reset `userInteraction` after sign out --- .../components/forms/create-user-form.svelte | 6 +++-- .../components/forms/edit-user-form.svelte | 5 ++-- .../navigation-bar/navigation-bar.svelte | 9 ++++--- .../side-bar/recent-albums.svelte | 6 +++++ .../side-bar/server-status.svelte | 9 ++++++- .../side-bar/storage-space.svelte | 13 ++++++---- web/src/lib/stores/server-info.store.ts | 4 --- web/src/lib/stores/user.svelte.ts | 26 +++++++++++++++++++ web/src/lib/utils/auth.ts | 5 ++-- 9 files changed, 63 insertions(+), 20 deletions(-) delete mode 100644 web/src/lib/stores/server-info.store.ts create mode 100644 web/src/lib/stores/user.svelte.ts diff --git a/web/src/lib/components/forms/create-user-form.svelte b/web/src/lib/components/forms/create-user-form.svelte index b1599a24b2..7aa1c76ed3 100644 --- a/web/src/lib/components/forms/create-user-form.svelte +++ b/web/src/lib/components/forms/create-user-form.svelte @@ -1,7 +1,7 @@ -{#if shouldShowHelpPanel && aboutInfo} - (shouldShowHelpPanel = false)} info={aboutInfo} /> +{#if shouldShowHelpPanel && info} + (shouldShowHelpPanel = false)} {info} /> {/if}
diff --git a/web/src/lib/components/shared-components/side-bar/recent-albums.svelte b/web/src/lib/components/shared-components/side-bar/recent-albums.svelte index d90d7dec01..b11935d643 100644 --- a/web/src/lib/components/shared-components/side-bar/recent-albums.svelte +++ b/web/src/lib/components/shared-components/side-bar/recent-albums.svelte @@ -4,13 +4,19 @@ import { getAllAlbums, type AlbumResponseDto } from '@immich/sdk'; import { handleError } from '$lib/utils/handle-error'; import { t } from 'svelte-i18n'; + import { userInteraction } from '$lib/stores/user.svelte'; let albums: AlbumResponseDto[] = $state([]); onMount(async () => { + if (userInteraction.recentAlbums) { + albums = userInteraction.recentAlbums; + return; + } try { const allAlbums = await getAllAlbums({}); albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3); + userInteraction.recentAlbums = albums; } catch (error) { handleError(error, $t('failed_to_load_assets')); } diff --git a/web/src/lib/components/shared-components/side-bar/server-status.svelte b/web/src/lib/components/shared-components/side-bar/server-status.svelte index 2a0e6a0821..e1d7340c46 100644 --- a/web/src/lib/components/shared-components/side-bar/server-status.svelte +++ b/web/src/lib/components/shared-components/side-bar/server-status.svelte @@ -12,17 +12,24 @@ } from '@immich/sdk'; import Icon from '$lib/components/elements/icon.svelte'; import { mdiAlert } from '@mdi/js'; + import { userInteraction } from '$lib/stores/user.svelte'; const { serverVersion, connected } = websocketStore; let isOpen = $state(false); - let info: ServerAboutResponseDto | undefined = $state(); let versions: ServerVersionHistoryResponseDto[] = $state([]); onMount(async () => { + if (userInteraction.aboutInfo && userInteraction.versions && $serverVersion) { + info = userInteraction.aboutInfo; + versions = userInteraction.versions; + return; + } await requestServerInfo(); [info, versions] = await Promise.all([getAboutInfo(), getVersionHistory()]); + userInteraction.aboutInfo = info; + userInteraction.versions = versions; }); let isMain = $derived(info?.sourceRef === 'main' && info.repository === 'immich-app/immich'); let version = $derived( diff --git a/web/src/lib/components/shared-components/side-bar/storage-space.svelte b/web/src/lib/components/shared-components/side-bar/storage-space.svelte index c0de9378ac..9472397565 100644 --- a/web/src/lib/components/shared-components/side-bar/storage-space.svelte +++ b/web/src/lib/components/shared-components/side-bar/storage-space.svelte @@ -1,18 +1,18 @@ @@ -54,7 +57,7 @@
- - +
diff --git a/web/src/lib/components/faces-page/people-list.svelte b/web/src/lib/components/faces-page/people-list.svelte index 511792e536..1c1eee39ec 100644 --- a/web/src/lib/components/faces-page/people-list.svelte +++ b/web/src/lib/components/faces-page/people-list.svelte @@ -3,18 +3,20 @@ import FaceThumbnail from './face-thumbnail.svelte'; import SearchPeople from '$lib/components/faces-page/people-search.svelte'; import { t } from 'svelte-i18n'; + import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; + import { mdiSwapVertical } from '@mdi/js'; interface Props { screenHeight: number; people: PersonResponseDto[]; peopleToNotShow: PersonResponseDto[]; onSelect: (person: PersonResponseDto) => void; + handleSearch?: (sortFaces: boolean) => void; } - let { screenHeight, people, peopleToNotShow, onSelect }: Props = $props(); - + let { screenHeight, people, peopleToNotShow, onSelect, handleSearch }: Props = $props(); let searchedPeopleLocal: PersonResponseDto[] = $state([]); - + let sortBySimilarirty = $state(false); let name = $state(''); const showPeople = $derived( @@ -24,12 +26,26 @@ ); -
- +
+
+ +
+ + {#if handleSearch} + { + sortBySimilarirty = !sortBySimilarirty; + handleSearch(sortBySimilarirty); + }} + color="neutral" + title={$t('sort_people_by_similarity')} + > + {/if}
From 6080e6e827250b069039538cb5d08ee7492b71c1 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 21 Dec 2024 13:26:01 -0600 Subject: [PATCH 37/42] fix(web): infinite loop browser navigation crash admin settings page (#14850) * fix(web): infinite loop browser navigation crash admin settings page * pr feedback --- .../settings/setting-accordion-state.svelte | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/web/src/lib/components/shared-components/settings/setting-accordion-state.svelte b/web/src/lib/components/shared-components/settings/setting-accordion-state.svelte index 4d97ee5cc6..6b3ae81685 100644 --- a/web/src/lib/components/shared-components/settings/setting-accordion-state.svelte +++ b/web/src/lib/components/shared-components/settings/setting-accordion-state.svelte @@ -9,9 +9,9 @@ import { writable, type Writable } from 'svelte/store'; import { createContext } from '$lib/utils/context'; import { page } from '$app/state'; - import { handlePromiseError } from '$lib/utils'; import { goto } from '$app/navigation'; import type { Snippet } from 'svelte'; + import { handlePromiseError } from '$lib/utils'; const getParamValues = (param: string) => { return new Set((page.url.searchParams.get(param) || '').split(' ').filter((x) => x !== '')); @@ -26,17 +26,16 @@ let { queryParam, state = writable(getParamValues(queryParam)), children }: Props = $props(); setAccordionState(state); - $effect(() => { - if (queryParam && $state) { - const searchParams = new URLSearchParams(page.url.searchParams); - if ($state.size > 0) { - searchParams.set(queryParam, [...$state].join(' ')); - } else { - searchParams.delete(queryParam); - } + const searchParams = new URLSearchParams(page.url.searchParams); - handlePromiseError(goto(`?${searchParams.toString()}`, { replaceState: true, noScroll: true, keepFocus: true })); + $effect(() => { + if ($state.size > 0) { + searchParams.set(queryParam, [...$state].join(' ')); + } else { + searchParams.delete(queryParam); } + + handlePromiseError(goto(`?${searchParams.toString()}`, { replaceState: true, noScroll: true, keepFocus: true })); }); From 4bc2aa54519f1b98f69f6ad9bcb588ce385b2215 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Sun, 22 Dec 2024 03:50:07 +0100 Subject: [PATCH 38/42] feat(server): Handle sidecars in external libraries (#14800) * handle sidecars in external libraries * don't add separate source --- e2e/src/api/specs/library.e2e-spec.ts | 358 ++++++++++++++++++-- e2e/test-assets | 2 +- server/src/services/library.service.spec.ts | 55 +-- server/src/services/library.service.ts | 14 +- server/src/services/metadata.service.ts | 9 +- 5 files changed, 355 insertions(+), 83 deletions(-) diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index 3f910fa1e3..dde2cf79eb 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -1,5 +1,5 @@ import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk'; -import { cpSync, existsSync } from 'node:fs'; +import { cpSync, existsSync, rmSync, unlinkSync } from 'node:fs'; import { Socket } from 'socket.io-client'; import { userDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; @@ -406,65 +406,93 @@ describe('/libraries', () => { it('should reimport a modified file', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, - importPaths: [`${testAssetDirInternal}/temp`], + importPaths: [`${testAssetDirInternal}/temp/reimport`], }); - utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); - await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000); + utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`); + await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000); await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); - cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`); - await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_001); + cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`); + await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001); const { status } = await request(app) .post(`/libraries/${library.id}/scan`) .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ refreshModifiedFiles: true }); + .send(); expect(status).toBe(204); await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); - utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, - model: 'NIKON D750', }); - expect(assets.count).toBe(1); + + expect(assets.count).toEqual(1); + + const asset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id); + + expect(asset).toEqual( + expect.objectContaining({ + originalFileName: 'asset.jpg', + exifInfo: expect.objectContaining({ + model: 'NIKON D750', + }), + }), + ); + + utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`); }); it('should not reimport unmodified files', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, - importPaths: [`${testAssetDirInternal}/temp`], + importPaths: [`${testAssetDirInternal}/temp/reimport`], }); - utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); - await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000); + utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`); + await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000); await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); - cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`); - await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000); + cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`); + await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000); const { status } = await request(app) .post(`/libraries/${library.id}/scan`) .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ refreshModifiedFiles: true }); + .send(); expect(status).toBe(204); await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); - utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, - model: 'NIKON D750', }); - expect(assets.count).toBe(0); + + expect(assets.count).toEqual(1); + + const asset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id); + + expect(asset).toEqual( + expect.objectContaining({ + originalFileName: 'asset.jpg', + exifInfo: expect.not.objectContaining({ + model: 'NIKON D750', + }), + }), + ); + + utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`); }); it('should set an asset offline if its file is missing', async () => { @@ -601,6 +629,298 @@ describe('/libraries', () => { expect(assets).toEqual(assetsBefore); }); + + describe('xmp metadata', async () => { + it('should import metadata from file.xmp', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/xmp`], + }); + + cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`); + cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`); + + await scan(admin.accessToken, library.id); + + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(newAssets.items).toEqual([ + expect.objectContaining({ + originalFileName: 'glarus.nef', + fileCreatedAt: '2000-09-27T12:35:33.000Z', + }), + ]); + + rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true }); + }); + + it('should import metadata from file.ext.xmp', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/xmp`], + }); + + cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`); + cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(newAssets.items).toEqual([ + expect.objectContaining({ + originalFileName: 'glarus.nef', + fileCreatedAt: '2000-09-27T12:35:33.000Z', + }), + ]); + + rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true }); + }); + + it('should import metadata in file.ext.xmp before file.xmp if both exist', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/xmp`], + }); + + cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`); + cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`); + cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(newAssets.items).toEqual([ + expect.objectContaining({ + originalFileName: 'glarus.nef', + fileCreatedAt: '2000-09-27T12:35:33.000Z', + }), + ]); + + rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true }); + }); + + it('should switch from using file.xmp to file.ext.xmp when asset refreshes', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/xmp`], + }); + + cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`); + cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`); + await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`); + unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`); + await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(newAssets.items).toEqual([ + expect.objectContaining({ + originalFileName: 'glarus.nef', + fileCreatedAt: '2010-09-27T12:35:33.000Z', + }), + ]); + + rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true }); + }); + + it('should switch from using file metadata to file.xmp metadata when asset refreshes', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/xmp`], + }); + + cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`); + await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`); + await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(newAssets.items).toEqual([ + expect.objectContaining({ + originalFileName: 'glarus.nef', + fileCreatedAt: '2000-09-27T12:35:33.000Z', + }), + ]); + + rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true }); + }); + + it('should switch from using file metadata to file.xmp metadata when asset refreshes', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/xmp`], + }); + + cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`); + await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`); + await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(newAssets.items).toEqual([ + expect.objectContaining({ + originalFileName: 'glarus.nef', + fileCreatedAt: '2000-09-27T12:35:33.000Z', + }), + ]); + + rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true }); + }); + + it('should switch from using file.ext.xmp to file.xmp when asset refreshes', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/xmp`], + }); + + cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`); + cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`); + await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`); + unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`); + await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(newAssets.items).toEqual([ + expect.objectContaining({ + originalFileName: 'glarus.nef', + fileCreatedAt: '2010-09-27T12:35:33.000Z', + }), + ]); + + rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true }); + }); + + it('should switch from using file.ext.xmp to file metadata', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/xmp`], + }); + + cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`); + cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`); + await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`); + await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(newAssets.items).toEqual([ + expect.objectContaining({ + originalFileName: 'glarus.nef', + fileCreatedAt: '2010-07-20T17:27:12.000Z', + }), + ]); + + rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true }); + }); + + it('should switch from using file.xmp to file metadata', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/xmp`], + }); + + cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`); + cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`); + await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`); + await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'sidecar'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(newAssets.items).toEqual([ + expect.objectContaining({ + originalFileName: 'glarus.nef', + fileCreatedAt: '2010-07-20T17:27:12.000Z', + }), + ]); + + rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true }); + }); + }); }); describe('POST /libraries/:id/validate', () => { diff --git a/e2e/test-assets b/e2e/test-assets index 99544a2004..9e3b964b08 160000 --- a/e2e/test-assets +++ b/e2e/test-assets @@ -1 +1 @@ -Subproject commit 99544a200412d553103cc7b8f1a28f339c7cffd9 +Subproject commit 9e3b964b080dca6f035b29b86e66454ae8aeda78 diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 43d6662d65..9b944045ab 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -414,7 +414,6 @@ describe(LibraryService.name, () => { localDateTime: expect.any(Date), type: AssetType.IMAGE, originalFileName: 'photo.jpg', - sidecarPath: null, isExternal: true, }, ], @@ -423,57 +422,9 @@ describe(LibraryService.name, () => { expect(jobMock.queue.mock.calls).toEqual([ [ { - name: JobName.METADATA_EXTRACTION, + name: JobName.SIDECAR_DISCOVERY, data: { id: assetStub.image.id, - source: 'upload', - }, - }, - ], - ]); - }); - - it('should import a new asset with sidecar', async () => { - const mockLibraryJob: ILibraryFileJob = { - id: libraryStub.externalLibrary1.id, - ownerId: mockUser.id, - assetPath: '/data/user1/photo.jpg', - }; - - assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); - assetMock.create.mockResolvedValue(assetStub.image); - storageMock.checkFileExists.mockResolvedValue(true); - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - - await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); - - expect(assetMock.create.mock.calls).toEqual([ - [ - { - ownerId: mockUser.id, - libraryId: libraryStub.externalLibrary1.id, - checksum: expect.any(Buffer), - originalPath: '/data/user1/photo.jpg', - deviceAssetId: expect.any(String), - deviceId: 'Library Import', - fileCreatedAt: expect.any(Date), - fileModifiedAt: expect.any(Date), - localDateTime: expect.any(Date), - type: AssetType.IMAGE, - originalFileName: 'photo.jpg', - sidecarPath: '/data/user1/photo.jpg.xmp', - isExternal: true, - }, - ], - ]); - - expect(jobMock.queue.mock.calls).toEqual([ - [ - { - name: JobName.METADATA_EXTRACTION, - data: { - id: assetStub.image.id, - source: 'upload', }, }, ], @@ -507,7 +458,6 @@ describe(LibraryService.name, () => { localDateTime: expect.any(Date), type: AssetType.VIDEO, originalFileName: 'video.mp4', - sidecarPath: null, isExternal: true, }, ], @@ -516,10 +466,9 @@ describe(LibraryService.name, () => { expect(jobMock.queue.mock.calls).toEqual([ [ { - name: JobName.METADATA_EXTRACTION, + name: JobName.SIDECAR_DISCOVERY, data: { id: assetStub.image.id, - source: 'upload', }, }, ], diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index c0d24fea9e..0deddc8941 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -396,12 +396,6 @@ export class LibraryService extends BaseService { const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`); - // TODO: doesn't xmp replace the file extension? Will need investigation - let sidecarPath: string | null = null; - if (await this.storageRepository.checkFileExists(`${assetPath}.xmp`, R_OK)) { - sidecarPath = `${assetPath}.xmp`; - } - const assetType = mimeTypes.isVideo(assetPath) ? AssetType.VIDEO : AssetType.IMAGE; const mtime = stat.mtime; @@ -418,8 +412,6 @@ export class LibraryService extends BaseService { localDateTime: mtime, type: assetType, originalFileName: parse(assetPath).base, - - sidecarPath, isExternal: true, }); @@ -431,7 +423,11 @@ export class LibraryService extends BaseService { async queuePostSyncJobs(asset: AssetEntity) { this.logger.debug(`Queueing metadata extraction for: ${asset.originalPath}`); - await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } }); + // We queue a sidecar discovery which, in turn, queues metadata extraction + await this.jobRepository.queue({ + name: JobName.SIDECAR_DISCOVERY, + data: { id: asset.id }, + }); } async queueScan(id: string) { diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 79a7d519d6..e0566c84b7 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -698,7 +698,7 @@ export class MetadataService extends BaseService { return JobStatus.FAILED; } - if (!isSync && (!asset.isVisible || asset.sidecarPath)) { + if (!isSync && (!asset.isVisible || asset.sidecarPath) && !asset.isExternal) { return JobStatus.FAILED; } @@ -720,6 +720,13 @@ export class MetadataService extends BaseService { sidecarPath = sidecarPathWithoutExt; } + if (asset.isExternal) { + if (sidecarPath !== asset.sidecarPath) { + await this.assetRepository.update({ id: asset.id, sidecarPath }); + } + return JobStatus.SUCCESS; + } + if (sidecarPath) { await this.assetRepository.update({ id: asset.id, sidecarPath }); return JobStatus.SUCCESS; From c3be74c450ba7d57ccfc9f6c0463478e579682a0 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Sun, 22 Dec 2024 23:22:16 +0100 Subject: [PATCH 39/42] fix(server): support import paths with special chars (#14856) --- e2e/src/api/specs/library.e2e-spec.ts | 61 +++++++++++++++++++ server/src/repositories/storage.repository.ts | 2 +- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index dde2cf79eb..23cdf092cf 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -403,6 +403,67 @@ describe('/libraries', () => { utils.removeImageFile(`${testAssetDir}/temp/folder} b/assetB.png`); }); + const annoyingChars = [ + "'", + '"', + '`', + '*', + '{', + '}', + ',', + '(', + ')', + '[', + ']', + '?', + '!', + '@', + '#', + '$', + '%', + '^', + '&', + '=', + '+', + '~', + '|', + '<', + '>', + ';', + ':', + '/', // We never got backslashes to work + ]; + + it.each(annoyingChars)('should scan multiple import paths with %s', async (char) => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/folder${char}1`, `${testAssetDirInternal}/temp/folder${char}2`], + }); + + utils.createImageFile(`${testAssetDir}/temp/folder${char}1/asset1.png`); + utils.createImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`); + + const { status } = await request(app) + .post(`/libraries/${library.id}/scan`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send(); + expect(status).toBe(204); + + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(assets.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ originalPath: expect.stringContaining(`folder${char}1/asset1.png`) }), + expect.objectContaining({ originalPath: expect.stringContaining(`folder${char}2/asset2.png`) }), + ]), + ); + + utils.removeImageFile(`${testAssetDir}/temp/folder${char}1/asset1.png`); + utils.removeImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`); + }); + it('should reimport a modified file', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index e4c0c68451..a8d3db15d8 100644 --- a/server/src/repositories/storage.repository.ts +++ b/server/src/repositories/storage.repository.ts @@ -214,7 +214,7 @@ export class StorageRepository implements IStorageRepository { } private asGlob(pathToCrawl: string): string { - const escapedPath = escapePath(pathToCrawl); + const escapedPath = escapePath(pathToCrawl).replaceAll('"', '["]').replaceAll("'", "[']").replaceAll('`', '[`]'); const extensions = `*{${mimeTypes.getSupportedFileExtensions().join(',')}}`; return `${escapedPath}/**/${extensions}`; } From b88f98bf66a4cec9f2d0c5ee77b1bcf6dcf92344 Mon Sep 17 00:00:00 2001 From: Ben <35833890+IMBeniamin@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:26:53 +0100 Subject: [PATCH 40/42] feat(web): Add "set as featured" option for an asset (#14879) --- i18n/en.json | 1 + .../actions/set-person-featured-action.svelte | 29 +++++++++++++++++++ .../asset-viewer/asset-viewer-nav-bar.svelte | 7 +++++ .../asset-viewer/asset-viewer.svelte | 4 +++ .../components/photos-page/asset-grid.svelte | 5 +++- .../[[assetId=id]]/+page.svelte | 1 + 6 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 web/src/lib/components/asset-viewer/actions/set-person-featured-action.svelte diff --git a/i18n/en.json b/i18n/en.json index e1538db1e4..b5f8f3ca9a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1142,6 +1142,7 @@ "set": "Set", "set_as_album_cover": "Set as album cover", "set_as_profile_picture": "Set as profile picture", + "set_as_featured_photo": "Set as featured photo", "set_date_of_birth": "Set date of birth", "set_profile_picture": "Set profile picture", "set_slideshow_to_fullscreen": "Set Slideshow to fullscreen", diff --git a/web/src/lib/components/asset-viewer/actions/set-person-featured-action.svelte b/web/src/lib/components/asset-viewer/actions/set-person-featured-action.svelte new file mode 100644 index 0000000000..70e1c4f1ba --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/set-person-featured-action.svelte @@ -0,0 +1,29 @@ + + + diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 65ca01b58a..442302198b 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -9,6 +9,7 @@ import FavoriteAction from '$lib/components/asset-viewer/actions/favorite-action.svelte'; import RestoreAction from '$lib/components/asset-viewer/actions/restore-action.svelte'; import SetAlbumCoverAction from '$lib/components/asset-viewer/actions/set-album-cover-action.svelte'; + import SetFeaturedPhotoAction from '$lib/components/asset-viewer/actions/set-person-featured-action.svelte'; import SetProfilePictureAction from '$lib/components/asset-viewer/actions/set-profile-picture-action.svelte'; import ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte'; import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-action.svelte'; @@ -27,6 +28,7 @@ AssetTypeEnum, type AlbumResponseDto, type AssetResponseDto, + type PersonResponseDto, type StackResponseDto, } from '@immich/sdk'; import { @@ -50,6 +52,7 @@ interface Props { asset: AssetResponseDto; album?: AlbumResponseDto | null; + person?: PersonResponseDto | null; stack?: StackResponseDto | null; showDetailButton: boolean; showSlideshow?: boolean; @@ -67,6 +70,7 @@ let { asset, album = null, + person = null, stack = null, showDetailButton, showSlideshow = false, @@ -169,6 +173,9 @@ {#if album} {/if} + {#if person} + + {/if} {#if asset.type === AssetTypeEnum.Image} {/if} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 69acc5bb0a..7a2f97bb65 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -30,6 +30,7 @@ type ActivityResponseDto, type AlbumResponseDto, type AssetResponseDto, + type PersonResponseDto, type StackResponseDto, } from '@immich/sdk'; import { onDestroy, onMount, untrack } from 'svelte'; @@ -56,6 +57,7 @@ withStacked?: boolean; isShared?: boolean; album?: AlbumResponseDto | null; + person?: PersonResponseDto | null; onAction?: OnAction | undefined; reactions?: ActivityResponseDto[]; onClose: (dto: { asset: AssetResponseDto }) => void; @@ -72,6 +74,7 @@ withStacked = false, isShared = false, album = null, + person = null, onAction = undefined, reactions = $bindable([]), onClose, @@ -429,6 +432,7 @@ void; onEscape?: () => void; @@ -70,6 +71,7 @@ showArchiveIcon = false, isShared = false, album = null, + person = null, isShowDeleteConfirmation = $bindable(false), onSelect = () => {}, onEscape = () => {}, @@ -914,6 +916,7 @@ preloadAssets={$preloadAssets} {isShared} {album} + {person} onAction={handleAction} onPrevious={handlePrevious} onNext={handleNext} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 6788c678ed..e1e50cfb2e 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -454,6 +454,7 @@ {#key person.id} Date: Mon, 23 Dec 2024 21:03:34 +0000 Subject: [PATCH 41/42] fix(deps): update dependency @nestjs/swagger to v8 (#13881) * fix(deps): update dependency @nestjs/swagger to v8 * chore: generate open api --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel Dietzler --- mobile/openapi/README.md | 8 +- .../openapi/lib/model/album_user_add_dto.dart | 18 +- .../openapi/lib/model/create_library_dto.dart | 20 +- .../openapi/lib/model/update_library_dto.dart | 20 +- .../lib/model/validate_library_dto.dart | 20 +- open-api/immich-openapi-specs.json | 289 +++++++++++++++--- server/package-lock.json | 36 ++- server/package.json | 2 +- 8 files changed, 301 insertions(+), 112 deletions(-) diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index b336b1bfb6..a28035c01a 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -93,17 +93,17 @@ Class | Method | HTTP request | Description *AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} | *AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} | *AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} | -*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | -*AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | +*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | Checks if assets exist by checksums +*AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | Checks if multiple assets exist on the server and returns all existing - used by background backup *AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets | *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original | -*AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | +*AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Get all asset of a device that are in the database, ID only. *AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} | *AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | *AssetsApi* | [**getMemoryLane**](doc//AssetsApi.md#getmemorylane) | **GET** /assets/memory-lane | *AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random | *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | -*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | +*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace the asset with new file, without changing its id *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | *AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets | diff --git a/mobile/openapi/lib/model/album_user_add_dto.dart b/mobile/openapi/lib/model/album_user_add_dto.dart index 3f72d5c893..e1f24377d7 100644 --- a/mobile/openapi/lib/model/album_user_add_dto.dart +++ b/mobile/openapi/lib/model/album_user_add_dto.dart @@ -13,17 +13,11 @@ part of openapi.api; class AlbumUserAddDto { /// Returns a new [AlbumUserAddDto] instance. AlbumUserAddDto({ - this.role, + this.role = AlbumUserRole.editor, required this.userId, }); - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - AlbumUserRole? role; + AlbumUserRole role; String userId; @@ -35,7 +29,7 @@ class AlbumUserAddDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (role == null ? 0 : role!.hashCode) + + (role.hashCode) + (userId.hashCode); @override @@ -43,11 +37,7 @@ class AlbumUserAddDto { Map toJson() { final json = {}; - if (this.role != null) { json[r'role'] = this.role; - } else { - // json[r'role'] = null; - } json[r'userId'] = this.userId; return json; } @@ -61,7 +51,7 @@ class AlbumUserAddDto { final json = value.cast(); return AlbumUserAddDto( - role: AlbumUserRole.fromJson(json[r'role']), + role: AlbumUserRole.fromJson(json[r'role']) ?? AlbumUserRole.editor, userId: mapValueOfType(json, r'userId')!, ); } diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart index bffa5f4279..2b8085be6f 100644 --- a/mobile/openapi/lib/model/create_library_dto.dart +++ b/mobile/openapi/lib/model/create_library_dto.dart @@ -13,15 +13,15 @@ part of openapi.api; class CreateLibraryDto { /// Returns a new [CreateLibraryDto] instance. CreateLibraryDto({ - this.exclusionPatterns = const [], - this.importPaths = const [], + this.exclusionPatterns = const {}, + this.importPaths = const {}, this.name, required this.ownerId, }); - List exclusionPatterns; + Set exclusionPatterns; - List importPaths; + Set importPaths; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -53,8 +53,8 @@ class CreateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns; - json[r'importPaths'] = this.importPaths; + json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); + json[r'importPaths'] = this.importPaths.toList(growable: false); if (this.name != null) { json[r'name'] = this.name; } else { @@ -74,11 +74,11 @@ class CreateLibraryDto { return CreateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) - : const [], + ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() + : const {}, importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) - : const [], + ? (json[r'importPaths'] as Iterable).cast().toSet() + : const {}, name: mapValueOfType(json, r'name'), ownerId: mapValueOfType(json, r'ownerId')!, ); diff --git a/mobile/openapi/lib/model/update_library_dto.dart b/mobile/openapi/lib/model/update_library_dto.dart index b85df40172..6a4f36906f 100644 --- a/mobile/openapi/lib/model/update_library_dto.dart +++ b/mobile/openapi/lib/model/update_library_dto.dart @@ -13,14 +13,14 @@ part of openapi.api; class UpdateLibraryDto { /// Returns a new [UpdateLibraryDto] instance. UpdateLibraryDto({ - this.exclusionPatterns = const [], - this.importPaths = const [], + this.exclusionPatterns = const {}, + this.importPaths = const {}, this.name, }); - List exclusionPatterns; + Set exclusionPatterns; - List importPaths; + Set importPaths; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -48,8 +48,8 @@ class UpdateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns; - json[r'importPaths'] = this.importPaths; + json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); + json[r'importPaths'] = this.importPaths.toList(growable: false); if (this.name != null) { json[r'name'] = this.name; } else { @@ -68,11 +68,11 @@ class UpdateLibraryDto { return UpdateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) - : const [], + ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() + : const {}, importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) - : const [], + ? (json[r'importPaths'] as Iterable).cast().toSet() + : const {}, name: mapValueOfType(json, r'name'), ); } diff --git a/mobile/openapi/lib/model/validate_library_dto.dart b/mobile/openapi/lib/model/validate_library_dto.dart index 08199e3aa6..79ddb9a540 100644 --- a/mobile/openapi/lib/model/validate_library_dto.dart +++ b/mobile/openapi/lib/model/validate_library_dto.dart @@ -13,13 +13,13 @@ part of openapi.api; class ValidateLibraryDto { /// Returns a new [ValidateLibraryDto] instance. ValidateLibraryDto({ - this.exclusionPatterns = const [], - this.importPaths = const [], + this.exclusionPatterns = const {}, + this.importPaths = const {}, }); - List exclusionPatterns; + Set exclusionPatterns; - List importPaths; + Set importPaths; @override bool operator ==(Object other) => identical(this, other) || other is ValidateLibraryDto && @@ -37,8 +37,8 @@ class ValidateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns; - json[r'importPaths'] = this.importPaths; + json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); + json[r'importPaths'] = this.importPaths.toList(growable: false); return json; } @@ -52,11 +52,11 @@ class ValidateLibraryDto { return ValidateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) - : const [], + ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() + : const {}, importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) - : const [], + ? (json[r'importPaths'] as Iterable).cast().toSet() + : const {}, ); } return null; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 7c8aba3b5e..2686d4f96d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1424,7 +1424,6 @@ }, "/assets/bulk-upload-check": { "post": { - "description": "Checks if assets exist by checksums", "operationId": "checkBulkUpload", "parameters": [], "requestBody": { @@ -1460,6 +1459,7 @@ "api_key": [] } ], + "summary": "Checks if assets exist by checksums", "tags": [ "Assets" ] @@ -1467,7 +1467,6 @@ }, "/assets/device/{deviceId}": { "get": { - "description": "Get all asset of a device that are in the database, ID only.", "operationId": "getAllUserAssetsByDeviceId", "parameters": [ { @@ -1505,6 +1504,7 @@ "api_key": [] } ], + "summary": "Get all asset of a device that are in the database, ID only.", "tags": [ "Assets" ] @@ -1512,7 +1512,6 @@ }, "/assets/exist": { "post": { - "description": "Checks if multiple assets exist on the server and returns all existing - used by background backup", "operationId": "checkExistingAssets", "parameters": [], "requestBody": { @@ -1548,6 +1547,7 @@ "api_key": [] } ], + "summary": "Checks if multiple assets exist on the server and returns all existing - used by background backup", "tags": [ "Assets" ] @@ -1903,7 +1903,6 @@ ] }, "put": { - "description": "Replace the asset with new file, without changing its id", "operationId": "replaceAsset", "parameters": [ { @@ -1957,6 +1956,7 @@ "api_key": [] } ], + "summary": "Replace the asset with new file, without changing its id", "tags": [ "Assets" ], @@ -7492,6 +7492,7 @@ "items": { "$ref": "#/components/schemas/Permission" }, + "minItems": 1, "type": "array" } }, @@ -7572,7 +7573,11 @@ "type": "string" }, "type": { - "$ref": "#/components/schemas/ReactionType" + "allOf": [ + { + "$ref": "#/components/schemas/ReactionType" + } + ] } }, "required": [ @@ -7599,7 +7604,11 @@ "type": "string" }, "type": { - "$ref": "#/components/schemas/ReactionType" + "allOf": [ + { + "$ref": "#/components/schemas/ReactionType" + } + ] }, "user": { "$ref": "#/components/schemas/UserResponseDto" @@ -7631,6 +7640,7 @@ "items": { "$ref": "#/components/schemas/AlbumUserAddDto" }, + "minItems": 1, "type": "array" } }, @@ -7699,7 +7709,11 @@ "type": "string" }, "order": { - "$ref": "#/components/schemas/AssetOrder" + "allOf": [ + { + "$ref": "#/components/schemas/AssetOrder" + } + ] }, "owner": { "$ref": "#/components/schemas/UserResponseDto" @@ -7759,7 +7773,12 @@ "AlbumUserAddDto": { "properties": { "role": { - "$ref": "#/components/schemas/AlbumUserRole" + "allOf": [ + { + "$ref": "#/components/schemas/AlbumUserRole" + } + ], + "default": "editor" }, "userId": { "format": "uuid", @@ -7774,7 +7793,11 @@ "AlbumUserCreateDto": { "properties": { "role": { - "$ref": "#/components/schemas/AlbumUserRole" + "allOf": [ + { + "$ref": "#/components/schemas/AlbumUserRole" + } + ] }, "userId": { "format": "uuid", @@ -7790,7 +7813,11 @@ "AlbumUserResponseDto": { "properties": { "role": { - "$ref": "#/components/schemas/AlbumUserRole" + "allOf": [ + { + "$ref": "#/components/schemas/AlbumUserRole" + } + ] }, "user": { "$ref": "#/components/schemas/UserResponseDto" @@ -8087,7 +8114,11 @@ "nullable": true }, "sourceType": { - "$ref": "#/components/schemas/SourceType" + "allOf": [ + { + "$ref": "#/components/schemas/SourceType" + } + ] } }, "required": [ @@ -8158,7 +8189,11 @@ "type": "integer" }, "sourceType": { - "$ref": "#/components/schemas/SourceType" + "allOf": [ + { + "$ref": "#/components/schemas/SourceType" + } + ] } }, "required": [ @@ -8254,7 +8289,11 @@ "type": "array" }, "name": { - "$ref": "#/components/schemas/AssetJobName" + "allOf": [ + { + "$ref": "#/components/schemas/AssetJobName" + } + ] } }, "required": [ @@ -8352,7 +8391,11 @@ "type": "string" }, "status": { - "$ref": "#/components/schemas/AssetMediaStatus" + "allOf": [ + { + "$ref": "#/components/schemas/AssetMediaStatus" + } + ] } }, "required": [ @@ -8490,7 +8533,11 @@ "type": "string" }, "type": { - "$ref": "#/components/schemas/AssetTypeEnum" + "allOf": [ + { + "$ref": "#/components/schemas/AssetTypeEnum" + } + ] }, "unassignedFaces": { "items": { @@ -8603,7 +8650,11 @@ "AvatarResponse": { "properties": { "color": { - "$ref": "#/components/schemas/UserAvatarColor" + "allOf": [ + { + "$ref": "#/components/schemas/UserAvatarColor" + } + ] } }, "required": [ @@ -8614,7 +8665,11 @@ "AvatarUpdate": { "properties": { "color": { - "$ref": "#/components/schemas/UserAvatarColor" + "allOf": [ + { + "$ref": "#/components/schemas/UserAvatarColor" + } + ] } }, "type": "object" @@ -8705,6 +8760,7 @@ "items": { "type": "string" }, + "minItems": 1, "type": "array" }, "deviceId": { @@ -8771,13 +8827,17 @@ "items": { "type": "string" }, - "type": "array" + "maxItems": 128, + "type": "array", + "uniqueItems": true }, "importPaths": { "items": { "type": "string" }, - "type": "array" + "maxItems": 128, + "type": "array", + "uniqueItems": true }, "name": { "type": "string" @@ -9246,10 +9306,18 @@ "type": "string" }, "entityType": { - "$ref": "#/components/schemas/PathEntityType" + "allOf": [ + { + "$ref": "#/components/schemas/PathEntityType" + } + ] }, "pathType": { - "$ref": "#/components/schemas/PathType" + "allOf": [ + { + "$ref": "#/components/schemas/PathType" + } + ] }, "pathValue": { "type": "string" @@ -9311,7 +9379,11 @@ "JobCommandDto": { "properties": { "command": { - "$ref": "#/components/schemas/JobCommand" + "allOf": [ + { + "$ref": "#/components/schemas/JobCommand" + } + ] }, "force": { "type": "boolean" @@ -9356,7 +9428,11 @@ "JobCreateDto": { "properties": { "name": { - "$ref": "#/components/schemas/ManualJobName" + "allOf": [ + { + "$ref": "#/components/schemas/ManualJobName" + } + ] } }, "required": [ @@ -9544,6 +9620,7 @@ "properties": { "email": { "example": "testuser@email.com", + "format": "email", "type": "string" }, "password": { @@ -9717,7 +9794,11 @@ "type": "string" }, "type": { - "$ref": "#/components/schemas/MemoryType" + "allOf": [ + { + "$ref": "#/components/schemas/MemoryType" + } + ] } }, "required": [ @@ -9782,7 +9863,11 @@ "type": "string" }, "type": { - "$ref": "#/components/schemas/MemoryType" + "allOf": [ + { + "$ref": "#/components/schemas/MemoryType" + } + ] }, "updatedAt": { "format": "date-time", @@ -9911,7 +9996,11 @@ "type": "string" }, "order": { - "$ref": "#/components/schemas/AssetOrder" + "allOf": [ + { + "$ref": "#/components/schemas/AssetOrder" + } + ] }, "originalFileName": { "type": "string" @@ -9962,7 +10051,11 @@ "type": "string" }, "type": { - "$ref": "#/components/schemas/AssetTypeEnum" + "allOf": [ + { + "$ref": "#/components/schemas/AssetTypeEnum" + } + ] }, "updatedAfter": { "format": "date-time", @@ -10046,7 +10139,11 @@ "PartnerResponseDto": { "properties": { "avatarColor": { - "$ref": "#/components/schemas/UserAvatarColor" + "allOf": [ + { + "$ref": "#/components/schemas/UserAvatarColor" + } + ] }, "email": { "type": "string" @@ -10564,7 +10661,11 @@ "type": "string" }, "type": { - "$ref": "#/components/schemas/AssetTypeEnum" + "allOf": [ + { + "$ref": "#/components/schemas/AssetTypeEnum" + } + ] }, "updatedAfter": { "format": "date-time", @@ -11232,7 +11333,11 @@ "type": "boolean" }, "type": { - "$ref": "#/components/schemas/SharedLinkType" + "allOf": [ + { + "$ref": "#/components/schemas/SharedLinkType" + } + ] } }, "required": [ @@ -11317,7 +11422,11 @@ "type": "string" }, "type": { - "$ref": "#/components/schemas/SharedLinkType" + "allOf": [ + { + "$ref": "#/components/schemas/SharedLinkType" + } + ] }, "userId": { "type": "string" @@ -11350,6 +11459,7 @@ "properties": { "email": { "example": "testuser@email.com", + "format": "email", "type": "string" }, "name": { @@ -11466,7 +11576,11 @@ "type": "string" }, "type": { - "$ref": "#/components/schemas/AssetTypeEnum" + "allOf": [ + { + "$ref": "#/components/schemas/AssetTypeEnum" + } + ] }, "updatedAfter": { "format": "date-time", @@ -11507,6 +11621,7 @@ "format": "uuid", "type": "string" }, + "minItems": 2, "type": "array" } }, @@ -11647,7 +11762,11 @@ "SystemConfigFFmpegDto": { "properties": { "accel": { - "$ref": "#/components/schemas/TranscodeHWAccel" + "allOf": [ + { + "$ref": "#/components/schemas/TranscodeHWAccel" + } + ] }, "accelDecode": { "type": "boolean" @@ -11676,7 +11795,11 @@ "type": "integer" }, "cqMode": { - "$ref": "#/components/schemas/CQMode" + "allOf": [ + { + "$ref": "#/components/schemas/CQMode" + } + ] }, "crf": { "maximum": 51, @@ -11702,13 +11825,21 @@ "type": "integer" }, "targetAudioCodec": { - "$ref": "#/components/schemas/AudioCodec" + "allOf": [ + { + "$ref": "#/components/schemas/AudioCodec" + } + ] }, "targetResolution": { "type": "string" }, "targetVideoCodec": { - "$ref": "#/components/schemas/VideoCodec" + "allOf": [ + { + "$ref": "#/components/schemas/VideoCodec" + } + ] }, "temporalAQ": { "type": "boolean" @@ -11718,10 +11849,18 @@ "type": "integer" }, "tonemap": { - "$ref": "#/components/schemas/ToneMapping" + "allOf": [ + { + "$ref": "#/components/schemas/ToneMapping" + } + ] }, "transcode": { - "$ref": "#/components/schemas/TranscodePolicy" + "allOf": [ + { + "$ref": "#/components/schemas/TranscodePolicy" + } + ] }, "twoPass": { "type": "boolean" @@ -11766,7 +11905,11 @@ "SystemConfigGeneratedImageDto": { "properties": { "format": { - "$ref": "#/components/schemas/ImageFormat" + "allOf": [ + { + "$ref": "#/components/schemas/ImageFormat" + } + ] }, "quality": { "maximum": 100, @@ -11788,7 +11931,11 @@ "SystemConfigImageDto": { "properties": { "colorspace": { - "$ref": "#/components/schemas/Colorspace" + "allOf": [ + { + "$ref": "#/components/schemas/Colorspace" + } + ] }, "extractEmbedded": { "type": "boolean" @@ -11906,7 +12053,11 @@ "type": "boolean" }, "level": { - "$ref": "#/components/schemas/LogLevel" + "allOf": [ + { + "$ref": "#/components/schemas/LogLevel" + } + ] } }, "required": [ @@ -11935,6 +12086,7 @@ "type": "string" }, "urls": { + "format": "uri", "items": { "format": "uri", "type": "string" @@ -11955,12 +12107,14 @@ "SystemConfigMapDto": { "properties": { "darkStyle": { + "format": "uri", "type": "string" }, "enabled": { "type": "boolean" }, "lightStyle": { + "format": "uri", "type": "string" } }, @@ -12035,6 +12189,7 @@ "type": "boolean" }, "mobileRedirectUri": { + "format": "uri", "type": "string" }, "profileSigningAlgorithm": { @@ -12097,6 +12252,7 @@ "SystemConfigServerDto": { "properties": { "externalDomain": { + "format": "uri", "type": "string" }, "loginPageMessage": { @@ -12353,6 +12509,7 @@ "TagCreateDto": { "properties": { "color": { + "pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$", "type": "string" }, "name": { @@ -12408,6 +12565,7 @@ "properties": { "color": { "nullable": true, + "pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$", "type": "string" } }, @@ -12570,7 +12728,11 @@ "type": "boolean" }, "order": { - "$ref": "#/components/schemas/AssetOrder" + "allOf": [ + { + "$ref": "#/components/schemas/AssetOrder" + } + ] } }, "type": "object" @@ -12578,7 +12740,11 @@ "UpdateAlbumUserDto": { "properties": { "role": { - "$ref": "#/components/schemas/AlbumUserRole" + "allOf": [ + { + "$ref": "#/components/schemas/AlbumUserRole" + } + ] } }, "required": [ @@ -12625,13 +12791,17 @@ "items": { "type": "string" }, - "type": "array" + "maxItems": 128, + "type": "array", + "uniqueItems": true }, "importPaths": { "items": { "type": "string" }, - "type": "array" + "maxItems": 128, + "type": "array", + "uniqueItems": true }, "name": { "type": "string" @@ -12697,6 +12867,7 @@ "UserAdminCreateDto": { "properties": { "email": { + "format": "email", "type": "string" }, "name": { @@ -12740,7 +12911,11 @@ "UserAdminResponseDto": { "properties": { "avatarColor": { - "$ref": "#/components/schemas/UserAvatarColor" + "allOf": [ + { + "$ref": "#/components/schemas/UserAvatarColor" + } + ] }, "createdAt": { "format": "date-time", @@ -12795,7 +12970,11 @@ "type": "boolean" }, "status": { - "$ref": "#/components/schemas/UserStatus" + "allOf": [ + { + "$ref": "#/components/schemas/UserStatus" + } + ] }, "storageLabel": { "nullable": true, @@ -12830,6 +13009,7 @@ "UserAdminUpdateDto": { "properties": { "email": { + "format": "email", "type": "string" }, "name": { @@ -12967,7 +13147,11 @@ "UserResponseDto": { "properties": { "avatarColor": { - "$ref": "#/components/schemas/UserAvatarColor" + "allOf": [ + { + "$ref": "#/components/schemas/UserAvatarColor" + } + ] }, "email": { "type": "string" @@ -13007,6 +13191,7 @@ "UserUpdateMeDto": { "properties": { "email": { + "format": "email", "type": "string" }, "name": { @@ -13035,13 +13220,17 @@ "items": { "type": "string" }, - "type": "array" + "maxItems": 128, + "type": "array", + "uniqueItems": true }, "importPaths": { "items": { "type": "string" }, - "type": "array" + "maxItems": 128, + "type": "array", + "uniqueItems": true } }, "type": "object" diff --git a/server/package-lock.json b/server/package-lock.json index 347757a90b..3bdc0dc3da 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -16,7 +16,7 @@ "@nestjs/platform-express": "^10.2.2", "@nestjs/platform-socket.io": "^10.2.2", "@nestjs/schedule": "^4.0.0", - "@nestjs/swagger": "^7.1.8", + "@nestjs/swagger": "^8.0.0", "@nestjs/typeorm": "^10.0.0", "@nestjs/websockets": "^10.2.2", "@opentelemetry/auto-instrumentations-node": "^0.54.0", @@ -2099,9 +2099,9 @@ } }, "node_modules/@nestjs/mapped-types": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", - "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz", + "integrity": "sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==", "license": "MIT", "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", @@ -2197,17 +2197,17 @@ "license": "MIT" }, "node_modules/@nestjs/swagger": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.2.tgz", - "integrity": "sha512-Mu6TEn1M/owIvAx2B4DUQObQXqo2028R2s9rSZ/hJEgBK95+doTwS0DjmVA2wTeZTyVtXOoN7CsoM5pONBzvKQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-8.1.0.tgz", + "integrity": "sha512-8hzH+r/31XshzXHC9vww4T0xjDAxMzvOaT1xAOvvY1LtXTWyNRCUP2iQsCYJOnnMrR+vydWjvRZiuB3hdvaHxA==", "license": "MIT", "dependencies": { "@microsoft/tsdoc": "^0.15.0", - "@nestjs/mapped-types": "2.0.5", + "@nestjs/mapped-types": "2.0.6", "js-yaml": "4.1.0", "lodash": "4.17.21", "path-to-regexp": "3.3.0", - "swagger-ui-dist": "5.17.14" + "swagger-ui-dist": "5.18.2" }, "peerDependencies": { "@fastify/static": "^6.0.0 || ^7.0.0", @@ -4464,6 +4464,13 @@ "win32" ] }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@selderee/plugin-htmlparser2": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", @@ -13764,10 +13771,13 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.17.14", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", - "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", - "license": "Apache-2.0" + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz", + "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } }, "node_modules/symbol-observable": { "version": "4.0.0", diff --git a/server/package.json b/server/package.json index dcb166bb06..074dafa5d3 100644 --- a/server/package.json +++ b/server/package.json @@ -41,7 +41,7 @@ "@nestjs/platform-express": "^10.2.2", "@nestjs/platform-socket.io": "^10.2.2", "@nestjs/schedule": "^4.0.0", - "@nestjs/swagger": "^7.1.8", + "@nestjs/swagger": "^8.0.0", "@nestjs/typeorm": "^10.0.0", "@nestjs/websockets": "^10.2.2", "@opentelemetry/auto-instrumentations-node": "^0.54.0", From ef0070c3fd0379d319670ec0828138b0791ede0d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 20:04:55 -0500 Subject: [PATCH 42/42] fix(deps): update machine-learning (#14891) --- machine-learning/poetry.lock | 246 +++++++++++++++++------------------ 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index bfed9fab8d..eb8fe31dff 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -1331,13 +1331,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "huggingface-hub" -version = "0.26.5" +version = "0.27.0" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.8.0" files = [ - {file = "huggingface_hub-0.26.5-py3-none-any.whl", hash = "sha256:fb7386090bbe892072e64b85f7c4479fd2d65eea5f2543327c970d5169e83924"}, - {file = "huggingface_hub-0.26.5.tar.gz", hash = "sha256:1008bd18f60bfb65e8dbc0a97249beeeaa8c99d3c2fa649354df9fa5a13ed83b"}, + {file = "huggingface_hub-0.27.0-py3-none-any.whl", hash = "sha256:8f2e834517f1f1ddf1ecc716f91b120d7333011b7485f665a9a412eacb1a2a81"}, + {file = "huggingface_hub-0.27.0.tar.gz", hash = "sha256:902cce1a1be5739f5589e560198a65a8edcfd3b830b1666f36e4b961f0454fac"}, ] [package.dependencies] @@ -2492,18 +2492,18 @@ files = [ [[package]] name = "pydantic" -version = "2.10.3" +version = "2.10.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, - {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, + {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, + {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.27.1" +pydantic-core = "2.27.2" typing-extensions = ">=4.12.2" [package.extras] @@ -2512,111 +2512,111 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.27.1" +version = "2.27.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, - {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, - {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, - {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, - {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, - {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, - {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, - {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, - {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, - {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, - {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, - {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, - {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, - {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, - {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, - {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, - {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, - {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, - {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, - {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, - {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, - {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, - {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, - {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, - {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, - {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, - {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, - {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, - {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, - {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, - {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, - {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, - {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, - {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, - {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, - {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, - {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, - {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, - {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, - {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, - {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, - {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, - {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, ] [package.dependencies] @@ -2624,13 +2624,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.6.1" +version = "2.7.0" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87"}, - {file = "pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0"}, + {file = "pydantic_settings-2.7.0-py3-none-any.whl", hash = "sha256:e00c05d5fa6cbbb227c84bd7487c5c1065084119b750df7c8c1a554aed236eb5"}, + {file = "pydantic_settings-2.7.0.tar.gz", hash = "sha256:ac4bfd4a36831a48dbf8b2d9325425b549a0a6f18cea118436d728eb4f1c4d66"}, ] [package.dependencies] @@ -2706,20 +2706,20 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-asyncio" -version = "0.24.0" +version = "0.25.0" description = "Pytest support for asyncio" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, - {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, + {file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"}, + {file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"}, ] [package.dependencies] pytest = ">=8.2,<9" [package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] @@ -2787,13 +2787,13 @@ cli = ["click (>=5.0)"] [[package]] name = "python-multipart" -version = "0.0.19" +version = "0.0.20" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.8" files = [ - {file = "python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d"}, - {file = "python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc"}, + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, ] [[package]] @@ -3393,13 +3393,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.32.1" +version = "0.34.0" description = "The lightning-fast ASGI server." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"}, - {file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"}, + {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, + {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, ] [package.dependencies]