Started working on settings screen
Added: - changePassword to change the password NOTE: this requires the old password, just to prevent account hijacking. - some basic user limit information - theme selector NOTE: the system theme is meant to function like auto-theme, and is directly translated into a flutter ThemeMode, however, this does not appear to be working on the web. This commit also adds the logout and delete account buttons, but they do not yet delete all rooms, nor do they properly logout the user. BUG: User is not logged out correctly, reloading the page fixes this. Maybe localstore.listen does not detect deletion?
This commit is contained in:
parent
569dda01fd
commit
30a19fcc1e
5 changed files with 557 additions and 7 deletions
84
lib/backend/themes.dart
Normal file
84
lib/backend/themes.dart
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:localstore/localstore.dart';
|
||||||
|
|
||||||
|
class AppTheme {
|
||||||
|
ThemeMode mode;
|
||||||
|
|
||||||
|
AppTheme(this.mode);
|
||||||
|
|
||||||
|
String get name {
|
||||||
|
if (mode == ThemeMode.light) {
|
||||||
|
return 'Light';
|
||||||
|
}
|
||||||
|
if (mode == ThemeMode.dark) {
|
||||||
|
return 'Dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'System';
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData get icon {
|
||||||
|
if (mode == ThemeMode.light) {
|
||||||
|
return Icons.light_mode;
|
||||||
|
}
|
||||||
|
if (mode == ThemeMode.dark) {
|
||||||
|
return Icons.dark_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Icons.brightness_auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get auto {
|
||||||
|
return AppTheme(ThemeMode.system);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get light {
|
||||||
|
return AppTheme(ThemeMode.light);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get dark {
|
||||||
|
return AppTheme(ThemeMode.dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<AppTheme> list() {
|
||||||
|
return [AppTheme.auto, AppTheme.light, AppTheme.dark];
|
||||||
|
}
|
||||||
|
|
||||||
|
static listen(Function(Map<String, dynamic>) cb) {
|
||||||
|
final db = Localstore.instance;
|
||||||
|
final stream = db.collection('settings').stream;
|
||||||
|
stream.listen(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toDisk() async {
|
||||||
|
final db = Localstore.instance;
|
||||||
|
await db.collection('settings').doc('ui').set({'theme': mode.index});
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<AppTheme> fromDisk() async {
|
||||||
|
final db = Localstore.instance;
|
||||||
|
final doc = await db.collection('settings').doc('ui').get();
|
||||||
|
try {
|
||||||
|
final index = doc?['theme'];
|
||||||
|
final mode = ThemeMode.values[index];
|
||||||
|
return AppTheme(mode);
|
||||||
|
} catch (_) {
|
||||||
|
return AppTheme(ThemeMode.system);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(other) {
|
||||||
|
if (other.runtimeType == runtimeType) {
|
||||||
|
if (other.hashCode == hashCode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => mode.index;
|
||||||
|
|
||||||
|
}
|
|
@ -41,12 +41,16 @@ class User {
|
||||||
final stream = db.collection('meta').stream;
|
final stream = db.collection('meta').stream;
|
||||||
stream.listen(cb);
|
stream.listen(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get humanReadable {
|
||||||
|
return '$username@${server.tag}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountMeta {
|
class AccountMeta {
|
||||||
final int permissions;
|
final int permissions;
|
||||||
final String username;
|
final String username;
|
||||||
final bool discvoverable;
|
final bool discoverable;
|
||||||
final int maxRoomCount;
|
final int maxRoomCount;
|
||||||
final int maxRoomSize;
|
final int maxRoomSize;
|
||||||
final int maxRoomMemberCount;
|
final int maxRoomMemberCount;
|
||||||
|
@ -57,7 +61,7 @@ class AccountMeta {
|
||||||
required this.maxRoomSize,
|
required this.maxRoomSize,
|
||||||
required this.maxRoomCount,
|
required this.maxRoomCount,
|
||||||
required this.maxRoomMemberCount,
|
required this.maxRoomMemberCount,
|
||||||
required this.discvoverable});
|
required this.discoverable});
|
||||||
|
|
||||||
factory AccountMeta.fromJSON(dynamic json) {
|
factory AccountMeta.fromJSON(dynamic json) {
|
||||||
return AccountMeta(
|
return AccountMeta(
|
||||||
|
@ -66,6 +70,6 @@ class AccountMeta {
|
||||||
maxRoomSize: json['maxRoomSize'],
|
maxRoomSize: json['maxRoomSize'],
|
||||||
maxRoomCount: json['maxRooms'],
|
maxRoomCount: json['maxRooms'],
|
||||||
maxRoomMemberCount: json['maxUsersPerRoom'],
|
maxRoomMemberCount: json['maxUsersPerRoom'],
|
||||||
discvoverable: json['viewable'] == 1);
|
discoverable: json['viewable'] == 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:outbag_app/backend/themes.dart';
|
||||||
import 'package:outbag_app/backend/user.dart';
|
import 'package:outbag_app/backend/user.dart';
|
||||||
import 'package:outbag_app/screens/room/edit.dart';
|
import 'package:outbag_app/screens/room/edit.dart';
|
||||||
import 'package:outbag_app/screens/room/join.dart';
|
import 'package:outbag_app/screens/room/join.dart';
|
||||||
import 'package:outbag_app/screens/room/members.dart';
|
import 'package:outbag_app/screens/room/members.dart';
|
||||||
import 'package:outbag_app/screens/room/permissions.dart';
|
import 'package:outbag_app/screens/room/permissions.dart';
|
||||||
import 'package:outbag_app/screens/room/new.dart';
|
import 'package:outbag_app/screens/room/new.dart';
|
||||||
|
import 'package:outbag_app/screens/settings/main.dart';
|
||||||
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import './screens/home.dart';
|
import './screens/home.dart';
|
||||||
|
@ -26,6 +28,7 @@ final routesUnauthorized = RouteMap(routes: {
|
||||||
// routes when user is logged in
|
// routes when user is logged in
|
||||||
final routesLoggedIn = RouteMap(routes: {
|
final routesLoggedIn = RouteMap(routes: {
|
||||||
'/': (_) => const MaterialPage(child: HomePage()),
|
'/': (_) => const MaterialPage(child: HomePage()),
|
||||||
|
'/settings': (_) => const MaterialPage(child: SettingsPage()),
|
||||||
'/add-room/new': (_) => const MaterialPage(child: NewRoomPage()),
|
'/add-room/new': (_) => const MaterialPage(child: NewRoomPage()),
|
||||||
'/add-room': (_) => const MaterialPage(child: JoinRoomPage()),
|
'/add-room': (_) => const MaterialPage(child: JoinRoomPage()),
|
||||||
'/r/:server/:tag/': (info) {
|
'/r/:server/:tag/': (info) {
|
||||||
|
@ -73,12 +76,14 @@ class _OutbagAppState extends State {
|
||||||
bool isAuthorized = true;
|
bool isAuthorized = true;
|
||||||
AccountMeta? info;
|
AccountMeta? info;
|
||||||
|
|
||||||
|
AppTheme theme = AppTheme.auto;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
// wait for user to be authorized
|
// wait for user to be authorized
|
||||||
User.listen((data) async {
|
User.listen((_) async {
|
||||||
try {
|
try {
|
||||||
await User.fromDisk();
|
await User.fromDisk();
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -87,6 +92,20 @@ class _OutbagAppState extends State {
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AppTheme.listen((_) async {
|
||||||
|
final theme = await AppTheme.fromDisk();
|
||||||
|
setState(() {
|
||||||
|
this.theme = theme;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
(() async {
|
||||||
|
final theme = await AppTheme.fromDisk();
|
||||||
|
setState(() {
|
||||||
|
this.theme = theme;
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => fetchInfo());
|
WidgetsBinding.instance.addPostFrameCallback((_) => fetchInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,12 +164,11 @@ class _OutbagAppState extends State {
|
||||||
Provider<AccountMeta?>.value(
|
Provider<AccountMeta?>.value(
|
||||||
value: info,
|
value: info,
|
||||||
),
|
),
|
||||||
|
Provider<AppTheme>.value(value: theme)
|
||||||
],
|
],
|
||||||
child: MaterialApp.router(
|
child: MaterialApp.router(
|
||||||
title: "Outbag",
|
title: "Outbag",
|
||||||
// TODO: change back to system (or load from disk)
|
themeMode: theme.mode,
|
||||||
//themeMode: ThemeMode.system,
|
|
||||||
themeMode: ThemeMode.dark,
|
|
||||||
theme: ThemeData(useMaterial3: true, brightness: Brightness.light),
|
theme: ThemeData(useMaterial3: true, brightness: Brightness.light),
|
||||||
darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark),
|
darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark),
|
||||||
routerDelegate: RoutemasterDelegate(
|
routerDelegate: RoutemasterDelegate(
|
||||||
|
|
177
lib/screens/settings/dialogs/password.dart
Normal file
177
lib/screens/settings/dialogs/password.dart
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:outbag_app/backend/crypto.dart';
|
||||||
|
import 'package:outbag_app/backend/request.dart';
|
||||||
|
import 'package:outbag_app/backend/user.dart';
|
||||||
|
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
||||||
|
import 'package:outbag_app/tools/snackbar.dart';
|
||||||
|
import 'package:routemaster/routemaster.dart';
|
||||||
|
|
||||||
|
class ChangePasswordDialog extends StatefulWidget {
|
||||||
|
const ChangePasswordDialog({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _ChangePasswordDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
|
||||||
|
final TextEditingController _ctrOldPassword = TextEditingController();
|
||||||
|
final TextEditingController _ctrNewPassword = TextEditingController();
|
||||||
|
final TextEditingController _ctrNewPasswordRepeat = TextEditingController();
|
||||||
|
|
||||||
|
User? user;
|
||||||
|
|
||||||
|
void loadUser() async {
|
||||||
|
final rmaster = Routemaster.of(context);
|
||||||
|
try {
|
||||||
|
final u = await User.fromDisk();
|
||||||
|
setState(() {
|
||||||
|
user = u;
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// logout user
|
||||||
|
await User.removeDisk();
|
||||||
|
// move to welcome screen
|
||||||
|
rmaster.replace('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
User.listen((data) async {
|
||||||
|
try {
|
||||||
|
final u = await User.fromDisk();
|
||||||
|
setState(() {
|
||||||
|
user = u;
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Change Password'),
|
||||||
|
icon: const Icon(Icons.password),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: TextField(
|
||||||
|
controller: _ctrOldPassword,
|
||||||
|
keyboardType: TextInputType.visiblePassword,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
prefixIcon: Icon(Icons.lock),
|
||||||
|
labelText: 'Old Password',
|
||||||
|
hintText: 'Your current password',
|
||||||
|
helperText:
|
||||||
|
'For safety, you have to type your current passwort',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: TextField(
|
||||||
|
controller: _ctrNewPassword,
|
||||||
|
keyboardType: TextInputType.visiblePassword,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
prefixIcon: Icon(Icons.lock),
|
||||||
|
labelText: 'New Password',
|
||||||
|
hintText: 'Your new password',
|
||||||
|
helperText: 'Password have to be at least six characters long',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: TextField(
|
||||||
|
controller: _ctrNewPasswordRepeat,
|
||||||
|
keyboardType: TextInputType.visiblePassword,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
prefixIcon: Icon(Icons.lock),
|
||||||
|
labelText: 'Repeat new Password',
|
||||||
|
hintText: 'Type your new password again',
|
||||||
|
helperText:
|
||||||
|
'Type your new password again, to make sure you know it',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
// close popup
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final scaffMgr = ScaffoldMessenger.of(context);
|
||||||
|
final nav = Navigator.of(context);
|
||||||
|
|
||||||
|
// validate password
|
||||||
|
if (_ctrNewPassword.text.length < 6) {
|
||||||
|
// password has to be at least 6 characters long
|
||||||
|
showSimpleSnackbar(scaffMgr,
|
||||||
|
text: 'Password has to be at least 6 characters longs',
|
||||||
|
action: 'Dismiss');
|
||||||
|
|
||||||
|
_ctrNewPasswordRepeat.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_ctrNewPassword.text != _ctrNewPasswordRepeat.text) {
|
||||||
|
// new passwords do not match
|
||||||
|
showSimpleSnackbar(scaffMgr,
|
||||||
|
text: 'New passwords do not match', action: 'Dismiss');
|
||||||
|
|
||||||
|
_ctrNewPasswordRepeat.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hashPassword(_ctrOldPassword.text) != user?.password) {
|
||||||
|
// current password wrong
|
||||||
|
showSimpleSnackbar(scaffMgr,
|
||||||
|
text: 'Old password is wrong', action: 'Dismiss');
|
||||||
|
|
||||||
|
_ctrOldPassword.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final password = hashPassword(_ctrNewPassword.text);
|
||||||
|
|
||||||
|
// send request
|
||||||
|
doNetworkRequest(scaffMgr,
|
||||||
|
needUser: false,
|
||||||
|
req: (_) => postWithCreadentials(
|
||||||
|
path: 'changePassword',
|
||||||
|
target: (user?.server)!,
|
||||||
|
body: {'accountKey': password},
|
||||||
|
credentials: user!),
|
||||||
|
onOK: (_) async {
|
||||||
|
// update local user struct
|
||||||
|
final updatedUser = User(
|
||||||
|
username: (user?.username)!,
|
||||||
|
password: password,
|
||||||
|
server: (user?.server)!);
|
||||||
|
await updatedUser.toDisk();
|
||||||
|
},
|
||||||
|
after: () {
|
||||||
|
// close popup
|
||||||
|
nav.pop();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Change password'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
267
lib/screens/settings/main.dart
Normal file
267
lib/screens/settings/main.dart
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:outbag_app/backend/request.dart';
|
||||||
|
import 'package:outbag_app/backend/themes.dart';
|
||||||
|
import 'package:outbag_app/backend/user.dart';
|
||||||
|
import 'package:outbag_app/screens/settings/dialogs/password.dart';
|
||||||
|
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:routemaster/routemaster.dart';
|
||||||
|
|
||||||
|
class SettingsPage extends StatefulWidget {
|
||||||
|
const SettingsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _SettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsPageState extends State<SettingsPage> {
|
||||||
|
User? user;
|
||||||
|
AccountMeta? meta;
|
||||||
|
|
||||||
|
void fetchMeta() {
|
||||||
|
doNetworkRequest(ScaffoldMessenger.of(context), req: (user) {
|
||||||
|
setState(() {
|
||||||
|
this.user = user;
|
||||||
|
});
|
||||||
|
|
||||||
|
return postWithCreadentials(
|
||||||
|
path: 'getMyAccount',
|
||||||
|
credentials: user!,
|
||||||
|
target: user.server,
|
||||||
|
body: {});
|
||||||
|
}, onOK: (body) {
|
||||||
|
final meta = AccountMeta.fromJSON(body['data']);
|
||||||
|
setState(() {
|
||||||
|
this.meta = meta;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => fetchMeta());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textTheme = Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Settings'),
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
// go back
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
tooltip: "Go back",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Center(
|
||||||
|
child: Column(children: [
|
||||||
|
// uswer information widget
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
child: Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Text('${user?.humanReadable}',
|
||||||
|
style: textTheme.titleLarge)),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Room count limit:'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'How many rooms you are allowed to own'),
|
||||||
|
trailing: Text('${meta?.maxRoomCount ?? ""}'),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Room size limit:'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'How many items/products/categories each room may contain'),
|
||||||
|
trailing: Text('${meta?.maxRoomSize ?? ""}'),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Room member limit:'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'How many members each of your rooms may have'),
|
||||||
|
trailing:
|
||||||
|
Text('${meta?.maxRoomMemberCount ?? ""}')),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Discoverable'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Determines if your account can be discovered by users from other servers'),
|
||||||
|
trailing: Checkbox(
|
||||||
|
tristate: true,
|
||||||
|
value: meta?.discoverable,
|
||||||
|
onChanged: (_) {},
|
||||||
|
))
|
||||||
|
],
|
||||||
|
)))),
|
||||||
|
|
||||||
|
// change theme button
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Change Theme'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'You can change between a light theme, a dark theme and automatic theme selection'),
|
||||||
|
// NOTE: have the trailing item be a value select widget
|
||||||
|
trailing: SegmentedButton<AppTheme>(
|
||||||
|
selected: {context.watch<AppTheme>()},
|
||||||
|
selectedIcon: Icon(context.watch<AppTheme>().icon),
|
||||||
|
showSelectedIcon: true,
|
||||||
|
multiSelectionEnabled: false,
|
||||||
|
emptySelectionAllowed: false,
|
||||||
|
segments: AppTheme.list().map((item) {
|
||||||
|
return ButtonSegment<AppTheme>(
|
||||||
|
value: item, icon: Icon(item.icon), label: Text(item.name));
|
||||||
|
}).toList(),
|
||||||
|
onSelectionChanged: (item) async {
|
||||||
|
try {
|
||||||
|
await item.first.toDisk();
|
||||||
|
} catch(_) {}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// change password button
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Change password'),
|
||||||
|
subtitle: const Text('Choose a new password for your account'),
|
||||||
|
onTap: () {
|
||||||
|
// TODO: show confirm dialog
|
||||||
|
// NOTE: needs an input field for the current password
|
||||||
|
// NOTE: might want to show a message explaining,
|
||||||
|
// that there is no way to reset the password
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const ChangePasswordDialog());
|
||||||
|
},
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
),
|
||||||
|
|
||||||
|
// export account to json
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Export account'),
|
||||||
|
subtitle: const Text('Export account data'),
|
||||||
|
onTap: () {
|
||||||
|
// TODO: show confirm dialog
|
||||||
|
// NOTE: json dump the localstore
|
||||||
|
// including users and rooms
|
||||||
|
// NOTE: feature not confirmed
|
||||||
|
},
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
),
|
||||||
|
|
||||||
|
// delete account button
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Delete account'),
|
||||||
|
subtitle: const Text('Delete your account from your homeserver'),
|
||||||
|
onTap: () {
|
||||||
|
// show confirm dialog
|
||||||
|
// NOTE: same as logout
|
||||||
|
// and performs a network request
|
||||||
|
// but deletes account beforehand
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Delete account'),
|
||||||
|
content: const Text(
|
||||||
|
'Do you really want to delete your account?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
// close popup
|
||||||
|
Navigator.of(ctx).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () async {
|
||||||
|
// send request
|
||||||
|
final scaffMgr = ScaffoldMessenger.of(ctx);
|
||||||
|
final nav = Navigator.of(ctx);
|
||||||
|
final rmaster = Routemaster.of(ctx);
|
||||||
|
|
||||||
|
doNetworkRequest(scaffMgr,
|
||||||
|
req: (user) => postWithCreadentials(
|
||||||
|
path: 'deleteAccount',
|
||||||
|
target: (user?.server)!,
|
||||||
|
body: {},
|
||||||
|
credentials: user!),
|
||||||
|
onOK: (_) async {
|
||||||
|
// delete everything
|
||||||
|
// delete user data (meta)
|
||||||
|
try {
|
||||||
|
await User.removeDisk();
|
||||||
|
} catch (_) {}
|
||||||
|
// TODO: delete all rooms
|
||||||
|
|
||||||
|
// go back home
|
||||||
|
rmaster.replace('/');
|
||||||
|
},
|
||||||
|
after: () {
|
||||||
|
// close popup
|
||||||
|
nav.pop();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Delete Account'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
));
|
||||||
|
},
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
),
|
||||||
|
|
||||||
|
// logout button
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: FilledButton.tonal(
|
||||||
|
child: const Text('Log out'),
|
||||||
|
onPressed: () {
|
||||||
|
// show confirm dialog
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Log out'),
|
||||||
|
content:
|
||||||
|
const Text('Do you really want to log out?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
// close popup
|
||||||
|
Navigator.of(ctx).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () async {
|
||||||
|
// send request
|
||||||
|
final rmaster = Routemaster.of(ctx);
|
||||||
|
|
||||||
|
// delete everything
|
||||||
|
// delete user data (meta)
|
||||||
|
try {
|
||||||
|
await User.removeDisk();
|
||||||
|
} catch (_) {}
|
||||||
|
// TODO: delete all rooms
|
||||||
|
|
||||||
|
// go back home
|
||||||
|
rmaster.replace('/');
|
||||||
|
},
|
||||||
|
child: const Text('Log out'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
));
|
||||||
|
},
|
||||||
|
))
|
||||||
|
]))));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue