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}