From b5751a3fa88aa7c850fe0393837884362e323002 Mon Sep 17 00:00:00 2001 From: Matthias Rupp Date: Sun, 6 Nov 2022 02:21:55 +0100 Subject: [PATCH] feat(mobile): Add selected assets to album (#901) * First implementation that uses new API * Various UI improvements * Create new album from home screen * Fix padding when in multiselect mode * Alex Suggestions * Change to album after creation --- mobile/assets/i18n/en-US.json | 8 +- .../modules/album/services/album.service.dart | 31 +++- .../album/views/album_viewer_page.dart | 4 +- .../disable_multi_select_button.dart | 2 +- .../home/ui/control_bottom_app_bar.dart | 169 ++++++++++++++---- mobile/lib/modules/home/views/home_page.dart | 98 ++++++++-- 6 files changed, 252 insertions(+), 60 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 3d1517e99c..826f5d017f 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -171,5 +171,11 @@ "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", "experimental_settings_title": "Experimental", - "experimental_settings_subtitle": "Use at your own risk!" + "experimental_settings_subtitle": "Use at your own risk!", + "control_bottom_app_bar_add_to_album": "Add to album", + "home_page_add_to_album_success": "Added {added} assets to album {album}.", + "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", + "control_bottom_app_bar_album_info": "{} items", + "control_bottom_app_bar_album_info_shared": "{} items ยท Shared", + "control_bottom_app_bar_create_new_album": "Create new album" } diff --git a/mobile/lib/modules/album/services/album.service.dart b/mobile/lib/modules/album/services/album.service.dart index a94d609ad6..21b6fb430b 100644 --- a/mobile/lib/modules/album/services/album.service.dart +++ b/mobile/lib/modules/album/services/album.service.dart @@ -46,6 +46,31 @@ class AlbumService { } } + /* + * Creates names like Untitled, Untitled (1), Untitled (2), ... + */ + String _getNextAlbumName(List? albums) { + const baseName = "Untitled"; + + if (albums != null) { + for (int round = 0; round < albums.length; round++) { + final proposedName = "$baseName${round == 0 ? "" : " ($round)"}"; + + if (albums.where((a) => a.albumName == proposedName).isEmpty) { + return proposedName; + } + } + } + return baseName; + } + + Future createAlbumWithGeneratedName( + Set assets, + ) async { + return createAlbum( + _getNextAlbumName(await getAlbums(isShared: false)), assets, []); + } + Future getAlbumDetail(String albumId) async { try { return await _apiService.albumApi.getAlbumInfo(albumId); @@ -55,7 +80,7 @@ class AlbumService { } } - Future addAdditionalAssetToAlbum( + Future addAdditionalAssetToAlbum( Set assets, String albumId, ) async { @@ -64,10 +89,10 @@ class AlbumService { albumId, AddAssetsDto(assetIds: assets.map((asset) => asset.id).toList()), ); - return result != null; + return result; } catch (e) { debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}"); - return false; + return null; } } diff --git a/mobile/lib/modules/album/views/album_viewer_page.dart b/mobile/lib/modules/album/views/album_viewer_page.dart index c525c9922b..1bb786e0e2 100644 --- a/mobile/lib/modules/album/views/album_viewer_page.dart +++ b/mobile/lib/modules/album/views/album_viewer_page.dart @@ -53,13 +53,13 @@ class AlbumViewerPage extends HookConsumerWidget { if (returnPayload.selectedAdditionalAsset.isNotEmpty) { ImmichLoadingOverlayController.appLoader.show(); - var isSuccess = + var addAssetsResult = await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum( returnPayload.selectedAdditionalAsset, albumId, ); - if (isSuccess) { + if (addAssetsResult != null && addAssetsResult.successfullyAdded > 0) { ref.refresh(sharedAlbumDetailProvider(albumId)); } diff --git a/mobile/lib/modules/home/ui/asset_grid/disable_multi_select_button.dart b/mobile/lib/modules/home/ui/asset_grid/disable_multi_select_button.dart index cca2e0b201..98f932ab2c 100644 --- a/mobile/lib/modules/home/ui/asset_grid/disable_multi_select_button.dart +++ b/mobile/lib/modules/home/ui/asset_grid/disable_multi_select_button.dart @@ -14,7 +14,7 @@ class DisableMultiSelectButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return Padding( - padding: const EdgeInsets.only(left: 16.0, top: 15), + padding: const EdgeInsets.only(left: 16.0, top: 16.0), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: ElevatedButton.icon( diff --git a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart index dceaea4ace..3ad89ac92f 100644 --- a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart +++ b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart @@ -1,62 +1,161 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:openapi/api.dart'; class ControlBottomAppBar extends ConsumerWidget { final Function onShare; final Function onDelete; + final Function(AlbumResponseDto album) onAddToAlbum; + final void Function() onCreateNewAlbum; - const ControlBottomAppBar( - {Key? key, required this.onShare, required this.onDelete}) - : super(key: key); + final List albums; + + const ControlBottomAppBar({ + Key? key, + required this.onShare, + required this.onDelete, + required this.albums, + required this.onAddToAlbum, + required this.onCreateNewAlbum, + }) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { + Widget renderActionButtons() { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ControlBoxButton( + iconData: Icons.delete_forever_rounded, + label: "control_bottom_app_bar_delete".tr(), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return DeleteDialog( + onDelete: onDelete, + ); + }, + ); + }, + ), + ControlBoxButton( + iconData: Icons.share, + label: "control_bottom_app_bar_share".tr(), + onPressed: () { + onShare(); + }, + ), + ], + ), + ); + } + + Widget renderAlbums() { + Widget renderAlbum(AlbumResponseDto album) { + final box = Hive.box(userInfoBox); + + return GestureDetector( + onTap: () => onAddToAlbum(album), + child: Container( + width: 112, + padding: const EdgeInsets.all(6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + width: 100, + height: 100, + fit: BoxFit.cover, + imageUrl: + getAlbumThumbnailUrl(album, type: ThumbnailFormat.JPEG), + httpHeaders: { + "Authorization": "Bearer ${box.get(accessTokenKey)}" + }, + cacheKey: "${album.albumThumbnailAssetId}", + ), + ), + Padding( + padding: const EdgeInsets.only(top: 12), + child: Text( + album.albumName, + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + Text(album.shared + ? "control_bottom_app_bar_album_info_shared" + : "control_bottom_app_bar_album_info") + .tr(args: [album.assetCount.toString()]), + ], + ), + ), + ); + } + + return SizedBox( + height: 200, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemBuilder: (buildContext, i) => renderAlbum(albums[i]), + itemCount: albums.length, + ), + ); + } + return Positioned( bottom: 0, left: 0, child: Container( width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height * 0.15, decoration: BoxDecoration( borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - topRight: Radius.circular(8), + topLeft: Radius.circular(10), + topRight: Radius.circular(10), ), - color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.95), + color: Theme.of(context).scaffoldBackgroundColor, ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + renderActionButtons(), + const Divider( + thickness: 2, + ), Padding( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ControlBoxButton( - iconData: Icons.delete_forever_rounded, - label: "control_bottom_app_bar_delete".tr(), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return DeleteDialog( - onDelete: onDelete, - ); - }, - ); - }, - ), - ControlBoxButton( - iconData: Icons.share, - label: "control_bottom_app_bar_share".tr(), - onPressed: () { - onShare(); - }, - ), - ], - ), - ) + padding: const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "control_bottom_app_bar_add_to_album", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ).tr(), + TextButton( + onPressed: onCreateNewAlbum, + child: Text( + "control_bottom_app_bar_create_new_album", + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + ), + ).tr(), + ), + ], + )), + renderAlbums(), ], ), ), diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 82045c6879..8062fa321a 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -1,6 +1,11 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/album/providers/album.provider.dart'; +import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; +import 'package:immich_mobile/modules/album/services/album.service.dart'; import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.dart'; import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; @@ -9,10 +14,12 @@ import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart'; import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/services/share.service.dart'; +import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:openapi/api.dart'; class HomePage extends HookConsumerWidget { @@ -23,14 +30,26 @@ class HomePage extends HookConsumerWidget { final appSettingService = ref.watch(appSettingsServiceProvider); var renderList = ref.watch(renderListProvider); final multiselectEnabled = ref.watch(multiselectProvider.notifier); + final selectionEnabledHook = useState(false); + final selection = useState({}); + final albums = ref.watch(albumProvider); + final albumService = ref.watch(albumServiceProvider); useEffect( () { ref.read(websocketProvider.notifier).connect(); ref.read(assetProvider.notifier).getAllAsset(); + ref.read(albumProvider.notifier).getAllAlbums(); ref.watch(serverInfoProvider.notifier).getServerVersion(); - return null; + + selectionEnabledHook.addListener(() { + multiselectEnabled.state = selectionEnabledHook.value; + }); + + return () { + selectionEnabledHook.dispose(); + }; }, [], ); @@ -44,41 +63,81 @@ class HomePage extends HookConsumerWidget { bool multiselect, Set selectedAssets, ) { - multiselectEnabled.state = multiselect; + selectionEnabledHook.value = multiselect; selection.value = selectedAssets; } void onShareAssets() { ref.watch(shareServiceProvider).shareAssets(selection.value.toList()); - multiselectEnabled.state = false; + selectionEnabledHook.value = false; } void onDelete() { ref.watch(assetProvider.notifier).deleteAssets(selection.value); - multiselectEnabled.state = false; + selectionEnabledHook.value = false; + } + + void onAddToAlbum(AlbumResponseDto album) async { + final result = await albumService.addAdditionalAssetToAlbum( + selection.value, album.id); + + if (result != null) { + + if (result.alreadyInAlbum.isNotEmpty) { + ImmichToast.show( + context: context, + msg: "home_page_add_to_album_conflicts".tr( + namedArgs: { + "album": album.albumName, + "added": result.successfullyAdded.toString(), + "failed": result.alreadyInAlbum.length.toString() + }, + ), + ); + } else { + ImmichToast.show( + context: context, + msg: "home_page_add_to_album_success".tr( + namedArgs: { + "album": album.albumName, + "added": result.successfullyAdded.toString(), + }, + ), + ); + } + + selectionEnabledHook.value = false; + } + } + + void onCreateNewAlbum() async { + final result = + await albumService.createAlbumWithGeneratedName(selection.value); + + if (result != null) { + ref.watch(albumProvider.notifier).getAllAlbums(); + selectionEnabledHook.value = false; + + AutoRouter.of(context).push(AlbumViewerRoute(albumId: result.id)); + } } return SafeArea( bottom: !multiselectEnabled.state, - top: !multiselectEnabled.state, + top: true, child: Stack( children: [ CustomScrollView( slivers: [ - multiselectEnabled.state - ? const SliverToBoxAdapter( - child: SizedBox( - height: 70, - child: null, - ), - ) - : ImmichSliverAppBar( - onPopBack: reloadAllAsset, - ), + if (!multiselectEnabled.state) + ImmichSliverAppBar( + onPopBack: reloadAllAsset, + ), ], ), Padding( - padding: const EdgeInsets.only(top: 60.0, bottom: 0.0), + padding: EdgeInsets.only( + top: selectionEnabledHook.value ? 0 : 60, bottom: 0.0), child: ImmichAssetGrid( renderList: renderList, assetsPerRow: @@ -86,13 +145,16 @@ class HomePage extends HookConsumerWidget { showStorageIndicator: appSettingService .getSetting(AppSettingsEnum.storageIndicator), listener: selectionListener, - selectionActive: multiselectEnabled.state, + selectionActive: selectionEnabledHook.value, ), ), - if (multiselectEnabled.state) ...[ + if (selectionEnabledHook.value) ...[ ControlBottomAppBar( onShare: onShareAssets, onDelete: onDelete, + onAddToAlbum: onAddToAlbum, + albums: albums, + onCreateNewAlbum: onCreateNewAlbum, ), ], ],