Browse Source

feat(mobile): Add multi selected assets to album (#1446)

* refactored to use multiple assets in AddToAlbumList

* add to album from multiselect

* consistent language

* fixed accidental boolean
martyfuhry 2 years ago
parent
commit
8d47798fa2

+ 129 - 0
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<Asset> 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,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 24 - 19
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<Asset> 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,
+                        ),
+                      );
+                    },
+                  ),
+                ],
               ),
             ],
           ),

+ 56 - 0
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<AlbumResponseDto> albums;
+  final List<AlbumResponseDto> 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),
+          );
+        }
+      ),
+
+    );
+  }
+}

+ 3 - 2
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],
           );
         },
       );

+ 46 - 89
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<AlbumResponseDto> albums;
+  final List<AlbumResponseDto> 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: Column(
-                children: <Widget>[
-                  const SizedBox(height: 12),
-                  const CustomDraggingHandle(),
-                  const SizedBox(height: 12),
-                  renderActionButtons(),
-                  const Divider(
-                    indent: 16,
-                    endIndent: 16,
-                    thickness: 1,
+            child: CustomScrollView(
+              controller: scrollController,
+              slivers: [
+                SliverToBoxAdapter(
+                  child: Column(
+                    children: <Widget>[
+                      const SizedBox(height: 12),
+                      const CustomDraggingHandle(),
+                      const SizedBox(height: 12),
+                      renderActionButtons(),
+                      const Divider(
+                        indent: 16,
+                        endIndent: 16,
+                        thickness: 1,
+                      ),
+                      AddToAlbumTitleRow(onCreateNewAlbum: onCreateNewAlbum),
+                    ],
                   ),
-                  AddToAlbumTitleRow(
-                    onCreateNewAlbum: () => onCreateNewAlbum(),
+                ),
+                SliverPadding(
+                  padding: const EdgeInsets.symmetric(horizontal: 16),
+                  sliver: AddToAlbumSliverList(
+                    albums: albums,
+                    sharedAlbums: sharedAlbums,
+                    onAddToAlbum: onAddToAlbum,
                   ),
-                  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,

+ 5 - 0
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(<Asset>{});
     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,
               ),
           ],

+ 6 - 1
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,