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:
parent
4bbdcaad4d
commit
cfd54e3bb5
2 changed files with 550 additions and 0 deletions
255
lib/backend/room.dart
Normal file
255
lib/backend/room.dart
Normal 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
295
lib/screens/room/new.dart
Normal 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)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue