Merge branch 'master' of github.com:ente-io/frame

This commit is contained in:
Vishnu Mohandas 2021-06-28 21:43:24 +05:30
commit 248746c45f
21 changed files with 1040 additions and 72 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View file

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:logging/logging.dart';
import 'package:photos/models/backup_status.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/models/location.dart';
import 'package:photos/models/file.dart';
@ -213,6 +214,23 @@ class FilesDB {
return ids;
}
Future<BackedUpFileIDs> getBackedUpIDs() async {
final db = await instance.database;
final results = await db.query(
table,
columns: [columnLocalID, columnUploadedFileID],
where:
'$columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
);
final localIDs = Set<String>();
final uploadedIDs = Set<int>();
for (final result in results) {
localIDs.add(result[columnLocalID]);
uploadedIDs.add(result[columnUploadedFileID]);
}
return BackedUpFileIDs(localIDs.toList(), uploadedIDs.toList());
}
Future<List<File>> getAllFiles(int startTime, int endTime,
{int limit, bool asc}) async {
final db = await instance.database;
@ -543,6 +561,34 @@ class FilesDB {
);
}
Future<void> deleteLocalFiles(List<String> localIDs) async {
String inParam = "";
for (final localID in localIDs) {
inParam += "'" + localID + "',";
}
inParam = inParam.substring(0, inParam.length - 1);
final db = await instance.database;
await db.rawQuery('''
UPDATE $table
SET $columnLocalID = NULL
WHERE $columnLocalID IN ($inParam);
''');
}
Future<List<File>> getLocalFiles(List<String> localIDs) async {
String inParam = "";
for (final localID in localIDs) {
inParam += "'" + localID + "',";
}
inParam = inParam.substring(0, inParam.length - 1);
final db = await instance.database;
final results = await db.query(
table,
where: '$columnLocalID IN ($inParam)',
);
return _convertToFiles(results);
}
Future<int> deleteFromCollection(int uploadedFileID, int collectionID) async {
final db = await instance.database;
return db.delete(

View file

@ -0,0 +1,13 @@
class BackupStatus {
final List<String> localIDs;
final int size;
BackupStatus(this.localIDs, this.size);
}
class BackedUpFileIDs {
final List<String> localIDs;
final List<int> uploadedIDs;
BackedUpFileIDs(this.localIDs, this.uploadedIDs);
}

View file

@ -10,10 +10,12 @@ import 'package:photos/core/constants.dart';
import 'package:photos/core/errors.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/core/network.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/permission_granted_event.dart';
import 'package:photos/events/subscription_purchased_event.dart';
import 'package:photos/events/sync_status_update_event.dart';
import 'package:photos/events/trigger_logout_event.dart';
import 'package:photos/models/backup_status.dart';
import 'package:photos/services/local_sync_service.dart';
import 'package:photos/services/notification_service.dart';
import 'package:photos/services/remote_sync_service.dart';
@ -176,6 +178,32 @@ class SyncService {
);
}
Future<BackupStatus> getBackupStatus() async {
final ids = await FilesDB.instance.getBackedUpIDs();
final size = await _getFileSize(ids.uploadedIDs);
return BackupStatus(ids.localIDs, size);
}
Future<int> _getFileSize(List<int> fileIDs) async {
try {
final response = await _dio.post(
Configuration.instance.getHttpEndpoint() + "/files/size",
options: Options(
headers: {
"X-Auth-Token": Configuration.instance.getToken(),
},
),
data: {
"fileIDs": fileIDs,
},
);
return response.data["size"];
} catch (e) {
_logger.severe(e);
throw e;
}
}
Future<void> _doSync() async {
await _localSyncService.sync();
if (_localSyncService.hasCompletedFirstImport()) {

View file

@ -1,6 +1,7 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/network.dart';
@ -10,9 +11,13 @@ import 'package:photos/models/key_gen_result.dart';
import 'package:photos/models/public_key.dart';
import 'package:photos/models/set_keys_request.dart';
import 'package:photos/models/set_recovery_key_request.dart';
import 'package:photos/ui/login_page.dart';
import 'package:photos/ui/ott_verification_page.dart';
import 'package:photos/ui/password_entry_page.dart';
import 'package:photos/ui/password_reentry_page.dart';
import 'package:photos/ui/two_factor_authentication_page.dart';
import 'package:photos/ui/two_factor_recovery_page.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/toast_util.dart';
@ -92,13 +97,18 @@ class UserService {
);
await dialog.hide();
if (response != null && response.statusCode == 200) {
await _saveConfiguration(response);
showToast("email verification successful!");
var page;
if (Configuration.instance.getEncryptedToken() != null) {
page = PasswordReentryPage();
final String twoFASessionID = response.data["twoFactorSessionID"];
if (twoFASessionID != null && twoFASessionID.isNotEmpty) {
page = TwoFactorAuthenticationPage(twoFASessionID);
} else {
page = PasswordEntryPage();
await _saveConfiguration(response);
if (Configuration.instance.getEncryptedToken() != null) {
page = PasswordReentryPage();
} else {
page = PasswordEntryPage();
}
}
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
@ -192,6 +202,170 @@ class UserService {
}
}
Future<void> verifyTwoFactor(
BuildContext context, String sessionID, String code) async {
final dialog = createProgressDialog(context, "authenticating...");
await dialog.show();
try {
final response = await _dio.post(
_config.getHttpEndpoint() + "/users/two-factor/verify",
data: {
"sessionID": sessionID,
"code": code,
},
);
await dialog.hide();
if (response != null && response.statusCode == 200) {
showToast("authentication successful!");
await _saveConfiguration(response);
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return PasswordReentryPage();
},
),
(route) => route.isFirst,
);
}
} on DioError catch (e) {
await dialog.hide();
_logger.severe(e);
if (e.response != null && e.response.statusCode == 404) {
showToast("session expired");
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return LoginPage();
},
),
(route) => route.isFirst,
);
} else {
showErrorDialog(context, "incorrect code",
"authentication failed, please try again");
}
} catch (e) {
await dialog.hide();
_logger.severe(e);
showErrorDialog(
context, "oops", "authentication failed, please try again");
}
}
Future<void> recoverTwoFactor(BuildContext context, String sessionID) async {
final dialog = createProgressDialog(context, "please wait...");
await dialog.show();
try {
final response = await _dio.get(
_config.getHttpEndpoint() + "/users/two-factor/recover",
queryParameters: {
"sessionID": sessionID,
},
);
if (response != null && response.statusCode == 200) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return TwoFactorRecoveryPage(
sessionID,
response.data["encryptedSecret"],
response.data["secretDecryptionNonce"]);
},
),
(route) => route.isFirst,
);
}
} on DioError catch (e) {
_logger.severe(e);
if (e.response != null && e.response.statusCode == 404) {
showToast("session expired");
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return LoginPage();
},
),
(route) => route.isFirst,
);
} else {
showErrorDialog(
context, "oops", "something went wrong, please try again");
}
} catch (e) {
_logger.severe(e);
showErrorDialog(
context, "oops", "something went wrong, please try again");
} finally {
await dialog.hide();
}
}
Future<void> removeTwoFactor(
BuildContext context,
String sessionID,
String recoveryKey,
String encryptedSecret,
String secretDecryptionNonce,
) async {
final dialog = createProgressDialog(context, "please wait...");
await dialog.show();
String secret;
try {
secret = Sodium.bin2base64(await CryptoUtil.decrypt(
Sodium.base642bin(encryptedSecret),
Sodium.hex2bin(recoveryKey.trim()),
Sodium.base642bin(secretDecryptionNonce)));
} catch (e) {
await dialog.hide();
showErrorDialog(context, "incorrect recovery key",
"the recovery key you entered is incorrect");
return;
}
try {
final response = await _dio.post(
_config.getHttpEndpoint() + "/users/two-factor/remove",
data: {
"sessionID": sessionID,
"secret": secret,
},
);
if (response != null && response.statusCode == 200) {
showToast("two-factor authentication successfully reset");
await _saveConfiguration(response);
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return PasswordReentryPage();
},
),
(route) => route.isFirst,
);
}
} on DioError catch (e) {
_logger.severe(e);
if (e.response != null && e.response.statusCode == 404) {
showToast("session expired");
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return LoginPage();
},
),
(route) => route.isFirst,
);
} else {
showErrorDialog(
context, "oops", "something went wrong, please try again");
}
} catch (e) {
_logger.severe(e);
showErrorDialog(
context, "oops", "something went wrong, please try again");
} finally {
await dialog.hide();
}
}
Future<void> _saveConfiguration(Response response) async {
await Configuration.instance.setUserID(response.data["id"]);
if (response.data["encryptedToken"] != null) {

161
lib/ui/free_space_page.dart Normal file
View file

@ -0,0 +1,161 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import 'package:photos/models/backup_status.dart';
import 'package:photos/ui/common_elements.dart';
import 'package:photos/utils/data_util.dart';
import 'package:photos/utils/delete_file_util.dart';
import 'package:photos/utils/dialog_util.dart';
class FreeSpacePage extends StatefulWidget {
final BackupStatus status;
FreeSpacePage(this.status, {Key key}) : super(key: key);
@override
_FreeSpacePageState createState() => _FreeSpacePageState();
}
class _FreeSpacePageState extends State<FreeSpacePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Hero(
tag: "free_up_space",
child: Material(
type: MaterialType.transparency,
child: Text(
"free up space",
style: TextStyle(
fontSize: 18,
),
),
),
),
),
body: _getBody(),
);
}
Widget _getBody() {
Logger("FreeSpacePage").info("Number of uploaded files: " +
widget.status.localIDs.length.toString());
Logger("FreeSpacePage")
.info("Space consumed: " + widget.status.size.toString());
return _getWidget(widget.status);
}
Widget _getWidget(BackupStatus status) {
final count = status.localIDs.length;
final formattedCount = NumberFormat().format(count);
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset(
"assets/backed_up_gallery.png",
height: 160,
),
Padding(padding: EdgeInsets.all(24)),
Padding(
padding: const EdgeInsets.only(left: 36, right: 40),
child: Row(
children: [
Icon(
Icons.cloud_done_outlined,
color: Color.fromRGBO(45, 194, 98, 1.0),
),
Padding(padding: EdgeInsets.all(10)),
Expanded(
child: Text(
count == 1
? formattedCount.toString() +
" file on this device has been backed up safely"
: formattedCount.toString() +
" files on this device have been backed up safely",
style: TextStyle(
fontSize: 14,
height: 1.3,
color: Colors.white.withOpacity(0.8),
),
),
),
],
),
),
Padding(padding: EdgeInsets.all(12)),
Padding(
padding: const EdgeInsets.only(left: 36, right: 40),
child: Row(
children: [
Icon(
Icons.delete_outline,
color: Color.fromRGBO(45, 194, 98, 1.0),
),
Padding(padding: EdgeInsets.all(10)),
Expanded(
child: Text(
(count == 1 ? "it" : "they") +
" can be deleted from this device to free up " +
formatBytes(status.size),
style: TextStyle(
fontSize: 14,
height: 1.3,
color: Colors.white.withOpacity(0.8),
),
),
),
],
),
),
Padding(padding: EdgeInsets.all(12)),
Padding(
padding: const EdgeInsets.only(left: 36, right: 40),
child: Row(
children: [
Icon(
Icons.devices,
color: Color.fromRGBO(45, 194, 98, 1.0),
),
Padding(padding: EdgeInsets.all(10)),
Expanded(
child: Text(
"you can still access " +
(count == 1 ? "it" : "them") +
" on ente as long as you have an active subscription",
style: TextStyle(
fontSize: 14,
height: 1.3,
color: Colors.white.withOpacity(0.8),
),
),
),
],
),
),
Padding(padding: EdgeInsets.all(32)),
Container(
width: double.infinity,
height: 64,
padding: const EdgeInsets.fromLTRB(60, 0, 60, 0),
child: button(
"free up " + formatBytes(status.size),
onPressed: () async {
await _freeStorage(status);
},
fontSize: 18,
),
),
],
),
);
}
Future<void> _freeStorage(BackupStatus status) async {
await deleteLocalFiles(context, status.localIDs);
Navigator.of(context).pop(true);
}
}

