diff --git a/lib/services/collections_service.dart b/lib/services/collections_service.dart index 631b630e9..87d394650 100644 --- a/lib/services/collections_service.dart +++ b/lib/services/collections_service.dart @@ -884,6 +884,8 @@ class CollectionsService { final params = {}; params["collectionID"] = toCollectionID; final toCollectionKey = getCollectionKey(toCollectionID); + final Set 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!); diff --git a/lib/services/trash_sync_service.dart b/lib/services/trash_sync_service.dart index 961436ef2..b687f56e9 100644 --- a/lib/services/trash_sync_service.dart +++ b/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? 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) { diff --git a/lib/services/user_remote_flag_service.dart b/lib/services/user_remote_flag_service.dart index 0c10150bf..b850deae8 100644 --- a/lib/services/user_remote_flag_service.dart +++ b/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 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 markRecoveryVerificationAsDone() async { - await _updateKeyValue(_passwordReminderFlag, true.toString()); + await _updateKeyValue(recoveryVerificationFlag, true.toString()); await _prefs.setBool(needRecoveryKeyVerification, false); } Future _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") { diff --git a/lib/ui/components/action_sheet_widget.dart b/lib/ui/components/action_sheet_widget.dart index 71e6a7a2e..6f619f9ae 100644 --- a/lib/ui/components/action_sheet_widget.dart +++ b/lib/ui/components/action_sheet_widget.dart @@ -24,6 +24,7 @@ Future showActionSheet({ bool isCheckIconGreen = false, String? title, String? body, + String? bodyHighlight, }) { return showMaterialModalBottomSheet( backgroundColor: Colors.transparent, @@ -36,6 +37,7 @@ Future showActionSheet({ return ActionSheetWidget( title: title, body: body, + bodyHighlight: bodyHighlight, actionButtons: buttons, actionSheetType: actionSheetType, isCheckIconGreen: isCheckIconGreen, @@ -47,6 +49,7 @@ Future showActionSheet({ class ActionSheetWidget extends StatelessWidget { final String? title; final String? body; + final String? bodyHighlight; final List 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(), ], ); } diff --git a/lib/ui/components/button_widget.dart b/lib/ui/components/button_widget.dart index 990c2e31f..d296beaeb 100644 --- a/lib/ui/components/button_widget.dart +++ b/lib/ui/components/button_widget.dart @@ -24,6 +24,7 @@ enum ButtonAction { first, second, third, + fourth, cancel, error; } diff --git a/lib/ui/home/landing_page_widget.dart b/lib/ui/home/landing_page_widget.dart index 3ae962d5b..240550533 100644 --- a/lib/ui/home/landing_page_widget.dart +++ b/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 { Future _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 { void _navigateToSignInPage() { UpdateService.instance.hideChangeLog().ignore(); - UserRemoteFlagService.instance.stopPasswordReminder().ignore(); Widget page; if (Configuration.instance.getEncryptedToken() == null) { page = const LoginPage(); diff --git a/lib/ui/home_widget.dart b/lib/ui/home_widget.dart index 2584fb22d..37b5cf4bb 100644 --- a/lib/ui/home_widget.dart +++ b/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 { List? _sharedFiles; late StreamSubscription _tabChangedEventSubscription; - late StreamSubscription _subscriptionPurchaseEvent; + late StreamSubscription + _subscriptionPurchaseEvent; late StreamSubscription _triggerLogoutEvent; late StreamSubscription _loggedOutEvent; late StreamSubscription _permissionGrantedEvent; @@ -320,9 +317,6 @@ class _HomeWidgetState extends State { return const LoadingPhotosWidget(); } - if (UserRemoteFlagService.instance.showPasswordReminder()) { - return const PasswordReminder(); - } if (_sharedFiles != null && _sharedFiles!.isNotEmpty) { ReceiveSharingIntent.reset(); return CreateCollectionPage(null, _sharedFiles); diff --git a/lib/ui/notification/prompts/password_reminder.dart b/lib/ui/notification/prompts/password_reminder.dart deleted file mode 100644 index d3b8ac5fc..000000000 --- a/lib/ui/notification/prompts/password_reminder.dart +++ /dev/null @@ -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 createState() => _PasswordReminderState(); -} - -class _PasswordReminderState extends State { - final _passwordController = TextEditingController(); - final Logger _logger = Logger((_PasswordReminderState).toString()); - bool _password2Visible = false; - bool _incorrectPassword = false; - - Future _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 _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 _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( - (Set 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( - (Set states) { - return enteTextTheme.bodyBold; - }, - ), - backgroundColor: MaterialStateProperty.resolveWith( - (Set states) { - return enteColor.fillFaint; - }, - ), - foregroundColor: MaterialStateProperty.resolveWith( - (Set 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 actions = []; - 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) - ], - ), - ), - ), - ); - }, - ), - ), - ); - } -} diff --git a/lib/ui/viewer/actions/file_selection_actions_widget.dart b/lib/ui/viewer/actions/file_selection_actions_widget.dart index 5fa3e8179..da412c148 100644 --- a/lib/ui/viewer/actions/file_selection_actions_widget.dart +++ b/lib/ui/viewer/actions/file_selection_actions_widget.dart @@ -266,7 +266,7 @@ class _FileSelectionActionWidgetState extends State { } Future _onDeleteClick() async { - showDeleteSheet(context, widget.selectedFiles); + return showDeleteSheet(context, widget.selectedFiles); } Future _removeFilesFromAlbum() async { diff --git a/lib/ui/viewer/file/fading_app_bar.dart b/lib/ui/viewer/file/fading_app_bar.dart index 54df8ba9f..39805ed5a 100644 --- a/lib/ui/viewer/file/fading_app_bar.dart +++ b/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 { } 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 { ); } - void _showDeleteSheet(File file) { - final List 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 _showSingleFileDeleteSheet(File file) async { + final List 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 - }, - child: const Text("Device"), - ), - ); - - actions.add( - CupertinoActionSheetAction( - isDestructiveAction: true, - onPressed: () async { + 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"); - Navigator.of(context, rootNavigator: true).pop(); - // TODO: Fix behavior when inside a collection + if (isRemoteOnly) { + Navigator.of(context, rootNavigator: true).pop(); + widget.onFileRemoved(file); + } }, - child: const Text("ente"), - ), - ); - - actions.add( - CupertinoActionSheetAction( - isDestructiveAction: true, - onPressed: () 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(); - }, + // 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); + } + }, + ), + ); + } + + 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); + }, + ), + ); + } + 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 _download(File file) async { diff --git a/lib/utils/delete_file_util.dart b/lib/utils/delete_file_util.dart index 3d412745e..e89862488 100644 --- a/lib/utils/delete_file_util.dart +++ b/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 shouldProceedWithDeletion(BuildContext context) async { return choice == DialogUserChoice.secondChoice; } -void showDeleteSheet(BuildContext context, SelectedFiles selectedFiles) { +Future 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 = []; - 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 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(); - }, - child: const Text("Everywhere"), - ), - ); - } else { - actions.add( - CupertinoActionSheetAction( - isDestructiveAction: true, - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - await deleteFilesFromEverywhere( - context, - selectedFiles.files.toList(), - ); - selectedFiles.clearAll(); - }, - 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(); - }, + // 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()); + }, + ), + ); + } + + 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(), + ); + // Navigator.of(context, rootNavigator: true).pop(); + // widget.onFileRemoved(file); + }, + ), + ); + } + 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(); + } } diff --git a/lib/utils/file_util.dart b/lib/utils/file_util.dart index ebdc6a285..892f0b307 100644 --- a/lib/utils/file_util.dart +++ b/lib/utils/file_util.dart @@ -51,8 +51,8 @@ Future 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; }