Bladeren bron

Merge branch 'main' into reset-ignored-files

ashilkn 2 jaren geleden
bovenliggende
commit
72ebeba3ba

+ 7 - 0
lib/services/collections_service.dart

@@ -884,6 +884,8 @@ class CollectionsService {
     final params = <String, dynamic>{};
     params["collectionID"] = toCollectionID;
     final toCollectionKey = getCollectionKey(toCollectionID);
+    final Set<String> existingLocalIDS =
+        await FilesDB.instance.getExistingLocalFileIDs();
     final batchedFiles = files.chunks(batchSize);
     for (final batch in batchedFiles) {
       params["files"] = [];
@@ -892,6 +894,11 @@ class CollectionsService {
         file.generatedID =
             null; // So that a new entry is created in the FilesDB
         file.collectionID = toCollectionID;
+        // During restore, if trash file local ID is not present in currently
+        // imported files, treat the file as deleted from device
+        if (file.localID != null && !existingLocalIDS.contains(file.localID)) {
+          file.localID = null;
+        }
         final encryptedKeyData = CryptoUtil.encryptSync(key, toCollectionKey);
         file.encryptedKey = Sodium.bin2base64(encryptedKeyData.encryptedData!);
         file.keyDecryptionNonce = Sodium.bin2base64(encryptedKeyData.nonce!);

+ 10 - 0
lib/services/trash_sync_service.dart

@@ -5,6 +5,7 @@ import 'package:logging/logging.dart';
 import 'package:photos/core/constants.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/network.dart';
+import 'package:photos/db/files_db.dart';
 import 'package:photos/db/trash_db.dart';
 import 'package:photos/events/collection_updated_event.dart';
 import 'package:photos/events/force_reload_trash_page_event.dart';
@@ -40,9 +41,18 @@ class TrashSyncService {
     bool isLocalTrashUpdated = false;
     _logger.fine('sync trash sinceTime : $lastSyncTime');
     final diff = await _diffFetcher.getTrashFilesDiff(lastSyncTime);
+    Set<String>? localFileIDs;
     if (diff.trashedFiles.isNotEmpty) {
       isLocalTrashUpdated = true;
+      localFileIDs ??= await FilesDB.instance.getExistingLocalFileIDs();
       _logger.fine("inserting ${diff.trashedFiles.length} items in trash");
+      // During sync, if trash file local ID is not present in currently
+      // imported files, treat the file as deleted from device
+      for (var trash in diff.trashedFiles) {
+        if (trash.localID != null && !localFileIDs.contains(trash.localID)) {
+          trash.localID = null;
+        }
+      }
       await _trashDB.insertMultiple(diff.trashedFiles);
     }
     if (diff.deletedUploadIDs.isNotEmpty) {

+ 3 - 18
lib/services/user_remote_flag_service.dart

@@ -20,8 +20,6 @@ class UserRemoteFlagService {
       UserRemoteFlagService._privateConstructor();
 
   static const String recoveryVerificationFlag = "recoveryKeyVerified";
-  static const String _passwordReminderFlag = "userNotify"
-      ".passwordReminderFlag";
   static const String needRecoveryKeyVerification =
       "needRecoveryKeyVerification";
 
@@ -29,20 +27,6 @@ class UserRemoteFlagService {
     _prefs = await SharedPreferences.getInstance();
   }
 
-  bool showPasswordReminder() {
-    if (Platform.isAndroid) {
-      return false;
-    }
-    return !_prefs.containsKey(_passwordReminderFlag);
-  }
-
-  Future<bool> stopPasswordReminder() async {
-    if (Platform.isAndroid) {
-      return Future.value(true);
-    }
-    return _prefs.setBool(_passwordReminderFlag, true);
-  }
-
   bool shouldShowRecoveryVerification() {
     if (!_prefs.containsKey(needRecoveryKeyVerification)) {
       // fetch the status from remote
@@ -62,13 +46,14 @@ class UserRemoteFlagService {
   // recovery key in the past or not. This helps in avoid showing the same
   // prompt to the user on re-install or signing into a different device
   Future<void> markRecoveryVerificationAsDone() async {
-    await _updateKeyValue(_passwordReminderFlag, true.toString());
+    await _updateKeyValue(recoveryVerificationFlag, true.toString());
     await _prefs.setBool(needRecoveryKeyVerification, false);
   }
 
   Future<void> _refreshRecoveryVerificationFlag() async {
     _logger.finest('refresh recovery key verification flag');
-    final remoteStatusValue = await _getValue(_passwordReminderFlag, "false");
+    final remoteStatusValue =
+        await _getValue(recoveryVerificationFlag, "false");
     final bool isNeedVerificationFlagSet =
         _prefs.containsKey(needRecoveryKeyVerification);
     if (remoteStatusValue.toLowerCase() == "true") {

+ 18 - 0
lib/ui/components/action_sheet_widget.dart

@@ -24,6 +24,7 @@ Future<ButtonAction?> showActionSheet({
   bool isCheckIconGreen = false,
   String? title,
   String? body,
+  String? bodyHighlight,
 }) {
   return showMaterialModalBottomSheet(
     backgroundColor: Colors.transparent,
@@ -36,6 +37,7 @@ Future<ButtonAction?> showActionSheet({
       return ActionSheetWidget(
         title: title,
         body: body,
+        bodyHighlight: bodyHighlight,
         actionButtons: buttons,
         actionSheetType: actionSheetType,
         isCheckIconGreen: isCheckIconGreen,
@@ -47,6 +49,7 @@ Future<ButtonAction?> showActionSheet({
 class ActionSheetWidget extends StatelessWidget {
   final String? title;
   final String? body;
+  final String? bodyHighlight;
   final List<ButtonWidget> actionButtons;
   final ActionSheetType actionSheetType;
   final bool isCheckIconGreen;
@@ -57,6 +60,7 @@ class ActionSheetWidget extends StatelessWidget {
     required this.isCheckIconGreen,
     this.title,
     this.body,
+    this.bodyHighlight,
     super.key,
   });
 
@@ -100,6 +104,7 @@ class ActionSheetWidget extends StatelessWidget {
                             child: ContentContainerWidget(
                               title: title,
                               body: body,
+                              bodyHighlight: bodyHighlight,
                               actionSheetType: actionSheetType,
                               isCheckIconGreen: isCheckIconGreen,
                             ),
@@ -121,6 +126,7 @@ class ActionSheetWidget extends StatelessWidget {
 class ContentContainerWidget extends StatelessWidget {
   final String? title;
   final String? body;
+  final String? bodyHighlight;
   final ActionSheetType actionSheetType;
   final bool isCheckIconGreen;
   const ContentContainerWidget({
@@ -128,6 +134,7 @@ class ContentContainerWidget extends StatelessWidget {
     required this.isCheckIconGreen,
     this.title,
     this.body,
+    this.bodyHighlight,
     super.key,
   });
 
@@ -165,7 +172,18 @@ class ContentContainerWidget extends StatelessWidget {
                 color: isCheckIconGreen
                     ? getEnteColorScheme(context).primary700
                     : strokeBaseDark,
+              ),
+        actionSheetType == ActionSheetType.defaultActionSheet &&
+                bodyHighlight != null
+            ? Padding(
+                padding: const EdgeInsets.only(top: 19.0),
+                child: Text(
+                  bodyHighlight!,
+                  style: textTheme.body
+                      .copyWith(color: textBaseDark), //constant color
+                ),
               )
+            : const SizedBox.shrink(),
       ],
     );
   }

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

@@ -24,6 +24,7 @@ enum ButtonAction {
   first,
   second,
   third,
+  fourth,
   cancel,
   error;
 }

+ 0 - 3
lib/ui/home/landing_page_widget.dart

@@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/services/update_service.dart';
-import 'package:photos/services/user_remote_flag_service.dart';
 import 'package:photos/ui/account/email_entry_page.dart';
 import 'package:photos/ui/account/login_page.dart';
 import 'package:photos/ui/account/password_entry_page.dart';
@@ -155,7 +154,6 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
 
   Future<void> _navigateToSignUpPage() async {
     UpdateService.instance.hideChangeLog().ignore();
-    UserRemoteFlagService.instance.stopPasswordReminder().ignore();
     Widget page;
     if (Configuration.instance.getEncryptedToken() == null) {
       page = const EmailEntryPage();
@@ -183,7 +181,6 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
 
   void _navigateToSignInPage() {
     UpdateService.instance.hideChangeLog().ignore();
-    UserRemoteFlagService.instance.stopPasswordReminder().ignore();
     Widget page;
     if (Configuration.instance.getEncryptedToken() == null) {
       page = const LoginPage();

+ 2 - 8
lib/ui/home_widget.dart

@@ -1,5 +1,3 @@
-
-
 import 'dart:async';
 import 'dart:io';
 
@@ -24,7 +22,6 @@ import 'package:photos/models/selected_files.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/services/update_service.dart';
-import 'package:photos/services/user_remote_flag_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/states/user_details_state.dart';
 import 'package:photos/theme/colors.dart';
@@ -41,7 +38,6 @@ import 'package:photos/ui/home/landing_page_widget.dart';
 import 'package:photos/ui/home/preserve_footer_widget.dart';
 import 'package:photos/ui/home/start_backup_hook_widget.dart';
 import 'package:photos/ui/loading_photos_widget.dart';
-import 'package:photos/ui/notification/prompts/password_reminder.dart';
 import 'package:photos/ui/notification/update/change_log_page.dart';
 import 'package:photos/ui/settings/app_update_dialog.dart';
 import 'package:photos/ui/settings_page.dart';
@@ -78,7 +74,8 @@ class _HomeWidgetState extends State<HomeWidget> {
   List<SharedMediaFile>? _sharedFiles;
 
   late StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
-  late StreamSubscription<SubscriptionPurchasedEvent> _subscriptionPurchaseEvent;
+  late StreamSubscription<SubscriptionPurchasedEvent>
+      _subscriptionPurchaseEvent;
   late StreamSubscription<TriggerLogoutEvent> _triggerLogoutEvent;
   late StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
   late StreamSubscription<PermissionGrantedEvent> _permissionGrantedEvent;
@@ -320,9 +317,6 @@ class _HomeWidgetState extends State<HomeWidget> {
       return const LoadingPhotosWidget();
     }
 
-    if (UserRemoteFlagService.instance.showPasswordReminder()) {
-      return const PasswordReminder();
-    }
     if (_sharedFiles != null && _sharedFiles!.isNotEmpty) {
       ReceiveSharingIntent.reset();
       return CreateCollectionPage(null, _sharedFiles);

+ 0 - 371
lib/ui/notification/prompts/password_reminder.dart

@@ -1,371 +0,0 @@
-import 'dart:ui';
-
-import 'package:flutter/material.dart';
-import 'package:logging/logging.dart';
-import 'package:pedantic/pedantic.dart';
-import 'package:photos/core/configuration.dart';
-import 'package:photos/ente_theme_data.dart';
-import 'package:photos/services/local_authentication_service.dart';
-import 'package:photos/services/user_remote_flag_service.dart';
-import 'package:photos/theme/colors.dart';
-import 'package:photos/theme/ente_theme.dart';
-import 'package:photos/ui/account/password_entry_page.dart';
-import 'package:photos/ui/common/gradient_button.dart';
-import 'package:photos/ui/home_widget.dart';
-import 'package:photos/utils/dialog_util.dart';
-import 'package:photos/utils/navigation_util.dart';
-
-class PasswordReminder extends StatefulWidget {
-  const PasswordReminder({Key? key}) : super(key: key);
-
-  @override
-  State<PasswordReminder> createState() => _PasswordReminderState();
-}
-
-class _PasswordReminderState extends State<PasswordReminder> {
-  final _passwordController = TextEditingController();
-  final Logger _logger = Logger((_PasswordReminderState).toString());
-  bool _password2Visible = false;
-  bool _incorrectPassword = false;
-
-  Future<void> _verifyRecoveryKey() async {
-    final dialog = createProgressDialog(context, "Verifying password...");
-    await dialog.show();
-    try {
-      final String inputKey = _passwordController.text;
-      await Configuration.instance.verifyPassword(inputKey);
-      await dialog.hide();
-      UserRemoteFlagService.instance.stopPasswordReminder().ignore();
-      // todo: change this as per figma once the component is ready
-      await showErrorDialog(
-        context,
-        "Password verified",
-        "Great! Thank you for verifying.\n"
-            "\nPlease"
-            " remember to keep your recovery key safely backed up.",
-      );
-
-      unawaited(
-        Navigator.of(context).pushAndRemoveUntil(
-          MaterialPageRoute(
-            builder: (BuildContext context) {
-              return const HomeWidget();
-            },
-          ),
-          (route) => false,
-        ),
-      );
-    } catch (e, s) {
-      _logger.severe("failed to verify password", e, s);
-      await dialog.hide();
-      _incorrectPassword = true;
-      if (mounted) {
-        setState(() => {});
-      }
-    }
-  }
-
-  Future<void> _onChangePasswordClick() async {
-    try {
-      final hasAuthenticated =
-          await LocalAuthenticationService.instance.requestLocalAuthentication(
-        context,
-        "Please authenticate to change your password",
-      );
-      if (hasAuthenticated) {
-        UserRemoteFlagService.instance.stopPasswordReminder().ignore();
-        await routeToPage(
-          context,
-          const PasswordEntryPage(
-            mode: PasswordEntryMode.update,
-          ),
-          forceCustomPageRoute: true,
-        );
-        unawaited(
-          Navigator.of(context).pushAndRemoveUntil(
-            MaterialPageRoute(
-              builder: (BuildContext context) {
-                return const HomeWidget();
-              },
-            ),
-            (route) => false,
-          ),
-        );
-      }
-    } catch (e) {
-      showGenericErrorDialog(context: context);
-      return;
-    }
-  }
-
-  Future<void> _onSkipClick() async {
-    final enteTextTheme = getEnteTextTheme(context);
-    final enteColor = getEnteColorScheme(context);
-    final content = Column(
-      crossAxisAlignment: CrossAxisAlignment.start,
-      mainAxisSize: MainAxisSize.min,
-      children: [
-        Text(
-          "You will not be able to access your photos if you forget "
-          "your password.\n\nIf you do not remember your password, "
-          "now is a good time to change it.",
-          style: enteTextTheme.body.copyWith(
-            color: enteColor.textMuted,
-          ),
-        ),
-        const Padding(padding: EdgeInsets.all(8)),
-        SizedBox(
-          width: double.infinity,
-          height: 52,
-          child: OutlinedButton(
-            style: Theme.of(context).outlinedButtonTheme.style?.copyWith(
-              textStyle: MaterialStateProperty.resolveWith<TextStyle>(
-                (Set<MaterialState> states) {
-                  return enteTextTheme.bodyBold;
-                },
-              ),
-            ),
-            onPressed: () async {
-              Navigator.of(context, rootNavigator: true).pop('dialog');
-
-              _onChangePasswordClick();
-            },
-            child: const Text(
-              "Change password",
-            ),
-          ),
-        ),
-        const Padding(padding: EdgeInsets.all(8)),
-        SizedBox(
-          width: double.infinity,
-          height: 52,
-          child: OutlinedButton(
-            style: Theme.of(context).outlinedButtonTheme.style?.copyWith(
-              textStyle: MaterialStateProperty.resolveWith<TextStyle>(
-                (Set<MaterialState> states) {
-                  return enteTextTheme.bodyBold;
-                },
-              ),
-              backgroundColor: MaterialStateProperty.resolveWith<Color>(
-                (Set<MaterialState> states) {
-                  return enteColor.fillFaint;
-                },
-              ),
-              foregroundColor: MaterialStateProperty.resolveWith<Color>(
-                (Set<MaterialState> states) {
-                  return Theme.of(context).colorScheme.defaultTextColor;
-                },
-              ),
-            ),
-            onPressed: () async {
-              Navigator.of(context, rootNavigator: true).pop('dialog');
-            },
-            child: Text(
-              "Cancel",
-              style: enteTextTheme.bodyBold,
-            ),
-          ),
-        )
-      ],
-    );
-    return showDialog(
-      context: context,
-      builder: (BuildContext context) {
-        return AlertDialog(
-          backgroundColor: enteColor.backgroundElevated,
-          title: Column(
-            crossAxisAlignment: CrossAxisAlignment.start,
-            children: [
-              Icon(
-                Icons.report_outlined,
-                size: 36,
-                color: getEnteColorScheme(context).strokeBase,
-              ),
-            ],
-          ),
-          content: content,
-        );
-      },
-      barrierColor: enteColor.backdropFaint,
-    );
-  }
-
-  @override
-  void dispose() {
-    _passwordController.dispose();
-    super.dispose();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    final enteTheme = Theme.of(context).colorScheme.enteTheme;
-    final List<Widget> actions = <Widget>[];
-    actions.add(
-      PopupMenuButton(
-        itemBuilder: (context) {
-          return [
-            PopupMenuItem(
-              value: 1,
-              child: SizedBox(
-                width: 120,
-                height: 32,
-                child: Row(
-                  mainAxisAlignment: MainAxisAlignment.start,
-                  crossAxisAlignment: CrossAxisAlignment.center,
-                  children: [
-                    const Icon(
-                      Icons.report_outlined,
-                      color: warning500,
-                      size: 20,
-                    ),
-                    const Padding(padding: EdgeInsets.symmetric(horizontal: 6)),
-                    Text(
-                      "Skip",
-                      style: getEnteTextTheme(context)
-                          .bodyBold
-                          .copyWith(color: warning500),
-                    ),
-                  ],
-                ),
-              ),
-            ),
-          ];
-        },
-        onSelected: (value) async {
-          _onSkipClick();
-        },
-      ),
-    );
-
-    return Scaffold(
-      appBar: AppBar(
-        elevation: 0,
-        leading: null,
-        automaticallyImplyLeading: false,
-        actions: actions,
-      ),
-      body: Padding(
-        padding: const EdgeInsets.symmetric(horizontal: 20.0),
-        child: LayoutBuilder(
-          builder: (context, constraints) {
-            return SingleChildScrollView(
-              child: ConstrainedBox(
-                constraints: BoxConstraints(
-                  minWidth: constraints.maxWidth,
-                  minHeight: constraints.maxHeight,
-                ),
-                child: IntrinsicHeight(
-                  child: Column(
-                    mainAxisAlignment: MainAxisAlignment.start,
-                    children: [
-                      SizedBox(
-                        width: double.infinity,
-                        child: Column(
-                          crossAxisAlignment: CrossAxisAlignment.start,
-                          mainAxisAlignment: MainAxisAlignment.start,
-                          children: [
-                            Text(
-                              'Password reminder',
-                              style: enteTheme.textTheme.h3Bold,
-                            ),
-                            Text(
-                              Configuration.instance.getEmail()!,
-                              style: enteTheme.textTheme.small.copyWith(
-                                color: enteTheme.colorScheme.textMuted,
-                              ),
-                            ),
-                          ],
-                        ),
-                      ),
-                      const SizedBox(height: 18),
-                      Text(
-                        "Enter your password to ensure you remember it."
-                        "\n\nThe developer account we use to publish ente on App Store will change in the next version, so you will need to login again when the next version is released.",
-                        style: enteTheme.textTheme.small
-                            .copyWith(color: enteTheme.colorScheme.textMuted),
-                      ),
-                      const SizedBox(height: 24),
-                      TextFormField(
-                        autofillHints: const [AutofillHints.password],
-                        decoration: InputDecoration(
-                          filled: true,
-                          hintText: "Password",
-                          suffixIcon: IconButton(
-                            icon: Icon(
-                              _password2Visible
-                                  ? Icons.visibility
-                                  : Icons.visibility_off,
-                              color: Theme.of(context).iconTheme.color,
-                              size: 20,
-                            ),
-                            onPressed: () {
-                              setState(() {
-                                _password2Visible = !_password2Visible;
-                              });
-                            },
-                          ),
-                          contentPadding: const EdgeInsets.all(20),
-                          border: UnderlineInputBorder(
-                            borderSide: BorderSide.none,
-                            borderRadius: BorderRadius.circular(6),
-                          ),
-                        ),
-                        style: const TextStyle(
-                          fontSize: 14,
-                          fontFeatures: [FontFeature.tabularFigures()],
-                        ),
-                        controller: _passwordController,
-                        autofocus: false,
-                        autocorrect: false,
-                        obscureText: !_password2Visible,
-                        keyboardType: TextInputType.visiblePassword,
-                        onChanged: (_) {
-                          _incorrectPassword = false;
-                          setState(() {});
-                        },
-                      ),
-                      _incorrectPassword
-                          ? const SizedBox(height: 2)
-                          : const SizedBox.shrink(),
-                      _incorrectPassword
-                          ? Align(
-                              alignment: Alignment.centerLeft,
-                              child: Text(
-                                "Incorrect password",
-                                style: enteTheme.textTheme.small.copyWith(
-                                  color: enteTheme.colorScheme.warning700,
-                                ),
-                              ),
-                            )
-                          : const SizedBox.shrink(),
-                      const SizedBox(height: 12),
-                      Expanded(
-                        child: Container(
-                          alignment: Alignment.bottomCenter,
-                          width: double.infinity,
-                          padding: const EdgeInsets.fromLTRB(0, 12, 0, 40),
-                          child: Column(
-                            mainAxisAlignment: MainAxisAlignment.end,
-                            crossAxisAlignment: CrossAxisAlignment.stretch,
-                            children: [
-                              GradientButton(
-                                onTap: _verifyRecoveryKey,
-                                text: "Verify",
-                              ),
-                              const SizedBox(height: 8),
-                            ],
-                          ),
-                        ),
-                      ),
-                      const SizedBox(height: 20)
-                    ],
-                  ),
-                ),
-              ),
-            );
-          },
-        ),
-      ),
-    );
-  }
-}

+ 1 - 1
lib/ui/viewer/actions/file_selection_actions_widget.dart

@@ -266,7 +266,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
   }
 
   Future<void> _onDeleteClick() async {
-    showDeleteSheet(context, widget.selectedFiles);
+    return showDeleteSheet(context, widget.selectedFiles);
   }
 
   Future<void> _removeFilesFromAlbum() async {

+ 93 - 50
lib/ui/viewer/file/fading_app_bar.dart

@@ -23,6 +23,9 @@ import 'package:photos/services/hidden_service.dart';
 import 'package:photos/services/ignored_files_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/ui/common/progress_dialog.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';
 import 'package:photos/ui/create_collection_page.dart';
 import 'package:photos/ui/viewer/file/custom_app_bar.dart';
 import 'package:photos/utils/delete_file_util.dart';
@@ -229,11 +232,11 @@ class FadingAppBarState extends State<FadingAppBar> {
           }
           return items;
         },
-        onSelected: (dynamic value) {
+        onSelected: (dynamic value) async {
           if (value == 1) {
             _download(widget.file);
           } else if (value == 2) {
-            _showDeleteSheet(widget.file);
+            await _showSingleFileDeleteSheet(widget.file);
           } else if (value == 3) {
             _setAs(widget.file);
           } else if (value == 4) {
@@ -341,71 +344,111 @@ class FadingAppBarState extends State<FadingAppBar> {
     );
   }
 
-  void _showDeleteSheet(File file) {
-    final List<Widget> actions = [];
-    if (file.uploadedFileID == null || file.localID == null) {
-      actions.add(
-        CupertinoActionSheetAction(
-          isDestructiveAction: true,
-          onPressed: () async {
-            await deleteFilesFromEverywhere(context, [file]);
-            Navigator.of(context, rootNavigator: true).pop();
-            widget.onFileRemoved(file);
-          },
-          child: const Text("Everywhere"),
-        ),
-      );
+  Future<void> _showSingleFileDeleteSheet(File file) async {
+    final List<ButtonWidget> buttons = [];
+    final String fileType = file.fileType == FileType.video ? "video" : "photo";
+    final bool isBothLocalAndRemote =
+        file.uploadedFileID != null && file.localID != null;
+    final bool isLocalOnly =
+        file.uploadedFileID == null && file.localID != null;
+    final bool isRemoteOnly =
+        file.uploadedFileID != null && file.localID == null;
+    final String title = "Delete $fileType${isBothLocalAndRemote ? '' : '?'}";
+    const String bodyHighlight = "It will be deleted from all albums.";
+    String body = "";
+    if (isBothLocalAndRemote) {
+      body = "This $fileType is in both ente and your device.";
+    } else if (isRemoteOnly) {
+      body = "This $fileType will be deleted from ente.";
+    } else if (isLocalOnly) {
+      body = "This $fileType will be deleted from your device.";
     } else {
-      // uploaded file which is present locally too
-      actions.add(
-        CupertinoActionSheetAction(
-          isDestructiveAction: true,
-          onPressed: () async {
-            await deleteFilesOnDeviceOnly(context, [file]);
-            showShortToast(context, "File deleted from device");
-            Navigator.of(context, rootNavigator: true).pop();
-            // TODO: Fix behavior when inside a device folder
+      throw AssertionError("Unexpected state");
+    }
+    // Add option to delete from ente
+    if (isBothLocalAndRemote || isRemoteOnly) {
+      buttons.add(
+        ButtonWidget(
+          labelText: isBothLocalAndRemote ? "Delete from ente" : "Yes, delete",
+          buttonType: ButtonType.neutral,
+          buttonSize: ButtonSize.large,
+          shouldStickToDarkTheme: true,
+          buttonAction: ButtonAction.first,
+          shouldSurfaceExecutionStates: true,
+          isInAlert: true,
+          onTap: () async {
+            await deleteFilesFromRemoteOnly(context, [file]);
+            showShortToast(context, "Moved to trash");
+            if (isRemoteOnly) {
+              Navigator.of(context, rootNavigator: true).pop();
+              widget.onFileRemoved(file);
+            }
           },
-          child: const Text("Device"),
         ),
       );
-
-      actions.add(
-        CupertinoActionSheetAction(
-          isDestructiveAction: true,
-          onPressed: () async {
-            await deleteFilesFromRemoteOnly(context, [file]);
-            showShortToast(context, "Moved to trash");
-            Navigator.of(context, rootNavigator: true).pop();
-            // TODO: Fix behavior when inside a collection
+    }
+    // Add option to delete from local
+    if (isBothLocalAndRemote || isLocalOnly) {
+      buttons.add(
+        ButtonWidget(
+          labelText:
+              isBothLocalAndRemote ? "Delete from device" : "Yes, delete",
+          buttonType: ButtonType.neutral,
+          buttonSize: ButtonSize.large,
+          shouldStickToDarkTheme: true,
+          buttonAction: ButtonAction.second,
+          shouldSurfaceExecutionStates: false,
+          isInAlert: true,
+          onTap: () async {
+            await deleteFilesOnDeviceOnly(context, [file]);
+            if (isLocalOnly) {
+              Navigator.of(context, rootNavigator: true).pop();
+              widget.onFileRemoved(file);
+            }
           },
-          child: const Text("ente"),
         ),
       );
+    }
 
-      actions.add(
-        CupertinoActionSheetAction(
-          isDestructiveAction: true,
-          onPressed: () async {
+    if (isBothLocalAndRemote) {
+      buttons.add(
+        ButtonWidget(
+          labelText: "Delete from both",
+          buttonType: ButtonType.neutral,
+          buttonSize: ButtonSize.large,
+          shouldStickToDarkTheme: true,
+          buttonAction: ButtonAction.third,
+          shouldSurfaceExecutionStates: true,
+          isInAlert: true,
+          onTap: () async {
             await deleteFilesFromEverywhere(context, [file]);
             Navigator.of(context, rootNavigator: true).pop();
             widget.onFileRemoved(file);
           },
-          child: const Text("Everywhere"),
         ),
       );
     }
-    final action = CupertinoActionSheet(
-      title: const Text("Delete file?"),
-      actions: actions,
-      cancelButton: CupertinoActionSheetAction(
-        child: const Text("Cancel"),
-        onPressed: () {
-          Navigator.of(context, rootNavigator: true).pop();
-        },
+    buttons.add(
+      const ButtonWidget(
+        labelText: "Cancel",
+        buttonType: ButtonType.secondary,
+        buttonSize: ButtonSize.large,
+        shouldStickToDarkTheme: true,
+        buttonAction: ButtonAction.fourth,
+        isInAlert: true,
       ),
     );
-    showCupertinoModalPopup(context: context, builder: (_) => action);
+    final ButtonAction? result = await showActionSheet(
+      context: context,
+      buttons: buttons,
+      actionSheetType: ActionSheetType.defaultActionSheet,
+      title: title,
+      body: body,
+      bodyHighlight: bodyHighlight,
+    );
+    if (result != null && result == ButtonAction.error) {
+      showGenericErrorDialog(context: context);
+    }
   }
 
   Future<void> _download(File file) async {

+ 87 - 62
lib/utils/delete_file_util.dart

@@ -22,6 +22,9 @@ import 'package:photos/services/sync_service.dart';
 import 'package:photos/services/trash_sync_service.dart';
 import 'package:photos/ui/common/dialogs.dart';
 import 'package:photos/ui/common/linear_progress_dialog.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';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/file_util.dart';
 import 'package:photos/utils/toast_util.dart';
@@ -486,99 +489,121 @@ Future<bool> shouldProceedWithDeletion(BuildContext context) async {
   return choice == DialogUserChoice.secondChoice;
 }
 
-void showDeleteSheet(BuildContext context, SelectedFiles selectedFiles) {
+Future<void> showDeleteSheet(
+  BuildContext context,
+  SelectedFiles selectedFiles,
+) async {
   final count = selectedFiles.files.length;
   bool containsUploadedFile = false, containsLocalFile = false;
   for (final file in selectedFiles.files) {
     if (file.uploadedFileID != null) {
+      debugPrint("${file.toString()} is uploaded");
       containsUploadedFile = true;
     }
     if (file.localID != null) {
+      debugPrint("${file.toString()} has local");
       containsLocalFile = true;
     }
   }
-  final actions = <Widget>[];
-  if (containsUploadedFile && containsLocalFile) {
-    actions.add(
-      CupertinoActionSheetAction(
-        isDestructiveAction: true,
-        onPressed: () async {
-          Navigator.of(context, rootNavigator: true).pop();
-          await deleteFilesOnDeviceOnly(
-            context,
-            selectedFiles.files.toList(),
-          );
-          selectedFiles.clearAll();
-          showToast(context, "Files deleted from device");
-        },
-        child: const Text("Device"),
-      ),
-    );
-    actions.add(
-      CupertinoActionSheetAction(
-        isDestructiveAction: true,
-        onPressed: () async {
-          Navigator.of(context, rootNavigator: true).pop();
+  final List<ButtonWidget> buttons = [];
+  final bool isBothLocalAndRemote = containsUploadedFile && containsLocalFile;
+  final bool isLocalOnly = !containsUploadedFile;
+  final bool isRemoteOnly = !containsLocalFile;
+  final String title = "Delete item${count > 1 ? 's' : ''}"
+      "${isBothLocalAndRemote ? '' : '?'}";
+  final String? bodyHighlight =
+      isBothLocalAndRemote ? "They will be deleted from all albums." : null;
+  String body = "";
+  if (isBothLocalAndRemote) {
+    body = "Some items are in both ente and your device.";
+  } else if (isRemoteOnly) {
+    body = "Selected items will be deleted from all albums and moved to trash.";
+  } else if (isLocalOnly) {
+    body = "These items will be deleted from your device.";
+  } else {
+    throw AssertionError("Unexpected state");
+  }
+  // Add option to delete from ente
+  if (isBothLocalAndRemote || isRemoteOnly) {
+    buttons.add(
+      ButtonWidget(
+        labelText: isBothLocalAndRemote ? "Delete from ente" : "Yes, delete",
+        buttonType: ButtonType.neutral,
+        buttonSize: ButtonSize.large,
+        shouldStickToDarkTheme: true,
+        buttonAction: ButtonAction.first,
+        shouldSurfaceExecutionStates: true,
+        isInAlert: true,
+        onTap: () async {
           await deleteFilesFromRemoteOnly(
             context,
             selectedFiles.files.toList(),
           );
-          selectedFiles.clearAll();
-
           showShortToast(context, "Moved to trash");
         },
-        child: const Text("ente"),
       ),
     );
-    actions.add(
-      CupertinoActionSheetAction(
-        isDestructiveAction: true,
-        onPressed: () async {
-          Navigator.of(context, rootNavigator: true).pop();
-          await deleteFilesFromEverywhere(
-            context,
-            selectedFiles.files.toList(),
-          );
-          selectedFiles.clearAll();
+  }
+  // Add option to delete from local
+  if (isBothLocalAndRemote || isLocalOnly) {
+    buttons.add(
+      ButtonWidget(
+        labelText: isBothLocalAndRemote ? "Delete from device" : "Yes, delete",
+        buttonType: ButtonType.neutral,
+        buttonSize: ButtonSize.large,
+        shouldStickToDarkTheme: true,
+        buttonAction: ButtonAction.second,
+        shouldSurfaceExecutionStates: false,
+        isInAlert: true,
+        onTap: () async {
+          await deleteFilesOnDeviceOnly(context, selectedFiles.files.toList());
         },
-        child: const Text("Everywhere"),
       ),
     );
-  } else {
-    actions.add(
-      CupertinoActionSheetAction(
-        isDestructiveAction: true,
-        onPressed: () async {
-          Navigator.of(context, rootNavigator: true).pop();
+  }
+
+  if (isBothLocalAndRemote) {
+    buttons.add(
+      ButtonWidget(
+        labelText: "Delete from both",
+        buttonType: ButtonType.neutral,
+        buttonSize: ButtonSize.large,
+        shouldStickToDarkTheme: true,
+        buttonAction: ButtonAction.third,
+        shouldSurfaceExecutionStates: true,
+        isInAlert: true,
+        onTap: () async {
           await deleteFilesFromEverywhere(
             context,
             selectedFiles.files.toList(),
           );
-          selectedFiles.clearAll();
+          // Navigator.of(context, rootNavigator: true).pop();
+          // widget.onFileRemoved(file);
         },
-        child: const Text("Delete"),
       ),
     );
   }
-  final action = CupertinoActionSheet(
-    title: Text(
-      "Delete " +
-          count.toString() +
-          " file" +
-          (count == 1 ? "" : "s") +
-          (containsUploadedFile && containsLocalFile ? " from" : "?"),
-    ),
-    actions: actions,
-    cancelButton: CupertinoActionSheetAction(
-      child: const Text("Cancel"),
-      onPressed: () {
-        Navigator.of(context, rootNavigator: true).pop();
-      },
+  buttons.add(
+    const ButtonWidget(
+      labelText: "Cancel",
+      buttonType: ButtonType.secondary,
+      buttonSize: ButtonSize.large,
+      shouldStickToDarkTheme: true,
+      buttonAction: ButtonAction.fourth,
+      isInAlert: true,
     ),
   );
-  showCupertinoModalPopup(
+  final ButtonAction? result = await showActionSheet(
     context: context,
-    builder: (_) => action,
-    barrierColor: Colors.black.withOpacity(0.75),
+    buttons: buttons,
+    actionSheetType: ActionSheetType.defaultActionSheet,
+    title: title,
+    body: body,
+    bodyHighlight: bodyHighlight,
   );
+  if (result != null && result == ButtonAction.error) {
+    showGenericErrorDialog(context: context);
+  } else {
+    selectedFiles.clearAll();
+  }
 }

+ 2 - 2
lib/utils/file_util.dart

@@ -51,8 +51,8 @@ Future<io.File?> getFile(
       );
       // do not cache origin file for IOS as they are immediately deleted
       // after usage
-      if (!(isOrigin && Platform.isIOS && diskFile != null)) {
-        FileLruCache.put(key, diskFile!);
+      if (!(isOrigin && Platform.isIOS) && diskFile != null) {
+        FileLruCache.put(key, diskFile);
       }
       return diskFile;
     }