Bläddra i källkod

Merge branch 'db_and_services' of github.com:ente-io/photos-app into db_and_services

Neeraj Gupta 2 år sedan
förälder
incheckning
628967e999

+ 23 - 0
lib/services/collections_service.dart

@@ -395,6 +395,29 @@ class CollectionsService {
     }
     }
   }
   }
 
 
+  Future<void> trashEmptyCollection(Collection collection) async {
+    try {
+      // While trashing empty albums, we must pass keepFiles flag as True.
+      // The server will verify that the collection is actually empty before
+      // deleting the files. If keepFiles is set as False and the collection
+      // is not empty, then the files in the collections will be moved to trash.
+      await _enteDio.delete(
+        "/collections/v3/${collection.id}?keepFiles=True&collectionID=${collection.id}",
+      );
+      final deletedCollection = collection.copyWith(isDeleted: true);
+      _collectionIDToCollections[collection.id] = deletedCollection;
+      unawaited(_db.insert([deletedCollection]));
+    } on DioError catch (e) {
+      if (e.response != null) {
+        debugPrint("Error " + e.response!.toString());
+      }
+      rethrow;
+    } catch (e) {
+      _logger.severe('failed to trash empty collection', e);
+      rethrow;
+    }
+  }
+
   Future<void> _handleCollectionDeletion(Collection collection) async {
   Future<void> _handleCollectionDeletion(Collection collection) async {
     await _filesDB.deleteCollection(collection.id);
     await _filesDB.deleteCollection(collection.id);
     final deletedCollection = collection.copyWith(isDeleted: true);
     final deletedCollection = collection.copyWith(isDeleted: true);

+ 11 - 3
lib/ui/collections_gallery_widget.dart

@@ -1,5 +1,3 @@
-
-
 import 'dart:async';
 import 'dart:async';
 
 
 import 'package:collection/collection.dart';
 import 'package:collection/collection.dart';
@@ -22,6 +20,7 @@ import 'package:photos/ui/collections/remote_collections_grid_view_widget.dart';
 import 'package:photos/ui/collections/section_title.dart';
 import 'package:photos/ui/collections/section_title.dart';
 import 'package:photos/ui/collections/trash_button_widget.dart';
 import 'package:photos/ui/collections/trash_button_widget.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/common/loading_widget.dart';
+import 'package:photos/ui/viewer/actions/delete_empty_albums.dart';
 import 'package:photos/ui/viewer/gallery/empty_state.dart';
 import 'package:photos/ui/viewer/gallery/empty_state.dart';
 import 'package:photos/utils/local_settings.dart';
 import 'package:photos/utils/local_settings.dart';
 
 
@@ -37,7 +36,8 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
     with AutomaticKeepAliveClientMixin {
     with AutomaticKeepAliveClientMixin {
   final _logger = Logger((_CollectionsGalleryWidgetState).toString());
   final _logger = Logger((_CollectionsGalleryWidgetState).toString());
   late StreamSubscription<LocalPhotosUpdatedEvent> _localFilesSubscription;
   late StreamSubscription<LocalPhotosUpdatedEvent> _localFilesSubscription;
-  late StreamSubscription<CollectionUpdatedEvent> _collectionUpdatesSubscription;
+  late StreamSubscription<CollectionUpdatedEvent>
+      _collectionUpdatesSubscription;
   late StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
   late StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
   AlbumSortKey? sortKey;
   AlbumSortKey? sortKey;
   String _loadReason = "init";
   String _loadReason = "init";
@@ -123,6 +123,8 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
   Widget _getCollectionsGalleryWidget(
   Widget _getCollectionsGalleryWidget(
     List<CollectionWithThumbnail>? collections,
     List<CollectionWithThumbnail>? collections,
   ) {
   ) {
+    final bool showDeleteAlbumsButton =
+        collections!.where((c) => c.thumbnail == null).length >= 3;
     final TextStyle trashAndHiddenTextStyle = Theme.of(context)
     final TextStyle trashAndHiddenTextStyle = Theme.of(context)
         .textTheme
         .textTheme
         .subtitle1!
         .subtitle1!
@@ -149,6 +151,12 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
                 _sortMenu(),
                 _sortMenu(),
               ],
               ],
             ),
             ),
+            showDeleteAlbumsButton
+                ? const Padding(
+                    padding: EdgeInsets.only(top: 2, left: 8.5, right: 48),
+                    child: DeleteEmptyAlbums(),
+                  )
+                : const SizedBox.shrink(),
             const SizedBox(height: 12),
             const SizedBox(height: 12),
             Configuration.instance.hasConfiguredAccount()
             Configuration.instance.hasConfiguredAccount()
                 ? RemoteCollectionsGridViewWidget(collections)
                 ? RemoteCollectionsGridViewWidget(collections)

+ 29 - 0
lib/ui/components/button_widget.dart

@@ -60,6 +60,11 @@ class ButtonWidget extends StatelessWidget {
   ///This should be set to true if the alert which uses this button needs to
   ///This should be set to true if the alert which uses this button needs to
   ///return the Button's action.
   ///return the Button's action.
   final bool isInAlert;
   final bool isInAlert;
+
+  /// progressStatus can be used to display information about the action
+  /// progress when ExecutionState is in Progress.
+  final ValueNotifier<String>? progressStatus;
+
   const ButtonWidget({
   const ButtonWidget({
     required this.buttonType,
     required this.buttonType,
     this.buttonSize = ButtonSize.large,
     this.buttonSize = ButtonSize.large,
@@ -72,6 +77,7 @@ class ButtonWidget extends StatelessWidget {
     this.isInAlert = false,
     this.isInAlert = false,
     this.iconColor,
     this.iconColor,
     this.shouldSurfaceExecutionStates = true,
     this.shouldSurfaceExecutionStates = true,
+    this.progressStatus,
     super.key,
     super.key,
   });
   });
 
 
@@ -137,6 +143,7 @@ class ButtonWidget extends StatelessWidget {
       icon: icon,
       icon: icon,
       buttonAction: buttonAction,
       buttonAction: buttonAction,
       shouldSurfaceExecutionStates: shouldSurfaceExecutionStates,
       shouldSurfaceExecutionStates: shouldSurfaceExecutionStates,
+      progressStatus: progressStatus,
     );
     );
   }
   }
 }
 }
