import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; 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:routemaster/routemaster.dart'; import 'dart:math'; class JoinRoomPage extends StatefulWidget { const JoinRoomPage({super.key}); @override State createState() => _JoinRoomPageState(); } class _JoinRoomPageState extends State { List rooms = []; void fetchData() async { User user; try { user = await User.fromDisk(); } catch (_) { return; } try { final resp = await postWithCreadentials( path: 'listPublicRooms', credentials: user, target: user.server, body: {}); if (resp.res == Result.ok) { // parse rooms final list = resp.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( path: 'listRooms', credentials: user, target: user.server, body: {}); if (resp.res == Result.ok) { final List list = resp.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 = []; processor: for (dynamic raw in list) { try { final room = Room.fromJSON(raw); // figure out if room is on blacklist // only add room to list, // if not on blacklist for (Room r in blacklist) { if (room.serverTag == r.serverTag && room.id == r.id) { // server on white list // move to next iteration on outer for loop continue processor; } } builder.add(room); } catch (_) { // 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); double width = MediaQuery.of(context).size.width; double height = MediaQuery.of(context).size.height; double smallest = min(width, height); return Scaffold( appBar: AppBar( title: const Text('Join Room'), leading: IconButton( onPressed: () { // go back Routemaster.of(context).history.back(); }, icon: const Icon(Icons.arrow_back), tooltip: "Go back", ), actions: [ IconButton( icon: const Icon(Icons.search), tooltip: "Search", onPressed: () { // show searchbar // NOTE: location currently unknown }, ), IconButton( icon: const Icon(Icons.refresh), tooltip: "Refresh", onPressed: () { // fetch public rooms again fetchData(); }, ), 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"); }), ]) ], ), 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); // 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); } }, )) ]) ], ); }, ); }); }, 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), onPressed: () { // create new room Routemaster.of(context).push("/new-room"); }, tooltip: 'Create Room', ), ); } }