Kaynağa Gözat

Implemented bottom app bar with control buttons for asset's operation (#15)

Alex 3 yıl önce
ebeveyn
işleme
f578ca6d47

+ 2 - 2
README.md

@@ -22,10 +22,10 @@ Loading ~4000 images/videos
 
 # Note
 
-This project is under heavy development, there will be continous functions, features and api changes.
-
 **!! NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS !!**
 
+This project is under heavy development, there will be continous functions, features and api changes.
+
 # Features
 
 [x] Upload assets(videos/images)

+ 1 - 1
mobile/lib/modules/home/providers/asset.provider.dart

@@ -14,7 +14,7 @@ class AssetNotifier extends StateNotifier<List<ImmichAssetGroupByDate>> {
   bool isFetching = false;
 
   // Get All assets
-  getImmichAssets() async {
+  getAllAssets() async {
     GetAllAssetResponse? res = await _assetService.getAllAsset();
     nextPageKey = res?.nextPageKey;
 

+ 75 - 0
mobile/lib/modules/home/ui/control_bottom_app_bar.dart

@@ -0,0 +1,75 @@
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
+
+class ControlBottomAppBar extends StatelessWidget {
+  const ControlBottomAppBar({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    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(15), topRight: Radius.circular(15)),
+          color: Colors.grey[300]?.withOpacity(0.98),
+        ),
+        child: Column(
+          children: [
+            Padding(
+              padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: [
+                  ControlBoxButton(
+                    iconData: Icons.delete_forever_rounded,
+                    label: "Delete",
+                    onPressed: () {
+                      showDialog(
+                        context: context,
+                        builder: (BuildContext context) {
+                          return const DeleteDialog();
+                        },
+                      );
+                    },
+                  ),
+                ],
+              ),
+            )
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+class ControlBoxButton extends StatelessWidget {
+  const ControlBoxButton({Key? key, required this.label, required this.iconData, required this.onPressed})
+      : super(key: key);
+
+  final String label;
+  final IconData iconData;
+  final Function onPressed;
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      width: 60,
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.start,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          IconButton(
+            onPressed: () {
+              onPressed();
+            },
+            icon: Icon(iconData, size: 30),
+          ),
+          Text(label)
+        ],
+      ),
+    );
+  }
+}

+ 34 - 26
mobile/lib/modules/home/ui/daily_title_text.dart

@@ -24,6 +24,31 @@ class DailyTitleText extends ConsumerWidget {
     var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
     var selectedItems = ref.watch(homePageStateProvider).selectedItems;
 
+    void _handleTitleIconClick() {
+      if (isMultiSelectEnable &&
+          selectedDateGroup.contains(dateText) &&
+          selectedDateGroup.length == 1 &&
+          selectedItems.length <= assetGroup.length) {
+        // Multi select is active - click again on the icon while it is the only active group -> disable multi select
+        ref.watch(homePageStateProvider.notifier).disableMultiSelect();
+      } else if (isMultiSelectEnable &&
+          selectedDateGroup.contains(dateText) &&
+          selectedItems.length != assetGroup.length) {
+        // Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items
+        ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText);
+        ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup);
+      } else if (isMultiSelectEnable && selectedDateGroup.contains(dateText) && selectedDateGroup.length > 1) {
+        ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText);
+        ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup);
+      } else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
+        ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText);
+        ref.watch(homePageStateProvider.notifier).addMultipleSelectedItems(assetGroup);
+      } else {
+        ref.watch(homePageStateProvider.notifier).enableMultiSelect(assetGroup.toSet());
+        ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText);
+      }
+    }
+
     return SliverToBoxAdapter(
       child: Padding(
         padding: const EdgeInsets.only(top: 29.0, bottom: 29.0, left: 12.0, right: 12.0),
@@ -39,33 +64,16 @@ class DailyTitleText extends ConsumerWidget {
             ),
             const Spacer(),
             GestureDetector(
-              onTap: () {
-                if (isMultiSelectEnable &&
-                    selectedDateGroup.contains(dateText) &&
-                    selectedDateGroup.length == 1 &&
-                    selectedItems.length == assetGroup.length) {
-                  ref.watch(homePageStateProvider.notifier).disableMultiSelect();
-                } else if (isMultiSelectEnable &&
-                    selectedDateGroup.contains(dateText) &&
-                    selectedItems.length != assetGroup.length) {
-                  ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText);
-                  ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup);
-                } else if (isMultiSelectEnable &&
-                    selectedDateGroup.contains(dateText) &&
-                    selectedDateGroup.length > 1) {
-                  ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText);
-                  ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup);
-                } else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
-                  ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText);
-                  ref.watch(homePageStateProvider.notifier).addMultipleSelectedItems(assetGroup);
-                } else {
-                  ref.watch(homePageStateProvider.notifier).enableMultiSelect(assetGroup.toSet());
-                  ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText);
-                }
-              },
+              onTap: _handleTitleIconClick,
               child: isMultiSelectEnable && selectedDateGroup.contains(dateText)
