Merge branch 'master' of github.com:ente-io/frame
This commit is contained in:
commit
248746c45f
21 changed files with 1040 additions and 72 deletions
BIN
assets/backed_up_gallery.png
Normal file
BIN
assets/backed_up_gallery.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 91 KiB |
|
@ -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(
|
||||
|
|
13
lib/models/backup_status.dart
Normal file
13
lib/models/backup_status.dart
Normal 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);
|
||||
}
|
|
@ -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()) {
|
||||
|
|
|
@ -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
161
lib/ui/free_space_page.dart
Normal 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);
|
||||
}
|
||||
}
|
47
lib/ui/linear_progress_dialog.dart
Normal file
47
lib/ui/linear_progress_dialog.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
155
lib/ui/settings/backup_section_widget.dart
Normal file
155
lib/ui/settings/backup_section_widget.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)),
|
||||
]);
|
||||
|
|
|
@ -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();
|
||||
|
|
128
lib/ui/two_factor_authentication_page.dart
Normal file
128
lib/ui/two_factor_authentication_page.dart
Normal 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);
|
||||
}
|
||||
}
|
109
lib/ui/two_factor_recovery_page.dart
Normal file
109
lib/ui/two_factor_recovery_page.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
25
pubspec.lock
25
pubspec.lock
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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');
|
||||
|
|
2
thirdparty/super_logging/pubspec.yaml
vendored
2
thirdparty/super_logging/pubspec.yaml
vendored
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue