Moved User into application wide conntext.

Every page (after login) has access
to the User object via context.read/watch<User>().

This reduces localstore and asnyc operations,
as the screens do not have to load the user every time.
Additionally this prevents anyone from
using the without a user object.
This commit is contained in:
Jakob Meier 2023-03-25 17:18:46 +01:00
parent 42b6da7461
commit bb9a8621a0
No known key found for this signature in database
GPG key ID: 66BDC7E6A01A6152
13 changed files with 471 additions and 527 deletions

View file

@ -16,47 +16,6 @@ import './screens/auth.dart';
import './backend/request.dart';
import 'package:routemaster/routemaster.dart';
// routes when user is not logged in
final routesUnauthorized = RouteMap(routes: {
'/welcome/': (_) => const MaterialPage(child: WelcomePage()),
'/signup': (_) => const MaterialPage(child: AuthPage(mode: Mode.signup)),
'/signupOTA': (_) =>
const MaterialPage(child: AuthPage(mode: Mode.signupOTA)),
'/signin': (_) => const MaterialPage(child: AuthPage(mode: Mode.signin)),
}, onUnknownRoute: (_) => const MaterialPage(child: WelcomePage()));
// routes when user is logged in
final routesLoggedIn = RouteMap(routes: {
'/': (_) => const MaterialPage(child: HomePage()),
'/settings': (_) => const MaterialPage(child: SettingsPage()),
'/add-room/new': (_) => const MaterialPage(child: NewRoomPage()),
'/add-room': (_) => const MaterialPage(child: JoinRoomPage()),
'/r/:server/:tag/': (info) {
final server = info.pathParameters['server'] ?? "";
final tag = info.pathParameters['tag'] ?? "";
return MaterialPage(child: RoomPage(server, tag));
},
'/r/:server/:tag/edit': (info) {
final server = info.pathParameters['server'] ?? "";
final tag = info.pathParameters['tag'] ?? "";
return MaterialPage(child: EditRoomPage(server, tag));
},
'/r/:server/:tag/members': (info) {
final server = info.pathParameters['server'] ?? "";
final tag = info.pathParameters['tag'] ?? "";
return MaterialPage(child: ManageRoomMembersPage(server, tag));
},
'/r/:server/:tag/permissions': (info) {
final server = info.pathParameters['server'] ?? "";
final tag = info.pathParameters['tag'] ?? "";
return MaterialPage(child: EditRoomPermissionSetPage(server, tag));
},
}, onUnknownRoute: (_) => const Redirect('/'));
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const OutbagApp());
@ -73,8 +32,8 @@ class _OutbagAppState extends State {
// assume user is logged in
// unless not userdata is found
// or the userdata turns out to be wrong
bool isAuthorized = true;
AccountMeta? info;
User? user;
AppTheme theme = AppTheme.auto;
@ -85,11 +44,17 @@ class _OutbagAppState extends State {
// wait for user to be authorized
User.listen((_) async {
try {
await User.fromDisk();
final user = await User.fromDisk();
setState(() {
isAuthorized = true;
this.user = user;
});
} catch (_) {}
fetchInfo();
} catch (_) {
// no userdata found
setState(() {
user = null;
});
}
});
AppTheme.listen((_) async {
@ -106,15 +71,30 @@ class _OutbagAppState extends State {
});
})();
WidgetsBinding.instance.addPostFrameCallback((_) => fetchInfo());
WidgetsBinding.instance.addPostFrameCallback((_) async {
try {
final user = await User.fromDisk();
setState(() {
this.user = user;
});
fetchInfo();
} catch (_) {
// user unavailable
// invalid credentials
// log out
setState(() {
user = null;
});
}
});
}
void fetchInfo() {
void fetchInfo() async {
// try to obtain user account information
// with existing details
// NOTE: also functions as a way to verify ther data
doNetworkRequest(null,
req: (user) => postWithCreadentials(
req: () => postWithCreadentials(
target: (user?.server)!,
path: 'getMyAccount',
credentials: user!,
@ -122,16 +102,16 @@ class _OutbagAppState extends State {
onOK: (body) {
final info = AccountMeta.fromJSON(body['data']);
setState(() {
isAuthorized = true;
this.info = info;
});
},
onServerErr: (body) {
onServerErr: (_) {
// credentials are wrong
// log out
setState(() {
isAuthorized = false;
info = null;
user = null;
});
return true;
},
@ -140,20 +120,7 @@ class _OutbagAppState extends State {
// approve login,
// until user goes back offline
// NOTE TODO: check user data once online
setState(() {
isAuthorized = true;
});
return true;
},
onUserErr: () {
// invalid credentials
// log out
setState(() {
isAuthorized = false;
});
// do not show snackbar,
// because the user was probably never logged in
return false;
});
}
@ -164,7 +131,12 @@ class _OutbagAppState extends State {
Provider<AccountMeta?>.value(
value: info,
),
Provider<AppTheme>.value(value: theme)
Provider<AppTheme>.value(value: theme),
// conditional user provider
// used to only provide a user outside of the login area
...(user!=null)?[
Provider<User>.value(value:user!)
]:[]
],
child: MaterialApp.router(
title: "Outbag",
@ -172,8 +144,74 @@ class _OutbagAppState extends State {
theme: ThemeData(useMaterial3: true, brightness: Brightness.light),
darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark),
routerDelegate: RoutemasterDelegate(
routesBuilder: (context) =>
isAuthorized ? routesLoggedIn : routesUnauthorized),
routesBuilder: (context) => RouteMap(
routes: {
// pre authentification routes
...(user == null)
? {
'/welcome/': (_) =>
const MaterialPage(child: WelcomePage()),
'/signup': (_) => const MaterialPage(
child: AuthPage(mode: Mode.signup)),
'/signupOTA': (_) => const MaterialPage(
child: AuthPage(mode: Mode.signupOTA)),
'/signin': (_) => const MaterialPage(
child: AuthPage(mode: Mode.signin)),
}
: {},
// routes that need the user
// to exis
...(user!=null)?{
'/': (_) => const MaterialPage(child: HomePage()),
// user-space settings
'/settings': (_) =>
const MaterialPage(child: SettingsPage()),
// server dashboard
// TODO: add permission check
// so routes are only available
// if user is allowed to view dashboard
...{
},
// room related settings
'/add-room/new': (_) =>
const MaterialPage(child: NewRoomPage()),
'/add-room': (_) =>
const MaterialPage(child: JoinRoomPage()),
'/r/:server/:tag/': (info) {
final server = info.pathParameters['server'] ?? "";
final tag = info.pathParameters['tag'] ?? "";
return MaterialPage(child: RoomPage(server, tag));
},
'/r/:server/:tag/edit': (info) {
final server = info.pathParameters['server'] ?? "";
final tag = info.pathParameters['tag'] ?? "";
return MaterialPage(child: EditRoomPage(server, tag));
},
'/r/:server/:tag/members': (info) {
final server = info.pathParameters['server'] ?? "";
final tag = info.pathParameters['tag'] ?? "";
return MaterialPage(
child: ManageRoomMembersPage(server, tag));
},
'/r/:server/:tag/permissions': (info) {
final server = info.pathParameters['server'] ?? "";
final tag = info.pathParameters['tag'] ?? "";
return MaterialPage(
child: EditRoomPermissionSetPage(server, tag));
},
}:{}
},
onUnknownRoute: (_) => (user == null)
? const MaterialPage(child: WelcomePage())
: const Redirect('/'))),
routeInformationParser: const RoutemasterParser(),
));
}

View file

@ -6,8 +6,6 @@ import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:outbag_app/tools/snackbar.dart';
import 'package:routemaster/routemaster.dart';
import '../backend/resolve_url.dart';
import 'package:crypto/crypto.dart';
import 'dart:convert';
enum Mode {
signin,
@ -225,7 +223,7 @@ class _AuthPageState extends State<AuthPage> {
// hash password
final password = hashPassword(_ctrPassword.text);
doNetworkRequest(scaffMgr, needUser: false, req: (_) {
doNetworkRequest(scaffMgr, req: () {
if (widget.mode == Mode.signin) {
return postUnauthorized(
target: server,

View file

@ -36,30 +36,28 @@ class _HomePageState extends State<HomePage> {
void fetchList() async {
final sm = ScaffoldMessenger.of(context);
final user = context.read<User>();
// load cached rooms
try {
final newRooms = await Room.listRooms();
setState(() {
rooms = newRooms;
rooms = newRooms;
});
} catch (_) {}
doNetworkRequest(sm,
req: (user) => postWithCreadentials(
path: 'listRooms',
credentials: user!,
target: user.server,
body: {}),
doNetworkRequest(
sm,
req: () => postWithCreadentials(
path: 'listRooms', credentials: user, target: user.server, body: {}),
onOK: (body) async {
final List<Room> list = body['data'].map<Room>((json) {
return Room.fromJSON(json);
return Room.fromJSON(json);
}).toList();
for (Room r in list) {
await r.toDisk();
}
},
onUserErr: ()=>false
);
}

View file

@ -5,6 +5,7 @@ import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/room.dart';
import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart';
import 'package:routemaster/routemaster.dart';
import 'dart:math';
@ -44,13 +45,14 @@ class _EditRoomPageState extends State<EditRoomPage> {
void fetchInfo() {
final sm = ScaffoldMessenger.of(context);
final rmaster = Routemaster.of(context);
final user = context.read<User>();
doNetworkRequest(
sm,
req: (user) => postWithCreadentials(
req: () => postWithCreadentials(
path: 'getRoomInfo',
credentials: user!,
target: (user.server),
credentials: user,
target: user.server,
body: {'room': widget.tag, 'server': widget.server}),
onOK: (body) async {
final room = Room.fromJSON(body['data']);
@ -139,7 +141,8 @@ class _EditRoomPageState extends State<EditRoomPage> {
tooltip: "Go back",
),
),
body: SingleChildScrollView(child: Center(
body: SingleChildScrollView(
child: Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: ConstrainedBox(
@ -159,8 +162,8 @@ class _EditRoomPageState extends State<EditRoomPage> {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title:
const Text('Choose a room Icon'),
title: const Text(
'Choose a room Icon'),
actions: const [],
content: SizedBox(
width: smallest * 0.3 * 3,
@ -174,15 +177,19 @@ class _EditRoomPageState extends State<EditRoomPage> {
icon: SvgPicture
.asset(
icon.img,
width: smallest *
width:
smallest *
0.3,
height: smallest *
height:
smallest *
0.3,
),
tooltip: icon.text,
tooltip:
icon.text,
onPressed: () {
setState(() {
_ctrIcon = icon;
_ctrIcon =
icon;
});
Navigator.of(
context)
@ -239,11 +246,7 @@ class _EditRoomPageState extends State<EditRoomPage> {
),
),
],
)
)
)
)
),
))))),
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
final scaffMgr = ScaffoldMessenger.of(context);

View file

@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/room.dart';
import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart';
import 'package:routemaster/routemaster.dart';
import 'dart:math';
@ -18,11 +20,12 @@ class _JoinRoomPageState extends State {
void fetchRooms() {
final sm = ScaffoldMessenger.of(context);
final user = context.read<User>();
doNetworkRequest(null,
req: (user) => postWithCreadentials(
req: () => postWithCreadentials(
path: 'listPublicRooms',
credentials: user!,
credentials: user,
target: user.server,
body: {}),
onOK: (body) async {
@ -33,9 +36,9 @@ class _JoinRoomPageState extends State {
// use an empty blacklist when request is not successful
final List<Room> blacklist = [];
doNetworkRequest(sm,
req: (user) => postWithCreadentials(
req: () => postWithCreadentials(
path: 'listRooms',
credentials: user!,
credentials: user,
target: user.server,
body: {}),
onOK: (body) {
@ -245,12 +248,14 @@ class _JoinRoomPageState extends State {
context);
final nav =
Navigator.of(context);
final user =
context.read<User>();
doNetworkRequest(scaffMgr,
req: (user) =>
req: () =>
postWithCreadentials(
credentials:
user!,
user,
target: user
.server,
path:

View file

@ -1,11 +1,13 @@
import 'package:flutter/material.dart';
import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/room.dart';
import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/screens/room/pages/about.dart';
import 'package:outbag_app/screens/room/pages/categories.dart';
import 'package:outbag_app/screens/room/pages/products.dart';
import 'package:outbag_app/screens/room/pages/list.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart';
import 'package:routemaster/routemaster.dart';
class RoomPage extends StatefulWidget {
@ -28,6 +30,7 @@ class _RoomPageState extends State<RoomPage> {
// fetch room information
void fetchInfo() async {
final sm = ScaffoldMessenger.of(context);
final user = context.read<User>();
try {
final diskRoom =
@ -38,10 +41,10 @@ class _RoomPageState extends State<RoomPage> {
} catch (_) {}
doNetworkRequest(sm,
req: (user) => postWithCreadentials(
req: () => postWithCreadentials(
path: 'getRoomInfo',
credentials: user!,
target: (user.server),
credentials: user,
target: user.server,
body: {'room': widget.tag, 'server': widget.server}),
onOK: (body) async {
final info = RoomInfo.fromJSON(body['data']);

View file

@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:outbag_app/backend/permissions.dart';
import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/room.dart';
import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart';
import 'package:routemaster/routemaster.dart';
class ManageRoomMembersPage extends StatefulWidget {
@ -22,13 +24,14 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
void fetchUserInfo() {
final rmaster = Routemaster.of(context);
final sm = ScaffoldMessenger.of(context);
final user = context.read<User>();
doNetworkRequest(
sm,
req: (user) => postWithCreadentials(
req: () => postWithCreadentials(
path: 'getRoomInfo',
credentials: user!,
target: (user.server),
credentials: user,
target: user.server,
body: {'room': widget.tag, 'server': widget.server}),
onAnyErr: () {
// user should not be here
@ -49,10 +52,11 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
void fetchMembers() {
final rmaster = Routemaster.of(context);
final sm = ScaffoldMessenger.of(context);
final user = context.read<User>();
doNetworkRequest(sm,
req: (user) => postWithCreadentials(
credentials: user!,
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getRoomMembers',
body: {'room': widget.tag, 'server': widget.server}),
@ -99,12 +103,12 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
},
icon: const Icon(Icons.arrow_back),
tooltip: "Go back",
),
//actions: [
// // NOTE: Maybe add a search icon
// // and general search functionality here
//],
),
//actions: [
// // NOTE: Maybe add a search icon
// // and general search functionality here
//],
),
body: ListView.builder(
itemBuilder: (BuildContext context, int index) {
final item = list[index];
@ -140,7 +144,9 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
title: Text(item.humanReadableName),
subtitle: Text(role),
leading: const Icon(Icons.person),
onTap: !enable?null:() {
onTap: !enable
? null
: () {
showModalBottomSheet(
context: context,
builder: (context) => BottomSheet(
@ -158,7 +164,8 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
...((info?.isAdmin)! ||
(info?.isOwner)! ||
((info?.permissions)! &
RoomPermission.changeAdmin !=
RoomPermission
.changeAdmin !=
0))
? [
ListTile(
@ -175,8 +182,10 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
showDialog(
context: context,
builder:
(context) => AlertDialog(
icon: const Icon(Icons
(context) =>
AlertDialog(
icon: const Icon(
Icons
.supervisor_account),
title: Text(item
.isAdmin
@ -188,12 +197,12 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
: 'Do you really want to make ${item.humanReadableName} admin?'),
actions: [
TextButton(
onPressed: () {
onPressed:
() {
// close popup
// NOTE: cancel only closes the dialog
// whilst OK closes both
Navigator.of(
context)
Navigator.of(context)
.pop();
},
child: const Text(
@ -204,20 +213,16 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
() async {
// send request
final scaffMgr =
ScaffoldMessenger.of(
context);
ScaffoldMessenger.of(context);
final nav =
Navigator.of(
context);
Navigator.of(context);
final user =
context.read<User>();
doNetworkRequest(
scaffMgr,
req: (user) =>
postWithCreadentials(
path: 'setAdminStatus',
credentials: user!,
target: user.server,
body: {
req: () =>
postWithCreadentials(path: 'setAdminStatus', credentials: user, target: user.server, body: {
'room': widget.tag,
'roomServer': widget.server,
'server': item.serverTag,
@ -233,8 +238,7 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
nav.pop();
});
},
child:
const Text(
child: const Text(
'OK'),
)
],
@ -251,9 +255,10 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
0))
? [
ListTile(
leading:
const Icon(Icons.person_remove),
title: const Text('Kick User'),
leading: const Icon(
Icons.person_remove),
title:
const Text('Kick User'),
subtitle: const Text(
"Temporarrily remove user from server (they'll be able to join the room again)"),
onTap: () {
@ -261,8 +266,10 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
showDialog(
context: context,
builder:
(context) => AlertDialog(
icon: const Icon(Icons
(context) =>
AlertDialog(
icon: const Icon(
Icons
.person_remove),
title: const Text(
'Kick User'),
@ -270,13 +277,13 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
'Do you really want to kick ${item.humanReadableName}?'),
actions: [
TextButton(
onPressed: () {
onPressed:
() {
// close popup
// NOTE: cancel only closes the dialog
// whilst OK closes both
Navigator.of(
context)
Navigator.of(context)
.pop();
},
child: const Text(
@ -287,18 +294,18 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
() async {
// send request
final scaffMgr =
ScaffoldMessenger.of(
context);
ScaffoldMessenger.of(context);
final nav =
Navigator.of(
context);
Navigator.of(context);
final user =
context.read<User>();
doNetworkRequest(
scaffMgr,
req: (user) =>
req: () =>
postWithCreadentials(
path: 'kickMember',
credentials: user!,
credentials: user,
target: user.server,
body: {
'room': widget.tag,

View file

@ -5,6 +5,7 @@ import 'package:outbag_app/backend/room.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:provider/provider.dart';
import 'package:routemaster/routemaster.dart';
import 'dart:math';
@ -77,8 +78,8 @@ class _NewRoomPageState extends State {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title:
const Text('Choose a room Icon'),
title: const Text(
'Choose a room Icon'),
actions: const [],
content: SizedBox(
width: smallest * 0.3 * 3,
@ -92,15 +93,19 @@ class _NewRoomPageState extends State {
icon: SvgPicture
.asset(
icon.img,
width: smallest *
width:
smallest *
0.3,
height: smallest *
height:
smallest *
0.3,
),
tooltip: icon.text,
tooltip:
icon.text,
onPressed: () {
setState(() {
_ctrIcon = icon;
_ctrIcon =
icon;
});
Navigator.of(
context)
@ -177,11 +182,7 @@ class _NewRoomPageState extends State {
selectedIcon: Icon(_ctrVis.icon),
),
],
)
)
)
)
),
))))),
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
final scaffMgr = ScaffoldMessenger.of(context);
@ -200,24 +201,13 @@ class _NewRoomPageState extends State {
// name may not be empty
if (_ctrName.text.isEmpty) {
showSimpleSnackbar(
scaffMgr,
text: 'Please specify a room name',
action: 'OK'
);
showSimpleSnackbar(scaffMgr,
text: 'Please specify a room name', action: 'OK');
return;
}
User user;
try {
user = await User.fromDisk();
} catch (_) {
// user data invalid
// shouldn't happen
return;
}
final user = context.read<User>();
final room = Room(
id: _ctrID.text,
serverTag: user.server.tag,
@ -227,7 +217,7 @@ class _NewRoomPageState extends State {
visibility: _ctrVis);
doNetworkRequest(scaffMgr,
req: (_) => postWithCreadentials(
req: () => postWithCreadentials(
target: user.server,
credentials: user,
path: 'createRoom',
@ -244,10 +234,7 @@ class _NewRoomPageState extends State {
await room.toDisk();
// move to home page
rmaster.replace('/');
},
// we manually fetch the user data above
// because we need the serverTag
needUser: false);
});
},
label: const Text('Create'),
icon: const Icon(Icons.add)),

View file

@ -3,8 +3,10 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:outbag_app/backend/permissions.dart';
import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/room.dart';
import 'package:outbag_app/backend/user.dart';
import 'dart:math';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart';
import 'package:routemaster/routemaster.dart';
class AboutRoomPage extends StatefulWidget {
@ -29,7 +31,7 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
double smallest = min(width, height);
return SingleChildScrollView(
child:Center(
child: Center(
child: Column(children: [
// room meta display
...(widget.room != null)
@ -51,7 +53,8 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
'${widget.room?.id}@${widget.room?.serverTag}',
style: textTheme.bodySmall,
),
Text(widget.room?.description ?? '',
Text(
widget.room?.description ?? '',
style: textTheme.bodyMedium,
textAlign: TextAlign.center,
),
@ -103,21 +106,19 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
final scaffMgr =
ScaffoldMessenger.of(context);
final nav = Navigator.of(context);
final user = context.read<User>();
doNetworkRequest(scaffMgr,
req: (user) =>
postWithCreadentials(
req: () => postWithCreadentials(
path: 'setVisibility',
target: (user?.server)!,
target: user.server,
body: {
'room':
widget.room?.id,
'server': (widget.room
?.serverTag)!,
'visibility':
vset.first
'room': widget.room?.id,
'server': (widget
.room?.serverTag)!,
'visibility': vset.first
},
credentials: user!),
credentials: user),
onOK: (_) {
Room r = widget.room!;
r.visibility = vis;
@ -193,7 +194,7 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
onTap: () {
// show checkbox screen
Routemaster.of(context).push(
'/r/${widget.room?.serverTag}/${widget.room?.id}/permissions');
'/r/${widget.room?.serverTag}/${widget.room?.id}/permissions');
},
),
]
@ -262,20 +263,20 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
ScaffoldMessenger.of(ctx);
final nav = Navigator.of(ctx);
final rmaster = Routemaster.of(ctx);
final user = ctx.read<User>();
doNetworkRequest(
scaffMgr,
req: (user)=>postWithCreadentials(
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
path: ((widget.info?.isOwner)!)
? 'deleteRoom'
: 'leaveRoom',
target: (user?.server)!,
target: user.server,
body: {
'room': widget.room?.id,
'server':
(widget.room?.serverTag)!,
},
credentials: user!),
credentials: user),
onOK: (_) async {
// try delete room from disk
try {
@ -288,8 +289,7 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
after: () {
// close popup
nav.pop();
}
);
});
},
child: Text(((widget.info?.isOwner)!)
? 'Delete'
@ -301,9 +301,6 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
))
]
: [],
]
)
)
);
])));
}
}

View file

@ -4,7 +4,9 @@ import 'package:flutter/material.dart';
import 'package:outbag_app/backend/permissions.dart';
import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/room.dart';
import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart';
import 'package:routemaster/routemaster.dart';
class EditRoomPermissionSetPage extends StatefulWidget {
@ -24,13 +26,14 @@ class _EditRoomPermissionSetPageState extends State<EditRoomPermissionSetPage> {
void fetchInfo() {
final rmaster = Routemaster.of(context);
final sm = ScaffoldMessenger.of(context);
final user = context.read<User>();
doNetworkRequest(
sm,
req: (user) => postWithCreadentials(
req: () => postWithCreadentials(
path: 'getRoomInfo',
credentials: user!,
target: (user.server),
credentials: user,
target: user.server,
body: {'room': widget.tag, 'server': widget.server}),
onAnyErr: () {
// user should not be here
@ -94,11 +97,13 @@ class _EditRoomPermissionSetPageState extends State<EditRoomPermissionSetPage> {
onPressed: () {
final rmaster = Routemaster.of(context);
final sm = ScaffoldMessenger.of(context);
final user = context.read<User>();
// update permissions
doNetworkRequest(sm,
req: (user) => postWithCreadentials(
req: () => postWithCreadentials(
path: 'setRoomRight',
credentials: user!,
credentials: user,
target: user.server,
body: {
'room': widget.tag,

View file

@ -4,7 +4,7 @@ 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';
import 'package:provider/provider.dart';
class ChangePasswordDialog extends StatefulWidget {
const ChangePasswordDialog({super.key});
@ -18,39 +18,6 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
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 (_) {}
});
WidgetsBinding.instance.addPostFrameCallback((_) => loadUser());
}
@override
Widget build(BuildContext context) {
return AlertDialog(
@ -120,6 +87,7 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
onPressed: () async {
final scaffMgr = ScaffoldMessenger.of(context);
final nav = Navigator.of(context);
final user = context.read<User>();
// validate password
if (_ctrNewPassword.text.length < 6) {
@ -139,7 +107,7 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
_ctrNewPasswordRepeat.clear();
return;
}
if (hashPassword(_ctrOldPassword.text) != user?.password) {
if (hashPassword(_ctrOldPassword.text) != user.password) {
// current password wrong
showSimpleSnackbar(scaffMgr,
text: 'Old password is wrong', action: 'Dismiss');
@ -152,18 +120,17 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
// send request
doNetworkRequest(scaffMgr,
needUser: false,
req: (_) => postWithCreadentials(
req: () => postWithCreadentials(
path: 'changePassword',
target: (user?.server)!,
target: user.server,
body: {'accountKey': password},
credentials: user!),
credentials: user),
onOK: (_) async {
// update local user struct
final updatedUser = User(
username: (user?.username)!,
username: user.username,
password: password,
server: (user?.server)!);
server: user.server);
await updatedUser.toDisk();
},
after: () {

View file

@ -15,247 +15,160 @@ class SettingsPage extends StatefulWidget {
}
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);
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
final user = context.watch<User>();
final meta = context.watch<AccountMeta?>();
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",
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(
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: (_) {},
))
],
)))),
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'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Change Theme'),
content: SingleChildScrollView(
child: Column(
children: [
const Padding(
padding: EdgeInsets.all(8),
child: Text('Choose your preferred theme'),
),
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 (_) {}
},
),
]
)
),
actions: [
FilledButton(
child: const Text('Close'),
onPressed: () {
Navigator.of(context).pop();
},
)
],
));
},
),
// 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'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Change Theme'),
content: SingleChildScrollView(
child: Column(children: [
const Padding(
padding: EdgeInsets.all(8),
child: Text('Choose your preferred theme'),
),
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 (_) {}
},
),
])),
actions: [
FilledButton(
child: const Text('Close'),
onPressed: () {
Navigator.of(context).pop();
},
)
],
));
},
),
// change password button
ListTile(
title: const Text('Change password'),
subtitle: const Text('Choose a new password for your account'),
onTap: () {
showDialog(
context: context,
builder: (context) => const ChangePasswordDialog());
},
trailing: const Icon(Icons.chevron_right),
),
// change password button
ListTile(
title: const Text('Change password'),
subtitle: const Text('Choose a new password for your account'),
onTap: () {
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),
),
// 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?'),
// 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: () {
@ -267,24 +180,82 @@ class _SettingsPageState extends State<SettingsPage> {
FilledButton(
onPressed: () async {
// send request
final scaffMgr = ScaffoldMessenger.of(ctx);
final nav = Navigator.of(ctx);
final rmaster = Routemaster.of(ctx);
// delete everything
// delete user data (meta)
try {
await User.removeDisk();
} catch (_) {}
// TODO: delete all rooms
doNetworkRequest(scaffMgr,
req: () => 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('/');
// go back home
rmaster.replace('/');
},
after: () {
// close popup
nav.pop();
});
},
child: const Text('Log out'),
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'),
)
],
));
},
))
]))));
]))));
}
}

View file

@ -1,54 +1,19 @@
import 'package:flutter/material.dart';
import 'package:outbag_app/backend/errors.dart';
import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/snackbar.dart';
void doNetworkRequest(ScaffoldMessengerState? sm,
{required Future<Response> Function(User?) req,
Function(Map<String, dynamic>)? onOK,
{required Future<Response> Function() req,
Function(Map<String, dynamic> body)? onOK,
bool Function()? onNetworkErr,
bool Function()? onUnknownErr,
bool Function()? onUserErr,
Function()? onAnyErr,
Function()? after,
bool Function(Map<String, dynamic>)? onServerErr,
bool needUser = true}) async {
User? user;
try {
user = await User.fromDisk();
} catch (_) {
// no user data available
if (needUser) {
// show error & quit
bool showBar = true;
if (onUserErr != null) {
showBar = onUserErr();
}
if (showBar && sm != null) {
showSimpleSnackbar(sm, text: 'No user found', action: 'Authorize',
onTap: () {
try {
// try to remove broken user
User.removeDisk();
} catch (_) {}
});
}
if (onAnyErr != null) {
onAnyErr();
}
if (after != null) {
after();
}
return;
}
}
bool Function(Map<String, dynamic>)? onServerErr}) async {
Response res;
try {
res = await req(user);
res = await req();
} catch (_) {
// network error
bool showBar = true;