Simplified network requests & snackbars

and rewrote the component's network requests.

showSimpleSnackbar, allows displaying a simple snackbar,
with text and one action button, that can be clicked.

doNetworkRequest is supposed to be a wrapper for the
already existing post* functions.
It aims to make network requests and error handling easier,
by containing all the try&catch blocks
and being able to show snackbars.
This commit is contained in:
Jakob Meier 2023-03-23 14:48:53 +01:00
parent eabb6b93ae
commit 5d333522a5
No known key found for this signature in database
GPG key ID: 66BDC7E6A01A6152
11 changed files with 637 additions and 751 deletions

View file

@ -187,10 +187,10 @@ class RoomIcon {
} }
class Room { class Room {
final String id; String id;
final String serverTag; String serverTag;
final String name; String name;
final String description; String description;
RoomIcon? icon = RoomIcon.other; RoomIcon? icon = RoomIcon.other;
RoomVisibility? visibility = RoomVisibility.private; RoomVisibility? visibility = RoomVisibility.private;
@ -202,6 +202,17 @@ class Room {
this.icon, this.icon,
this.visibility}); 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 // get list of all known rooms
static Future<List<Room>> listRooms() async { static Future<List<Room>> listRooms() async {
final db = Localstore.instance; final db = Localstore.instance;

View file

@ -30,6 +30,12 @@ class User {
); );
} }
static Future<void> removeDisk() async {
final db = Localstore.instance;
await db.collection('meta').doc('auth').delete();
return;
}
static listen(Function(Map<String, dynamic>) cb) async { static listen(Function(Map<String, dynamic>) cb) async {
final db = Localstore.instance; final db = Localstore.instance;
final stream = db.collection('meta').stream; final stream = db.collection('meta').stream;
@ -55,12 +61,11 @@ class AccountMeta {
factory AccountMeta.fromJSON(dynamic json) { factory AccountMeta.fromJSON(dynamic json) {
return AccountMeta( return AccountMeta(
permissions: json['rights'], permissions: json['rights'],
username: json['name'], username: json['name'],
maxRoomSize: json['maxRoomSize'], maxRoomSize: json['maxRoomSize'],
maxRoomCount: json['maxRooms'], maxRoomCount: json['maxRooms'],
maxRoomMemberCount: json['maxUsersPerRoom'], maxRoomMemberCount: json['maxUsersPerRoom'],
discvoverable: json['viewable'] == 1 discvoverable: json['viewable'] == 1);
);
} }
} }

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/screens/room/join.dart'; import 'package:outbag_app/screens/room/join.dart';
import 'package:outbag_app/screens/room/new.dart'; import 'package:outbag_app/screens/room/new.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import './screens/home.dart'; import './screens/home.dart';
import './screens/welcome.dart'; import './screens/welcome.dart';
@ -58,47 +59,48 @@ class _OutbagAppState extends State {
// try to obtain user account information // try to obtain user account information
// with existing details // with existing details
// NOTE: also functions as a way to verify ther data // NOTE: also functions as a way to verify ther data
(() async { doNetworkRequest(
User credentials; null,
try { req: (user) => postWithCreadentials(
credentials = await User.fromDisk(); target: (user?.server)!,
} catch (_) { path: 'getMyAccount',
// invalid credentials credentials: user!,
// log out body: {}),
setState(() { onOK: (body) {
isAuthorized = false; final info = AccountMeta.fromJSON(body['data']);
}); setState(() {
return; isAuthorized = true;
} this.info = info;
try { });
final resp = await postWithCreadentials( },
target: credentials.server, onServerErr: (body) {
path: 'getMyAccount', // credentials are wrong
credentials: credentials, // log out
body: {});
if (resp.res == Result.ok) { setState(() {
final info = AccountMeta.fromJSON(resp.body['data']); isAuthorized = false;
setState(() { });
isAuthorized = true; return true;
this.info = info; },
}); onNetworkErr: () {
} else { // user is currently offline
// credentials are wrong // approve login,
// log out // until user goes back offline
setState(() { // NOTE TODO: check user data once online
isAuthorized = false; setState(() {
}); isAuthorized = true;
} });
} catch (_) { return true;
// user is currently offline },
// approve login, onUserErr: () {
// until user goes back offline // invalid credentials
// NOTE TODO: check user data once online // log out
setState(() { setState(() {
isAuthorized = true; isAuthorized = false;
}); });
} return true;
})(); }
);
// wait for user to be authorized // wait for user to be authorized
User.listen((data) async { User.listen((data) async {
@ -115,7 +117,9 @@ class _OutbagAppState extends State {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiProvider( return MultiProvider(
providers: [ providers: [
Provider<AccountMeta?>.value(value: info,), Provider<AccountMeta?>.value(
value: info,
),
], ],
child: MaterialApp.router( child: MaterialApp.router(
title: "Outbag", title: "Outbag",

View file

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:outbag_app/backend/request.dart'; import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/user.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:routemaster/routemaster.dart';
import '../backend/resolve_url.dart'; import '../backend/resolve_url.dart';
import '../backend/errors.dart'; import '../backend/errors.dart';
@ -173,6 +175,8 @@ class _AuthPageState extends State<AuthPage> {
showSpinner = true; showSpinner = true;
}); });
final scaffMgr = ScaffoldMessenger.of(context);
// verify that both passwords are the same // verify that both passwords are the same
if (widget.mode != Mode.signin) { if (widget.mode != Mode.signin) {
if (_ctrPassword.text != _ctrPasswordRpt.text) { if (_ctrPassword.text != _ctrPasswordRpt.text) {
@ -180,19 +184,8 @@ class _AuthPageState extends State<AuthPage> {
showSpinner = false; showSpinner = false;
}); });
final snackBar = SnackBar( showSimpleSnackbar(scaffMgr,
behavior: SnackBarBehavior.floating, text: 'Passwords do not match', action: 'Dismiss');
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);
_ctrPasswordRpt.clear(); _ctrPasswordRpt.clear();
return; return;
@ -205,28 +198,15 @@ class _AuthPageState extends State<AuthPage> {
showSpinner = false; showSpinner = false;
}); });
final snackBar = SnackBar( showSimpleSnackbar(scaffMgr,
behavior: SnackBarBehavior.floating, text: 'Password has to be at least 6 characters longs',
content: const Text( action: 'Dismiss');
'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);
_ctrPasswordRpt.clear(); _ctrPasswordRpt.clear();
return; return;
} }
final scaffMgr = ScaffoldMessenger.of(context); // resolve homeserver url
// TODO: resolve homeserver url
OutbagServer server; OutbagServer server;
try { try {
server = await getOutbagServerUrl(_ctrServer.text); server = await getOutbagServerUrl(_ctrServer.text);
@ -238,50 +218,43 @@ class _AuthPageState extends State<AuthPage> {
showSpinner = false; showSpinner = false;
}); });
final snackBar = SnackBar( showSimpleSnackbar(scaffMgr,
behavior: SnackBarBehavior.floating, text:
content: Text( 'Unable to find valid outbag server on ${_ctrServer.text}',
'Unable to find valid outbag server on ${_ctrServer.text}'), action: 'Dismiss');
action: SnackBarAction(
label: 'Dismiss',
onPressed: () {
scaffMgr.hideCurrentSnackBar();
},
),
);
scaffMgr.hideCurrentSnackBar();
scaffMgr.showSnackBar(snackBar);
return; return;
} }
// hash password
var bytes = utf8.encode(_ctrPassword.text); var bytes = utf8.encode(_ctrPassword.text);
final password = sha256.convert(bytes).toString(); final password = sha256.convert(bytes).toString();
try {
Response resp; doNetworkRequest(
// validate account scaffMgr,
if (widget.mode == Mode.signin) { needUser: false,
resp = await postUnauthorized( req: (_) {
if (widget.mode == Mode.signin) {
return postUnauthorized(
target: server, target: server,
path: 'signin', path: 'signin',
body: { body: {
'name': _ctrUsername.text, 'name': _ctrUsername.text,
'server': server.tag, 'server': server.tag,
'accountKey': password 'accountKey': password
}); });
} else if (widget.mode == Mode.signup) { } else if (widget.mode == Mode.signup) {
resp = await postUnauthorized( return postUnauthorized(
target: server, target: server,
path: 'signup', path: 'signup',
body: { body: {
'name': _ctrUsername.text, 'name': _ctrUsername.text,
'server': server.tag, 'server': server.tag,
'accountKey': password 'accountKey': password
}); });
} else { } else {
// signup OTA // signup OTA
resp = await postUnauthorized( return postUnauthorized(
target: server, target: server,
path: 'signupOTA', path: 'signupOTA',
body: { body: {
@ -289,51 +262,23 @@ class _AuthPageState extends State<AuthPage> {
'server': server.tag, 'server': server.tag,
'accountKey': password, 'accountKey': password,
'OTA': _ctrOTA.text 'OTA': _ctrOTA.text
}); });
} }
},
if (resp.res == Result.err) { onOK: (body) async {
// 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 {
// authorize user // authorize user
await User( await User(
username: _ctrUsername.text, username: _ctrUsername.text,
password: password, password: password,
server: server) server: server)
.toDisk(); .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), label: Text(modeName),
icon: const Icon(Icons.check), icon: const Icon(Icons.check),

View file

@ -5,6 +5,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:outbag_app/backend/permissions.dart'; import 'package:outbag_app/backend/permissions.dart';
import 'package:outbag_app/backend/request.dart'; import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:routemaster/routemaster.dart'; import 'package:routemaster/routemaster.dart';
import '../backend/room.dart'; import '../backend/room.dart';
@ -31,43 +32,38 @@ class _HomePageState extends State<HomePage> {
}); });
} catch (_) {} } catch (_) {}
}); });
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final sm = ScaffoldMessenger.of(context);
// load cached rooms // load cached rooms
(() async { (() async {
try { try {
final newRooms = await Room.listRooms(); final newRooms = await Room.listRooms();
setState(() { setState(() {
rooms = newRooms; rooms = newRooms;
}); });
} catch (_) {} } catch (_) {}
})(); })();
// fetch room list doNetworkRequest(sm,
(() async { req: (user) => postWithCreadentials(
User user; path: 'listRooms',
try { credentials: user!,
user = await User.fromDisk(); target: user.server,
} catch (_) { body: {}),
// probably not logged in onOK: (body) async {
return; final List<Room> list = body['data'].map<Room>((json) {
}
try {
final resp = await postWithCreadentials(
path: 'listRooms',
credentials: user,
target: user.server,
body: {});
if (resp.res == Result.ok) {
final List<Room> list = resp.body['data'].map<Room>((json){
return Room.fromJSON(json); return Room.fromJSON(json);
}).toList(); }).toList();
for (Room r in list) { for (Room r in list) {
await r.toDisk(); await r.toDisk();
}
} }
} catch (_) {} });
})();
} }
@override @override
@ -105,16 +101,20 @@ class _HomePageState extends State<HomePage> {
// show settings screen // show settings screen
Routemaster.of(context).push("/settings"); Routemaster.of(context).push("/settings");
}), }),
...(context.watch<AccountMeta?>() != null && ...(context.watch<AccountMeta?>() != null &&
(context.watch<AccountMeta?>()?.permissions)! & ServerPermission.allManagement != 0)?[ (context.watch<AccountMeta?>()?.permissions)! &
MenuItemButton( ServerPermission.allManagement !=
leadingIcon: const Icon(Icons.dns), 0)
child: const Text('Server Dashboard'), ? [
onPressed: () { MenuItemButton(
// show settings screen leadingIcon: const Icon(Icons.dns),
Routemaster.of(context).push("/server"); child: const Text('Server Dashboard'),
}), onPressed: () {
]:[], // show settings screen
Routemaster.of(context).push("/server");
}),
]
: [],
MenuItemButton( MenuItemButton(
leadingIcon: const Icon(Icons.info_rounded), leadingIcon: const Icon(Icons.info_rounded),
child: const Text('About'), child: const Text('About'),

View file

@ -4,6 +4,7 @@ import 'package:outbag_app/backend/errors.dart';
import 'package:outbag_app/backend/request.dart'; import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/room.dart'; import 'package:outbag_app/backend/room.dart';
import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:routemaster/routemaster.dart'; import 'package:routemaster/routemaster.dart';
import 'dart:math'; import 'dart:math';
@ -17,45 +18,42 @@ class JoinRoomPage extends StatefulWidget {
class _JoinRoomPageState extends State { class _JoinRoomPageState extends State {
List<Room> rooms = []; List<Room> rooms = [];
void fetchData() async { @override
User user; void didChangeDependencies() {
try { super.didChangeDependencies();
user = await User.fromDisk();
} catch (_) {
return;
}
try { final sm = ScaffoldMessenger.of(context);
final resp = await postWithCreadentials(
doNetworkRequest(null,
req: (user) => postWithCreadentials(
path: 'listPublicRooms', path: 'listPublicRooms',
credentials: user, credentials: user!,
target: user.server, target: user.server,
body: {}); body: {}),
if (resp.res == Result.ok) { onOK: (body) async {
// parse rooms // parse rooms
final list = resp.body['data']; final list = body['data'];
// try to fetch a list of rooms the user is a member of // try to fetch a list of rooms the user is a member of
// use an empty blacklist when request is not successful // use an empty blacklist when request is not successful
final blacklist = []; final List<Room> blacklist = [];
try { doNetworkRequest(sm,
final resp = await postWithCreadentials( req: (user) => postWithCreadentials(
path: 'listRooms', path: 'listRooms',
credentials: user, credentials: user!,
target: user.server, target: user.server,
body: {}); body: {}),
if (resp.res == Result.ok) { onOK: (body) {
final List<Room> list = resp.body['data'].map<Room>((json) { final List<Room> list = body['data'].map<Room>((json) {
return Room.fromJSON(json); return Room.fromJSON(json);
}).toList(); }).toList();
for (Room r in list) { for (Room r in list) {
blacklist.add(r); blacklist.add(r);
} }
} });
} catch (_) {}
// process the list of public rooms // process the list of public rooms
List<Room> builder = []; final List<Room> builder = [];
processor: processor:
for (dynamic raw in list) { for (dynamic raw in list) {
try { try {
@ -65,7 +63,7 @@ class _JoinRoomPageState extends State {
// only add room to list, // only add room to list,
// if not on blacklist // if not on blacklist
for (Room r in blacklist) { for (Room r in blacklist) {
if (room.serverTag == r.serverTag && room.id == r.id) { if (r.compareTo(room) == 0) {
// server on white list // server on white list
// move to next iteration on outer for loop // move to next iteration on outer for loop
continue processor; continue processor;
@ -77,32 +75,22 @@ class _JoinRoomPageState extends State {
// ignore room // ignore room
} }
} }
builder.sort();
setState(() { setState(() {
rooms = builder; rooms = builder;
}); });
} else { });
throw Error();
}
} catch (_) {
// network error
// unable to load room list
// NOTE: might want to show snackbar
// with warning
}
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
fetchData();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textTheme = Theme.of(context) final textTheme = Theme.of(context)
.textTheme .textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface); .apply(displayColor: Theme.of(context).colorScheme.onSurface);
double width = MediaQuery.of(context).size.width; double width = MediaQuery.of(context).size.width;
double height = MediaQuery.of(context).size.height; double height = MediaQuery.of(context).size.height;
@ -133,241 +121,178 @@ class _JoinRoomPageState extends State {
tooltip: "Refresh", tooltip: "Refresh",
onPressed: () { onPressed: () {
// fetch public rooms again // fetch public rooms again
fetchData(); didChangeDependencies();
}, },
), ),
MenuAnchor( MenuAnchor(
builder: (ctx, controller, child) { builder: (ctx, controller, child) {
return IconButton( return IconButton(
onPressed: () { onPressed: () {
if (controller.isOpen) { if (controller.isOpen) {
controller.close(); controller.close();
} else { } else {
controller.open(); controller.open();
} }
}, },
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
); );
}, },
menuChildren: [ menuChildren: [
MenuItemButton( MenuItemButton(
leadingIcon: const Icon(Icons.drafts), leadingIcon: const Icon(Icons.drafts),
child: const Text('Join invite-only room'), child: const Text('Join invite-only room'),
onPressed: () { onPressed: () {
// show settings screen // show settings screen
Routemaster.of(context).push("/add-room/by-id"); Routemaster.of(context).push("/add-room/by-id");
}), }),
]) ])
], ],
), ),
body: rooms.isEmpty body: rooms.isEmpty
? Center( ? Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text('No new Rooms found', style: textTheme.titleLarge), Text('No new Rooms found', style: textTheme.titleLarge),
], ],
)) ))
: ListView.builder( : ListView.builder(
itemCount: rooms.length, itemCount: rooms.length,
itemBuilder: (ctx, i) { itemBuilder: (ctx, i) {
final room = rooms[i]; final room = rooms[i];
return Card( return Card(
margin: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0),
clipBehavior: Clip.antiAliasWithSaveLayer, clipBehavior: Clip.antiAliasWithSaveLayer,
semanticContainer: true, semanticContainer: true,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
// TODO: show modalBottomSheet // TODO: show modalBottomSheet
// with room information // with room information
// and join button // and join button
showModalBottomSheet( showModalBottomSheet(
context: ctx, context: ctx,
builder: (ctx) { builder: (ctx) {
return BottomSheet( return BottomSheet(
onClosing: () {}, onClosing: () {},
builder: (ctx) { builder: (ctx) {
return Column( return Column(
crossAxisAlignment: crossAxisAlignment:
CrossAxisAlignment.center, CrossAxisAlignment.center,
mainAxisAlignment: mainAxisAlignment:
MainAxisAlignment.center, MainAxisAlignment.center,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
child: Column(children: [ child: Column(children: [
// room icon // room icon
SvgPicture.asset( SvgPicture.asset(
(room.icon?.img)!, (room.icon?.img)!,
width: smallest * 0.2, width: smallest * 0.2,
height: smallest * 0.2, height: smallest * 0.2,
), ),
// room name // room name
Text( Text(
room.name, room.name,
style: textTheme.displayMedium, style: textTheme.displayMedium,
), ),
Text( Text(
'${room.id}@${room.serverTag}', '${room.id}@${room.serverTag}',
style: textTheme.labelSmall, style: textTheme.labelSmall,
), ),
// description // description
Text(room.description, Text(room.description,
style: textTheme.bodyLarge), style: textTheme.bodyLarge),
// visibility // visibility
Row( Row(
mainAxisAlignment: mainAxisAlignment:
MainAxisAlignment.center, MainAxisAlignment.center,
children: [ children: [
Icon(room.visibility?.icon), Icon(room.visibility?.icon),
Text((room Text((room
.visibility?.text)!), .visibility?.text)!),
]), ]),
])), ])),
// action buttons // action buttons
Row( Row(
mainAxisAlignment: mainAxisAlignment:
MainAxisAlignment.center, MainAxisAlignment.center,
children: [ children: [
// cancel button // cancel button
Padding( Padding(
padding: padding:
const EdgeInsets.all(14), const EdgeInsets.all(14),
child: ElevatedButton.icon( child: ElevatedButton.icon(
icon: icon:
const Icon(Icons.close), const Icon(Icons.close),
label: const Text('Cancel'), label: const Text('Cancel'),
onPressed: () { onPressed: () {
// close sheet // close sheet
Navigator.pop(context); Navigator.pop(context);
}, },
)), )),
// join room button // join room button
Padding( Padding(
padding: padding:
const EdgeInsets.all(14), const EdgeInsets.all(14),
child: FilledButton.icon( child: FilledButton.icon(
icon: icon:
const Icon(Icons.check), const Icon(Icons.check),
label: const Text('Join'), label: const Text('Join'),
onPressed: () async { onPressed: () async {
final scaffMgr = final scaffMgr =
ScaffoldMessenger.of( ScaffoldMessenger.of(
context); context);
final rmaster = final rmaster =
Routemaster.of( Routemaster.of(
context); context);
final nav = final nav =
Navigator.of(context); Navigator.of(context);
// join room & close screen doNetworkRequest(scaffMgr,
User user; req: (user) =>
try { postWithCreadentials(
user = await User credentials:
.fromDisk(); user!,
} catch (_) { target: user
// user data invalid .server,
// NOTE: shouldn't happen path:
// because the main.dart watches the auth meta data 'joinPublicRoom',
// and auto logs-out the user body: {
return; 'room':
} room.id,
'server': room
try { .serverTag
final resp = }),
await postWithCreadentials( onOK: (body) async {
target: await room.toDisk();
user.server, nav.pop();
path: rmaster.replace(
'joinPublicRoom', '/r/${room.serverTag}/${room.id}');
body: { });
'room': },
room.id, ))
'server': room ])
.serverTag ],
}, );
credentials: },
user); );
if (resp.res == });
Result.ok) { },
// successfully joined room child: Container(
await room.toDisk(); padding: const EdgeInsets.fromLTRB(10, 5, 5, 10),
nav.pop(); child: ListTile(
rmaster.replace( title: Text(room.name),
'/r/${room.serverTag}/${room.id}'); visualDensity: const VisualDensity(vertical: 3),
} else { subtitle: Text(room.description),
// server error leading: AspectRatio(
final snackBar = aspectRatio: 1 / 1,
SnackBar( child: SvgPicture.asset("${room.icon?.img}"),
behavior: ),
SnackBarBehavior hoverColor: Colors.transparent,
.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);
}
},
))
])
],
);
},
);
});
}, },
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( floatingActionButton: FloatingActionButton.extended(
label: const Text('New'), label: const Text('New'),
icon: const Icon(Icons.add), icon: const Icon(Icons.add),

View file

@ -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/categories.dart';
import 'package:outbag_app/screens/room/pages/products.dart'; import 'package:outbag_app/screens/room/pages/products.dart';
import 'package:outbag_app/screens/room/pages/list.dart'; import 'package:outbag_app/screens/room/pages/list.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:routemaster/routemaster.dart'; import 'package:routemaster/routemaster.dart';
class RoomPage extends StatefulWidget { class RoomPage extends StatefulWidget {
@ -27,83 +28,81 @@ class _RoomPageState extends State<RoomPage> {
// fetch room information // fetch room information
void fetchInfo() async { void fetchInfo() async {
bool foundData = false; final sm = ScaffoldMessenger.of(context);
try { try {
final diskRoom = final diskRoom =
await Room.fromDisk(serverTag: widget.server, id: widget.tag); await Room.fromDisk(serverTag: widget.server, id: widget.tag);
foundData = true;
setState(() { setState(() {
room = diskRoom; room = diskRoom;
}); });
} catch (_) {} } catch (_) {}
// fetch additional data from web doNetworkRequest(sm,
User user; req: (user) => postWithCreadentials(
try { path: 'getRoomInfo',
user = await User.fromDisk(); credentials: user!,
} catch (_) { target: (user.server),
// probably not logged in body: {'room': widget.tag, 'server': widget.server}),
return; onOK: (body) async {
} final info = RoomInfo.fromJSON(body['data']);
final room = Room.fromJSON(body['data']);
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']);
room.toDisk(); room.toDisk();
foundData = true;
setState(() { setState(() {
this.info = info; this.info = info;
}); });
} return true;
} catch (_) {} },
onNetworkErr: () {
if (!foundData) { // user offline
// no room data available if (room == null) {
// TODO: close room // no room data available
// or show snackbar // NOTE: close room?
// BUG: there is currently no way of implementing this, }
// because there is no context available here return true;
} },
onServerErr: (json) {
// user no longer in room
// TODO: close room
return true;
});
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// schedule info-get
fetchInfo();
_ctr.addListener(() { _ctr.addListener(() {
setState(() { setState(() {
page = _ctr.page?.toInt() ?? _ctr.initialPage; page = _ctr.page?.toInt() ?? _ctr.initialPage;
}); });
}); });
Room.listen((_) async { Room.listen((_) async {
// rooms changed on disk // rooms changed on disk
// probably this one, // probably this one,
// because it is currently open // because it is currently open
// NOTE: might be a different room // NOTE: might be a different room
// (if a background listener is implemented at some point, // (if a background listener is implemented at some point,
// checking if this room changed might improve performance) // checking if this room changed might improve performance)
try { try {
final r = await Room.fromDisk(serverTag: widget.server, id: widget.tag); final r = await Room.fromDisk(serverTag: widget.server, id: widget.tag);
setState(() { setState(() {
room = r; room = r;
}); });
} catch (_) {} } catch (_) {}
}); });
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
// schedule info-get
fetchInfo();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -130,27 +129,27 @@ class _RoomPageState extends State<RoomPage> {
bottomNavigationBar: NavigationBar( bottomNavigationBar: NavigationBar(
onDestinationSelected: (int index) { onDestinationSelected: (int index) {
_ctr.animateToPage(index, _ctr.animateToPage(index,
curve: Curves.easeInOut, curve: Curves.easeInOut,
duration: const Duration(milliseconds: 300)); duration: const Duration(milliseconds: 300));
}, },
selectedIndex: page, selectedIndex: page,
destinations: const [ destinations: const [
NavigationDestination( NavigationDestination(
icon: Icon(Icons.list), icon: Icon(Icons.list),
label: "List", label: "List",
tooltip: 'View shopping list'), tooltip: 'View shopping list'),
NavigationDestination( NavigationDestination(
icon: Icon(Icons.inventory_2), icon: Icon(Icons.inventory_2),
label: "Products", label: "Products",
tooltip: 'View saved items'), tooltip: 'View saved items'),
NavigationDestination( NavigationDestination(
icon: Icon(Icons.category), icon: Icon(Icons.category),
label: "Categories", label: "Categories",
tooltip: 'View categories'), tooltip: 'View categories'),
NavigationDestination( NavigationDestination(
icon: Icon(Icons.info_rounded), icon: Icon(Icons.info_rounded),
label: "About", label: "About",
tooltip: 'View room info'), tooltip: 'View room info'),
], ],
), ),
); );

View file

@ -4,6 +4,8 @@ import 'package:outbag_app/backend/errors.dart';
import 'package:outbag_app/backend/request.dart'; import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/room.dart'; import 'package:outbag_app/backend/room.dart';
import 'package:outbag_app/backend/user.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:routemaster/routemaster.dart';
import 'dart:math'; import 'dart:math';
@ -175,118 +177,73 @@ class _NewRoomPageState extends State {
), ),
], ],
)))), )))),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
onPressed: () async { onPressed: () async {
final scaffMgr = ScaffoldMessenger.of(context); final scaffMgr = ScaffoldMessenger.of(context);
final rmaster = Routemaster.of(context); final rmaster = Routemaster.of(context);
// ID should be at least three characters long // ID should be at least three characters long
if (_ctrID.text.length < 3) { if (_ctrID.text.length < 3) {
final snackBar = SnackBar( showSimpleSnackbar(scaffMgr,
behavior: SnackBarBehavior.floating, text: _ctrID.text.isEmpty
content: Text(_ctrID.text.isEmpty ? 'Please specify a Room ID'
? 'Please specify a Room ID' : 'Room ID has to be at least three characters long',
: 'Room ID has to be at least three characters long'), action: 'OK');
action: SnackBarAction(
label: 'Ok',
onPressed: () {
scaffMgr.hideCurrentSnackBar();
},
),
);
scaffMgr.hideCurrentSnackBar(); return;
scaffMgr.showSnackBar(snackBar); }
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 return;
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();
},
),
);
scaffMgr.hideCurrentSnackBar(); User user;
scaffMgr.showSnackBar(snackBar); 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; doNetworkRequest(scaffMgr,
try { req: (_) => postWithCreadentials(
user = await User.fromDisk(); target: user.server,
} catch (_) { credentials: user,
// user data invalid path: 'createRoom',
// shouldn't happen body: {
return; 'room': room.id,
} 'title': room.name,
'description': room.description,
final room = Room( 'icon': room.icon?.type,
id: _ctrID.text, 'visibility': room.visibility?.type
serverTag: user.server.tag, }),
name: _ctrName.text, onOK: (_) async {
description: _ctrDescription.text, // room was created
icon: _ctrIcon, // save room
visibility: _ctrVis); await room.toDisk();
try { // move to home page
final resp = await postWithCreadentials( rmaster.replace('/');
target: user.server, },
credentials: user, // we manually fetch the user data above
path: 'createRoom', // because we need the serverTag
body: { needUser: false);
'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();
}, },
),
);
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'), label: const Text('Create'),
icon: const Icon(Icons.add)), icon: const Icon(Icons.add)),
); );

