Merge remote-tracking branch 'origin/main' into keynav_timeline

This commit is contained in:
Min Idzelis 2025-05-22 22:13:13 +00:00
commit e11a8d3730
22 changed files with 131 additions and 47 deletions

View File

@ -504,6 +504,11 @@ jobs:
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version-file: ./mobile/pubspec.yaml
- name: Generate translation file
run: make translation
working-directory: ./mobile
- name: Run tests - name: Run tests
working-directory: ./mobile working-directory: ./mobile
run: flutter test -j 1 run: flutter test -j 1

View File

@ -218,7 +218,7 @@ const roadmap: Item[] = [
iconColor: 'indianred', iconColor: 'indianred',
title: 'Stable release', title: 'Stable release',
description: 'Immich goes stable', description: 'Immich goes stable',
getDateLabel: () => 'Planned for early 2025', getDateLabel: () => 'Planned for 2025',
}, },
{ {
done: false, done: false,

View File

@ -26,7 +26,7 @@
"add_to_album": "Add to album", "add_to_album": "Add to album",
"add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {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_to_shared_album": "Add to shared album",
"add_url": "Add URL", "add_url": "Add URL",
"added_to_archive": "Added to archive", "added_to_archive": "Added to archive",
@ -1157,7 +1157,7 @@
"location_picker_longitude_error": "Enter a valid longitude", "location_picker_longitude_error": "Enter a valid longitude",
"location_picker_longitude_hint": "Enter your longitude here", "location_picker_longitude_hint": "Enter your longitude here",
"lock": "Lock", "lock": "Lock",
"locked_folder": "Locked Folder", "locked_folder": "Locked folder",
"log_out": "Log out", "log_out": "Log out",
"log_out_all_devices": "Log Out All Devices", "log_out_all_devices": "Log Out All Devices",
"logged_out_all_devices": "Logged out all devices", "logged_out_all_devices": "Logged out all devices",
@ -1254,9 +1254,9 @@
"monthly_title_text_date_format": "MMMM y", "monthly_title_text_date_format": "MMMM y",
"more": "More", "more": "More",
"move": "Move", "move": "Move",
"move_off_locked_folder": "Move out of Locked Folder", "move_off_locked_folder": "Move out of locked folder",
"move_to_locked_folder": "Move to 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_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_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive",
"moved_to_library": "Moved {count, plural, one {# asset} other {# assets}} to library", "moved_to_library": "Moved {count, plural, one {# asset} other {# assets}} to library",
"moved_to_trash": "Moved to trash", "moved_to_trash": "Moved to trash",
@ -1292,7 +1292,7 @@
"no_explore_results_message": "Upload more photos to explore your collection.", "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_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_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_name": "No Name",
"no_notifications": "No notifications", "no_notifications": "No notifications",
"no_people_found": "No matching people found", "no_people_found": "No matching people found",
@ -1502,8 +1502,8 @@
"remove_deleted_assets": "Remove Deleted Assets", "remove_deleted_assets": "Remove Deleted Assets",
"remove_from_album": "Remove from album", "remove_from_album": "Remove from album",
"remove_from_favorites": "Remove from favorites", "remove_from_favorites": "Remove from favorites",
"remove_from_locked_folder": "Remove from Locked Folder", "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_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_from_shared_link": "Remove from shared link",
"remove_memory": "Remove memory", "remove_memory": "Remove memory",
"remove_photo_from_memory": "Remove photo from this memory", "remove_photo_from_memory": "Remove photo from this memory",

View File

@ -1,3 +1,3 @@
{ {
"flutter": "3.29.3" "flutter": "3.29.3"
} }

View File

@ -1,5 +1,5 @@
{ {
"dart.flutterSdkPath": ".fvm/versions/3.24.3", "dart.flutterSdkPath": ".fvm/versions/3.29.3",
"search.exclude": { "search.exclude": {
"**/.fvm": true "**/.fvm": true
}, },

View File

@ -6,4 +6,4 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'map_service.provider.g.dart'; part 'map_service.provider.g.dart';
@riverpod @riverpod
MapSerivce mapService(Ref ref) => MapSerivce(ref.watch(apiServiceProvider)); MapService mapService(Ref ref) => MapService(ref.watch(apiServiceProvider));

View File

@ -6,11 +6,11 @@ part of 'map_service.provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$mapServiceHash() => r'7b26bcd231ed5728ac51fe015dddbf8f91491abb'; String _$mapServiceHash() => r'ffc8f38b726083452b9df236ed58903879348987';
/// See also [mapService]. /// See also [mapService].
@ProviderFor(mapService) @ProviderFor(mapService)
final mapServiceProvider = AutoDisposeProvider<MapSerivce>.internal( final mapServiceProvider = AutoDisposeProvider<MapService>.internal(
mapService, mapService,
name: r'mapServiceProvider', name: r'mapServiceProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@ -21,6 +21,6 @@ final mapServiceProvider = AutoDisposeProvider<MapSerivce>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef MapServiceRef = AutoDisposeProviderRef<MapSerivce>; typedef MapServiceRef = AutoDisposeProviderRef<MapService>;
// ignore_for_file: type=lint // 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 // 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

View File

@ -10,6 +10,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/utils/url_helper.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/user_agent.dart';
class ApiService implements Authentication { class ApiService implements Authentication {
late ApiClient _apiClient; late ApiClient _apiClient;
@ -48,6 +49,7 @@ class ApiService implements Authentication {
setEndpoint(String endpoint) { setEndpoint(String endpoint) {
_apiClient = ApiClient(basePath: endpoint, authentication: this); _apiClient = ApiClient(basePath: endpoint, authentication: this);
_setUserAgentHeader();
if (_accessToken != null) { if (_accessToken != null) {
setAccessToken(_accessToken!); setAccessToken(_accessToken!);
} }
@ -72,6 +74,11 @@ class ApiService implements Authentication {
memoriesApi = MemoriesApi(_apiClient); memoriesApi = MemoriesApi(_apiClient);
} }
Future<void> _setUserAgentHeader() async {
final userAgent = await getUserAgentString();
_apiClient.addDefaultHeader('User-Agent', userAgent);
}
Future<String> resolveAndSetEndpoint(String serverUrl) async { Future<String> resolveAndSetEndpoint(String serverUrl) async {
final endpoint = await resolveEndpoint(serverUrl); final endpoint = await resolveEndpoint(serverUrl);
setEndpoint(endpoint); setEndpoint(endpoint);

View File

@ -1,10 +1,10 @@
// ignore_for_file: implementation_imports // 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/easy_localization_controller.dart';
import 'package:easy_localization/src/localization.dart'; import 'package:easy_localization/src/localization.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/constants/locales.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 /// Workaround to manually load translations in another Isolate
Future<bool> loadTranslations() async { Future<bool> loadTranslations() async {
@ -14,7 +14,7 @@ Future<bool> loadTranslations() async {
supportedLocales: locales.values.toList(), supportedLocales: locales.values.toList(),
useFallbackTranslations: true, useFallbackTranslations: true,
saveLocale: true, saveLocale: true,
assetLoader: const RootBundleAssetLoader(), assetLoader: const CodegenLoader(),
path: translationsPath, path: translationsPath,
useOnlyLangCode: false, useOnlyLangCode: false,
onLoadError: (e) => debugPrint(e.toString()), onLoadError: (e) => debugPrint(e.toString()),

View File

@ -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/models/map/map_marker.model.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.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; final ApiService _apiService;
@override @override
final logger = Logger("MapService"); 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({ Future<Iterable<MapMarker>> getMapMarkers({
bool? isFavorite, bool? isFavorite,

View File

@ -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}';
}

View File

@ -495,6 +495,7 @@
zoom={12.5} zoom={12.5}
simplified simplified
useLocationPin useLocationPin
showSimpleControls={!showEditFaces}
onOpenInMapView={() => goto(`${AppRoute.MAP}#12.5/${latlng.lat}/${latlng.lng}`)} onOpenInMapView={() => goto(`${AppRoute.MAP}#12.5/${latlng.lat}/${latlng.lng}`)}
> >
{#snippet popup({ marker })} {#snippet popup({ marker })}

View File

@ -55,7 +55,7 @@
}; };
</script> </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} {#if showTime}
<span class="pt-2"> <span class="pt-2">
{#if remainingSeconds < 60} {#if remainingSeconds < 60}

View File

@ -577,12 +577,9 @@
return; return;
} }
// Select/deselect assets in range (start,end] // Select/deselect assets in range (start,end)
let started = false; let started = false;
for (const bucket of assetStore.buckets) { for (const bucket of assetStore.buckets) {
if (bucket === startBucket) {
started = true;
}
if (bucket === endBucket) { if (bucket === endBucket) {
break; break;
} }
@ -596,27 +593,31 @@
} }
} }
} }
if (bucket === startBucket) {
started = true;
}
} }
// Update date group selection // Update date group selection in range [start,end]
started = false; started = false;
for (const bucket of assetStore.buckets) { for (const bucket of assetStore.buckets) {
if (bucket === startBucket) { if (bucket === startBucket) {
started = true; 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) { if (bucket === endBucket) {
break; 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);
}
}
} }
} }

View File

@ -45,7 +45,7 @@
onscroll={onScroll} onscroll={onScroll}
> >
{#if canScrollLeft || canScrollRight} {#if canScrollLeft || canScrollRight}
<div class="sticky start-0"> <div class="sticky start-0 z-1">
{#if canScrollLeft} {#if canScrollLeft}
<div class="absolute start-4 top-24" transition:fade={{ duration: 200 }}> <div class="absolute start-4 top-24" transition:fade={{ duration: 200 }}>
<button <button
@ -60,7 +60,7 @@
</div> </div>
{/if} {/if}
{#if canScrollRight} {#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 <button
type="button" type="button"
class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100" class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"

View File

@ -54,6 +54,7 @@
onClickPoint?: ({ lat, lng }: { lat: number; lng: number }) => void; onClickPoint?: ({ lat, lng }: { lat: number; lng: number }) => void;
popup?: import('svelte').Snippet<[{ marker: MapMarkerResponseDto }]>; popup?: import('svelte').Snippet<[{ marker: MapMarkerResponseDto }]>;
rounded?: boolean; rounded?: boolean;
showSimpleControls?: boolean;
} }
let { let {
@ -70,6 +71,7 @@
onClickPoint = () => {}, onClickPoint = () => {},
popup, popup,
rounded = false, rounded = false,
showSimpleControls = true,
}: Props = $props(); }: Props = $props();
let map: maplibregl.Map | undefined = $state(); let map: maplibregl.Map | undefined = $state();
@ -266,13 +268,15 @@
bind:map bind:map
> >
{#snippet children({ map }: { map: maplibregl.Map })} {#snippet children({ map }: { map: maplibregl.Map })}
<NavigationControl position="top-left" showCompass={!simplified} /> {#if showSimpleControls}
<NavigationControl position="top-left" showCompass={!simplified} />
{#if !simplified} {#if !simplified}
<GeolocateControl position="top-left" /> <GeolocateControl position="top-left" />
<FullscreenControl position="top-left" /> <FullscreenControl position="top-left" />
<ScaleControl /> <ScaleControl />
<AttributionControl compact={false} /> <AttributionControl compact={false} />
{/if}
{/if} {/if}
{#if showSettings} {#if showSettings}
@ -285,7 +289,7 @@
</Control> </Control>
{/if} {/if}
{#if onOpenInMapView} {#if onOpenInMapView && showSimpleControls}
<Control position="top-right"> <Control position="top-right">
<ControlGroup> <ControlGroup>
<ControlButton onclick={() => onOpenInMapView()}> <ControlButton onclick={() => onOpenInMapView()}>

View File

@ -88,6 +88,7 @@
} else if (target.value !== '') { } else if (target.value !== '') {
pinValues[index] = ''; pinValues[index] = '';
} }
value = pinValues.join('').trim();
return; return;
} }
case 'ArrowLeft': { case 'ArrowLeft': {

View File

@ -22,6 +22,7 @@
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
import RemoveFromAlbum from '$lib/components/photos-page/actions/remove-from-album.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 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 TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.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[]) => { const handleRemoveAssets = async (assetIds: string[]) => {
assetStore.removeAssets(assetIds); assetStore.removeAssets(assetIds);
await refreshAlbum(); await refreshAlbum();
@ -603,6 +609,7 @@
/> />
{/if} {/if}
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} /> <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
{/if} {/if}
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}

View File

@ -20,6 +20,7 @@
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte';
interface Props { interface Props {
data: PageData; data: PageData;
@ -38,6 +39,11 @@
return; return;
} }
}; };
const handleSetVisibility = (assetIds: string[]) => {
assetStore.removeAssets(assetIds);
assetInteraction.clearMultiselect();
};
</script> </script>
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}> <UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
@ -83,6 +89,7 @@
/> />
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem /> <DownloadAction menuItem />
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
<DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} /> <DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
</ButtonContextMenu> </ButtonContextMenu>
</AssetSelectControlBar> </AssetSelectControlBar>

View File

@ -10,6 +10,7 @@
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import FavoriteAction from '$lib/components/photos-page/actions/favorite-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 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 TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
@ -42,6 +43,11 @@
return; return;
} }
}; };
const handleSetVisibility = (assetIds: string[]) => {
assetStore.removeAssets(assetIds);
assetInteraction.clearMultiselect();
};
</script> </script>
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}> <UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
@ -85,6 +91,7 @@
{#if $preferences.tags.enabled} {#if $preferences.tags.enabled}
<TagAction menuItem /> <TagAction menuItem />
{/if} {/if}
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
<DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} /> <DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
</ButtonContextMenu> </ButtonContextMenu>
</AssetSelectControlBar> </AssetSelectControlBar>

