diff --git a/lib/backend/room.dart b/lib/backend/room.dart index 799edcf..9aa011a 100644 --- a/lib/backend/room.dart +++ b/lib/backend/room.dart @@ -187,10 +187,10 @@ class RoomIcon { } class Room { - final String id; - final String serverTag; - final String name; - final String description; + String id; + String serverTag; + String name; + String description; RoomIcon? icon = RoomIcon.other; RoomVisibility? visibility = RoomVisibility.private; @@ -202,6 +202,17 @@ class Room { this.icon, this.visibility}); + int compareTo(Room r) { + final me = humanReadable; + final other = r.humanReadable; + + return me.compareTo(other); + } + + String get humanReadable { + return '$id@$serverTag'; + } + // get list of all known rooms static Future> listRooms() async { final db = Localstore.instance; diff --git a/lib/backend/user.dart b/lib/backend/user.dart index d9d3471..5816d95 100644 --- a/lib/backend/user.dart +++ b/lib/backend/user.dart @@ -30,6 +30,12 @@ class User { ); } + static Future removeDisk() async { + final db = Localstore.instance; + await db.collection('meta').doc('auth').delete(); + return; + } + static listen(Function(Map) cb) async { final db = Localstore.instance; final stream = db.collection('meta').stream; @@ -55,12 +61,11 @@ class AccountMeta { factory AccountMeta.fromJSON(dynamic json) { return AccountMeta( - permissions: json['rights'], - username: json['name'], - maxRoomSize: json['maxRoomSize'], - maxRoomCount: json['maxRooms'], - maxRoomMemberCount: json['maxUsersPerRoom'], - discvoverable: json['viewable'] == 1 - ); + permissions: json['rights'], + username: json['name'], + maxRoomSize: json['maxRoomSize'], + maxRoomCount: json['maxRooms'], + maxRoomMemberCount: json['maxUsersPerRoom'], + discvoverable: json['viewable'] == 1); } } diff --git a/lib/main.dart b/lib/main.dart index db0ad6f..6104cae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/screens/room/join.dart'; import 'package:outbag_app/screens/room/new.dart'; +import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:provider/provider.dart'; import './screens/home.dart'; import './screens/welcome.dart'; @@ -58,47 +59,48 @@ class _OutbagAppState extends State { // try to obtain user account information // with existing details // NOTE: also functions as a way to verify ther data - (() async { - User credentials; - try { - credentials = await User.fromDisk(); - } catch (_) { - // invalid credentials - // log out - setState(() { - isAuthorized = false; - }); - return; - } - try { - final resp = await postWithCreadentials( - target: credentials.server, - path: 'getMyAccount', - credentials: credentials, - body: {}); - if (resp.res == Result.ok) { - final info = AccountMeta.fromJSON(resp.body['data']); - setState(() { - isAuthorized = true; - this.info = info; - }); - } else { - // credentials are wrong - // log out - setState(() { - isAuthorized = false; - }); - } - } catch (_) { - // user is currently offline - // approve login, - // until user goes back offline - // NOTE TODO: check user data once online - setState(() { - isAuthorized = true; - }); - } - })(); + doNetworkRequest( + null, + req: (user) => postWithCreadentials( + target: (user?.server)!, + path: 'getMyAccount', + credentials: user!, + body: {}), + onOK: (body) { + final info = AccountMeta.fromJSON(body['data']); + setState(() { + isAuthorized = true; + this.info = info; + }); + }, + onServerErr: (body) { + // credentials are wrong + // log out + + setState(() { + isAuthorized = false; + }); + return true; + }, + onNetworkErr: () { + // user is currently offline + // 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; + }); + return true; + } + ); // wait for user to be authorized User.listen((data) async { @@ -115,7 +117,9 @@ class _OutbagAppState extends State { Widget build(BuildContext context) { return MultiProvider( providers: [ - Provider.value(value: info,), + Provider.value( + value: info, + ), ], child: MaterialApp.router( title: "Outbag", diff --git a/lib/screens/auth.dart b/lib/screens/auth.dart index c892c1d..a8200be 100644 --- a/lib/screens/auth.dart +++ b/lib/screens/auth.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.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'; import '../backend/resolve_url.dart'; import '../backend/errors.dart'; @@ -173,6 +175,8 @@ class _AuthPageState extends State { showSpinner = true; }); + final scaffMgr = ScaffoldMessenger.of(context); + // verify that both passwords are the same if (widget.mode != Mode.signin) { if (_ctrPassword.text != _ctrPasswordRpt.text) { @@ -180,19 +184,8 @@ class _AuthPageState extends State { showSpinner = false; }); - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - content: const Text('Passwords do not match'), - action: SnackBarAction( - label: 'Dismiss', - onPressed: () { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - }, - ), - ); - - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + showSimpleSnackbar(scaffMgr, + text: 'Passwords do not match', action: 'Dismiss'); _ctrPasswordRpt.clear(); return; @@ -205,28 +198,15 @@ class _AuthPageState extends State { showSpinner = false; }); - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - content: const Text( - 'Password has to be at least 6 characters long'), - action: SnackBarAction( - label: 'Dismiss', - onPressed: () { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - }, - ), - ); - - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + showSimpleSnackbar(scaffMgr, + text: 'Password has to be at least 6 characters longs', + action: 'Dismiss'); _ctrPasswordRpt.clear(); return; } - final scaffMgr = ScaffoldMessenger.of(context); - - // TODO: resolve homeserver url + // resolve homeserver url OutbagServer server; try { server = await getOutbagServerUrl(_ctrServer.text); @@ -238,50 +218,43 @@ class _AuthPageState extends State { showSpinner = false; }); - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - content: Text( - 'Unable to find valid outbag server on ${_ctrServer.text}'), - action: SnackBarAction( - label: 'Dismiss', - onPressed: () { - scaffMgr.hideCurrentSnackBar(); - }, - ), - ); - - scaffMgr.hideCurrentSnackBar(); - scaffMgr.showSnackBar(snackBar); + showSimpleSnackbar(scaffMgr, + text: + 'Unable to find valid outbag server on ${_ctrServer.text}', + action: 'Dismiss'); return; } + // hash password var bytes = utf8.encode(_ctrPassword.text); final password = sha256.convert(bytes).toString(); - try { - Response resp; - // validate account - if (widget.mode == Mode.signin) { - resp = await postUnauthorized( + + doNetworkRequest( + scaffMgr, + needUser: false, + req: (_) { + if (widget.mode == Mode.signin) { + return postUnauthorized( target: server, path: 'signin', body: { 'name': _ctrUsername.text, 'server': server.tag, 'accountKey': password - }); - } else if (widget.mode == Mode.signup) { - resp = await postUnauthorized( + }); + } else if (widget.mode == Mode.signup) { + return postUnauthorized( target: server, path: 'signup', body: { 'name': _ctrUsername.text, 'server': server.tag, 'accountKey': password - }); - } else { - // signup OTA - resp = await postUnauthorized( + }); + } else { + // signup OTA + return postUnauthorized( target: server, path: 'signupOTA', body: { @@ -289,51 +262,23 @@ class _AuthPageState extends State { 'server': server.tag, 'accountKey': password, 'OTA': _ctrOTA.text - }); - } - - if (resp.res == Result.err) { - // error - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - content: Text(errorAsString(resp.body)), - action: SnackBarAction( - label: 'Dismiss', - onPressed: () { - scaffMgr.hideCurrentSnackBar(); - }, - ), - ); - - scaffMgr.hideCurrentSnackBar(); - scaffMgr.showSnackBar(snackBar); - } else { + }); + } + }, + onOK: (body) async { // authorize user await User( - username: _ctrUsername.text, - password: password, - server: server) - .toDisk(); + username: _ctrUsername.text, + password: password, + server: server) + .toDisk(); + }, + after: () { + setState(() { + showSpinner = false; + }); } - } catch (_) { - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - content: const Text('Network error'), - action: SnackBarAction( - label: 'Dismiss', - onPressed: () { - scaffMgr.hideCurrentSnackBar(); - }, - ), - ); - - scaffMgr.hideCurrentSnackBar(); - scaffMgr.showSnackBar(snackBar); - } - - setState(() { - showSpinner = false; - }); + ); }, label: Text(modeName), icon: const Icon(Icons.check), diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 350f38f..f0350cf 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -5,6 +5,7 @@ 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/user.dart'; +import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:provider/provider.dart'; import 'package:routemaster/routemaster.dart'; import '../backend/room.dart'; @@ -31,43 +32,38 @@ class _HomePageState extends State { }); } catch (_) {} }); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final sm = ScaffoldMessenger.of(context); // load cached rooms (() async { - try { - final newRooms = await Room.listRooms(); - setState(() { - rooms = newRooms; - }); - } catch (_) {} + try { + final newRooms = await Room.listRooms(); + setState(() { + rooms = newRooms; + }); + } catch (_) {} })(); - // fetch room list - (() async { - User user; - try { - user = await User.fromDisk(); - } catch (_) { - // probably not logged in - return; - } - - try { - final resp = await postWithCreadentials( - path: 'listRooms', - credentials: user, - target: user.server, - body: {}); - if (resp.res == Result.ok) { - final List list = resp.body['data'].map((json){ + doNetworkRequest(sm, + req: (user) => postWithCreadentials( + path: 'listRooms', + credentials: user!, + target: user.server, + body: {}), + onOK: (body) async { + final List list = body['data'].map((json) { return Room.fromJSON(json); }).toList(); - for (Room r in list) { - await r.toDisk(); - } + for (Room r in list) { + await r.toDisk(); } - } catch (_) {} - })(); + }); } @override @@ -105,16 +101,20 @@ class _HomePageState extends State { // show settings screen Routemaster.of(context).push("/settings"); }), - ...(context.watch() != null && - (context.watch()?.permissions)! & ServerPermission.allManagement != 0)?[ - MenuItemButton( - leadingIcon: const Icon(Icons.dns), - child: const Text('Server Dashboard'), - onPressed: () { - // show settings screen - Routemaster.of(context).push("/server"); - }), - ]:[], + ...(context.watch() != null && + (context.watch()?.permissions)! & + ServerPermission.allManagement != + 0) + ? [ + MenuItemButton( + leadingIcon: const Icon(Icons.dns), + child: const Text('Server Dashboard'), + onPressed: () { + // show settings screen + Routemaster.of(context).push("/server"); + }), + ] + : [], MenuItemButton( leadingIcon: const Icon(Icons.info_rounded), child: const Text('About'), diff --git a/lib/screens/room/join.dart b/lib/screens/room/join.dart index 59c8f53..d0689c3 100644 --- a/lib/screens/room/join.dart +++ b/lib/screens/room/join.dart @@ -4,6 +4,7 @@ import 'package:outbag_app/backend/errors.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:routemaster/routemaster.dart'; import 'dart:math'; @@ -17,45 +18,42 @@ class JoinRoomPage extends StatefulWidget { class _JoinRoomPageState extends State { List rooms = []; - void fetchData() async { - User user; - try { - user = await User.fromDisk(); - } catch (_) { - return; - } + @override + void didChangeDependencies() { + super.didChangeDependencies(); - try { - final resp = await postWithCreadentials( + final sm = ScaffoldMessenger.of(context); + + doNetworkRequest(null, + req: (user) => postWithCreadentials( path: 'listPublicRooms', - credentials: user, + credentials: user!, target: user.server, - body: {}); - if (resp.res == Result.ok) { + body: {}), + onOK: (body) async { // parse rooms - final list = resp.body['data']; + final list = body['data']; // try to fetch a list of rooms the user is a member of // use an empty blacklist when request is not successful - final blacklist = []; - try { - final resp = await postWithCreadentials( + final List blacklist = []; + doNetworkRequest(sm, + req: (user) => postWithCreadentials( path: 'listRooms', - credentials: user, + credentials: user!, target: user.server, - body: {}); - if (resp.res == Result.ok) { - final List list = resp.body['data'].map((json) { + body: {}), + onOK: (body) { + final List list = body['data'].map((json) { return Room.fromJSON(json); }).toList(); for (Room r in list) { blacklist.add(r); } - } - } catch (_) {} + }); // process the list of public rooms - List builder = []; + final List builder = []; processor: for (dynamic raw in list) { try { @@ -65,7 +63,7 @@ class _JoinRoomPageState extends State { // only add room to list, // if not on blacklist for (Room r in blacklist) { - if (room.serverTag == r.serverTag && room.id == r.id) { + if (r.compareTo(room) == 0) { // server on white list // move to next iteration on outer for loop continue processor; @@ -77,32 +75,22 @@ class _JoinRoomPageState extends State { // ignore room } } - builder.sort(); setState(() { rooms = builder; }); - } else { - throw Error(); - } - } catch (_) { - // network error - // unable to load room list - // NOTE: might want to show snackbar - // with warning - } + }); } @override void initState() { super.initState(); - fetchData(); } @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); double width = MediaQuery.of(context).size.width; double height = MediaQuery.of(context).size.height; @@ -133,241 +121,178 @@ class _JoinRoomPageState extends State { tooltip: "Refresh", onPressed: () { // fetch public rooms again - fetchData(); + didChangeDependencies(); }, ), MenuAnchor( - builder: (ctx, controller, child) { - return IconButton( - onPressed: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - icon: const Icon(Icons.more_vert), - ); - }, - menuChildren: [ - MenuItemButton( - leadingIcon: const Icon(Icons.drafts), - child: const Text('Join invite-only room'), - onPressed: () { - // show settings screen - Routemaster.of(context).push("/add-room/by-id"); - }), - ]) + builder: (ctx, controller, child) { + return IconButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + icon: const Icon(Icons.more_vert), + ); + }, + menuChildren: [ + MenuItemButton( + leadingIcon: const Icon(Icons.drafts), + child: const Text('Join invite-only room'), + onPressed: () { + // show settings screen + Routemaster.of(context).push("/add-room/by-id"); + }), + ]) ], ), body: rooms.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text('No new Rooms found', style: textTheme.titleLarge), - ], - )) - : ListView.builder( - itemCount: rooms.length, - itemBuilder: (ctx, i) { - final room = rooms[i]; - return Card( - margin: const EdgeInsets.all(8.0), - clipBehavior: Clip.antiAliasWithSaveLayer, - semanticContainer: true, - child: InkWell( - onTap: () { - // TODO: show modalBottomSheet - // with room information - // and join button - showModalBottomSheet( - context: ctx, - builder: (ctx) { - return BottomSheet( - onClosing: () {}, - builder: (ctx) { - return Column( - crossAxisAlignment: - CrossAxisAlignment.center, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(14), - child: Column(children: [ - // room icon - SvgPicture.asset( - (room.icon?.img)!, - width: smallest * 0.2, - height: smallest * 0.2, - ), - // room name - Text( - room.name, - style: textTheme.displayMedium, - ), - Text( - '${room.id}@${room.serverTag}', - style: textTheme.labelSmall, - ), - // description - Text(room.description, - style: textTheme.bodyLarge), - // visibility - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon(room.visibility?.icon), - Text((room - .visibility?.text)!), - ]), - ])), - // action buttons - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - // cancel button - Padding( - padding: - const EdgeInsets.all(14), - child: ElevatedButton.icon( - icon: - const Icon(Icons.close), - label: const Text('Cancel'), - onPressed: () { - // close sheet - Navigator.pop(context); - }, - )), - // join room button - Padding( - padding: - const EdgeInsets.all(14), - child: FilledButton.icon( - icon: - const Icon(Icons.check), - label: const Text('Join'), - onPressed: () async { - final scaffMgr = - ScaffoldMessenger.of( - context); - final rmaster = - Routemaster.of( - context); - final nav = - Navigator.of(context); + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('No new Rooms found', style: textTheme.titleLarge), + ], + )) + : ListView.builder( + itemCount: rooms.length, + itemBuilder: (ctx, i) { + final room = rooms[i]; + return Card( + margin: const EdgeInsets.all(8.0), + clipBehavior: Clip.antiAliasWithSaveLayer, + semanticContainer: true, + child: InkWell( + onTap: () { + // TODO: show modalBottomSheet + // with room information + // and join button + showModalBottomSheet( + context: ctx, + builder: (ctx) { + return BottomSheet( + onClosing: () {}, + builder: (ctx) { + return Column( + crossAxisAlignment: + CrossAxisAlignment.center, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(14), + child: Column(children: [ + // room icon + SvgPicture.asset( + (room.icon?.img)!, + width: smallest * 0.2, + height: smallest * 0.2, + ), + // room name + Text( + room.name, + style: textTheme.displayMedium, + ), + Text( + '${room.id}@${room.serverTag}', + style: textTheme.labelSmall, + ), + // description + Text(room.description, + style: textTheme.bodyLarge), + // visibility + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon(room.visibility?.icon), + Text((room + .visibility?.text)!), + ]), + ])), + // action buttons + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + // cancel button + Padding( + padding: + const EdgeInsets.all(14), + child: ElevatedButton.icon( + icon: + const Icon(Icons.close), + label: const Text('Cancel'), + onPressed: () { + // close sheet + Navigator.pop(context); + }, + )), + // join room button + Padding( + padding: + const EdgeInsets.all(14), + child: FilledButton.icon( + icon: + const Icon(Icons.check), + label: const Text('Join'), + onPressed: () async { + final scaffMgr = + ScaffoldMessenger.of( + context); + final rmaster = + Routemaster.of( + context); + final nav = + Navigator.of(context); - // join room & close screen - User user; - try { - user = await User - .fromDisk(); - } catch (_) { - // user data invalid - // NOTE: shouldn't happen - // because the main.dart watches the auth meta data - // and auto logs-out the user - return; - } - - try { - final resp = - await postWithCreadentials( - target: - user.server, - path: - 'joinPublicRoom', - body: { - 'room': - room.id, - 'server': room - .serverTag - }, - credentials: - user); - if (resp.res == - Result.ok) { - // successfully joined room - await room.toDisk(); - nav.pop(); - rmaster.replace( - '/r/${room.serverTag}/${room.id}'); - } else { - // server error - final snackBar = - SnackBar( - behavior: - SnackBarBehavior - .floating, - content: Text( - errorAsString( - resp.body)), - action: - SnackBarAction( - label: 'Dismiss', - onPressed: () { - scaffMgr - .hideCurrentSnackBar(); - }, - ), - ); - - scaffMgr - .hideCurrentSnackBar(); - scaffMgr.showSnackBar( - snackBar); - } - } catch (_) { - // network error - final snackBar = - SnackBar( - behavior: - SnackBarBehavior - .floating, - content: const Text( - 'Network error'), - action: - SnackBarAction( - label: 'Dismiss', - onPressed: () { - scaffMgr - .hideCurrentSnackBar(); - }, - ), - ); - - scaffMgr - .hideCurrentSnackBar(); - scaffMgr.showSnackBar( - snackBar); - } - }, - )) - ]) - ], - ); - }, - ); - }); + doNetworkRequest(scaffMgr, + req: (user) => + postWithCreadentials( + credentials: + user!, + target: user + .server, + path: + 'joinPublicRoom', + body: { + 'room': + room.id, + 'server': room + .serverTag + }), + onOK: (body) async { + await room.toDisk(); + nav.pop(); + rmaster.replace( + '/r/${room.serverTag}/${room.id}'); + }); + }, + )) + ]) + ], + ); + }, + ); + }); + }, + child: Container( + padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), + child: ListTile( + title: Text(room.name), + visualDensity: const VisualDensity(vertical: 3), + subtitle: Text(room.description), + leading: AspectRatio( + aspectRatio: 1 / 1, + child: SvgPicture.asset("${room.icon?.img}"), + ), + hoverColor: Colors.transparent, + )))); }, - child: Container( - padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), - child: ListTile( - title: Text(room.name), - visualDensity: const VisualDensity(vertical: 3), - subtitle: Text(room.description), - leading: AspectRatio( - aspectRatio: 1 / 1, - child: SvgPicture.asset("${room.icon?.img}"), - ), - hoverColor: Colors.transparent, - )))); - }, - ), + ), floatingActionButton: FloatingActionButton.extended( label: const Text('New'), icon: const Icon(Icons.add), diff --git a/lib/screens/room/main.dart b/lib/screens/room/main.dart index f6d1ee9..f542f9f 100644 --- a/lib/screens/room/main.dart +++ b/lib/screens/room/main.dart @@ -6,6 +6,7 @@ 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:routemaster/routemaster.dart'; class RoomPage extends StatefulWidget { @@ -27,83 +28,81 @@ class _RoomPageState extends State { // fetch room information void fetchInfo() async { - bool foundData = false; + final sm = ScaffoldMessenger.of(context); try { final diskRoom = - await Room.fromDisk(serverTag: widget.server, id: widget.tag); - foundData = true; + await Room.fromDisk(serverTag: widget.server, id: widget.tag); setState(() { - room = diskRoom; + room = diskRoom; }); } catch (_) {} - // fetch additional data from web - User user; - try { - user = await User.fromDisk(); - } catch (_) { - // probably not logged in - return; - } - - try { - final resp = await postWithCreadentials( - path: 'getRoomInfo', - credentials: user, - target: user.server, - body: {'room': widget.tag, 'server': widget.server}); - if (resp.res == Result.ok) { - final info = RoomInfo.fromJSON(resp.body['data']); - final room = Room.fromJSON(resp.body['data']); + doNetworkRequest(sm, + req: (user) => postWithCreadentials( + path: 'getRoomInfo', + credentials: user!, + target: (user.server), + body: {'room': widget.tag, 'server': widget.server}), + onOK: (body) async { + final info = RoomInfo.fromJSON(body['data']); + final room = Room.fromJSON(body['data']); room.toDisk(); - foundData = true; setState(() { - this.info = info; + this.info = info; }); - } - } catch (_) {} - - if (!foundData) { - // no room data available - // TODO: close room - // or show snackbar - // BUG: there is currently no way of implementing this, - // because there is no context available here - } + return true; + }, + onNetworkErr: () { + // user offline + if (room == null) { + // no room data available + // NOTE: close room? + } + return true; + }, + onServerErr: (json) { + // user no longer in room + // TODO: close room + return true; + }); } @override void initState() { super.initState(); - // schedule info-get - fetchInfo(); - _ctr.addListener(() { - setState(() { - page = _ctr.page?.toInt() ?? _ctr.initialPage; - }); + setState(() { + page = _ctr.page?.toInt() ?? _ctr.initialPage; + }); }); Room.listen((_) async { - // rooms changed on disk - // probably this one, - // because it is currently open - // NOTE: might be a different room - // (if a background listener is implemented at some point, - // checking if this room changed might improve performance) - try { - final r = await Room.fromDisk(serverTag: widget.server, id: widget.tag); - setState(() { - room = r; - }); - } catch (_) {} + // rooms changed on disk + // probably this one, + // because it is currently open + // NOTE: might be a different room + // (if a background listener is implemented at some point, + // checking if this room changed might improve performance) + try { + final r = await Room.fromDisk(serverTag: widget.server, id: widget.tag); + setState(() { + room = r; + }); + } catch (_) {} }); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // schedule info-get + fetchInfo(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -130,27 +129,27 @@ class _RoomPageState extends State { bottomNavigationBar: NavigationBar( onDestinationSelected: (int index) { _ctr.animateToPage(index, - curve: Curves.easeInOut, - duration: const Duration(milliseconds: 300)); + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 300)); }, selectedIndex: page, destinations: const [ NavigationDestination( - icon: Icon(Icons.list), - label: "List", - tooltip: 'View shopping list'), + icon: Icon(Icons.list), + label: "List", + tooltip: 'View shopping list'), NavigationDestination( - icon: Icon(Icons.inventory_2), - label: "Products", - tooltip: 'View saved items'), + icon: Icon(Icons.inventory_2), + label: "Products", + tooltip: 'View saved items'), NavigationDestination( - icon: Icon(Icons.category), - label: "Categories", - tooltip: 'View categories'), + icon: Icon(Icons.category), + label: "Categories", + tooltip: 'View categories'), NavigationDestination( - icon: Icon(Icons.info_rounded), - label: "About", - tooltip: 'View room info'), + icon: Icon(Icons.info_rounded), + label: "About", + tooltip: 'View room info'), ], ), ); diff --git a/lib/screens/room/new.dart b/lib/screens/room/new.dart index 6303cce..cae3a20 100644 --- a/lib/screens/room/new.dart +++ b/lib/screens/room/new.dart @@ -4,6 +4,8 @@ import 'package:outbag_app/backend/errors.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:outbag_app/tools/snackbar.dart'; import 'package:routemaster/routemaster.dart'; import 'dart:math'; @@ -175,118 +177,73 @@ class _NewRoomPageState extends State { ), ], )))), - floatingActionButton: FloatingActionButton.extended( - onPressed: () async { - final scaffMgr = ScaffoldMessenger.of(context); - final rmaster = Routemaster.of(context); + floatingActionButton: FloatingActionButton.extended( + onPressed: () async { + final scaffMgr = ScaffoldMessenger.of(context); + final rmaster = Routemaster.of(context); - // ID should be at least three characters long - if (_ctrID.text.length < 3) { - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - content: Text(_ctrID.text.isEmpty - ? 'Please specify a Room ID' - : 'Room ID has to be at least three characters long'), - action: SnackBarAction( - label: 'Ok', - onPressed: () { - scaffMgr.hideCurrentSnackBar(); - }, - ), - ); + // ID should be at least three characters long + if (_ctrID.text.length < 3) { + showSimpleSnackbar(scaffMgr, + text: _ctrID.text.isEmpty + ? 'Please specify a Room ID' + : 'Room ID has to be at least three characters long', + action: 'OK'); - scaffMgr.hideCurrentSnackBar(); - scaffMgr.showSnackBar(snackBar); + return; + } - return; - } + // name may not be empty + if (_ctrName.text.isEmpty) { + showSimpleSnackbar( + scaffMgr, + text: 'Please specify a room name', + action: 'OK' + ); - // name may not be empty - if (_ctrName.text.isEmpty) { - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - content: const Text('Please specify a room name'), - action: SnackBarAction( - label: 'Ok', - onPressed: () { - scaffMgr.hideCurrentSnackBar(); - }, - ), - ); + return; + } - scaffMgr.hideCurrentSnackBar(); - scaffMgr.showSnackBar(snackBar); + User user; + try { + user = await User.fromDisk(); + } catch (_) { + // user data invalid + // shouldn't happen + return; + } - return; - } + final room = Room( + id: _ctrID.text, + serverTag: user.server.tag, + name: _ctrName.text, + description: _ctrDescription.text, + icon: _ctrIcon, + visibility: _ctrVis); - User user; - try { - user = await User.fromDisk(); - } catch (_) { - // user data invalid - // shouldn't happen - return; - } - - final room = Room( - id: _ctrID.text, - serverTag: user.server.tag, - name: _ctrName.text, - description: _ctrDescription.text, - icon: _ctrIcon, - visibility: _ctrVis); - try { - final resp = await postWithCreadentials( - target: user.server, - credentials: user, - path: 'createRoom', - body: { - 'room': room.id, - 'title': room.name, - 'description': room.description, - 'icon': room.icon?.type, - 'visibility': room.visibility?.type - } - ); - if (resp.res == Result.ok) { - // room was created - // save room - await room.toDisk(); - // move to home page - rmaster.replace('/'); - } else { - // error - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - content: Text(errorAsString(resp.body)), - action: SnackBarAction( - label: 'Dismiss', - onPressed: () { - scaffMgr.hideCurrentSnackBar(); + doNetworkRequest(scaffMgr, + req: (_) => postWithCreadentials( + target: user.server, + credentials: user, + path: 'createRoom', + body: { + 'room': room.id, + 'title': room.name, + 'description': room.description, + 'icon': room.icon?.type, + 'visibility': room.visibility?.type + }), + onOK: (_) async { + // room was created + // save room + await room.toDisk(); + // move to home page + rmaster.replace('/'); + }, + // we manually fetch the user data above + // because we need the serverTag + needUser: false); }, - ), - ); - - scaffMgr.hideCurrentSnackBar(); - scaffMgr.showSnackBar(snackBar); - } - } catch (_) { - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - content: const Text('Network error'), - action: SnackBarAction( - label: 'Dismiss', - onPressed: () { - scaffMgr.hideCurrentSnackBar(); - }, - ), - ); - - scaffMgr.hideCurrentSnackBar(); - scaffMgr.showSnackBar(snackBar); - } - }, 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 d236d79..1dcb544 100644 --- a/lib/screens/room/pages/about.dart +++ b/lib/screens/room/pages/about.dart @@ -8,6 +8,7 @@ import 'dart:math'; import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/screens/room/edit.dart'; +import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:routemaster/routemaster.dart'; class AboutRoomPage extends StatefulWidget { @@ -109,71 +110,28 @@ class _AboutRoomPageState extends State { ScaffoldMessenger.of(context); final nav = Navigator.of(context); - User user; - try { - user = await User.fromDisk(); - } catch (_) { - // probably not logged in - nav.pop(); - return; - } - - try { - final resp = - await postWithCreadentials( - path: 'setVisibility', - target: user.server, - body: { - 'room': widget.room?.id, - 'server': (widget - .room?.serverTag)!, - 'visibility': vset.first - }, - credentials: user); - if (resp.res == Result.ok) { - Room r = widget.room!; - r.visibility = vis; - r.toDisk(); - } else { - // server error - final snackBar = SnackBar( - behavior: - SnackBarBehavior.floating, - content: Text( - errorAsString(resp.body)), - action: SnackBarAction( - label: 'Dismiss', - onPressed: () { - scaffMgr - .hideCurrentSnackBar(); - }, - ), - ); - - scaffMgr.hideCurrentSnackBar(); - scaffMgr.showSnackBar(snackBar); - } - } catch (_) { - // network error - final snackBar = SnackBar( - behavior: - SnackBarBehavior.floating, - content: - const Text('Network error'), - action: SnackBarAction( - label: 'Dismiss', - onPressed: () { - scaffMgr - .hideCurrentSnackBar(); - }, - ), - ); - - scaffMgr.hideCurrentSnackBar(); - scaffMgr.showSnackBar(snackBar); - } - - nav.pop(); + doNetworkRequest(scaffMgr, + req: (user) => + postWithCreadentials( + path: 'setVisibility', + target: (user?.server)!, + body: { + 'room': + widget.room?.id, + 'server': (widget.room + ?.serverTag)!, + 'visibility': + vset.first + }, + credentials: user!), + onOK: (_) { + Room r = widget.room!; + r.visibility = vis; + r.toDisk(); + }, + after: () { + nav.pop(); + }); }, child: const Text('Ok'), ) @@ -209,11 +167,10 @@ class _AboutRoomPageState extends State { onTap: () { // show edit room screen showDialog( - context: context, - builder: (context)=>Dialog.fullscreen( - child: EditRoomPage(widget.room!), - ) - ); + context: context, + builder: (context) => Dialog.fullscreen( + child: EditRoomPage(widget.room!), + )); }, ), ] @@ -284,28 +241,20 @@ class _AboutRoomPageState extends State { final nav = Navigator.of(ctx); final rmaster = Routemaster.of(ctx); - User user; - try { - user = await User.fromDisk(); - } catch (_) { - // probably not logged in - nav.pop(); - return; - } - - try { - final resp = await postWithCreadentials( - path: ((widget.info?.isOwner)!) - ? 'deleteRoom' - : 'leaveRoom', - target: user.server, - body: { - 'room': widget.room?.id, - 'server': - (widget.room?.serverTag)!, - }, - credentials: user); - if (resp.res == Result.ok) { + doNetworkRequest( + scaffMgr, + req: (user)=>postWithCreadentials( + path: ((widget.info?.isOwner)!) + ? 'deleteRoom' + : 'leaveRoom', + target: (user?.server)!, + body: { + 'room': widget.room?.id, + 'server': + (widget.room?.serverTag)!, + }, + credentials: user!), + onOK: (_) async { // try delete room from disk try { await widget.room?.removeDisk(); @@ -313,42 +262,12 @@ class _AboutRoomPageState extends State { // go back home rmaster.replace('/'); - } else { - // server error - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - content: - Text(errorAsString(resp.body)), - action: SnackBarAction( - label: 'Dismiss', - onPressed: () { - scaffMgr.hideCurrentSnackBar(); - }, - ), - ); - - scaffMgr.hideCurrentSnackBar(); - scaffMgr.showSnackBar(snackBar); + }, + after: () { + // close popup + nav.pop(); } - } catch (_) { - // network error - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - content: const Text('Network error'), - action: SnackBarAction( - label: 'Dismiss', - onPressed: () { - scaffMgr.hideCurrentSnackBar(); - }, - ), - ); - - scaffMgr.hideCurrentSnackBar(); - scaffMgr.showSnackBar(snackBar); - } - - // close popup - nav.pop(); + ); }, child: Text(((widget.info?.isOwner)!) ? 'Delete' diff --git a/lib/tools/fetch_wrapper.dart b/lib/tools/fetch_wrapper.dart new file mode 100644 index 0000000..69bc666 --- /dev/null +++ b/lib/tools/fetch_wrapper.dart @@ -0,0 +1,100 @@ +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, + bool Function()? onNetworkErr, + bool Function()? onUnknownErr, + bool Function()? onUserErr, + 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 (after != null) { + after(); + } + return; + } + } + + Response res; + try { + res = await req(user); + } catch (_) { + // network error + bool showBar = true; + + if (onNetworkErr != null) { + showBar = onNetworkErr(); + } + + if (showBar && sm != null) { + showSimpleSnackbar(sm, text: 'Network Error', action: 'Dismiss'); + } + + if (after != null) { + after(); + } + return; + } + + try { + if (res.res == Result.ok) { + if (onOK != null) { + await onOK(res.body); + } + } else { + // server error + bool showBar = true; + + if (onServerErr != null) { + showBar = onServerErr(res.body); + } + + if (showBar && sm != null) { + showSimpleSnackbar(sm, text: errorAsString(res.body), action: 'OK'); + } + } + } catch (e) { + bool showBar = true; + + print(e); + + if (onUnknownErr != null) { + showBar = onUnknownErr(); + } + + if (showBar && sm != null) { + showSimpleSnackbar(sm, text: 'Unknown Error', action: 'OK'); + } + } + + if (after != null) { + after(); + } +} diff --git a/lib/tools/snackbar.dart b/lib/tools/snackbar.dart new file mode 100644 index 0000000..f3ba750 --- /dev/null +++ b/lib/tools/snackbar.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +void showSimpleSnackbar(ScaffoldMessengerState scaffMgr, + {required String text, required String action, Function? onTap}) { + final snackBar = SnackBar( + behavior: SnackBarBehavior.floating, + content: Text(text), + action: SnackBarAction( + label: action, + onPressed: () { + scaffMgr.hideCurrentSnackBar(); + if (onTap != null) { + onTap(); + } + }, + ), + ); + + scaffMgr.hideCurrentSnackBar(); + scaffMgr.showSnackBar(snackBar); +}