View file

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
class LinearProgressDialog extends StatefulWidget {
final String message;
const LinearProgressDialog(this.message, {Key key}) : super(key: key);
@override
LinearProgressDialogState createState() => LinearProgressDialogState();
}
class LinearProgressDialogState extends State<LinearProgressDialog> {
double _progress;
@override
void initState() {
_progress = 0;
super.initState();
}
void setProgress(double progress) {
setState(() {
_progress = progress;
});
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async => false,
child: AlertDialog(
title: Text(
widget.message,
style: TextStyle(
fontSize: 16,
),
textAlign: TextAlign.center,
),
content: LinearProgressIndicator(
value: _progress,
valueColor:
AlwaysStoppedAnimation<Color>(Theme.of(context).buttonColor),
),
),
);
}
}

View file

@ -3,7 +3,6 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/services/billing_service.dart';
import 'package:photos/ui/backup_folder_selection_widget.dart';
import 'package:photos/ui/loading_widget.dart';
import 'package:photos/ui/settings/settings_section_title.dart';
import 'package:photos/ui/settings/settings_text_item.dart';
@ -19,7 +18,7 @@ class AccountSectionWidget extends StatefulWidget {
}
class AccountSectionWidgetState extends State<AccountSectionWidget> {
double _usageInGBs;
String _usage;
@override
void initState() {
@ -56,55 +55,6 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
Divider(height: 4),
Platform.isIOS
? Padding(padding: EdgeInsets.all(2))
: Padding(padding: EdgeInsets.all(4)),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: const BackupFolderSelectionWidget("backup"),
backgroundColor: Color.fromRGBO(8, 18, 18, 1),
insetPadding: const EdgeInsets.all(24),
contentPadding: const EdgeInsets.all(24),
);
},
barrierColor: Colors.black.withOpacity(0.85),
);
},
child: SettingsTextItem(
text: "backed up folders", icon: Icons.navigate_next),
),
Platform.isIOS
? Padding(padding: EdgeInsets.all(2))
: Padding(padding: EdgeInsets.all(2)),
Divider(height: 4),
Platform.isIOS
? Padding(padding: EdgeInsets.all(2))
: Padding(padding: EdgeInsets.all(4)),
Container(
height: 36,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("backup over mobile data"),
Switch(
value: Configuration.instance.shouldBackupOverMobileData(),
onChanged: (value) async {
Configuration.instance.setBackupOverMobileData(value);
setState(() {});
},
),
],
),
),
Platform.isIOS
? Padding(padding: EdgeInsets.all(2))
: Padding(padding: EdgeInsets.all(4)),
Divider(height: 4),
Platform.isIOS
? Padding(padding: EdgeInsets.all(6))
: Padding(padding: EdgeInsets.all(8)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -112,11 +62,7 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
Text("total data backed up"),
Container(
height: 20,
child: _usageInGBs == null
? loadWidget
: Text(
_usageInGBs.toString() + " GB",
),
child: _usage == null ? loadWidget : Text(_usage),
),
],
),
@ -180,7 +126,7 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
BillingService.instance.fetchUsage().then((usage) async {
if (mounted) {
setState(() {
_usageInGBs = convertBytesToGBs(usage);
_usage = formatBytes(usage);
});
}
});