-                  ? const Icon(Icons.check_circle_rounded)
-                  : const Icon(Icons.check_circle_outline_rounded),
+                  ? Icon(
+                      Icons.check_circle_rounded,
+                      color: Theme.of(context).primaryColor,
+                    )
+                  : const Icon(
+                      Icons.check_circle_outline_rounded,
+                      color: Colors.grey,
+                    ),
             )
           ],
         ),

+ 33 - 0
mobile/lib/modules/home/ui/delete_diaglog.dart

@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+
+class DeleteDialog extends StatelessWidget {
+  const DeleteDialog({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      backgroundColor: Colors.grey[200],
+      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
+      title: const Text("Delete Permanently"),
+      content: const Text("These items will be permanently deleted from Immich and from your device"),
+      actions: [
+        TextButton(
+          onPressed: () {
+            Navigator.of(context).pop();
+          },
+          child: const Text(
+            "Cancel",
+            style: TextStyle(color: Colors.blueGrey),
+          ),
+        ),
+        TextButton(
+          onPressed: () {},
+          child: Text(
+            "Delete",
+            style: TextStyle(color: Colors.red[400]),
+          ),
+        ),
+      ],
+    );
+  }
+}

+ 47 - 0
mobile/lib/modules/home/ui/disable_multi_select_button.dart

