diff --git a/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart b/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart new file mode 100644 index 0000000000..a98608296b --- /dev/null +++ b/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart @@ -0,0 +1,129 @@ +import 'package:auto_route/auto_route.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/asset_selection.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/album/ui/add_to_album_sliverlist.dart'; +import 'package:immich_mobile/modules/album/ui/album_thumbnail_listtile.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/ui/drag_sheet.dart'; +import 'package:immich_mobile/shared/ui/immich_toast.dart'; +import 'package:openapi/api.dart'; + +class AddToAlbumBottomSheet extends HookConsumerWidget { + + /// The asset to add to an album + final List assets; + + const AddToAlbumBottomSheet({ + Key? key, + required this.assets, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final albums = ref.watch(albumProvider); + final albumService = ref.watch(albumServiceProvider); + final sharedAlbums = ref.watch(sharedAlbumProvider); + + useEffect( + () { + // Fetch album updates, e.g., cover image + ref.read(albumProvider.notifier).getAllAlbums(); + ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); + + return null; + }, + [], + ); + + void addToAlbum(AlbumResponseDto album) async { + final result = await albumService.addAdditionalAssetToAlbum( + assets, + album.id, + ); + + if (result != null) { + if (result.alreadyInAlbum.isNotEmpty) { + ImmichToast.show( + context: context, + msg: 'Already in ${album.albumName}', + ); + } else { + ImmichToast.show( + context: context, + msg: 'Added to ${album.albumName}', + ); + } + } + + ref.read(albumProvider.notifier).getAllAlbums(); + ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); + + Navigator.pop(context); + } + + + return Card( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + topRight: Radius.circular(15), + ), + ), + child: CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Align( + alignment: Alignment.center, + child: CustomDraggingHandle(), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Add to album', + style: Theme.of(context).textTheme.headline2, + ), + TextButton.icon( + icon: const Icon(Icons.add), + label: const Text('Create new album'), + onPressed: () { + ref.watch(assetSelectionProvider.notifier).removeAll(); + ref.watch(assetSelectionProvider.notifier).addNewAssets(assets); + AutoRouter.of(context).push( + CreateAlbumRoute( + isSharedAlbum: false, + initialAssets: assets, + ), + ); + }, + ), + ], + ), + ], + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: AddToAlbumSliverList( + albums: albums, + sharedAlbums: sharedAlbums, + onAddToAlbum: addToAlbum, + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/modules/album/ui/add_to_album_list.dart b/mobile/lib/modules/album/ui/add_to_album_list.dart index 30cb064022..c2ba72179c 100644 --- a/mobile/lib/modules/album/ui/add_to_album_list.dart +++ b/mobile/lib/modules/album/ui/add_to_album_list.dart @@ -16,11 +16,11 @@ import 'package:openapi/api.dart'; class AddToAlbumList extends HookConsumerWidget { /// The asset to add to an album - final Asset asset; + final List assets; const AddToAlbumList({ Key? key, - required this.asset, + required this.assets, }) : super(key: key); @override @@ -42,7 +42,7 @@ class AddToAlbumList extends HookConsumerWidget { void addToAlbum(AlbumResponseDto album) async { final result = await albumService.addAdditionalAssetToAlbum( - [asset], + assets, album.id, ); @@ -84,22 +84,27 @@ class AddToAlbumList extends HookConsumerWidget { child: CustomDraggingHandle(), ), const SizedBox(height: 12), - Text('Add to album', - style: Theme.of(context).textTheme.headline1, - ), - TextButton.icon( - icon: const Icon(Icons.add), - label: const Text('New album'), - onPressed: () { - ref.watch(assetSelectionProvider.notifier).removeAll(); - ref.watch(assetSelectionProvider.notifier).addNewAssets([asset]); - AutoRouter.of(context).push( - CreateAlbumRoute( - isSharedAlbum: false, - initialAssets: [asset], - ), - ); - }, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Add to album', + style: Theme.of(context).textTheme.headline2, + ), + TextButton.icon( + icon: const Icon(Icons.add), + label: const Text('New album'), + onPressed: () { + ref.watch(assetSelectionProvider.notifier).removeAll(); + ref.watch(assetSelectionProvider.notifier).addNewAssets(assets); + AutoRouter.of(context).push( + CreateAlbumRoute( + isSharedAlbum: false, + initialAssets: assets, + ), + ); + }, + ), + ], ), ], ), diff --git a/mobile/lib/modules/album/ui/add_to_album_sliverlist.dart b/mobile/lib/modules/album/ui/add_to_album_sliverlist.dart new file mode 100644 index 0000000000..fd1be2374f --- /dev/null +++ b/mobile/lib/modules/album/ui/add_to_album_sliverlist.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/album/ui/album_thumbnail_listtile.dart'; +import 'package:openapi/api.dart'; + +class AddToAlbumSliverList extends HookConsumerWidget { + + /// The asset to add to an album + final List albums; + final List sharedAlbums; + final void Function(AlbumResponseDto) onAddToAlbum; + + const AddToAlbumSliverList({ + Key? key, + required this.onAddToAlbum, + required this.albums, + required this.sharedAlbums, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SliverList( + delegate: SliverChildBuilderDelegate( + childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1), + (context, index) { + // Build shared expander + if (index == 0 && sharedAlbums.isNotEmpty) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: ExpansionTile( + title: const Text('Shared'), + tilePadding: const EdgeInsets.symmetric(horizontal: 10.0), + leading: const Icon(Icons.group), + children: sharedAlbums.map((album) => + AlbumThumbnailListTile( + album: album, + onTap: () => onAddToAlbum(album), + ), + ).toList(), + ), + ); + } + + // Build albums list + final offset = index - (sharedAlbums.isNotEmpty ? 1 : 0); + final album = albums[offset]; + return AlbumThumbnailListTile( + album: album, + onTap: () => onAddToAlbum(album), + ); + } + ), + + ); + } +} diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index 22375da341..b14e5dc81a 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.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/album/ui/add_to_album_bottom_sheet.dart'; import 'package:immich_mobile/modules/album/ui/add_to_album_list.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; @@ -115,8 +116,8 @@ class GalleryViewerPage extends HookConsumerWidget { backgroundColor: Colors.transparent, context: context, builder: (BuildContext _) { - return AddToAlbumList( - asset: addToAlbumAsset, + return AddToAlbumBottomSheet( + assets: [addToAlbumAsset], ); }, ); 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 22d47b71ae..edc608637c 100644 --- a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart +++ b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart @@ -1,12 +1,9 @@ -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/album/ui/add_to_album_sliverlist.dart'; import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart'; import 'package:immich_mobile/shared/ui/drag_sheet.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:openapi/api.dart'; class ControlBottomAppBar extends ConsumerWidget { @@ -16,11 +13,13 @@ class ControlBottomAppBar extends ConsumerWidget { final void Function() onCreateNewAlbum; final List albums; + final List sharedAlbums; const ControlBottomAppBar({ Key? key, required this.onShare, required this.onDelete, + required this.sharedAlbums, required this.albums, required this.onAddToAlbum, required this.onCreateNewAlbum, @@ -56,60 +55,6 @@ class ControlBottomAppBar extends ConsumerWidget { ); } - Widget renderAlbums() { - Widget renderAlbum(AlbumResponseDto album) { - final box = Hive.box(userInfoBox); - - return Padding( - padding: const EdgeInsets.only(left: 8.0), - child: 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), - httpHeaders: { - "Authorization": "Bearer ${box.get(accessTokenKey)}" - }, - cacheKey: getAlbumThumbNailCacheKey(album), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 12), - child: Text( - album.albumName, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12.0, - ), - ), - ), - ], - ), - ), - ), - ); - } - - return SizedBox( - height: 200, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemBuilder: (buildContext, i) => renderAlbum(albums[i]), - itemCount: albums.length, - ), - ); - } - return DraggableScrollableSheet( initialChildSize: 0.30, minChildSize: 0.15, @@ -119,42 +64,53 @@ class ControlBottomAppBar extends ConsumerWidget { BuildContext context, ScrollController scrollController, ) { - return SingleChildScrollView( - controller: scrollController, - child: Card( - elevation: 12.0, - shape: const RoundedRectangleBorder( + return Card( + elevation: 12.0, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + margin: const EdgeInsets.all(0), + child: Container( + decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), ), - margin: const EdgeInsets.all(0), - child: Container( - decoration: const BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), + child: CustomScrollView( + controller: scrollController, + slivers: [ + SliverToBoxAdapter( + child: Column( + children: [ + const SizedBox(height: 12), + const CustomDraggingHandle(), + const SizedBox(height: 12), + renderActionButtons(), + const Divider( + indent: 16, + endIndent: 16, + thickness: 1, + ), + AddToAlbumTitleRow(onCreateNewAlbum: onCreateNewAlbum), + ], + ), ), - ), - child: Column( - children: [ - const SizedBox(height: 12), - const CustomDraggingHandle(), - const SizedBox(height: 12), - renderActionButtons(), - const Divider( - indent: 16, - endIndent: 16, - thickness: 1, + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: AddToAlbumSliverList( + albums: albums, + sharedAlbums: sharedAlbums, + onAddToAlbum: onAddToAlbum, ), - AddToAlbumTitleRow( - onCreateNewAlbum: () => onCreateNewAlbum(), - ), - renderAlbums(), - const SizedBox(height: 200), - ], - ), + ), + const SliverToBoxAdapter( + child: SizedBox(height: 200), + ) + ], ), ), ); @@ -185,9 +141,10 @@ class AddToAlbumTitleRow extends StatelessWidget { fontWeight: FontWeight.bold, ), ).tr(), - TextButton( + TextButton.icon( onPressed: onCreateNewAlbum, - child: Text( + icon: const Icon(Icons.add), + label: Text( "control_bottom_app_bar_create_new_album", style: TextStyle( color: Theme.of(context).primaryColor, diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 1a6170a064..d032a61651 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -8,6 +8,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.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/multiselect.provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; @@ -37,6 +38,7 @@ class HomePage extends HookConsumerWidget { final selection = useState({}); final albums = ref.watch(albumProvider); + final sharedAlbums = ref.watch(sharedAlbumProvider); final albumService = ref.watch(albumServiceProvider); final tipOneOpacity = useState(0.0); @@ -46,6 +48,7 @@ class HomePage extends HookConsumerWidget { ref.read(websocketProvider.notifier).connect(); ref.read(assetProvider.notifier).getAllAsset(); ref.read(albumProvider.notifier).getAllAlbums(); + ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); ref.watch(serverInfoProvider.notifier).getServerVersion(); selectionEnabledHook.addListener(() { @@ -147,6 +150,7 @@ class HomePage extends HookConsumerWidget { if (result != null) { ref.watch(albumProvider.notifier).getAllAlbums(); + ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums(); selectionEnabledHook.value = false; AutoRouter.of(context).push(AlbumViewerRoute(albumId: result.id)); @@ -220,6 +224,7 @@ class HomePage extends HookConsumerWidget { onDelete: onDelete, onAddToAlbum: onAddToAlbum, albums: albums, + sharedAlbums: sharedAlbums, onCreateNewAlbum: onCreateNewAlbum, ), ], diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/utils/immich_app_theme.dart index a180627d2c..b9fa16f0df 100644 --- a/mobile/lib/utils/immich_app_theme.dart +++ b/mobile/lib/utils/immich_app_theme.dart @@ -31,6 +31,11 @@ ThemeData immichDarkTheme = ThemeData( snackBarTheme: const SnackBarThemeData( contentTextStyle: TextStyle(fontFamily: 'WorkSans'), ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: immichDarkThemePrimaryColor, + ), + ), appBarTheme: AppBarTheme( titleTextStyle: TextStyle( fontFamily: 'WorkSans', @@ -59,7 +64,7 @@ ThemeData immichDarkTheme = ThemeData( headline2: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 148, 151, 155), + color: Color.fromARGB(255, 255, 255, 255), ), headline3: TextStyle( fontSize: 12,