View file

@ -0,0 +1,155 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/models/backup_status.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/ui/backup_folder_selection_widget.dart';
import 'package:photos/ui/free_space_page.dart';
import 'package:photos/ui/settings/settings_section_title.dart';
import 'package:photos/ui/settings/settings_text_item.dart';
import 'package:photos/utils/data_util.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/navigation_util.dart';
import 'package:url_launcher/url_launcher.dart';
class BackupSectionWidget extends StatefulWidget {
BackupSectionWidget({Key key}) : super(key: key);
@override
BackupSectionWidgetState createState() => BackupSectionWidgetState();
}
class BackupSectionWidgetState extends State<BackupSectionWidget> {
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
SettingsSectionTitle("backup"),
Padding(
padding: EdgeInsets.all(4),
),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: const BackupFolderSelectionWidget("backup"),
backgroundColor: Color.fromRGBO(8, 18, 18, 1),
insetPadding: const EdgeInsets.all(24),
contentPadding: const EdgeInsets.all(24),
);
},
barrierColor: Colors.black.withOpacity(0.85),
);
},
child: SettingsTextItem(
text: "backed up folders", icon: Icons.navigate_next),
),
Platform.isIOS
? Padding(padding: EdgeInsets.all(2))
: Padding(padding: EdgeInsets.all(2)),
Divider(height: 4),
Platform.isIOS
? Padding(padding: EdgeInsets.all(2))
: Padding(padding: EdgeInsets.all(4)),
Container(
height: 36,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("backup over mobile data"),
Switch(
value: Configuration.instance.shouldBackupOverMobileData(),
onChanged: (value) async {
Configuration.instance.setBackupOverMobileData(value);
setState(() {});
},
),
],
),
),
Platform.isIOS
? Padding(padding: EdgeInsets.all(2))
: Padding(padding: EdgeInsets.all(4)),
Divider(height: 4),
Platform.isIOS
? Padding(padding: EdgeInsets.all(2))
: Padding(padding: EdgeInsets.all(2)),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
final dialog = createProgressDialog(context, "calculating...");
await dialog.show();
final status = await SyncService.instance.getBackupStatus();
await dialog.hide();
if (status.localIDs.isEmpty) {
showErrorDialog(context, "✨ all clear",
"you've no files on this device that can be deleted");
} else {
bool result = await routeToPage(context, FreeSpacePage(status));
if (result == true) {
_showSpaceFreedDialog(status);
}
}
},
child: SettingsTextItem(
text: "free up space",
icon: Icons.navigate_next,
),
),
],
),
);
}
void _showSpaceFreedDialog(BackupStatus status) {
AlertDialog alert = AlertDialog(
title: Text("success"),
content: Text(
"you have successfully freed up " + formatBytes(status.size) + "!"),
actions: [
TextButton(
child: Text(
"rate us",
style: TextStyle(
color: Theme.of(context).buttonColor,
),
),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
if (Platform.isAndroid) {
launch(
"https://play.google.com/store/apps/details?id=io.ente.photos");
} else {
launch("https://apps.apple.com/in/app/ente-photos/id1542026904");
}
},
),
TextButton(
child: Text(
"ok",
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
},
),
],
);
showConfettiDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
barrierColor: Colors.black87,
confettiAlignment: Alignment.topCenter,
useRootNavigator: true,
);
}
}

View file

@ -1,9 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:photos/ui/settings/account_section_widget.dart';
import 'package:photos/ui/settings/backup_section_widget.dart';
import 'package:photos/ui/settings/debug_section_widget.dart';
import 'package:photos/ui/settings/info_section_widget.dart';
import 'package:photos/ui/settings/security_section_widget.dart';
@ -28,6 +28,8 @@ class SettingsPage extends StatelessWidget {
final List<Widget> contents = [];
if (hasLoggedIn) {
contents.addAll([
BackupSectionWidget(),
Padding(padding: EdgeInsets.all(12)),
AccountSectionWidget(),
Padding(padding: EdgeInsets.all(12)),
]);

View file

@ -168,8 +168,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
if (snapshot.hasData) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Text("current usage is " +
convertBytesToReadableFormat(snapshot.data).toString()),
child: Text("current usage is " + formatBytes(snapshot.data)),
);
} else if (snapshot.hasError) {
return Container();

View file

@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/ui/common_elements.dart';
import 'package:photos/ui/two_factor_recovery_page.dart';
import 'package:pinput/pin_put/pin_put.dart';
class TwoFactorAuthenticationPage extends StatefulWidget {
final String sessionID;
const TwoFactorAuthenticationPage(this.sessionID, {Key key})
: super(key: key);
@override
_TwoFactorAuthenticationPageState createState() =>
_TwoFactorAuthenticationPageState();
}
class _TwoFactorAuthenticationPageState
extends State<TwoFactorAuthenticationPage> {
final _pinController = TextEditingController();
final _pinPutDecoration = BoxDecoration(
border: Border.all(color: Color.fromRGBO(45, 194, 98, 1.0)),
borderRadius: BorderRadius.circular(15.0),
);
String _code = "";
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"two-factor authentication",
),
),
body: _getBody(),
);
}
Widget _getBody() {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Text(
"enter the 6-digit code from\nyour authenticator app",
style: TextStyle(
height: 1.4,
fontSize: 16,
),
textAlign: TextAlign.center,
),
Padding(padding: EdgeInsets.all(32)),
Padding(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 0),
child: PinPut(
fieldsCount: 6,
onSubmit: (String code) {
_verifyTwoFactorCode(code);
},
onChanged: (String pin) {
setState(() {
_code = pin;
});
},
controller: _pinController,
submittedFieldDecoration: _pinPutDecoration.copyWith(
borderRadius: BorderRadius.circular(20.0),
),
selectedFieldDecoration: _pinPutDecoration,
followingFieldDecoration: _pinPutDecoration.copyWith(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(
color: Color.fromRGBO(45, 194, 98, 0.5),
),
),
inputDecoration: InputDecoration(
focusedBorder: InputBorder.none,
border: InputBorder.none,
counterText: '',
),
autofocus: true,
),
),
Padding(padding: EdgeInsets.all(24)),
Container(
padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
width: double.infinity,
height: 64,
child: button(
"authenticate",
fontSize: 18,
onPressed: _code.length == 6
? () async {
_verifyTwoFactorCode(_code);
}
: null,
),
),
Padding(padding: EdgeInsets.all(30)),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
UserService.instance.recoverTwoFactor(context, widget.sessionID);
},
child: Container(
padding: EdgeInsets.all(10),
child: Center(
child: Text(
"lost device?",
style: TextStyle(
decoration: TextDecoration.underline,
fontSize: 12,
),
),
),
),
),
],
);
}
Future<void> _verifyTwoFactorCode(String code) async {
await UserService.instance.verifyTwoFactor(context, widget.sessionID, code);
}
}