@@ -0,0 +1,47 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
+
+class DisableMultiSelectButton extends ConsumerWidget {
+  const DisableMultiSelectButton({
+    Key? key,
+    required this.onPressed,
+    required this.selectedItemCount,
+  }) : super(key: key);
+
+  final Function onPressed;
+  final int selectedItemCount;
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    return Positioned(
+      top: 0,
+      left: 0,
+      child: Padding(
+        padding: const EdgeInsets.only(left: 16.0, top: 46),
+        child: Material(
+          elevation: 20,
+          borderRadius: BorderRadius.circular(35),
+          child: Container(
+            decoration: BoxDecoration(
+              borderRadius: BorderRadius.circular(35),
+              color: Colors.grey[100],
+            ),
+            child: Padding(
+              padding: const EdgeInsets.symmetric(horizontal: 4.0),
+              child: TextButton.icon(
+                  onPressed: () {
+                    onPressed();
+                  },
+                  icon: const Icon(Icons.close_rounded),
+                  label: Text(
+                    selectedItemCount.toString(),
+                    style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
+                  )),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 1 - 1
mobile/lib/modules/home/ui/profile_drawer.dart

@@ -57,8 +57,8 @@ class ProfileDrawer extends ConsumerWidget {
               bool res = await ref.read(authenticationProvider.notifier).logout();
 
               if (res) {
+                ref.watch(assetProvider.notifier).clearAllAsset();
                 AutoRouter.of(context).popUntilRoot();
-                ref.read(assetProvider.notifier).clearAllAsset();
               }
             },
           )

+ 1 - 0
mobile/lib/modules/home/ui/thumbnail_image.dart

@@ -49,6 +49,7 @@ class ThumbnailImage extends HookConsumerWidget {
         } else if (isMultiSelectEnable && !selectedAsset.contains(asset)) {
           ref.watch(homePageStateProvider.notifier).addSingleSelectedItem(asset);
         } else {
+          print(asset.id);
           if (asset.type == 'IMAGE') {
             AutoRouter.of(context).push(
               ImageViewerRoute(

+ 49 - 17
mobile/lib/modules/home/views/home_page.dart

@@ -1,7 +1,10 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
+import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
 import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
+import 'package:immich_mobile/modules/home/ui/disable_multi_select_button.dart';
 import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
 import 'package:immich_mobile/modules/home/ui/image_grid.dart';
 import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
@@ -9,6 +12,7 @@ import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
 import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
 import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
 import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
+import 'package:sliver_tools/sliver_tools.dart';
 
 class HomePage extends HookConsumerWidget {
   const HomePage({Key? key}) : super(key: key);
@@ -18,6 +22,8 @@ class HomePage extends HookConsumerWidget {
     ScrollController _scrollController = useScrollController();
     List<ImmichAssetGroupByDate> _assetGroup = ref.watch(assetProvider);
     List<Widget> _imageGridGroup = [];
+    var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
+    var homePageState = ref.watch(homePageStateProvider);
 
     _scrollControllerCallback() {
       var endOfPage = _scrollController.position.maxScrollExtent;
@@ -28,10 +34,9 @@ class HomePage extends HookConsumerWidget {
     }
 
     useEffect(() {
-      ref.read(assetProvider.notifier).getImmichAssets();
+      ref.read(assetProvider.notifier).getAllAssets();
 
       _scrollController.addListener(_scrollControllerCallback);
-
       return () {
         _scrollController.removeListener(_scrollControllerCallback);
       };
@@ -45,7 +50,7 @@ class HomePage extends HookConsumerWidget {
       if (_imageGridGroup.isNotEmpty && _imageGridGroup.length < 20) {
         ref.read(assetProvider.notifier).getOlderAsset();
       } else if (_imageGridGroup.isEmpty) {
-        ref.read(assetProvider.notifier).getImmichAssets();
+        ref.read(assetProvider.notifier).getAllAssets();
       }
     }
 
@@ -72,7 +77,10 @@ class HomePage extends HookConsumerWidget {
 
           // Add Daily Title Group
           _imageGridGroup.add(
-            DailyTitleText(isoDate: dateTitle, assetGroup: assetGroup),
+            DailyTitleText(
+              isoDate: dateTitle,
+              assetGroup: assetGroup,
+            ),
           );
 
           // Add Image Group
@@ -85,25 +93,49 @@ class HomePage extends HookConsumerWidget {
       }
 
       return SafeArea(
-        child: DraggableScrollbar.semicircle(
-          backgroundColor: Theme.of(context).primaryColor,
-          controller: _scrollController,
-          heightScrollThumb: 48.0,
-          child: CustomScrollView(
-            controller: _scrollController,
-            slivers: [
-              ImmichSliverAppBar(
-                imageGridGroup: _imageGridGroup,
-                onPopBack: onPopBackFromBackupPage,
+        bottom: !isMultiSelectEnable,
+        top: !isMultiSelectEnable,
+        child: Stack(
+          children: [
+            DraggableScrollbar.semicircle(
+              backgroundColor: Theme.of(context).primaryColor,
+              controller: _scrollController,
+              heightScrollThumb: 48.0,
+              child: CustomScrollView(
+                controller: _scrollController,
+                slivers: [
+                  SliverAnimatedSwitcher(
+                    child: isMultiSelectEnable
+                        ? const SliverToBoxAdapter(
+                            child: SizedBox(
+                              height: 70,
+                              child: null,
+                            ),
+                          )
+                        : ImmichSliverAppBar(
+                            imageGridGroup: _imageGridGroup,
+                            onPopBack: onPopBackFromBackupPage,
+                          ),
+                    duration: const Duration(milliseconds: 350),
+                  ),
+                  ..._imageGridGroup
+                ],
               ),
-              ..._imageGridGroup,
-            ],
-          ),
+            ),
+            isMultiSelectEnable
+                ? DisableMultiSelectButton(
+                    onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
+                    selectedItemCount: homePageState.selectedItems.length,
+                  )
+                : Container(),
+            isMultiSelectEnable ? const ControlBottomAppBar() : Container(),
+          ],
         ),
       );
     }
 
     return Scaffold(
+      // key: _scaffoldKey,
       drawer: const ProfileDrawer(),
       body: _buildBody(),
     );

+ 8 - 8
mobile/pubspec.lock

@@ -513,13 +513,6 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.12.11"
-  material_color_utilities:
-    dependency: transitive
-    description:
-      name: material_color_utilities
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "0.1.3"
   meta:
     dependency: transitive
     description:
@@ -721,6 +714,13 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.99"
+  sliver_tools:
+    dependency: "direct main"
+    description:
+      name: sliver_tools
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.5"
   source_gen:
     dependency: transitive
     description:
@@ -818,7 +818,7 @@ packages:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.8"
+    version: "0.4.3"
   timing:
     dependency: transitive
     description:

+ 1 - 0
mobile/pubspec.yaml

@@ -30,6 +30,7 @@ dependencies:
   fluttertoast: ^8.0.8
   video_player: ^2.2.18
   chewie: ^1.2.2
+  sliver_tools: ^0.2.5
 
 dev_dependencies:
   flutter_test: