Merge branch 'main' into reset-ignored-files
This commit is contained in:
commit
72ebeba3ba
12 changed files with 242 additions and 533 deletions
|
@ -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!);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ enum ButtonAction {
|
|||
first,
|
||||
second,
|
||||
third,
|
||||
fourth,
|
||||
cancel,
|
||||
error;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue