From bb9a8621a094e6c4a69d1fc673992aae629bc721 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Sat, 25 Mar 2023 17:18:46 +0100 Subject: [PATCH] Moved User into application wide conntext. Every page (after login) has access to the User object via context.read/watch(). 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. --- lib/main.dart | 172 +++++--- lib/screens/auth.dart | 4 +- lib/screens/home.dart | 16 +- lib/screens/room/edit.dart | 33 +- lib/screens/room/join.dart | 17 +- lib/screens/room/main.dart | 9 +- lib/screens/room/members.dart | 93 +++-- lib/screens/room/new.dart | 47 +-- lib/screens/room/pages/about.dart | 45 +- lib/screens/room/permissions.dart | 15 +- lib/screens/settings/dialogs/password.dart | 49 +-- lib/screens/settings/main.dart | 455 ++++++++++----------- lib/tools/fetch_wrapper.dart | 43 +- 13 files changed, 471 insertions(+), 527 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 26a4171..f27d0ad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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.value( value: info, ), - Provider.value(value: theme) + Provider.value(value: theme), + // conditional user provider + // used to only provide a user outside of the login area + ...(user!=null)?[ + Provider.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(), )); } diff --git a/lib/screens/auth.dart b/lib/screens/auth.dart index 19f31a6..525613d 100644 --- a/lib/screens/auth.dart +++ b/lib/screens/auth.dart @@ -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 { // hash password final password = hashPassword(_ctrPassword.text); - doNetworkRequest(scaffMgr, needUser: false, req: (_) { + doNetworkRequest(scaffMgr, req: () { if (widget.mode == Mode.signin) { return postUnauthorized( target: server, diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 24cfa85..d8fb930 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -36,30 +36,28 @@ class _HomePageState extends State { void fetchList() async { final sm = ScaffoldMessenger.of(context); + final user = context.read(); // 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 list = body['data'].map((json) { - return Room.fromJSON(json); + return Room.fromJSON(json); }).toList(); for (Room r in list) { await r.toDisk(); } }, - onUserErr: ()=>false ); } diff --git a/lib/screens/room/edit.dart b/lib/screens/room/edit.dart index a3a19d0..4b58926 100644 --- a/lib/screens/room/edit.dart +++ b/lib/screens/room/edit.dart @@ -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 { void fetchInfo() { final sm = ScaffoldMessenger.of(context); final rmaster = Routemaster.of(context); + final user = context.read(); 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 { 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 { 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 { 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 { ), ), ], - ) - ) - ) - ) - ), + ))))), floatingActionButton: FloatingActionButton.extended( onPressed: () async { final scaffMgr = ScaffoldMessenger.of(context); diff --git a/lib/screens/room/join.dart b/lib/screens/room/join.dart index 58594e6..7eb19f1 100644 --- a/lib/screens/room/join.dart +++ b/lib/screens/room/join.dart @@ -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(); 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 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(); doNetworkRequest(scaffMgr, - req: (user) => + req: () => postWithCreadentials( credentials: - user!, + user, target: user .server, path: diff --git a/lib/screens/room/main.dart b/lib/screens/room/main.dart index 9681600..bbffa2a 100644 --- a/lib/screens/room/main.dart +++ b/lib/screens/room/main.dart @@ -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 { // fetch room information void fetchInfo() async { final sm = ScaffoldMessenger.of(context); + final user = context.read(); try { final diskRoom = @@ -38,10 +41,10 @@ class _RoomPageState extends State { } 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']); diff --git a/lib/screens/room/members.dart b/lib/screens/room/members.dart index 82bc2f6..bb538fa 100644 --- a/lib/screens/room/members.dart +++ b/lib/screens/room/members.dart @@ -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 { void fetchUserInfo() { final rmaster = Routemaster.of(context); final sm = ScaffoldMessenger.of(context); + final user = context.read(); 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 { void fetchMembers() { final rmaster = Routemaster.of(context); final sm = ScaffoldMessenger.of(context); + final user = context.read(); 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 { }, 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 { 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 { ...((info?.isAdmin)! || (info?.isOwner)! || ((info?.permissions)! & - RoomPermission.changeAdmin != + RoomPermission + .changeAdmin != 0)) ? [ ListTile( @@ -175,8 +182,10 @@ class _ManageRoomMembersPageState extends State { 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 { : '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 { () async { // send request final scaffMgr = - ScaffoldMessenger.of( - context); + ScaffoldMessenger.of(context); final nav = - Navigator.of( - context); + Navigator.of(context); + final user = + context.read(); 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 { nav.pop(); }); }, - child: - const Text( + child: const Text( 'OK'), ) ], @@ -251,9 +255,10 @@ class _ManageRoomMembersPageState extends State { 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 { 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 { '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 { () async { // send request final scaffMgr = - ScaffoldMessenger.of( - context); + ScaffoldMessenger.of(context); final nav = - Navigator.of( - context); + Navigator.of(context); + final user = + context.read(); doNetworkRequest( scaffMgr, - req: (user) => + req: () => postWithCreadentials( path: 'kickMember', - credentials: user!, + credentials: user, target: user.server, body: { 'room': widget.tag, diff --git a/lib/screens/room/new.dart b/lib/screens/room/new.dart index f1d9313..03024ba 100644 --- a/lib/screens/room/new.dart +++ b/lib/screens/room/new.dart @@ -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(); 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)), diff --git a/lib/screens/room/pages/about.dart b/lib/screens/room/pages/about.dart index 4e48dd5..51a97c5 100644 --- a/lib/screens/room/pages/about.dart +++ b/lib/screens/room/pages/about.dart @@ -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 { 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 { '${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 { final scaffMgr = ScaffoldMessenger.of(context); final nav = Navigator.of(context); + final user = context.read(); 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 { 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 { ScaffoldMessenger.of(ctx); final nav = Navigator.of(ctx); final rmaster = Routemaster.of(ctx); + final user = ctx.read(); - 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 { after: () { // close popup nav.pop(); - } - ); + }); }, child: Text(((widget.info?.isOwner)!) ? 'Delete' @@ -301,9 +301,6 @@ class _AboutRoomPageState extends State { )) ] : [], - ] - ) - ) - ); + ]))); } } diff --git a/lib/screens/room/permissions.dart b/lib/screens/room/permissions.dart index 8e925a1..9d57b0d 100644 --- a/lib/screens/room/permissions.dart +++ b/lib/screens/room/permissions.dart @@ -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 { void fetchInfo() { final rmaster = Routemaster.of(context); final sm = ScaffoldMessenger.of(context); + final user = context.read(); 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 { onPressed: () { final rmaster = Routemaster.of(context); final sm = ScaffoldMessenger.of(context); + final user = context.read(); + // update permissions doNetworkRequest(sm, - req: (user) => postWithCreadentials( + req: () => postWithCreadentials( path: 'setRoomRight', - credentials: user!, + credentials: user, target: user.server, body: { 'room': widget.tag, diff --git a/lib/screens/settings/dialogs/password.dart b/lib/screens/settings/dialogs/password.dart index 3a5935b..65be0f0 100644 --- a/lib/screens/settings/dialogs/password.dart +++ b/lib/screens/settings/dialogs/password.dart @@ -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 { 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 { onPressed: () async { final scaffMgr = ScaffoldMessenger.of(context); final nav = Navigator.of(context); + final user = context.read(); // validate password if (_ctrNewPassword.text.length < 6) { @@ -139,7 +107,7 @@ class _ChangePasswordDialogState extends State { _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 { // 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: () { diff --git a/lib/screens/settings/main.dart b/lib/screens/settings/main.dart index 7424bce..a9a20b4 100644 --- a/lib/screens/settings/main.dart +++ b/lib/screens/settings/main.dart @@ -15,247 +15,160 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { - 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(); + final meta = context.watch(); 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( - selected: {context.watch()}, - selectedIcon: Icon(context.watch().icon), - showSelectedIcon: true, - multiSelectionEnabled: false, - emptySelectionAllowed: false, - segments: AppTheme.list().map((item) { - return ButtonSegment( - 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( + selected: {context.watch()}, + selectedIcon: Icon(context.watch().icon), + showSelectedIcon: true, + multiSelectionEnabled: false, + emptySelectionAllowed: false, + segments: AppTheme.list().map((item) { + return ButtonSegment( + 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 { 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'), + ) + ], + )); + }, )) - ])))); + ])))); } } diff --git a/lib/tools/fetch_wrapper.dart b/lib/tools/fetch_wrapper.dart index 5b9859d..2897ad2 100644 --- a/lib/tools/fetch_wrapper.dart +++ b/lib/tools/fetch_wrapper.dart @@ -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 Function(User?) req, - Function(Map)? onOK, + {required Future Function() req, + Function(Map body)? onOK, bool Function()? onNetworkErr, bool Function()? onUnknownErr, - bool Function()? onUserErr, Function()? onAnyErr, Function()? after, - bool Function(Map)? 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)? onServerErr}) async { Response res; try { - res = await req(user); + res = await req(); } catch (_) { // network error bool showBar = true;