View file

@ -0,0 +1,109 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/ui/common_elements.dart';
import 'package:photos/utils/dialog_util.dart';
class TwoFactorRecoveryPage extends StatefulWidget {
final String sessionID;
final String encryptedSecret;
final String secretDecryptionNonce;
TwoFactorRecoveryPage(
this.sessionID, this.encryptedSecret, this.secretDecryptionNonce,
{Key key})
: super(key: key);
@override
_TwoFactorRecoveryPageState createState() => _TwoFactorRecoveryPageState();
}
class _TwoFactorRecoveryPageState extends State<TwoFactorRecoveryPage> {
final _recoveryKey = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"recover account",
style: TextStyle(
fontSize: 18,
),
),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(60, 0, 60, 0),
child: TextFormField(
decoration: InputDecoration(
hintText: "enter your recovery key",
contentPadding: EdgeInsets.all(20),
),
style: TextStyle(
fontSize: 14,
fontFeatures: [FontFeature.tabularFigures()],
),
controller: _recoveryKey,
autofocus: false,
autocorrect: false,
keyboardType: TextInputType.multiline,
maxLines: null,
onChanged: (_) {
setState(() {});
},
),
),
Padding(padding: EdgeInsets.all(24)),
Container(
padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
width: double.infinity,
height: 64,
child: button(
"recover",
fontSize: 18,
onPressed: _recoveryKey.text.isNotEmpty
? () async {
await UserService.instance.removeTwoFactor(
context,
widget.sessionID,
_recoveryKey.text,
widget.encryptedSecret,
widget.secretDecryptionNonce);
}
: null,
),
),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
showErrorDialog(
context,
"contact support",
"please drop an email to support@ente.io from your registered email address",
);
},
child: Container(
padding: EdgeInsets.all(40),
child: Center(
child: Text(
"no recovery key?",
style: TextStyle(
decoration: TextDecoration.underline,
fontSize: 12,
color: Colors.white.withOpacity(0.9),
),
),
),
),
),
],
),
);
}
}