@@ -152,6 +159,8 @@ class ButtonChildWidget extends StatefulWidget {
   final ButtonAction? buttonAction;
   final ButtonAction? buttonAction;
   final bool isInAlert;
   final bool isInAlert;
   final bool shouldSurfaceExecutionStates;
   final bool shouldSurfaceExecutionStates;
+  final ValueNotifier<String>? progressStatus;
+
   const ButtonChildWidget({
   const ButtonChildWidget({
     required this.buttonStyle,
     required this.buttonStyle,
     required this.buttonType,
     required this.buttonType,
@@ -159,6 +168,7 @@ class ButtonChildWidget extends StatefulWidget {
     required this.buttonSize,
     required this.buttonSize,
     required this.isInAlert,
     required this.isInAlert,
     required this.shouldSurfaceExecutionStates,
     required this.shouldSurfaceExecutionStates,
+    this.progressStatus,
     this.onTap,
     this.onTap,
     this.labelText,
     this.labelText,
     this.icon,
     this.icon,
@@ -177,14 +187,17 @@ class _ButtonChildWidgetState extends State<ButtonChildWidget> {
   late TextStyle labelStyle;
   late TextStyle labelStyle;
   late Color checkIconColor;
   late Color checkIconColor;
   late Color loadingIconColor;
   late Color loadingIconColor;
+  ValueNotifier<String>? progressStatus;
 
 
   ///This is used to store the width of the button in idle state (small button)
   ///This is used to store the width of the button in idle state (small button)
   ///to be used as width for the button when the loading/succes states comes.
   ///to be used as width for the button when the loading/succes states comes.
   double? widthOfButton;
   double? widthOfButton;
   final _debouncer = Debouncer(const Duration(milliseconds: 300));
   final _debouncer = Debouncer(const Duration(milliseconds: 300));
   ExecutionState executionState = ExecutionState.idle;
   ExecutionState executionState = ExecutionState.idle;
+
   @override
   @override
   void initState() {
   void initState() {
+    progressStatus = widget.progressStatus;
     checkIconColor = widget.buttonStyle.checkIconColor ??
     checkIconColor = widget.buttonStyle.checkIconColor ??
         widget.buttonStyle.defaultIconColor;
         widget.buttonStyle.defaultIconColor;
     loadingIconColor = widget.buttonStyle.defaultIconColor;
     loadingIconColor = widget.buttonStyle.defaultIconColor;
@@ -203,6 +216,7 @@ class _ButtonChildWidgetState extends State<ButtonChildWidget> {
       iconColor = widget.buttonStyle.defaultIconColor;
       iconColor = widget.buttonStyle.defaultIconColor;
       labelStyle = widget.buttonStyle.defaultLabelStyle;
       labelStyle = widget.buttonStyle.defaultLabelStyle;
     }
     }
+
     super.initState();
     super.initState();
   }
   }
 
 
@@ -315,6 +329,21 @@ class _ButtonChildWidgetState extends State<ButtonChildWidget> {
                             mainAxisAlignment: MainAxisAlignment.center,
                             mainAxisAlignment: MainAxisAlignment.center,
                             mainAxisSize: MainAxisSize.min,
                             mainAxisSize: MainAxisSize.min,
                             children: [
                             children: [
+                              progressStatus == null
+                                  ? const SizedBox.shrink()
+                                  : ValueListenableBuilder<String>(
+                                      valueListenable: progressStatus!,
+                                      builder: (
+                                        BuildContext context,
+                                        String value,
+                                        Widget? child,
+                                      ) {
+                                        return Text(
+                                          value,
+                                          style: lightTextTheme.smallBold,
+                                        );
+                                      },
+                                    ),
                               EnteLoadingWidget(
                               EnteLoadingWidget(
                                 is20pts: true,
                                 is20pts: true,
                                 color: loadingIconColor,
                                 color: loadingIconColor,

+ 108 - 0
lib/ui/viewer/actions/delete_empty_albums.dart

@@ -0,0 +1,108 @@
+import 'package:flutter/material.dart';
+import 'package:photos/core/event_bus.dart';
+import 'package:photos/events/collection_updated_event.dart';
+import 'package:photos/models/file.dart';
+import 'package:photos/services/collections_service.dart';
+import 'package:photos/ui/components/action_sheet_widget.dart';
+import 'package:photos/ui/components/button_widget.dart';
+import 'package:photos/ui/components/models/button_type.dart';
+
+class DeleteEmptyAlbums extends StatefulWidget {
+  const DeleteEmptyAlbums({Key? key}) : super(key: key);
+
+  @override
+  State<DeleteEmptyAlbums> createState() => _DeleteEmptyAlbumsState();
+}
+
+class _DeleteEmptyAlbumsState extends State<DeleteEmptyAlbums> {
+  final ValueNotifier<String> _deleteProgress = ValueNotifier("");
+  bool _isCancelled = false;
+
+  @override
+  void dispose() {
+    _deleteProgress.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Align(
+      alignment: Alignment.centerLeft,
+      child: ButtonWidget(
+        buttonSize: ButtonSize.small,
+        buttonType: ButtonType.secondary,
+        labelText: "Delete empty albums",
+        icon: Icons.delete_sweep_outlined,
+        shouldSurfaceExecutionStates: false,
+        onTap: () async {
+          await showActionSheet(
+            context: context,
+            isDismissible: false,
+            buttons: [
+              ButtonWidget(
+                labelText: "Yes",
+                buttonType: ButtonType.neutral,
+                buttonSize: ButtonSize.large,
+                shouldStickToDarkTheme: true,
+                shouldSurfaceExecutionStates: true,
+                progressStatus: _deleteProgress,
+                onTap: () async {
+                  await _deleteEmptyAlbums();
+                  if (!_isCancelled) {
+                    Navigator.of(context, rootNavigator: true).pop();
+                  }
+                  Bus.instance.fire(
+                    CollectionUpdatedEvent(
+                      0,
+                      <File>[],
+                      "empty_albums_deleted",
+                    ),
+                  );
+                  CollectionsService.instance.sync().ignore();
+                  _isCancelled = false;
+                },
+              ),
+              ButtonWidget(
+                labelText: "Cancel",
+                buttonType: ButtonType.secondary,
+                buttonSize: ButtonSize.large,
+                shouldStickToDarkTheme: true,
+                onTap: () async {
+                  _isCancelled = true;
+                  Navigator.of(context, rootNavigator: true).pop();
+                },
+              )
+            ],
+            title: "Delete empty albums",
+            body:
+                "This will delete all empty albums. This is useful when you want to reduce the clutter in your album list.",
+            actionSheetType: ActionSheetType.defaultActionSheet,
+          );
+        },
+      ),
+    );
+  }
+
+  Future<void> _deleteEmptyAlbums() async {
+    final collections =
+        await CollectionsService.instance.getCollectionsWithThumbnails();
+    collections.removeWhere((element) => element.thumbnail != null);
+    int failedCount = 0;
+    for (int i = 0; i < collections.length; i++) {
+      if (mounted && !_isCancelled) {
+        _deleteProgress.value =
+            "Deleting ${(i + 1).toString().padLeft(collections.length.toString().length, '0')}/ "
+            "${collections.length} ";
+        try {
+          await CollectionsService.instance
+              .trashEmptyCollection(collections[i].collection);
+        } catch (_) {
+          failedCount++;
+        }
+      }
+    }
+    if (failedCount > 0) {
+      debugPrint("Delete ops failed for $failedCount collections");
+    }
+  }
+}

+ 0 - 19
lib/ui/viewer/actions/file_selection_common_actions_widget.dart

@@ -1,19 +0,0 @@
-import 'package:flutter/cupertino.dart';
-import 'package:photos/models/gallery_type.dart';
-import 'package:photos/models/selected_files.dart';
-
-class FileSelectionCommonActionWidget extends StatelessWidget {
-  final GalleryType type;
-  final SelectedFiles selectedFiles;
-
-  const FileSelectionCommonActionWidget({
-    super.key,
-    required this.type,
-    required this.selectedFiles,
-  });
-  @override
-  Widget build(BuildContext context) {
-    // TODO: implement build
-    throw UnimplementedError();
-  }
-}