diff --git a/lib/backend/room.dart b/lib/backend/room.dart new file mode 100644 index 0000000..dd478f3 --- /dev/null +++ b/lib/backend/room.dart @@ -0,0 +1,255 @@ +import 'package:flutter/material.dart'; +import 'package:localstore/localstore.dart'; + +class RoomVisibility { + final int type; + const RoomVisibility(this.type); + + static RoomVisibility get private { + return const RoomVisibility(0); + } + + static RoomVisibility get local { + return const RoomVisibility(1); + } + + static RoomVisibility get public { + return const RoomVisibility(2); + } + + IconData get icon { + if (type == 2) { + return Icons.public; + } else if (type == 1) { + return Icons.home; + } + + return Icons.lock; + } + + String get text { + if (type == 2) { + return "Global"; + } else if (type == 1) { + return "Local"; + } + + return "Private"; + } + + static List list() { + return [ + RoomVisibility.private, + RoomVisibility.local, + RoomVisibility.public, + ]; + } +} + +class RoomIcon { + final String type; + RoomIcon({required this.type}); + + static RoomIcon get love { + return RoomIcon(type: "Love"); + } + + static RoomIcon get sports { + return RoomIcon(type: "Sports"); + } + + static RoomIcon get pets { + return RoomIcon(type: "Pets"); + } + + static RoomIcon get vacation { + return RoomIcon(type: "Vacation"); + } + + static RoomIcon get gifts { + return RoomIcon(type: "Gifts"); + } + + static RoomIcon get groceries { + return RoomIcon(type: "Groceries"); + } + + static RoomIcon get fashion { + return RoomIcon(type: "Fashion"); + } + + static RoomIcon get art { + return RoomIcon(type: "Art"); + } + + static RoomIcon get tech { + return RoomIcon(type: "Tech"); + } + + static RoomIcon get home { + return RoomIcon(type: "Home"); + } + + static RoomIcon get family { + return RoomIcon(type: "Family"); + } + + static RoomIcon get social { + return RoomIcon(type: "Social"); + } + + static RoomIcon get other { + return RoomIcon(type: "Other"); + } + + static List list() { + return [ + RoomIcon.love, + RoomIcon.sports, + RoomIcon.pets, + RoomIcon.vacation, + RoomIcon.gifts, + RoomIcon.groceries, + RoomIcon.fashion, + RoomIcon.art, + RoomIcon.tech, + RoomIcon.home, + RoomIcon.family, + RoomIcon.social, + RoomIcon.other, + ]; + } + + String get text { + switch (type.toLowerCase()) { + case 'love': + return 'Friends'; + case 'sports': + return 'Sports'; + case 'pets': + return 'Pets'; + case 'vacation': + return 'Vacation'; + case 'gifts': + return 'Gifts'; + case 'groceries': + return 'Groceries'; + case 'fashion': + return 'Clothing'; + case 'art': + return 'Arts & Crafts'; + case 'tech': + return 'Electronics'; + case 'home': + return 'Home supplies'; + case 'family': + return 'Family'; + case 'social': + return 'Social'; + case 'other': + default: + return 'Other'; + } + } + + // return image name + String get img { + switch (type.toLowerCase()) { + case 'love': + return 'undraw/undraw_couple.svg'; + case 'sports': + return 'undraw/undraw_greek_freak.svg'; + case 'pets': + return 'undraw/undraw_dog.svg'; + case 'vacation': + return 'undraw/undraw_trip.svg'; + case 'gifts': + return 'undraw/undraw_gifts.svg'; + case 'groceries': + return 'undraw/undraw_gone_shopping.svg'; + case 'fashion': + return 'undraw/undraw_jewelry.svg'; + case 'art': + return 'undraw/undraw_sculpting.svg'; + case 'tech': + return 'undraw/undraw_progressive_app.svg'; + case 'home': + return 'undraw/undraw_under_construction.svg'; + case 'family': + return 'undraw/undraw_family.svg'; + case 'social': + return 'undraw/undraw_pizza_sharing.svg'; + case 'other': + default: + return 'undraw/undraw_file_manager.svg'; + } + } +} + +class Room { + final String id; + final String serverTag; + final String name; + final String description; + RoomIcon? icon = RoomIcon.other; + RoomVisibility? visibility = RoomVisibility.private; + + Room( + {required this.id, + required this.serverTag, + this.name = "", + this.description = "", + this.icon, + this.visibility}); + + // get list of all known rooms + static Future> listRooms() async { + final db = Localstore.instance; + final rooms = (await db.collection('rooms').get())!; + List builder = []; + for (MapEntry entry in rooms.entries) { + try { + builder.add(Room.fromMap(entry.value)); + } catch (e) { + // skip invalid rooms + // NOTE: might want to autodelete them in the future + // although keeping them might be ok, + // in case we ever get a new dataset to fix the current state + } + } + return builder; + } + + // listen to room change + static listen(Function(Map) cb) async { + final db = Localstore.instance; + final stream = db.collection('rooms').stream; + stream.listen(cb); + } + + factory Room.fromMap(Map map) { + return Room( + id: map['id'], + serverTag: map['server'], + name: map['name'], + description: map['description']??'', + icon: RoomIcon(type: map['icon']??'Other'), + visibility: RoomVisibility(map['visibility']??0)); + } + + Map toMap() { + return { + 'id': id, + 'server': serverTag, + 'description': description, + 'name': name, + 'icon': icon?.type, + 'visibility': visibility?.type + }; + } + + Future toDisk() async { + final db = Localstore.instance; + await db.collection('rooms').doc('$id@$serverTag').set(toMap()); + } +} diff --git a/lib/screens/room/new.dart b/lib/screens/room/new.dart new file mode 100644 index 0000000..34a8891 --- /dev/null +++ b/lib/screens/room/new.dart @@ -0,0 +1,295 @@ +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 NewRoomPage extends StatefulWidget { + const NewRoomPage({super.key}); + + @override + State createState() => _NewRoomPageState(); +} + +class _NewRoomPageState extends State { + final TextEditingController _ctrID = TextEditingController(); + final TextEditingController _ctrName = TextEditingController(); + final TextEditingController _ctrDescription = TextEditingController(); + RoomVisibility _ctrVis = RoomVisibility.private; + RoomIcon _ctrIcon = RoomIcon.other; + + bool showSpinner = false; + + @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(min(width, height), 400); + + return showSpinner + ? Scaffold( + body: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + Text('Loading', style: textTheme.titleLarge), + ]))) + : Scaffold( + appBar: AppBar( + title: const Text('New Room'), + leading: IconButton( + onPressed: () { + // go back + Routemaster.of(context).history.back(); + }, + icon: const Icon(Icons.arrow_back), + tooltip: "Go back", + ), + ), + body: Center( + child: Flexible( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: SvgPicture.asset( + _ctrIcon.img, + width: smallest * 0.3, + height: smallest * 0.3, + ), + tooltip: 'Change room icon', + onPressed: () { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: + const Text('Choose a room Icon'), + actions: const [], + content: SizedBox( + width: smallest * 0.3 * 3, + height: smallest * 0.3 * 3, + child: GridView.count( + crossAxisCount: 3, + children: RoomIcon.list() + .map((icon) { + return GridTile( + child: IconButton( + icon: SvgPicture + .asset( + icon.img, + width: smallest * + 0.3, + height: smallest * + 0.3, + ), + tooltip: icon.text, + onPressed: () { + setState(() { + _ctrIcon = icon; + }); + Navigator.of( + context) + .pop(); + })); + }).toList())), + )); + }, + ), + Padding( + padding: const EdgeInsets.all(8), + child: TextField( + controller: _ctrID, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration( + prefixIcon: Icon(Icons.fact_check), + labelText: 'Room ID', + hintText: 'Unique room id', + helperText: + 'the room id and server tag allow the room to be identified', + border: OutlineInputBorder(), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: TextField( + controller: _ctrName, + keyboardType: TextInputType.name, + decoration: const InputDecoration( + prefixIcon: Icon(Icons.badge), + labelText: 'Room Name', + hintText: 'Give your room a name', + helperText: + 'Easily identify a room with a human readable name', + border: OutlineInputBorder(), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: TextField( + controller: _ctrDescription, + keyboardType: TextInputType.text, + decoration: const InputDecoration( + prefixIcon: Icon(Icons.dns), + labelText: 'Room Description', + hintText: 'Briefly describe your Room', + helperText: + 'Make it easier for other to know what this room is used for', + border: OutlineInputBorder(), + ), + ), + ), + Text('Visibility', style: textTheme.labelLarge), + Text('Specify who has access to your room', + style: textTheme.bodySmall), + SegmentedButton( + showSelectedIcon: true, + multiSelectionEnabled: false, + emptySelectionAllowed: false, + segments: RoomVisibility.list().map((vis) { + return ButtonSegment( + value: vis, + label: Text(vis.text), + icon: Icon(vis.icon)); + }).toList(), + onSelectionChanged: ((vset) { + setState(() { + _ctrVis = vset.single; + }); + }), + selected: {_ctrVis}, + selectedIcon: Icon(_ctrVis.icon), + ), + ], + )))), + 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(); + }, + ), + ); + + scaffMgr.hideCurrentSnackBar(); + scaffMgr.showSnackBar(snackBar); + + return; + } + + // 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(); + }, + ), + ); + + scaffMgr.hideCurrentSnackBar(); + scaffMgr.showSnackBar(snackBar); + + return; + } + + 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, + 'server': room.serverTag, + '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'), + icon: const Icon(Icons.add)), + ); + } +}