View file

@ -1,3 +1,5 @@
import 'dart:math';
double convertBytesToGBs(final int bytes, {int precision = 2}) {
return double.parse(
(bytes / (1024 * 1024 * 1024)).toStringAsFixed(precision));
@ -13,3 +15,11 @@ String convertBytesToReadableFormat(int bytes) {
}
return bytes.toString() + " " + kStorageUnits[storageUnitIndex];
}
String formatBytes(int bytes, [int decimals = 2]) {
if (bytes == 0) return '0 bytes';
const k = 1024;
int dm = decimals < 0 ? 0 : decimals;
int i = (log(bytes) / log(k)).floor();
return ((bytes / pow(k, i)).toStringAsFixed(dm)) + ' ' + kStorageUnits[i];
}

View file

@ -1,5 +1,8 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:photo_manager/photo_manager.dart';
@ -11,6 +14,7 @@ import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/file.dart';
import 'package:photos/services/remote_sync_service.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/ui/linear_progress_dialog.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/toast_util.dart';
@ -135,3 +139,79 @@ Future<void> deleteFilesOnDeviceOnly(
}
await dialog.hide();
}
Future<void> deleteLocalFiles(
BuildContext context, List<String> localIDs) async {
List<String> deletedIDs = [];
if (Platform.isAndroid) {
await _deleteLocalFilesOnAndroid(context, localIDs, deletedIDs);
} else {
await _deleteLocalFilesOnIOS(context, localIDs, deletedIDs);
}
if (deletedIDs.isNotEmpty) {
final deletedFiles = await FilesDB.instance.getLocalFiles(deletedIDs);
await FilesDB.instance.deleteLocalFiles(deletedIDs);
_logger.info(deletedFiles.length.toString() + " files deleted locally");
Bus.instance
.fire(LocalPhotosUpdatedEvent(deletedFiles, type: EventType.deleted));
}
}
Future<void> _deleteLocalFilesOnIOS(BuildContext context, List<String> localIDs,
List<String> deletedIDs) async {
final dialog = createProgressDialog(context,
"deleting " + localIDs.length.toString() + " backed up files...");
await dialog.show();
try {
deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(localIDs));
} catch (e, s) {
_logger.severe("Could not delete files ", e, s);
}
await dialog.hide();
}
Future<void> _deleteLocalFilesOnAndroid(BuildContext context,
List<String> localIDs, List<String> deletedIDs) async {
final dialogKey = GlobalKey<LinearProgressDialogState>();
final dialog = LinearProgressDialog(
"deleting " + localIDs.length.toString() + " backed up files...",
key: dialogKey,
);
showDialog(
context: context,
builder: (context) {
return dialog;
},
);
const minimumParts = 10;
const minimumBatchSize = 1;
const maximumBatchSize = 100;
final batchSize = min(
max(minimumBatchSize, (localIDs.length / minimumParts).round()),
maximumBatchSize);
for (int index = 0; index < localIDs.length; index += batchSize) {
if (dialogKey.currentState != null) {
dialogKey.currentState.setProgress(index / localIDs.length);
}
final ids = localIDs
.getRange(index, min(localIDs.length, index + batchSize))
.toList();
_logger.info("Trying to delete " + ids.toString());
try {
deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(ids));
_logger.info("Deleted " + ids.toString());
} catch (e, s) {
_logger.severe("Could not delete batch " + ids.toString(), e, s);
for (final id in ids) {
try {
deletedIDs.addAll(await PhotoManager.editor.deleteWithIds([id]));
_logger.info("Deleted " + id);
} catch (e, s) {
_logger.severe("Could not delete file " + id, e, s);
}
}
}
}
Navigator.of(dialogKey.currentContext, rootNavigator: true).pop('dialog');
}

