diff --git a/lib/models/user_details.dart b/lib/models/user_details.dart index de1ba3a82..02f5048c4 100644 --- a/lib/models/user_details.dart +++ b/lib/models/user_details.dart @@ -54,8 +54,8 @@ class UserDetails { return UserDetails( map['email'] as String, map['usage'] as int, - map['fileCount'] as int, - map['sharedCollectionsCount'] as int, + map['fileCount'] as int ?? 0, + map['sharedCollectionsCount'] as int ?? 0, Subscription.fromMap(map['subscription']), FamilyData.fromMap(map['familyData']), ); diff --git a/lib/ui/common/dialogs.dart b/lib/ui/common/dialogs.dart index e676a9c63..0cf8f8d26 100644 --- a/lib/ui/common/dialogs.dart +++ b/lib/ui/common/dialogs.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; enum DialogUserChoice { firstChoice, secondChoice } @@ -8,7 +7,7 @@ enum ActionType { critical, } // if dialog is dismissed by tapping outside, this will return null -Future showChoiceDialog( +Future showChoiceDialog( BuildContext context, String title, String content, { diff --git a/lib/ui/payment/stripe_subscription_page.dart b/lib/ui/payment/stripe_subscription_page.dart index 7f1d642f4..7b86c9a51 100644 --- a/lib/ui/payment/stripe_subscription_page.dart +++ b/lib/ui/payment/stripe_subscription_page.dart @@ -210,37 +210,37 @@ class _StripeSubscriptionPageState extends State { ), ), ]); + } - if (!widget.isOnboarding) { - widgets.addAll([ - Align( - alignment: Alignment.topCenter, - child: GestureDetector( - onTap: () async { - await _launchFamilyPortal(); - }, - child: Container( - padding: EdgeInsets.fromLTRB(40, 0, 40, 80), - child: Column( - children: [ - RichText( - text: TextSpan( - text: "manage family", - style: TextStyle( - color: Colors.blue, - fontFamily: 'Ubuntu', - fontSize: 15, - ), + if (!widget.isOnboarding) { + widgets.addAll([ + Align( + alignment: Alignment.topCenter, + child: GestureDetector( + onTap: () async { + await _launchFamilyPortal(); + }, + child: Container( + padding: EdgeInsets.fromLTRB(40, 0, 40, 80), + child: Column( + children: [ + RichText( + text: TextSpan( + text: "manage family", + style: TextStyle( + color: Colors.blue, + fontFamily: 'Ubuntu', + fontSize: 15, ), - textAlign: TextAlign.center, ), - ], - ), + textAlign: TextAlign.center, + ), + ], ), ), ), - ]); - } + ), + ]); } return SingleChildScrollView( @@ -270,6 +270,13 @@ class _StripeSubscriptionPageState extends State { } Future _launchFamilyPortal() async { + if (_userDetails.subscription.productID == kFreeProductID) { + await showErrorDialog( + context, + "Now you can share your storage plan with your family members!", + "Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space."); + return; + } await _dialog.show(); try { final String jwtToken = await _userService.getFamiliesToken(); diff --git a/lib/ui/payment/subscription_page.dart b/lib/ui/payment/subscription_page.dart index 247e9c36b..3152b1cd9 100644 --- a/lib/ui/payment/subscription_page.dart +++ b/lib/ui/payment/subscription_page.dart @@ -237,6 +237,8 @@ class _SubscriptionPageState extends State { ), ), ]); + } + if (!widget.isOnboarding) { widgets.addAll([ Align( alignment: Alignment.topCenter, @@ -434,7 +436,15 @@ class _SubscriptionPageState extends State { ); } + // todo: refactor manage family in common widget Future _launchFamilyPortal() async { + if (_userDetails.subscription.productID == kFreeProductID) { + await showErrorDialog( + context, + "Now you can share your storage plan with your family members!", + "Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space."); + return; + } await _dialog.show(); try { final String jwtToken = await _userService.getFamiliesToken(); diff --git a/lib/utils/delete_file_util.dart b/lib/utils/delete_file_util.dart index 7909e057a..6d9d0e3f6 100644 --- a/lib/utils/delete_file_util.dart +++ b/lib/utils/delete_file_util.dart @@ -5,7 +5,6 @@ import 'dart:math'; import 'package:device_info/device_info.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:photos/core/configuration.dart'; @@ -36,6 +35,7 @@ Future deleteFilesFromEverywhere( final List localAssetIDs = []; final List localSharedMediaIDs = []; final List alreadyDeletedIDs = []; // to ignore already deleted files + bool hasLocalOnlyFiles = false; for (final file in files) { if (file.localID != null) { if (!(await _localFileExist(file))) { @@ -47,6 +47,16 @@ Future deleteFilesFromEverywhere( localAssetIDs.add(file.localID); } } + if (file.uploadedFileID == null) { + hasLocalOnlyFiles = true; + } + } + if (hasLocalOnlyFiles && Platform.isAndroid) { + final shouldProceed = await shouldProceedWithDeletion(context); + if (!shouldProceed) { + await dialog.hide(); + return; + } } Set deletedIDs = {}; try { @@ -108,7 +118,11 @@ Future deleteFilesFromEverywhere( if (deletedFiles.isNotEmpty) { Bus.instance.fire(LocalPhotosUpdatedEvent(deletedFiles, type: EventType.deletedFromEverywhere)); - showShortToast("moved to trash"); + if (hasLocalOnlyFiles && Platform.isAndroid) { + showShortToast("files deleted"); + } else { + showShortToast("moved to trash"); + } } await dialog.hide(); if (uploadedFilesToBeTrashed.isNotEmpty) { @@ -151,8 +165,8 @@ Future deleteFilesFromRemoteOnly( type: EventType.deletedFromRemote, )); } - Bus.instance.fire( - LocalPhotosUpdatedEvent(files, type: EventType.deletedFromRemote)); + Bus.instance + .fire(LocalPhotosUpdatedEvent(files, type: EventType.deletedFromRemote)); SyncService.instance.sync(); await dialog.hide(); RemoteSyncService.instance.sync(silently: true); @@ -166,6 +180,7 @@ Future deleteFilesOnDeviceOnly( final List localAssetIDs = []; final List localSharedMediaIDs = []; final List alreadyDeletedIDs = []; // to ignore already deleted files + bool hasLocalOnlyFiles = false; for (final file in files) { if (file.localID != null) { if (!(await _localFileExist(file))) { @@ -177,6 +192,16 @@ Future deleteFilesOnDeviceOnly( localAssetIDs.add(file.localID); } } + if (file.uploadedFileID == null) { + hasLocalOnlyFiles = true; + } + } + if (hasLocalOnlyFiles && Platform.isAndroid) { + final shouldProceed = await shouldProceedWithDeletion(context); + if (!shouldProceed) { + await dialog.hide(); + return; + } } Set deletedIDs = {}; try { @@ -216,8 +241,8 @@ Future deleteFromTrash(BuildContext context, List files) async { await TrashSyncService.instance.deleteFromTrash(files); showToast("successfully deleted"); await dialog.hide(); - Bus.instance.fire( - FilesUpdatedEvent(files, type: EventType.deletedFromEverywhere)); + Bus.instance + .fire(FilesUpdatedEvent(files, type: EventType.deletedFromEverywhere)); return true; } catch (e, s) { _logger.info("failed to delete from trash", e, s); @@ -390,3 +415,15 @@ Future> _tryDeleteSharedMediaFiles(List localIDs) { return Future.value(actuallyDeletedIDs); } } + +Future shouldProceedWithDeletion(BuildContext context) async { + final choice = await showChoiceDialog( + context, + "are you sure?", + "some of the files you are trying to delete are only available on your device and cannot be recovered if deleted", + firstAction: "cancel", + secondAction: "delete", + secondActionColor: Colors.red, + ); + return choice == DialogUserChoice.secondChoice; +} diff --git a/pubspec.yaml b/pubspec.yaml index da75f33de..083fa89a6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: ente photos application # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.5.16+296 +version: 0.5.21+301 environment: sdk: ">=2.10.0 <3.0.0"