diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e19f9db6fc..dc7d22fc57 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -504,6 +504,11 @@ jobs:
         with:
           channel: 'stable'
           flutter-version-file: ./mobile/pubspec.yaml
+
+      - name: Generate translation file
+        run: make translation
+        working-directory: ./mobile
+
       - name: Run tests
         working-directory: ./mobile
         run: flutter test -j 1
diff --git a/docs/src/pages/roadmap.tsx b/docs/src/pages/roadmap.tsx
index 50f7a47902..1258000052 100644
--- a/docs/src/pages/roadmap.tsx
+++ b/docs/src/pages/roadmap.tsx
@@ -218,7 +218,7 @@ const roadmap: Item[] = [
     iconColor: 'indianred',
     title: 'Stable release',
     description: 'Immich goes stable',
-    getDateLabel: () => 'Planned for early 2025',
+    getDateLabel: () => 'Planned for 2025',
   },
   {
     done: false,
diff --git a/i18n/en.json b/i18n/en.json
index 94a59f686f..fbc5f32f59 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -26,7 +26,7 @@
   "add_to_album": "Add to album",
   "add_to_album_bottom_sheet_added": "Added to {album}",
   "add_to_album_bottom_sheet_already_exists": "Already in {album}",
-  "add_to_locked_folder": "Add to Locked Folder",
+  "add_to_locked_folder": "Add to locked folder",
   "add_to_shared_album": "Add to shared album",
   "add_url": "Add URL",
   "added_to_archive": "Added to archive",
@@ -1157,7 +1157,7 @@
   "location_picker_longitude_error": "Enter a valid longitude",
   "location_picker_longitude_hint": "Enter your longitude here",
   "lock": "Lock",
-  "locked_folder": "Locked Folder",
+  "locked_folder": "Locked folder",
   "log_out": "Log out",
   "log_out_all_devices": "Log Out All Devices",
   "logged_out_all_devices": "Logged out all devices",
@@ -1254,9 +1254,9 @@
   "monthly_title_text_date_format": "MMMM y",
   "more": "More",
   "move": "Move",
-  "move_off_locked_folder": "Move out of Locked Folder",
-  "move_to_locked_folder": "Move to Locked Folder",
-  "move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the Locked Folder",
+  "move_off_locked_folder": "Move out of locked folder",
+  "move_to_locked_folder": "Move to locked folder",
+  "move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the locked folder",
   "moved_to_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive",
   "moved_to_library": "Moved {count, plural, one {# asset} other {# assets}} to library",
   "moved_to_trash": "Moved to trash",
@@ -1292,7 +1292,7 @@
   "no_explore_results_message": "Upload more photos to explore your collection.",
   "no_favorites_message": "Add favorites to quickly find your best pictures and videos",
   "no_libraries_message": "Create an external library to view your photos and videos",
-  "no_locked_photos_message": "Photos and videos in Locked Folder are hidden and won't show up as you browse your library.",
+  "no_locked_photos_message": "Photos and videos in the locked folder are hidden and won't show up as you browse or search your library.",
   "no_name": "No Name",
   "no_notifications": "No notifications",
   "no_people_found": "No matching people found",
@@ -1502,8 +1502,8 @@
   "remove_deleted_assets": "Remove Deleted Assets",
   "remove_from_album": "Remove from album",
   "remove_from_favorites": "Remove from favorites",
-  "remove_from_locked_folder": "Remove from Locked Folder",
-  "remove_from_locked_folder_confirmation": "Are you sure you want to move these photos and videos out of Locked Folder? They will be visible in your library",
+  "remove_from_locked_folder": "Remove from locked folder",
+  "remove_from_locked_folder_confirmation": "Are you sure you want to move these photos and videos out of the locked folder? They will be visible in your library",
   "remove_from_shared_link": "Remove from shared link",
   "remove_memory": "Remove memory",
   "remove_photo_from_memory": "Remove photo from this memory",
diff --git a/mobile/.fvmrc b/mobile/.fvmrc
index 07470f9cab..b987073ac6 100644
--- a/mobile/.fvmrc
+++ b/mobile/.fvmrc
@@ -1,3 +1,3 @@
 {
   "flutter": "3.29.3"
-}
+}
\ No newline at end of file
diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json
index ceaf9a6ab8..9c5244f098 100644
--- a/mobile/.vscode/settings.json
+++ b/mobile/.vscode/settings.json
@@ -1,5 +1,5 @@
 {
-  "dart.flutterSdkPath": ".fvm/versions/3.24.3",
+  "dart.flutterSdkPath": ".fvm/versions/3.29.3",
   "search.exclude": {
     "**/.fvm": true
   },
diff --git a/mobile/lib/providers/map/map_service.provider.dart b/mobile/lib/providers/map/map_service.provider.dart
index 0d998c5173..4ae199789f 100644
--- a/mobile/lib/providers/map/map_service.provider.dart
+++ b/mobile/lib/providers/map/map_service.provider.dart
@@ -6,4 +6,4 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
 part 'map_service.provider.g.dart';
 
 @riverpod
-MapSerivce mapService(Ref ref) => MapSerivce(ref.watch(apiServiceProvider));
+MapService mapService(Ref ref) => MapService(ref.watch(apiServiceProvider));
diff --git a/mobile/lib/providers/map/map_service.provider.g.dart b/mobile/lib/providers/map/map_service.provider.g.dart
index 70e44da621..0bb5094c61 100644
--- a/mobile/lib/providers/map/map_service.provider.g.dart
+++ b/mobile/lib/providers/map/map_service.provider.g.dart
@@ -6,11 +6,11 @@ part of 'map_service.provider.dart';
 // RiverpodGenerator
 // **************************************************************************
 
-String _$mapServiceHash() => r'7b26bcd231ed5728ac51fe015dddbf8f91491abb';
+String _$mapServiceHash() => r'ffc8f38b726083452b9df236ed58903879348987';
 
 /// See also [mapService].
 @ProviderFor(mapService)
-final mapServiceProvider = AutoDisposeProvider<MapSerivce>.internal(
+final mapServiceProvider = AutoDisposeProvider<MapService>.internal(
   mapService,
   name: r'mapServiceProvider',
   debugGetCreateSourceHash:
@@ -21,6 +21,6 @@ final mapServiceProvider = AutoDisposeProvider<MapSerivce>.internal(
 
 @Deprecated('Will be removed in 3.0. Use Ref instead')
 // ignore: unused_element
-typedef MapServiceRef = AutoDisposeProviderRef<MapSerivce>;
+typedef MapServiceRef = AutoDisposeProviderRef<MapService>;
 // ignore_for_file: type=lint
 // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart
index 92b077ef59..24bdccc04d 100644
--- a/mobile/lib/services/api.service.dart
+++ b/mobile/lib/services/api.service.dart
@@ -10,6 +10,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/utils/url_helper.dart';
 import 'package:logging/logging.dart';
 import 'package:openapi/api.dart';
+import 'package:immich_mobile/utils/user_agent.dart';
 
 class ApiService implements Authentication {
   late ApiClient _apiClient;
@@ -48,6 +49,7 @@ class ApiService implements Authentication {
 
   setEndpoint(String endpoint) {
     _apiClient = ApiClient(basePath: endpoint, authentication: this);
+    _setUserAgentHeader();
     if (_accessToken != null) {
       setAccessToken(_accessToken!);
     }
@@ -72,6 +74,11 @@ class ApiService implements Authentication {
     memoriesApi = MemoriesApi(_apiClient);
   }
 
+  Future<void> _setUserAgentHeader() async {
+    final userAgent = await getUserAgentString();
+    _apiClient.addDefaultHeader('User-Agent', userAgent);
+  }
+
   Future<String> resolveAndSetEndpoint(String serverUrl) async {
     final endpoint = await resolveEndpoint(serverUrl);
     setEndpoint(endpoint);
diff --git a/mobile/lib/services/localization.service.dart b/mobile/lib/services/localization.service.dart
index c8ef662896..8bee710544 100644
--- a/mobile/lib/services/localization.service.dart
+++ b/mobile/lib/services/localization.service.dart
@@ -1,10 +1,10 @@
 // ignore_for_file: implementation_imports
 
-import 'package:flutter/foundation.dart';
-import 'package:easy_localization/src/asset_loader.dart';
 import 'package:easy_localization/src/easy_localization_controller.dart';
 import 'package:easy_localization/src/localization.dart';
+import 'package:flutter/foundation.dart';
 import 'package:immich_mobile/constants/locales.dart';
+import 'package:immich_mobile/generated/codegen_loader.g.dart';
 
 /// Workaround to manually load translations in another Isolate
 Future<bool> loadTranslations() async {
@@ -14,7 +14,7 @@ Future<bool> loadTranslations() async {
     supportedLocales: locales.values.toList(),
     useFallbackTranslations: true,
     saveLocale: true,
-    assetLoader: const RootBundleAssetLoader(),
+    assetLoader: const CodegenLoader(),
     path: translationsPath,
     useOnlyLangCode: false,
     onLoadError: (e) => debugPrint(e.toString()),
diff --git a/mobile/lib/services/map.service.dart b/mobile/lib/services/map.service.dart
index 26a0746414..2d236f77ef 100644
--- a/mobile/lib/services/map.service.dart
+++ b/mobile/lib/services/map.service.dart
@@ -2,13 +2,22 @@ import 'package:immich_mobile/mixins/error_logger.mixin.dart';
 import 'package:immich_mobile/models/map/map_marker.model.dart';
 import 'package:immich_mobile/services/api.service.dart';
 import 'package:logging/logging.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import 'package:immich_mobile/utils/user_agent.dart';
 
-class MapSerivce with ErrorLoggerMixin {
+class MapService with ErrorLoggerMixin {
   final ApiService _apiService;
   @override
   final logger = Logger("MapService");
 
-  MapSerivce(this._apiService);
+  MapService(this._apiService) {
+    _setMapUserAgentHeader();
+  }
+
+  Future<void> _setMapUserAgentHeader() async {
+    final userAgent = await getUserAgentString();
+    setHttpHeaders({'User-Agent': userAgent});
+  }
 
   Future<Iterable<MapMarker>> getMapMarkers({
     bool? isFavorite,
diff --git a/mobile/lib/utils/user_agent.dart b/mobile/lib/utils/user_agent.dart
new file mode 100644
index 0000000000..232bcaec38
--- /dev/null
+++ b/mobile/lib/utils/user_agent.dart
@@ -0,0 +1,15 @@
+import 'dart:io' show Platform;
+import 'package:package_info_plus/package_info_plus.dart';
+
+Future<String> getUserAgentString() async {
+  final packageInfo = await PackageInfo.fromPlatform();
+  String platform;
+  if (Platform.isAndroid) {
+    platform = 'Android';
+  } else if (Platform.isIOS) {
+    platform = 'iOS';
+  } else {
+    platform = 'Unknown';
+  }
+  return 'Immich_${platform}_${packageInfo.version}';
+}
diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte
index 24329d13bf..4d0c164ae1 100644
--- a/web/src/lib/components/asset-viewer/detail-panel.svelte
+++ b/web/src/lib/components/asset-viewer/detail-panel.svelte
@@ -495,6 +495,7 @@
         zoom={12.5}
         simplified
         useLocationPin
+        showSimpleControls={!showEditFaces}
         onOpenInMapView={() => goto(`${AppRoute.MAP}#12.5/${latlng.lat}/${latlng.lng}`)}
       >
         {#snippet popup({ marker })}
diff --git a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte
index d59a03158a..2c03ca48f8 100644
--- a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte
+++ b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte
@@ -55,7 +55,7 @@
   };
 </script>
 
-<div class="absolute end-0 top-0 flex place-items-center gap-1 text-xs font-medium text-white">
+<div class="absolute end-0 top-0 z-1 flex place-items-center gap-1 text-xs font-medium text-white">
   {#if showTime}
     <span class="pt-2">
       {#if remainingSeconds < 60}
diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte
index 0e0c7013cd..d0c4b90f25 100644
--- a/web/src/lib/components/photos-page/asset-grid.svelte
+++ b/web/src/lib/components/photos-page/asset-grid.svelte
@@ -577,12 +577,9 @@
         return;
       }
 
-      // Select/deselect assets in range (start,end]
+      // Select/deselect assets in range (start,end)
       let started = false;
       for (const bucket of assetStore.buckets) {
-        if (bucket === startBucket) {
-          started = true;
-        }
         if (bucket === endBucket) {
           break;
         }
@@ -596,27 +593,31 @@
             }
           }
         }
+        if (bucket === startBucket) {
+          started = true;
+        }
       }
 
-      // Update date group selection
+      // Update date group selection in range [start,end]
       started = false;
       for (const bucket of assetStore.buckets) {
         if (bucket === startBucket) {
           started = true;
         }
+        if (started) {
+          // Split bucket into date groups and check each group
+          for (const dateGroup of bucket.dateGroups) {
+            const dateGroupTitle = dateGroup.groupTitle;
+            if (dateGroup.getAssets().every((a) => assetInteraction.hasSelectedAsset(a.id))) {
+              assetInteraction.addGroupToMultiselectGroup(dateGroupTitle);
+            } else {
+              assetInteraction.removeGroupFromMultiselectGroup(dateGroupTitle);
+            }
+          }
+        }
         if (bucket === endBucket) {
           break;
         }
-
-        // Split bucket into date groups and check each group
-        for (const dateGroup of bucket.dateGroups) {
-          const dateGroupTitle = dateGroup.groupTitle;
-          if (dateGroup.getAssets().every((a) => assetInteraction.hasSelectedAsset(a.id))) {
-            assetInteraction.addGroupToMultiselectGroup(dateGroupTitle);
-          } else {
-            assetInteraction.removeGroupFromMultiselectGroup(dateGroupTitle);
-          }
-        }
       }
     }
 
diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte
index 475f6ceef9..bb1e29e4a0 100644
--- a/web/src/lib/components/photos-page/memory-lane.svelte
+++ b/web/src/lib/components/photos-page/memory-lane.svelte
@@ -45,7 +45,7 @@
     onscroll={onScroll}
   >
     {#if canScrollLeft || canScrollRight}
-      <div class="sticky start-0">
+      <div class="sticky start-0 z-1">
         {#if canScrollLeft}
           <div class="absolute start-4 top-24" transition:fade={{ duration: 200 }}>
             <button
@@ -60,7 +60,7 @@
           </div>
         {/if}
         {#if canScrollRight}
-          <div class="absolute end-4 top-24" transition:fade={{ duration: 200 }}>
+          <div class="absolute end-4 top-24 z-1" transition:fade={{ duration: 200 }}>
             <button
               type="button"
               class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte
index 6e1e10049d..0c8094a1a1 100644
--- a/web/src/lib/components/shared-components/map/map.svelte
+++ b/web/src/lib/components/shared-components/map/map.svelte
@@ -54,6 +54,7 @@
     onClickPoint?: ({ lat, lng }: { lat: number; lng: number }) => void;
     popup?: import('svelte').Snippet<[{ marker: MapMarkerResponseDto }]>;
     rounded?: boolean;
+    showSimpleControls?: boolean;
   }
 
   let {
@@ -70,6 +71,7 @@
     onClickPoint = () => {},
     popup,
     rounded = false,
+    showSimpleControls = true,
   }: Props = $props();
 
   let map: maplibregl.Map | undefined = $state();
@@ -266,13 +268,15 @@
   bind:map
 >
   {#snippet children({ map }: { map: maplibregl.Map })}
-    <NavigationControl position="top-left" showCompass={!simplified} />
+    {#if showSimpleControls}
+      <NavigationControl position="top-left" showCompass={!simplified} />
 
-    {#if !simplified}
-      <GeolocateControl position="top-left" />
-      <FullscreenControl position="top-left" />
-      <ScaleControl />
-      <AttributionControl compact={false} />
+      {#if !simplified}
+        <GeolocateControl position="top-left" />
+        <FullscreenControl position="top-left" />
+        <ScaleControl />
+        <AttributionControl compact={false} />
+      {/if}
     {/if}
 
     {#if showSettings}
@@ -285,7 +289,7 @@
       </Control>
     {/if}
 
-    {#if onOpenInMapView}
+    {#if onOpenInMapView && showSimpleControls}
       <Control position="top-right">
         <ControlGroup>
           <ControlButton onclick={() => onOpenInMapView()}>
diff --git a/web/src/lib/components/user-settings-page/PinCodeInput.svelte b/web/src/lib/components/user-settings-page/PinCodeInput.svelte
index 0b0e499445..0a078f9870 100644
--- a/web/src/lib/components/user-settings-page/PinCodeInput.svelte
+++ b/web/src/lib/components/user-settings-page/PinCodeInput.svelte
@@ -88,6 +88,7 @@
         } else if (target.value !== '') {
           pinValues[index] = '';
         }
+        value = pinValues.join('').trim();
         return;
       }
       case 'ArrowLeft': {
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 bd9186e3c0..ffeef43db1 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
@@ -22,6 +22,7 @@
   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
   import RemoveFromAlbum from '$lib/components/photos-page/actions/remove-from-album.svelte';
   import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
+  import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte';
   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
@@ -306,6 +307,11 @@
     }
   };
 
+  const handleSetVisibility = (assetIds: string[]) => {
+    assetStore.removeAssets(assetIds);
+    assetInteraction.clearMultiselect();
+  };
+
   const handleRemoveAssets = async (assetIds: string[]) => {
     assetStore.removeAssets(assetIds);
     await refreshAlbum();
@@ -603,6 +609,7 @@
               />
             {/if}
             <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
+            <SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
           {/if}
 
           {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
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 0ee796ee0a..b1cafa9d98 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
@@ -20,6 +20,7 @@
   import { onDestroy } from 'svelte';
   import { t } from 'svelte-i18n';
   import type { PageData } from './$types';
+  import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte';
 
   interface Props {
     data: PageData;
@@ -38,6 +39,11 @@
       return;
     }
   };
+
+  const handleSetVisibility = (assetIds: string[]) => {
+    assetStore.removeAssets(assetIds);
+    assetInteraction.clearMultiselect();
+  };
 </script>
 
 <UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
@@ -83,6 +89,7 @@
     />
     <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
       <DownloadAction menuItem />
+      <SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
       <DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
     </ButtonContextMenu>
   </AssetSelectControlBar>
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 b67c4fb6b7..c08b080637 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
@@ -10,6 +10,7 @@
   import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
   import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
+  import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte';
   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
@@ -42,6 +43,11 @@
       return;
     }
   };
+
+  const handleSetVisibility = (assetIds: string[]) => {
+    assetStore.removeAssets(assetIds);
+    assetInteraction.clearMultiselect();
+  };
 </script>
 
 <UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
@@ -85,6 +91,7 @@
       {#if $preferences.tags.enabled}
         <TagAction menuItem />
       {/if}
+      <SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
       <DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
     </ButtonContextMenu>
   </AssetSelectControlBar>
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 6e47d74413..8995483c84 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
@@ -18,6 +18,7 @@
   import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
   import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
+  import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte';
   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
@@ -359,6 +360,11 @@
       handlePromiseError(updateAssetCount());
     }
   });
+
+  const handleSetVisibility = (assetIds: string[]) => {
+    assetStore.removeAssets(assetIds);
+    assetInteraction.clearMultiselect();
+  };
 </script>
 
 <main
@@ -446,7 +452,7 @@
             {/if}
           </section>
           {#if isEditingName}
-            <div class="absolute w-64 sm:w-96">
+            <div class="absolute w-64 sm:w-96 z-1">
               {#if isSearchingPeople}
                 <div
                   class="flex border h-14 rounded-b-lg border-gray-400 dark:border-immich-dark-gray place-items-center bg-gray-200 p-2 dark:bg-gray-700"
@@ -525,6 +531,7 @@
         {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
           <TagAction menuItem />
         {/if}
+        <SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
         <DeleteAssets menuItem onAssetDelete={(assetIds) => handleDeleteAssets(assetIds)} />
       </ButtonContextMenu>
     </AssetSelectControlBar>
diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 2cabb534d7..32600cdec3 100644
--- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -15,6 +15,7 @@
   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
   import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
+  import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte';
   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
@@ -25,7 +26,7 @@
   import { AppRoute, QueryParameter } from '$lib/constants';
   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
-  import type { TimelineAsset, Viewport } from '$lib/stores/assets-store.svelte';
+  import { AssetStore, type TimelineAsset, type Viewport } from '$lib/stores/assets-store.svelte';
   import { lang, locale } from '$lib/stores/preferences.store';
   import { featureFlags } from '$lib/stores/server-config.store';
   import { preferences } from '$lib/stores/user.store';
@@ -79,6 +80,8 @@
     });
   });
 
+  let assetStore = new AssetStore();
+
   const onEscape = () => {
     if ($showAssetViewer) {
       return;
@@ -125,6 +128,13 @@
     const assetIdSet = new Set(assetIds);
     searchResultAssets = searchResultAssets.filter((asset: TimelineAsset) => !assetIdSet.has(asset.id));
   };
+
+  const handleSetVisibility = (assetIds: string[]) => {
+    assetStore.removeAssets(assetIds);
+    assetInteraction.clearMultiselect();
+    onAssetDelete(assetIds);
+  };
+
   const handleSelectAll = () => {
     assetInteraction.selectAssets(searchResultAssets);
   };
@@ -414,6 +424,9 @@
             <ChangeDescription menuItem />
             <ChangeLocation menuItem />
             <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
+            {#if assetInteraction.isAllUserOwned}
+              <SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
+            {/if}
             {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
               <TagAction menuItem />
             {/if}