View file

@ -1,3 +1,6 @@
import 'dart:math';
import 'package:confetti/confetti.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:photos/ui/loading_widget.dart';
@ -48,3 +51,47 @@ void showErrorDialog(BuildContext context, String title, String content) {
void showGenericErrorDialog(BuildContext context) {
showErrorDialog(context, "something went wrong", "please try again.");
}
Future<T> showConfettiDialog<T>({
@required BuildContext context,
WidgetBuilder builder,
bool barrierDismissible = true,
Color barrierColor,
bool useSafeArea = true,
bool useRootNavigator = true,
RouteSettings routeSettings,
Alignment confettiAlignment = Alignment.center,
}) {
final pageBuilder = Builder(
builder: builder,
);
ConfettiController _confettiController =
ConfettiController(duration: const Duration(seconds: 1));
_confettiController.play();
return showDialog(
context: context,
builder: (BuildContext buildContext) {
return Stack(
children: [
pageBuilder,
Align(
alignment: confettiAlignment,
child: ConfettiWidget(
confettiController: _confettiController,
blastDirection: pi / 2,
emissionFrequency: 0,
numberOfParticles: 100, // a lot of particles at once
gravity: 1,
blastDirectionality: BlastDirectionality.explosive,
),
),
],
);
},
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
useSafeArea: useSafeArea,
useRootNavigator: useRootNavigator,
routeSettings: routeSettings,
);
}

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
void routeToPage(BuildContext context, Widget page) {
Navigator.of(context).push(
Future<T> routeToPage<T extends Object>(BuildContext context, Widget page) {
return Navigator.of(context).push(
_buildPageRoute(page),
);
}
@ -12,7 +12,7 @@ void replacePage(BuildContext context, Widget page) {
);
}
PageRouteBuilder<dynamic> _buildPageRoute(Widget page) {
PageRouteBuilder<T> _buildPageRoute<T extends Object>(Widget page) {
return PageRouteBuilder(
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {

View file

@ -92,6 +92,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
confetti:
dependency: "direct main"
description:
name: confetti
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.5"
connectivity:
dependency: "direct main"
description:
@ -432,7 +439,7 @@ packages:
source: path
version: "0.3.6"
intl:
dependency: transitive
dependency: "direct main"
description:
name: intl
url: "https://pub.dartlang.org"
@ -479,7 +486,7 @@ packages:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.4"
version: "1.0.1"
matcher:
dependency: transitive
description:
@ -641,6 +648,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.1"
pinput:
dependency: "direct main"
description:
name: pinput
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
platform:
dependency: transitive
description:
@ -676,6 +690,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
random_color:
dependency: transitive
description:
name: random_color
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
rxdart:
dependency: transitive
description:

View file

@ -42,7 +42,7 @@ dependencies:
archive: ^3.1.2
flutter_email_sender: ^5.0.0
like_button: ^2.0.2
logging: ^0.11.4
logging: ^1.0.1
flutter_image_compress:
path: thirdparty/flutter_image_compress
flutter_typeahead: ^1.8.1
@ -88,6 +88,9 @@ dependencies:
image_editor: ^1.0.0
syncfusion_flutter_sliders: ^19.1.67-beta
syncfusion_flutter_core: ^19.1.67
pinput: ^1.2.0
intl: ^0.17.0
confetti: ^0.5.5
dev_dependencies:
flutter_test:

View file

@ -196,7 +196,6 @@ class SuperLogging {
error,
stackTrace: stack,
);
$.info('Error sent to sentry.io: $error');
} catch (e) {
$.info('Sending report to sentry.io failed: $e');
$.info('Original error: $error');

View file

@ -13,7 +13,7 @@ dependencies:
package_info_plus: ^1.0.1
device_info: ^0.4.1+4
logging: ^0.11.4
logging: ^1.0.1
sentry: ^5.0.0
intl: ^0.17.0
path: ^1.6.4