Pārlūkot izejas kodu

feat: remove asset from memory

martabal 1 gadu atpakaļ
vecāks
revīzija
b9c0a3c641

+ 12 - 0
cli/src/api/open-api/api.ts

@@ -399,6 +399,12 @@ export interface AssetBulkUpdateDto {
      * @memberof AssetBulkUpdateDto
      */
     'isFavorite'?: boolean;
+    /**
+     * 
+     * @type {boolean}
+     * @memberof AssetBulkUpdateDto
+     */
+    'isShownInMemory'?: boolean;
 }
 /**
  * 
@@ -682,6 +688,12 @@ export interface AssetResponseDto {
      * @memberof AssetResponseDto
      */
     'isReadOnly': boolean;
+    /**
+     * 
+     * @type {boolean}
+     * @memberof AssetResponseDto
+     */
+    'isShownInMemory': boolean;
     /**
      * 
      * @type {boolean}

+ 1 - 0
mobile/openapi/doc/AssetBulkUpdateDto.md

@@ -11,6 +11,7 @@ Name | Type | Description | Notes
 **ids** | **List<String>** |  | [default to const []]
 **isArchived** | **bool** |  | [optional] 
 **isFavorite** | **bool** |  | [optional] 
+**isShownInMemory** | **bool** |  | [optional] 
 
 [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 

+ 1 - 0
mobile/openapi/doc/AssetResponseDto.md

@@ -22,6 +22,7 @@ Name | Type | Description | Notes
 **isFavorite** | **bool** |  | 
 **isOffline** | **bool** |  | 
 **isReadOnly** | **bool** |  | 
+**isShownInMemory** | **bool** |  | 
 **isTrashed** | **bool** |  | 
 **libraryId** | **String** |  | 
 **livePhotoVideoId** | **String** |  | [optional] 

+ 20 - 3
mobile/openapi/lib/model/asset_bulk_update_dto.dart

@@ -16,6 +16,7 @@ class AssetBulkUpdateDto {
     this.ids = const [],
     this.isArchived,
     this.isFavorite,
+    this.isShownInMemory,
   });
 
   List<String> ids;
@@ -36,21 +37,31 @@ class AssetBulkUpdateDto {
   ///
   bool? isFavorite;
 
+  ///
+  /// 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.
+  ///
+  bool? isShownInMemory;
+
   @override
   bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
      other.ids == ids &&
      other.isArchived == isArchived &&
-     other.isFavorite == isFavorite;
+     other.isFavorite == isFavorite &&
+     other.isShownInMemory == isShownInMemory;
 
   @override
   int get hashCode =>
     // ignore: unnecessary_parenthesis
     (ids.hashCode) +
     (isArchived == null ? 0 : isArchived!.hashCode) +
-    (isFavorite == null ? 0 : isFavorite!.hashCode);
+    (isFavorite == null ? 0 : isFavorite!.hashCode) +
+    (isShownInMemory == null ? 0 : isShownInMemory!.hashCode);
 
   @override
-  String toString() => 'AssetBulkUpdateDto[ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite]';
+  String toString() => 'AssetBulkUpdateDto[ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, isShownInMemory=$isShownInMemory]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -65,6 +76,11 @@ class AssetBulkUpdateDto {
     } else {
     //  json[r'isFavorite'] = null;
     }
+    if (this.isShownInMemory != null) {
+      json[r'isShownInMemory'] = this.isShownInMemory;
+    } else {
+    //  json[r'isShownInMemory'] = null;
+    }
     return json;
   }
 
@@ -81,6 +97,7 @@ class AssetBulkUpdateDto {
             : const [],
         isArchived: mapValueOfType<bool>(json, r'isArchived'),
         isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
+        isShownInMemory: mapValueOfType<bool>(json, r'isShownInMemory'),
       );
     }
     return null;

+ 9 - 1
mobile/openapi/lib/model/asset_response_dto.dart

@@ -27,6 +27,7 @@ class AssetResponseDto {
     required this.isFavorite,
     required this.isOffline,
     required this.isReadOnly,
+    required this.isShownInMemory,
     required this.isTrashed,
     required this.libraryId,
     this.livePhotoVideoId,
@@ -79,6 +80,8 @@ class AssetResponseDto {
 
   bool isReadOnly;
 
+  bool isShownInMemory;
+
   bool isTrashed;
 
   String libraryId;
@@ -137,6 +140,7 @@ class AssetResponseDto {
      other.isFavorite == isFavorite &&
      other.isOffline == isOffline &&
      other.isReadOnly == isReadOnly &&
+     other.isShownInMemory == isShownInMemory &&
      other.isTrashed == isTrashed &&
      other.libraryId == libraryId &&
      other.livePhotoVideoId == livePhotoVideoId &&
@@ -170,6 +174,7 @@ class AssetResponseDto {
     (isFavorite.hashCode) +
     (isOffline.hashCode) +
     (isReadOnly.hashCode) +
+    (isShownInMemory.hashCode) +
     (isTrashed.hashCode) +
     (libraryId.hashCode) +
     (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
@@ -187,7 +192,7 @@ class AssetResponseDto {
     (updatedAt.hashCode);
 
   @override
-  String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]';
+  String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, isShownInMemory=$isShownInMemory, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -209,6 +214,7 @@ class AssetResponseDto {
       json[r'isFavorite'] = this.isFavorite;
       json[r'isOffline'] = this.isOffline;
       json[r'isReadOnly'] = this.isReadOnly;
+      json[r'isShownInMemory'] = this.isShownInMemory;
       json[r'isTrashed'] = this.isTrashed;
       json[r'libraryId'] = this.libraryId;
     if (this.livePhotoVideoId != null) {
@@ -265,6 +271,7 @@ class AssetResponseDto {
         isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
         isOffline: mapValueOfType<bool>(json, r'isOffline')!,
         isReadOnly: mapValueOfType<bool>(json, r'isReadOnly')!,
+        isShownInMemory: mapValueOfType<bool>(json, r'isShownInMemory')!,
         isTrashed: mapValueOfType<bool>(json, r'isTrashed')!,
         libraryId: mapValueOfType<String>(json, r'libraryId')!,
         livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
@@ -340,6 +347,7 @@ class AssetResponseDto {
     'isFavorite',
     'isOffline',
     'isReadOnly',
+    'isShownInMemory',
     'isTrashed',
     'libraryId',
     'localDateTime',

+ 5 - 0
mobile/openapi/test/asset_bulk_update_dto_test.dart

@@ -31,6 +31,11 @@ void main() {
       // TODO
     });
 
+    // bool isShownInMemory
+    test('to test the property `isShownInMemory`', () async {
+      // TODO
+    });
+
 
   });
 

+ 5 - 0
mobile/openapi/test/asset_response_dto_test.dart

@@ -87,6 +87,11 @@ void main() {
       // TODO
     });
 
+    // bool isShownInMemory
+    test('to test the property `isShownInMemory`', () async {
+      // TODO
+    });
+
     // bool isTrashed
     test('to test the property `isTrashed`', () async {
       // TODO

+ 7 - 0
server/immich-openapi-specs.json

@@ -5696,6 +5696,9 @@
           },
           "isFavorite": {
             "type": "boolean"
+          },
+          "isShownInMemory": {
+            "type": "boolean"
           }
         },
         "required": [
@@ -5903,6 +5906,9 @@
           "isReadOnly": {
             "type": "boolean"
           },
+          "isShownInMemory": {
+            "type": "boolean"
+          },
           "isTrashed": {
             "type": "boolean"
           },
@@ -5971,6 +5977,7 @@
           "fileCreatedAt",
           "fileModifiedAt",
           "updatedAt",
+          "isShownInMemory",
           "isFavorite",
           "isArchived",
           "isTrashed",

+ 1 - 1
server/src/domain/asset/asset.service.ts

@@ -165,7 +165,7 @@ export class AssetService {
     const assets = await this.assetRepository.getByDayOfYear(authUser.id, dto);
 
     return _.chain(assets)
-      .filter((asset) => asset.localDateTime.getFullYear() < currentYear)
+      .filter((asset) => asset.localDateTime.getFullYear() < currentYear && asset.isShownInMemory)
       .map((asset) => {
         const years = currentYear - asset.localDateTime.getFullYear();
 

+ 4 - 0
server/src/domain/asset/dto/asset.dto.ts

@@ -11,6 +11,10 @@ export class AssetBulkUpdateDto extends BulkIdsDto {
   @Optional()
   @IsBoolean()
   isArchived?: boolean;
+
+  @Optional()
+  @IsBoolean()
+  isShownInMemory?: boolean;
 }
 
 export class UpdateAssetDto {

+ 2 - 0
server/src/domain/asset/response-dto/asset-response.dto.ts

@@ -30,6 +30,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
   fileCreatedAt!: Date;
   fileModifiedAt!: Date;
   updatedAt!: Date;
+  isShownInMemory!: boolean;
   isFavorite!: boolean;
   isArchived!: boolean;
   isTrashed!: boolean;
@@ -77,6 +78,7 @@ export function mapAsset(entity: AssetEntity, stripMetadata = false): AssetRespo
     fileModifiedAt: entity.fileModifiedAt,
     localDateTime: entity.localDateTime,
     updatedAt: entity.updatedAt,
+    isShownInMemory: entity.isShownInMemory,
     isFavorite: entity.isFavorite,
     isArchived: entity.isArchived,
     isTrashed: !!entity.deletedAt,

+ 3 - 0
server/src/infra/entities/asset.entity.ts

@@ -106,6 +106,9 @@ export class AssetEntity {
   @Column({ type: 'boolean', default: false })
   isOffline!: boolean;
 
+  @Column({ type: 'boolean', default: true })
+  isShownInMemory!: boolean;
+
   @Column({ type: 'bytea' })
   @Index()
   checksum!: Buffer; // sha1 checksum

+ 28 - 0
server/src/infra/migrations/1697484859613-RemoveFromMemory.ts

@@ -0,0 +1,28 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class RemoveFromMemory1697484859613 implements MigrationInterface {
+    name = 'RemoveFromMemory1697484859613'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "isSkipMotion" TO "isShownInMemory"`);
+        await queryRunner.query(`ALTER TABLE "asset_faces" DROP CONSTRAINT "PK_6df76ab2eb6f5b57b7c2f1fc684"`);
+        await queryRunner.query(`ALTER TABLE "asset_faces" DROP COLUMN "id"`);
+        await queryRunner.query(`ALTER TABLE "asset_faces" ADD CONSTRAINT "PK_bf339a24070dac7e71304ec530a" PRIMARY KEY ("assetId", "personId")`);
+        await queryRunner.query(`ALTER TABLE "asset_faces" DROP CONSTRAINT "FK_95ad7106dd7b484275443f580f9"`);
+        await queryRunner.query(`ALTER TABLE "asset_faces" ALTER COLUMN "personId" SET NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "isShownInMemory" SET DEFAULT true`);
+        await queryRunner.query(`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_95ad7106dd7b484275443f580f9" FOREIGN KEY ("personId") REFERENCES "person"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "asset_faces" DROP CONSTRAINT "FK_95ad7106dd7b484275443f580f9"`);
+        await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "isShownInMemory" SET DEFAULT false`);
+        await queryRunner.query(`ALTER TABLE "asset_faces" ALTER COLUMN "personId" DROP NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_95ad7106dd7b484275443f580f9" FOREIGN KEY ("personId") REFERENCES "person"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
+        await queryRunner.query(`ALTER TABLE "asset_faces" DROP CONSTRAINT "PK_bf339a24070dac7e71304ec530a"`);
+        await queryRunner.query(`ALTER TABLE "asset_faces" ADD "id" uuid NOT NULL DEFAULT uuid_generate_v4()`);
+        await queryRunner.query(`ALTER TABLE "asset_faces" ADD CONSTRAINT "PK_6df76ab2eb6f5b57b7c2f1fc684" PRIMARY KEY ("id")`);
+        await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "isShownInMemory" TO "isSkipMotion"`);
+    }
+
+}

+ 12 - 0
server/test/fixtures/asset.stub.ts

@@ -24,6 +24,7 @@ export const assetStub = {
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
+    isShownInMemory: true,
     isFavorite: true,
     isArchived: false,
     duration: null,
@@ -59,6 +60,7 @@ export const assetStub = {
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
+    isShownInMemory: true,
     isFavorite: true,
     isArchived: false,
     duration: null,
@@ -98,6 +100,7 @@ export const assetStub = {
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
+    isShownInMemory: true,
     isFavorite: true,
     isArchived: false,
     isReadOnly: false,
@@ -134,6 +137,7 @@ export const assetStub = {
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
+    isShownInMemory: true,
     isFavorite: true,
     isArchived: false,
     isReadOnly: false,
@@ -173,6 +177,7 @@ export const assetStub = {
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
+    isShownInMemory: true,
     isFavorite: true,
     isArchived: false,
     isReadOnly: false,
@@ -212,6 +217,7 @@ export const assetStub = {
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
+    isShownInMemory: true,
     isFavorite: true,
     isArchived: false,
     isReadOnly: false,
@@ -251,6 +257,7 @@ export const assetStub = {
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
+    isShownInMemory: true,
     isFavorite: true,
     isArchived: false,
     isReadOnly: false,
@@ -291,6 +298,7 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     deletedAt: null,
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
+    isShownInMemory: true,
     isFavorite: true,
     isArchived: false,
     isReadOnly: false,
@@ -329,6 +337,7 @@ export const assetStub = {
     createdAt: new Date('2015-02-23T05:06:29.716Z'),
     updatedAt: new Date('2015-02-23T05:06:29.716Z'),
     localDateTime: new Date('2015-02-23T05:06:29.716Z'),
+    isShownInMemory: true,
     isFavorite: true,
     isArchived: false,
     isExternal: false,
@@ -369,6 +378,7 @@ export const assetStub = {
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
+    isShownInMemory: true,
     isFavorite: true,
     isArchived: false,
     isReadOnly: false,
@@ -439,6 +449,7 @@ export const assetStub = {
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
+    isShownInMemory: true,
     isFavorite: false,
     isArchived: false,
     isReadOnly: false,
@@ -479,6 +490,7 @@ export const assetStub = {
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
+    isShownInMemory: true,
     isFavorite: true,
     isArchived: false,
     isReadOnly: false,

+ 2 - 0
server/test/fixtures/shared-link.stub.ts

@@ -51,6 +51,7 @@ const assetResponse: AssetResponseDto = {
   resized: false,
   thumbhash: null,
   fileModifiedAt: today,
+  isShownInMemory: true,
   isExternal: false,
   isReadOnly: false,
   isOffline: false,
@@ -191,6 +192,7 @@ export const sharedLinkStub = {
           localDateTime: today,
           createdAt: today,
           updatedAt: today,
+          isShownInMemory: true,
           isFavorite: false,
           isArchived: false,
           isExternal: false,

+ 12 - 0
web/src/api/open-api/api.ts

@@ -399,6 +399,12 @@ export interface AssetBulkUpdateDto {
      * @memberof AssetBulkUpdateDto
      */
     'isFavorite'?: boolean;
