Merge branch 'main' into reset-ignored-files

This commit is contained in:
ashilkn 2023-01-06 17:24:04 +05:30
commit 72ebeba3ba
12 changed files with 242 additions and 533 deletions

View file

@ -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!);

View file

@ -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) {

View file

@ -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") {

View file

@ -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(),
],
);
}

View file

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

View file

@ -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();

View file

@ -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);

View file

@ -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)
],
),
),
),
);
},
),
),
);
}
}

View file

@ -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 {

View file

@ -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
},
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<void> _download(File file) async {

View file

@ -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();
},
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();
}
}

View file

@ -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;
}