Added room creation screen

If the room was created successfully,
the wizard automatically saves the room data on disk.
This allows it to be cached,
even if the user loses internet connection
right before the wizard exists.
That way the room will be cached.
This commit is contained in:
Jakob Meier 2023-03-18 20:28:30 +01:00
parent 4bbdcaad4d
commit cfd54e3bb5
No known key found for this signature in database
GPG key ID: 66BDC7E6A01A6152
2 changed files with 550 additions and 0 deletions

255
lib/backend/room.dart Normal file
View file

@ -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<RoomVisibility> 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<RoomIcon> 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<List<Room>> listRooms() async {
final db = Localstore.instance;
final rooms = (await db.collection('rooms').get())!;
List<Room> 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<String, dynamic>) cb) async {
final db = Localstore.instance;
final stream = db.collection('rooms').stream;
stream.listen(cb);
}
factory Room.fromMap(Map<String, dynamic> 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<String, dynamic> toMap() {
return {
'id': id,
'server': serverTag,
'description': description,
'name': name,
'icon': icon?.type,
'visibility': visibility?.type
};
}
Future<void> toDisk() async {
final db = Localstore.instance;
await db.collection('rooms').doc('$id@$serverTag').set(toMap());
}
}

295
lib/screens/room/new.dart Normal file
View file

@ -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<StatefulWidget> 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<RoomVisibility>(
showSelectedIcon: true,
multiSelectionEnabled: false,
emptySelectionAllowed: false,
segments: RoomVisibility.list().map((vis) {
return ButtonSegment<RoomVisibility>(
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)),
);
}
}