+    /**
+     * 
+     * @type {boolean}
+     * @memberof AssetBulkUpdateDto
+     */
+    'isShownInMemory'?: boolean;
 }
 /**
  * 
@@ -682,6 +688,12 @@ export interface AssetResponseDto {
      * @memberof AssetResponseDto
      */
     'isReadOnly': boolean;
+    /**
+     * 
+     * @type {boolean}
+     * @memberof AssetResponseDto
+     */
+    'isShownInMemory': boolean;
     /**
      * 
      * @type {boolean}

+ 4 - 0
web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte

@@ -49,6 +49,7 @@
     asProfileImage: void;
     runJob: AssetJobName;
     playSlideShow: void;
+    enableMemories: void;
   }>();
 
   let contextMenuPosition = { x: 0, y: 0 };
@@ -185,6 +186,9 @@
                   text={api.getAssetJobName(AssetJobName.TranscodeVideo)}
                 />
               {/if}
+              {#if !asset.isShownInMemory}
+                <MenuOption on:click={() => dispatch('enableMemories')} text="Show in memories" />
+              {/if}
             {/if}
           </ContextMenu>
         {/if}

+ 15 - 0
web/src/lib/components/asset-viewer/asset-viewer.svelte

@@ -346,6 +346,20 @@
     }
   };
 
+  const handleEnableMemories = async () => {
+    try {
+      await api.assetApi.updateAssets({ assetBulkUpdateDto: { ids: [asset.id], isShownInMemory: true } });
+
+      notificationController.show({
+        message: `Asset can be shown in memories`,
+        type: NotificationType.Info,
+      });
+      asset.isShownInMemory = true;
+    } catch (error) {
+      handleError(error, "Can't remove asset from memory");
+    }
+  };
+
   const handleStopSlideshow = async () => {
     try {
       await document.exitFullscreen();
@@ -408,6 +422,7 @@
         on:asProfileImage={() => (isShowProfileImageCrop = true)}
         on:runJob={({ detail: job }) => handleRunJob(job)}
         on:playSlideShow={handlePlaySlideshow}
+        on:enableMemories={handleEnableMemories}
       />
     {/if}
   </div>

+ 79 - 0
web/src/lib/components/memory-page/memory-viewer.svelte

@@ -20,6 +20,12 @@
   import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
   import { fade } from 'svelte/transition';
   import { tweened } from 'svelte/motion';
+  import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
+  import MenuOption from '../shared-components/context-menu/menu-option.svelte';
+  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
+  import { clickOutside } from '$lib/utils/click-outside';
+  import { handleError } from '$lib/utils/handle-error';
+  import { NotificationType, notificationController } from '../shared-components/notification/notification';
 
   const parseIndex = (s: string | null, max: number | null) => Math.max(Math.min(parseInt(s ?? '') || 0, max ?? 0), 0);
 
@@ -58,6 +64,69 @@
 
   let paused = false;
 
+  let isShowAssetOptions = false;
+  let contextMenuPosition = { x: 0, y: 0 };
+
+  const updateMemoryStoreAsset = () => {
+    if ($memoryStore && $memoryStore[memoryIndex].assets[assetIndex]) {
+      memoryStore.update((memoryStore) => {
+        const updatedMemoryStore = [...memoryStore];
+
+        if (memoryIndex >= 0 && memoryIndex < updatedMemoryStore.length) {
+          const memoryItem = updatedMemoryStore[memoryIndex];
+
+          if (assetIndex >= 0 && memoryItem.assets.length > 1) {
+            memoryItem.assets.splice(assetIndex, 1);
+          }
+        }
+        return updatedMemoryStore; // Return the updated memoryStore
+      });
+    }
+  };
+
+  const updateMemoryStoreMemory = () => {
+    if ($memoryStore && $memoryStore[memoryIndex].assets[assetIndex]) {
+      memoryStore.update((memoryStore) => {
+        const updatedMemoryStore = [...memoryStore];
+        updatedMemoryStore.splice(memoryIndex, 1);
+
+        return updatedMemoryStore; // Return the updated memoryStore
+      });
+    }
+  };
+
+  const removeFromMemory = async () => {
+    try {
+      await api.assetApi.updateAssets({ assetBulkUpdateDto: { ids: [currentAsset.id], isShownInMemory: false } });
+
+      notificationController.show({
+        message: `Removed asset from memory`,
+        type: NotificationType.Info,
+      });
+      if (currentMemory?.assets.length === 1) {
+        if ($memoryStore?.length === 1) {
+          goto(AppRoute.PHOTOS);
+        } else {
+          if ($memoryStore?.length === memoryIndex + 1) {
+            toPreviousMemory();
+          } else {
+            toNextMemory();
+          }
+          updateMemoryStoreAsset();
+          updateMemoryStoreMemory();
+        }
+      } else {
+        updateMemoryStoreAsset();
+      }
+      isShowAssetOptions = false;
+    } catch (error) {
+      handleError(error, "Can't remove asset from memory");
+    }
+  };
+  const handleRemoveFromMemory = () => {
+    isShowAssetOptions = !isShowAssetOptions;
+  };
+
   // Play or pause progress when the paused state changes.
   $: paused ? pause() : play();
 
@@ -222,6 +291,16 @@
                 {currentAsset.exifInfo?.country || ''}
               </p>
             </div>
+            <div class="absolute right-8 top-4 text-sm font-medium text-white">
+              <div use:clickOutside on:outclick={() => (isShowAssetOptions = false)}>
+                <CircleIconButton isOpacity={true} logo={DotsVertical} on:click={handleRemoveFromMemory} title="More" />
+                {#if isShowAssetOptions}
+                  <ContextMenu {...contextMenuPosition} direction="left">
+                    <MenuOption on:click={removeFromMemory} text="Remove from memories" />
+                  </ContextMenu>
+                {/if}
+              </div>
+            </div>
           </div>
         </div>