View File

@ -18,6 +18,7 @@
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import FavoriteAction from '$lib/components/photos-page/actions/favorite-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 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 TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
@ -359,6 +360,11 @@
handlePromiseError(updateAssetCount()); handlePromiseError(updateAssetCount());
} }
}); });
const handleSetVisibility = (assetIds: string[]) => {
assetStore.removeAssets(assetIds);
assetInteraction.clearMultiselect();
};
</script> </script>
<main <main
@ -446,7 +452,7 @@
{/if} {/if}
</section> </section>
{#if isEditingName} {#if isEditingName}
<div class="absolute w-64 sm:w-96"> <div class="absolute w-64 sm:w-96 z-1">
{#if isSearchingPeople} {#if isSearchingPeople}
<div <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" 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} {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
<TagAction menuItem /> <TagAction menuItem />
{/if} {/if}
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
<DeleteAssets menuItem onAssetDelete={(assetIds) => handleDeleteAssets(assetIds)} /> <DeleteAssets menuItem onAssetDelete={(assetIds) => handleDeleteAssets(assetIds)} />
</ButtonContextMenu> </ButtonContextMenu>
</AssetSelectControlBar> </AssetSelectControlBar>

View File

@ -15,6 +15,7 @@
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import FavoriteAction from '$lib/components/photos-page/actions/favorite-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 TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.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'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
@ -25,7 +26,7 @@
import { AppRoute, QueryParameter } from '$lib/constants'; import { AppRoute, QueryParameter } from '$lib/constants';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; 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 { lang, locale } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/server-config.store';
import { preferences } from '$lib/stores/user.store'; import { preferences } from '$lib/stores/user.store';
@ -79,6 +80,8 @@
}); });
}); });
let assetStore = new AssetStore();
const onEscape = () => { const onEscape = () => {
if ($showAssetViewer) { if ($showAssetViewer) {
return; return;
@ -125,6 +128,13 @@
const assetIdSet = new Set(assetIds); const assetIdSet = new Set(assetIds);
searchResultAssets = searchResultAssets.filter((asset: TimelineAsset) => !assetIdSet.has(asset.id)); searchResultAssets = searchResultAssets.filter((asset: TimelineAsset) => !assetIdSet.has(asset.id));
}; };
const handleSetVisibility = (assetIds: string[]) => {
assetStore.removeAssets(assetIds);
assetInteraction.clearMultiselect();
onAssetDelete(assetIds);
};
const handleSelectAll = () => { const handleSelectAll = () => {
assetInteraction.selectAssets(searchResultAssets); assetInteraction.selectAssets(searchResultAssets);
}; };
@ -414,6 +424,9 @@
<ChangeDescription menuItem /> <ChangeDescription menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} /> <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
{#if assetInteraction.isAllUserOwned}
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
{/if}
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
<TagAction menuItem /> <TagAction menuItem />
{/if} {/if}