ente/lib/ui/settings_page.dart

712 lines
22 KiB
Dart
Raw Normal View History

import 'dart:io';
import 'package:archive/archive_io.dart';
2020-08-29 03:52:12 +00:00
import 'package:crisp/crisp.dart';
2020-09-09 14:04:11 +00:00
import 'package:flutter/foundation.dart';
2020-08-28 23:50:34 +00:00
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
2020-08-29 00:39:52 +00:00
import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
2021-03-21 12:34:24 +00:00
import 'package:flutter_windowmanager/flutter_windowmanager.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photos/core/constants.dart';
2021-03-21 08:32:10 +00:00
import 'package:photos/ui/app_lock.dart';
2021-03-26 16:13:32 +00:00
import 'package:photos/ui/password_entry_page.dart';
2021-03-19 13:27:39 +00:00
import 'package:photos/utils/auth_util.dart';
2020-08-28 23:50:34 +00:00
import 'package:photos/core/configuration.dart';
import 'package:photos/services/billing_service.dart';
2020-08-28 23:50:34 +00:00
import 'package:photos/ui/loading_widget.dart';
import 'package:photos/ui/subscription_page.dart';
2021-01-08 10:04:49 +00:00
import 'package:photos/ui/web_page.dart';
import 'package:photos/utils/data_util.dart';
import 'package:photos/utils/dialog_util.dart';
2020-12-07 08:01:40 +00:00
import 'package:photos/utils/toast_util.dart';
2021-01-13 10:22:17 +00:00
import 'package:share/share.dart';
import 'package:url_launcher/url_launcher.dart';
2020-08-28 23:50:34 +00:00
class SettingsPage extends StatelessWidget {
const SettingsPage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
2021-01-08 10:04:49 +00:00
title: Text("settings"),
2020-08-28 23:50:34 +00:00
),
body: _getBody(),
);
}
Widget _getBody() {
final hasLoggedIn = Configuration.instance.getToken() != null;
final List<Widget> contents = [];
2021-03-21 20:06:31 +00:00
if (hasLoggedIn) {
contents.addAll([
2021-03-21 20:03:51 +00:00
AccountSettingsWidget(),
Padding(padding: EdgeInsets.all(12)),
]);
}
contents.addAll([
2021-03-19 13:27:39 +00:00
SecuritySectionWidget(),
Padding(padding: EdgeInsets.all(12)),
2020-09-09 14:04:11 +00:00
SupportSectionWidget(),
Padding(padding: EdgeInsets.all(12)),
InfoSectionWidget(),
]);
contents.add(
2021-03-17 21:07:17 +00:00
FutureBuilder(
future: _getAppVersion(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
"app version: " + snapshot.data,
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.6),
),
),
);
}
return Container();
},
),
);
2021-03-21 11:21:45 +00:00
if (kDebugMode && hasLoggedIn) {
contents.add(DebugWidget());
}
2020-08-28 23:50:34 +00:00
return SingleChildScrollView(
2020-08-29 00:39:52 +00:00
child: Padding(
2021-01-08 10:04:49 +00:00
padding: const EdgeInsets.all(12.0),
2020-08-29 00:39:52 +00:00
child: Column(
2020-09-09 14:04:11 +00:00
children: contents,
2020-08-28 23:50:34 +00:00
),
),
);
}
2021-03-17 21:07:17 +00:00
static Future<String> _getAppVersion() async {
var pkgInfo = await PackageInfo.fromPlatform();
return "${pkgInfo.version}";
}
2020-08-28 23:50:34 +00:00
}
2021-03-21 20:03:51 +00:00
class AccountSettingsWidget extends StatefulWidget {
AccountSettingsWidget({Key key}) : super(key: key);
2020-08-28 23:50:34 +00:00
@override
2021-03-21 20:03:51 +00:00
AccountSettingsWidgetState createState() => AccountSettingsWidgetState();
2020-08-28 23:50:34 +00:00
}
2021-03-21 20:03:51 +00:00
class AccountSettingsWidgetState extends State<AccountSettingsWidget> {
2020-08-28 23:50:34 +00:00
double _usageInGBs;
2020-11-19 18:22:30 +00:00
2020-08-28 23:50:34 +00:00
@override
void initState() {
_getUsage();
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
2020-08-29 00:39:52 +00:00
child: Column(
children: [
2021-03-21 20:03:51 +00:00
SettingsSectionTitle("account"),
Padding(
padding: EdgeInsets.all(4),
),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
2021-03-21 20:03:51 +00:00
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return SubscriptionPage();
},
),
);
},
child: SettingsTextItem(
2021-03-21 20:03:51 +00:00
text: "subscription plan", icon: Icons.navigate_next),
),
2021-03-25 19:34:23 +00:00
Platform.isIOS
? Padding(padding: EdgeInsets.all(2))
: Padding(padding: EdgeInsets.all(2)),
Divider(height: 4),
2021-03-25 19:34:23 +00:00
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(() {});
},
),
],
),
),
2021-03-25 19:34:23 +00:00
Platform.isIOS
? Padding(padding: EdgeInsets.all(2))
: Padding(padding: EdgeInsets.all(4)),
Divider(height: 4),
2021-03-25 19:34:23 +00:00
Platform.isIOS
? Padding(padding: EdgeInsets.all(6))
: Padding(padding: EdgeInsets.all(8)),
2020-08-29 00:39:52 +00:00
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
2021-01-08 10:04:49 +00:00
Text("total data backed up"),
2021-01-31 21:02:28 +00:00
Container(
height: 20,
child: _usageInGBs == null
? loadWidget
: Text(
_usageInGBs.toString() + " GB",
),
),
2020-08-29 00:39:52 +00:00
],
),
2021-03-25 19:34:23 +00:00
Platform.isIOS
? Padding(padding: EdgeInsets.all(6))
: Padding(padding: EdgeInsets.all(8)),
2021-03-21 20:03:51 +00:00
Divider(height: 4),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
AlertDialog alert = AlertDialog(
title: Text("logout"),
content: Text("are you sure you want to logout?"),
actions: [
TextButton(
child: Text(
"no",
style: TextStyle(
color: Theme.of(context).buttonColor,
),
),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
},
),
TextButton(
child: Text(
"yes, logout",
style: TextStyle(
color: Colors.red,
),
),
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop('dialog');
final dialog =
createProgressDialog(context, "logging out...");
await dialog.show();
await Configuration.instance.logout();
await dialog.hide();
Navigator.of(context).popUntil((route) => route.isFirst);
},
),
],
);
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
},
child: SettingsTextItem(text: "logout", icon: Icons.navigate_next),
),
2020-08-29 00:39:52 +00:00
],
2020-08-28 23:50:34 +00:00
),
);
}
void _getUsage() {
BillingService.instance.fetchUsage().then((usage) async {
if (mounted) {
setState(() {
_usageInGBs = convertBytesToGBs(usage);
});
2020-08-28 23:50:34 +00:00
}
});
}
}
2020-08-29 00:39:52 +00:00
2021-03-19 13:27:39 +00:00
class SecuritySectionWidget extends StatefulWidget {
SecuritySectionWidget({Key key}) : super(key: key);
@override
_SecuritySectionWidgetState createState() => _SecuritySectionWidgetState();
}
class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
2021-03-21 12:34:24 +00:00
final List<Widget> children = [];
children.addAll([
SettingsSectionTitle("security"),
2021-03-21 20:03:51 +00:00
Padding(
padding: EdgeInsets.all(4),
),
2021-03-26 16:13:32 +00:00
]);
children.addAll([
2021-03-21 12:34:24 +00:00
Container(
height: 36,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("lockscreen"),
Switch(
value: Configuration.instance.shouldShowLockScreen(),
onChanged: (value) async {
AppLock.of(context).disable();
final result = await requestAuthentication();
if (result) {
AppLock.of(context).setEnabled(value);
Configuration.instance.setShouldShowLockScreen(value);
setState(() {});
} else {
AppLock.of(context).setEnabled(
Configuration.instance.shouldShowLockScreen());
}
},
2021-03-19 13:27:39 +00:00
),
2021-03-21 12:34:24 +00:00
],
),
),
]);
if (Platform.isAndroid) {
children.addAll([
Padding(padding: EdgeInsets.all(4)),
Divider(height: 4),
Padding(padding: EdgeInsets.all(4)),
Container(
height: 36,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("hide from recents"),
Switch(
value: Configuration.instance.shouldHideFromRecents(),
onChanged: (value) async {
if (value) {
AlertDialog alert = AlertDialog(
title: Text("hide from recents?"),
content: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"hiding from the task switcher will prevent you from taking screenshots in this app.",
style: TextStyle(
height: 1.5,
),
),
Padding(padding: EdgeInsets.all(8)),
Text(
"are you sure?",
style: TextStyle(
height: 1.5,
),
),
],
),
),
actions: [
TextButton(
child:
Text("no", style: TextStyle(color: Colors.white)),
onPressed: () {
Navigator.of(context, rootNavigator: true)
.pop('dialog');
},
),
TextButton(
child: Text("yes",
style: TextStyle(
color: Colors.white.withOpacity(0.8))),
onPressed: () async {
Navigator.of(context, rootNavigator: true)
.pop('dialog');
await Configuration.instance
.setShouldHideFromRecents(true);
await FlutterWindowManager.addFlags(
FlutterWindowManager.FLAG_SECURE);
setState(() {});
},
),
],
);
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
} else {
await Configuration.instance
.setShouldHideFromRecents(false);
await FlutterWindowManager.clearFlags(
FlutterWindowManager.FLAG_SECURE);
setState(() {});
}
},
),
],
2021-03-19 13:27:39 +00:00
),
2021-03-21 12:34:24 +00:00
),
]);
}
2021-03-26 16:13:32 +00:00
if (Configuration.instance.hasConfiguredAccount()) {
children.addAll(
[
Platform.isIOS
? Padding(padding: EdgeInsets.all(2))
: Padding(padding: EdgeInsets.all(4)),
Divider(height: 4),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
final result = await requestAuthentication();
if (!result) {
return;
}
2021-03-26 16:13:32 +00:00
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return PasswordEntryPage(
isUpdatePassword: true,
);
},
),
);
},
child: SettingsTextItem(
text: "change password", icon: Icons.navigate_next),
),
],
);
}
2021-03-21 12:34:24 +00:00
return Container(
child: Column(
children: children,
2021-03-19 13:27:39 +00:00
),
);
}
}
2020-08-29 00:39:52 +00:00
class SettingsSectionTitle extends StatelessWidget {
final String title;
const SettingsSectionTitle(this.title, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Column(children: [
Padding(padding: EdgeInsets.all(4)),
Align(
alignment: Alignment.centerLeft,
child: Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
2021-01-08 10:04:49 +00:00
fontSize: 16,
2020-08-29 00:39:52 +00:00
color: Theme.of(context).accentColor,
),
),
),
Padding(padding: EdgeInsets.all(4)),
]));
}
}
class SupportSectionWidget extends StatelessWidget {
const SupportSectionWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Column(children: [
2021-01-08 10:04:49 +00:00
SettingsSectionTitle("support"),
2020-12-06 11:54:07 +00:00
GestureDetector(
behavior: HitTestBehavior.translucent,
2021-01-08 10:04:49 +00:00
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
final endpoint = Configuration.instance.getHttpEndpoint() +
"/users/roadmap";
final isLoggedIn = Configuration.instance.getToken() != null;
final url = isLoggedIn
? endpoint + "?token=" + Configuration.instance.getToken()
: ROADMAP_URL;
return WebPage("roadmap", url);
2021-01-08 10:04:49 +00:00
},
),
);
2020-12-06 11:54:07 +00:00
},
2021-03-17 18:49:45 +00:00
child: SettingsTextItem(text: "roadmap", icon: Icons.navigate_next),
2020-12-06 11:54:07 +00:00
),
Divider(height: 4),
2020-08-29 00:39:52 +00:00
GestureDetector(
2020-08-29 03:52:12 +00:00
behavior: HitTestBehavior.translucent,
2020-08-29 00:39:52 +00:00
onTap: () async {
final Email email = Email(
2020-12-01 21:19:20 +00:00
recipients: ['hey@ente.io'],
2020-08-29 00:39:52 +00:00
isHTML: false,
);
try {
await FlutterEmailSender.send(email);
} catch (e) {
2020-12-01 21:19:20 +00:00
showGenericErrorDialog(context);
}
2020-08-29 00:39:52 +00:00
},
2020-12-07 08:01:40 +00:00
onLongPress: () async {
2021-01-08 10:04:49 +00:00
showToast("thanks for reporting a bug!");
final dialog = createProgressDialog(context, "preparing logs...");
2020-12-01 21:19:20 +00:00
await dialog.show();
final tempPath = (await getTemporaryDirectory()).path;
final zipFilePath = tempPath + "/logs.zip";
final logsDirectory = Directory(tempPath + "/logs");
var encoder = ZipFileEncoder();
encoder.create(zipFilePath);
encoder.addDirectory(logsDirectory);
encoder.close();
await dialog.hide();
final Email email = Email(
recipients: ['bug@ente.io'],
attachmentPaths: [zipFilePath],
isHTML: false,
);
try {
await FlutterEmailSender.send(email);
} catch (e) {
2021-01-13 10:22:17 +00:00
return Share.shareFiles([zipFilePath]);
2020-12-01 21:19:20 +00:00
}
},
2021-01-08 10:04:49 +00:00
child: SettingsTextItem(text: "email", icon: Icons.navigate_next),
2020-12-07 08:01:40 +00:00
),
Divider(height: 4),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return CrispChatPage();
},
),
);
},
2021-01-08 10:04:49 +00:00
child: SettingsTextItem(text: "chat", icon: Icons.navigate_next),
2020-12-01 21:19:20 +00:00
),
2020-09-09 14:04:11 +00:00
]),
);
}
}
class InfoSectionWidget extends StatelessWidget {
const InfoSectionWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Column(children: [
2021-02-01 09:06:39 +00:00
SettingsSectionTitle("about"),
2020-12-07 08:01:40 +00:00
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
2021-01-08 10:04:49 +00:00
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return WebPage("faq", "https://ente.io/faq");
},
),
);
2020-12-07 08:01:40 +00:00
},
2021-01-08 10:04:49 +00:00
child: SettingsTextItem(text: "faq", icon: Icons.navigate_next),
2020-12-07 08:01:40 +00:00
),
Divider(height: 4),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
2021-01-08 10:04:49 +00:00
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return WebPage("terms", "https://ente.io/terms");
},
),
);
},
2021-01-08 10:04:49 +00:00
child: SettingsTextItem(text: "terms", icon: Icons.navigate_next),
),
Divider(height: 4),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
2021-01-08 10:04:49 +00:00
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return WebPage("privacy", "https://ente.io/privacy");
},
),
);
},
2021-01-08 10:04:49 +00:00
child: SettingsTextItem(text: "privacy", icon: Icons.navigate_next),
),
Divider(height: 4),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
2020-12-01 21:19:20 +00:00
launch("https://github.com/ente-io/frame");
},
2020-12-01 21:19:20 +00:00
child:
2021-01-08 10:04:49 +00:00
SettingsTextItem(text: "source code", icon: Icons.navigate_next),
),
2021-03-17 21:07:17 +00:00
]),
);
}
}
2020-09-09 14:04:11 +00:00
class DebugWidget extends StatelessWidget {
const DebugWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Column(children: [
Padding(padding: EdgeInsets.all(12)),
2021-01-08 10:04:49 +00:00
SettingsSectionTitle("debug"),
2020-09-09 14:04:11 +00:00
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
_showKeyAttributesDialog(context);
},
child: SettingsTextItem(
2021-03-21 11:21:45 +00:00
text: "key attributes", icon: Icons.navigate_next),
2020-08-29 00:39:52 +00:00
),
]),
);
}
2020-09-09 14:04:11 +00:00
void _showKeyAttributesDialog(BuildContext context) {
final keyAttributes = Configuration.instance.getKeyAttributes();
AlertDialog alert = AlertDialog(
2021-01-08 10:04:49 +00:00
title: Text("key attributes"),
2020-09-09 14:04:11 +00:00
content: SingleChildScrollView(
child: Column(children: [
Text("Key", style: TextStyle(fontWeight: FontWeight.bold)),
Text(Sodium.bin2base64(Configuration.instance.getKey())),
2020-09-09 14:04:11 +00:00
Padding(padding: EdgeInsets.all(12)),
Text("Encrypted Key", style: TextStyle(fontWeight: FontWeight.bold)),
Text(keyAttributes.encryptedKey),
Padding(padding: EdgeInsets.all(12)),
Text("Key Decryption Nonce",
2020-09-09 14:04:11 +00:00
style: TextStyle(fontWeight: FontWeight.bold)),
Text(keyAttributes.keyDecryptionNonce),
2020-09-09 14:04:11 +00:00
Padding(padding: EdgeInsets.all(12)),
Text("KEK Salt", style: TextStyle(fontWeight: FontWeight.bold)),
Text(keyAttributes.kekSalt),
Padding(padding: EdgeInsets.all(12)),
]),
),
actions: [
FlatButton(
child: Text("OK"),
onPressed: () {
2021-03-21 11:21:45 +00:00
Navigator.of(context, rootNavigator: true).pop('dialog');
2020-09-09 14:04:11 +00:00
},
),
],
);
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
}
class SettingsTextItem extends StatelessWidget {
final String text;
final IconData icon;
const SettingsTextItem({
Key key,
@required this.text,
@required this.icon,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(padding: EdgeInsets.all(Platform.isIOS ? 4 : 6)),
2020-09-09 14:04:11 +00:00
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Align(alignment: Alignment.centerLeft, child: Text(text)),
Icon(icon),
],
),
2021-03-25 19:34:23 +00:00
Padding(padding: EdgeInsets.all(Platform.isIOS ? 4 : 6)),
2020-09-09 14:04:11 +00:00
],
);
}
2020-08-29 00:39:52 +00:00
}
2020-08-29 03:52:12 +00:00
class CrispChatPage extends StatefulWidget {
CrispChatPage({Key key}) : super(key: key);
@override
_CrispChatPageState createState() => _CrispChatPageState();
}
class _CrispChatPageState extends State<CrispChatPage> {
static const websiteID = "86d56ea2-68a2-43f9-8acb-95e06dee42e8";
@override
void initState() {
crisp.initialize(
websiteId: websiteID,
);
crisp.register(
CrispUser(
email: Configuration.instance.getEmail(),
),
);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
2021-01-08 10:04:49 +00:00
title: Text("support chat"),
2020-08-29 03:52:12 +00:00
),
body: CrispView(
loadingWidget: loadWidget,
),
);
}
}