diff --git a/i18n/en.json b/i18n/en.json index e8726b3d20..e08c8ec545 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1082,7 +1082,9 @@ "remove_url": "Remove URL", "remove_user": "Remove user", "removed_api_key": "Removed API Key: {name}", + "remove_memory": "Remove memory", "removed_memory": "Removed memory", + "remove_photo_from_memory": "Remove photo from this memory", "removed_photo_from_memory": "Removed photo from memory", "removed_from_archive": "Removed from archive", "removed_from_favorites": "Removed from favorites", diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index 9eef78d4d0..36f4631ef5 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -2,6 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { MemoryType } from 'src/enum'; import { MemoryItem } from 'src/types'; @@ -88,7 +89,7 @@ export class MemoryResponseDto { assets!: AssetResponseDto[]; } -export const mapMemory = (entity: MemoryItem): MemoryResponseDto => { +export const mapMemory = (entity: MemoryItem, auth: AuthDto): MemoryResponseDto => { return { id: entity.id, createdAt: entity.createdAt, @@ -102,6 +103,6 @@ export const mapMemory = (entity: MemoryItem): MemoryResponseDto => { type: entity.type as MemoryType, data: entity.data as unknown as MemoryData, isSaved: entity.isSaved, - assets: ('assets' in entity ? entity.assets : []).map((asset) => mapAsset(asset as AssetEntity)), + assets: ('assets' in entity ? entity.assets : []).map((asset) => mapAsset(asset as AssetEntity, { auth })), }; }; diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 28c90f6576..8ad3c27b4d 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -74,13 +74,13 @@ export class MemoryService extends BaseService { async search(auth: AuthDto, dto: MemorySearchDto) { const memories = await this.memoryRepository.search(auth.user.id, dto); - return memories.map((memory) => mapMemory(memory)); + return memories.map((memory) => mapMemory(memory, auth)); } async get(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.MEMORY_READ, ids: [id] }); const memory = await this.findOrFail(id); - return mapMemory(memory); + return mapMemory(memory, auth); } async create(auth: AuthDto, dto: MemoryCreateDto) { @@ -104,7 +104,7 @@ export class MemoryService extends BaseService { allowedAssetIds, ); - return mapMemory(memory); + return mapMemory(memory, auth); } async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise { @@ -116,7 +116,7 @@ export class MemoryService extends BaseService { seenAt: dto.seenAt, }); - return mapMemory(memory); + return mapMemory(memory, auth); } async remove(auth: AuthDto, id: string): Promise { diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index 9a6ad628a3..ceaabd8387 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -1,6 +1,6 @@ @@ -350,24 +315,24 @@ - + - + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {/if} - + {/if}
- {#if current && current.memory.assets.length > 0} + {#if current} goto(AppRoute.PHOTOS)} forceDark multiRow> {#snippet leading()} {#if current} @@ -381,22 +346,14 @@ handleAction(paused ? 'play' : 'pause')} + onclick={() => handlePromiseError(handleAction('PlayPauseButtonClick', paused ? 'play' : 'pause'))} class="hover:text-black" /> {#each current.memory.assets as asset, index (asset.id)} - + - {#await resetPromise} - - {:then} - current.assetIndex ? 0 : $progressBarController * 100}%`} - > - {/await} + {/each} @@ -474,7 +431,7 @@
{#key current.asset.id}
- {#if current.asset.type == AssetTypeEnum.Video} + {#if current.asset.type === AssetTypeEnum.Video}
diff --git a/web/src/lib/stores/memory.store.svelte.ts b/web/src/lib/stores/memory.store.svelte.ts new file mode 100644 index 0000000000..76e406682d --- /dev/null +++ b/web/src/lib/stores/memory.store.svelte.ts @@ -0,0 +1,119 @@ +import { asLocalTimeISO } from '$lib/utils/date-time'; +import { + type AssetResponseDto, + deleteMemory, + type MemoryResponseDto, + removeMemoryAssets, + searchMemories, + updateMemory, +} from '@immich/sdk'; +import { DateTime } from 'luxon'; + +type MemoryIndex = { + memoryIndex: number; + assetIndex: number; +}; + +export type MemoryAsset = MemoryIndex & { + memory: MemoryResponseDto; + asset: AssetResponseDto; + previousMemory?: MemoryResponseDto; + previous?: MemoryAsset; + next?: MemoryAsset; + nextMemory?: MemoryResponseDto; +}; + +class MemoryStoreSvelte { + memories = $state([]); + private initialized = false; + private memoryAssets = $derived.by(() => { + const memoryAssets: MemoryAsset[] = []; + let previous: MemoryAsset | undefined; + for (const [memoryIndex, memory] of this.memories.entries()) { + for (const [assetIndex, asset] of memory.assets.entries()) { + const current = { + memory, + memoryIndex, + previousMemory: this.memories[memoryIndex - 1], + nextMemory: this.memories[memoryIndex + 1], + asset, + assetIndex, + previous, + }; + + memoryAssets.push(current); + + if (previous) { + previous.next = current; + } + + previous = current; + } + } + + return memoryAssets; + }); + + getMemoryAsset(assetId: string | undefined) { + return this.memoryAssets.find((memoryAsset) => memoryAsset.asset.id === assetId) ?? this.memoryAssets[0]; + } + + hideAssetsFromMemory(ids: string[]) { + const idSet = new Set(ids); + for (const memory of this.memories) { + memory.assets = memory.assets.filter((asset) => !idSet.has(asset.id)); + } + // if we removed all assets from a memory, then lets remove those memories (we don't show memories with 0 assets) + this.memories = this.memories.filter((memory) => memory.assets.length > 0); + } + + async deleteMemory(id: string) { + const memory = this.memories.find((memory) => memory.id === id); + if (memory) { + await deleteMemory({ id: memory.id }); + this.memories = this.memories.filter((memory) => memory.id !== id); + } + } + + async deleteAssetFromMemory(assetId: string) { + const memoryWithAsset = this.memories.find((memory) => memory.assets.some((asset) => asset.id === assetId)); + + if (memoryWithAsset) { + if (memoryWithAsset.assets.length === 1) { + await this.deleteMemory(memoryWithAsset.id); + } else { + await removeMemoryAssets({ id: memoryWithAsset.id, bulkIdsDto: { ids: [assetId] } }); + memoryWithAsset.assets = memoryWithAsset.assets.filter((asset) => asset.id !== assetId); + } + } + } + + async updateMemorySaved(id: string, isSaved: boolean) { + const memory = this.memories.find((memory) => memory.id === id); + if (memory) { + await updateMemory({ + id, + memoryUpdateDto: { + isSaved, + }, + }); + memory.isSaved = isSaved; + } + } + + async initialize() { + if (this.initialized) { + return; + } + this.initialized = true; + + await this.loadAllMemories(); + } + + private async loadAllMemories() { + const memories = await searchMemories({ $for: asLocalTimeISO(DateTime.now()) }); + this.memories = memories.filter((memory) => memory.assets.length > 0); + } +} + +export const memoryStore = new MemoryStoreSvelte(); diff --git a/web/src/lib/stores/memory.store.ts b/web/src/lib/stores/memory.store.ts deleted file mode 100644 index a927ab648a..0000000000 --- a/web/src/lib/stores/memory.store.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { asLocalTimeISO } from '$lib/utils/date-time'; -import { searchMemories, type MemoryResponseDto } from '@immich/sdk'; -import { DateTime } from 'luxon'; -import { writable } from 'svelte/store'; - -export const memoryStore = writable(); - -export const loadMemories = async () => { - const memories = await searchMemories({ $for: asLocalTimeISO(DateTime.now()) }); - memoryStore.set(memories); -};