View file

@ -8,6 +8,7 @@ import 'dart:math';
import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/screens/room/edit.dart'; import 'package:outbag_app/screens/room/edit.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:routemaster/routemaster.dart'; import 'package:routemaster/routemaster.dart';
class AboutRoomPage extends StatefulWidget { class AboutRoomPage extends StatefulWidget {
@ -109,71 +110,28 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
ScaffoldMessenger.of(context); ScaffoldMessenger.of(context);
final nav = Navigator.of(context); final nav = Navigator.of(context);
User user; doNetworkRequest(scaffMgr,
try { req: (user) =>
user = await User.fromDisk(); postWithCreadentials(
} catch (_) { path: 'setVisibility',
// probably not logged in target: (user?.server)!,
nav.pop(); body: {
return; 'room':
} widget.room?.id,
'server': (widget.room
try { ?.serverTag)!,
final resp = 'visibility':
await postWithCreadentials( vset.first
path: 'setVisibility', },
target: user.server, credentials: user!),
body: { onOK: (_) {
'room': widget.room?.id, Room r = widget.room!;
'server': (widget r.visibility = vis;
.room?.serverTag)!, r.toDisk();
'visibility': vset.first },
}, after: () {
credentials: user); nav.pop();
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();
}, },
child: const Text('Ok'), child: const Text('Ok'),
) )
@ -209,11 +167,10 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
onTap: () { onTap: () {
// show edit room screen // show edit room screen
showDialog( showDialog(
context: context, context: context,
builder: (context)=>Dialog.fullscreen( builder: (context) => Dialog.fullscreen(
child: EditRoomPage(widget.room!), child: EditRoomPage(widget.room!),
) ));
);
}, },
), ),
] ]
@ -284,28 +241,20 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
final nav = Navigator.of(ctx); final nav = Navigator.of(ctx);
final rmaster = Routemaster.of(ctx); final rmaster = Routemaster.of(ctx);
User user; doNetworkRequest(
try { scaffMgr,
user = await User.fromDisk(); req: (user)=>postWithCreadentials(
} catch (_) { path: ((widget.info?.isOwner)!)
// probably not logged in ? 'deleteRoom'
nav.pop(); : 'leaveRoom',
return; target: (user?.server)!,
} body: {
'room': widget.room?.id,
try { 'server':
final resp = await postWithCreadentials( (widget.room?.serverTag)!,
path: ((widget.info?.isOwner)!) },
? 'deleteRoom' credentials: user!),
: 'leaveRoom', onOK: (_) async {
target: user.server,
body: {
'room': widget.room?.id,
'server':
(widget.room?.serverTag)!,
},
credentials: user);
if (resp.res == Result.ok) {
// try delete room from disk // try delete room from disk
try { try {
await widget.room?.removeDisk(); await widget.room?.removeDisk();
@ -313,42 +262,12 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
// go back home // go back home
rmaster.replace('/'); rmaster.replace('/');
} else { },
// server error after: () {
final snackBar = SnackBar( // close popup
behavior: SnackBarBehavior.floating, nav.pop();
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);
}
// close popup
nav.pop();
}, },
child: Text(((widget.info?.isOwner)!) child: Text(((widget.info?.isOwner)!)
? 'Delete' ? 'Delete'

View file

@ -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<Response> Function(User?) req,
Function(Map<String, dynamic>)? onOK,
bool Function()? onNetworkErr,
bool Function()? onUnknownErr,
bool Function()? onUserErr,
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 (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();
}
}

21
lib/tools/snackbar.dart Normal file
View file

@ -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);
}