Skip to content

Instantly share code, notes, and snippets.

@prabowomurti
Created June 24, 2020 09:55
Show Gist options
  • Save prabowomurti/67d8d3d3359a8d88980930cf1c780fc9 to your computer and use it in GitHub Desktop.
Save prabowomurti/67d8d3d3359a8d88980930cf1c780fc9 to your computer and use it in GitHub Desktop.
Input Screen
import 'package:retroapp/variables.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:retroapp/lounge_screen.dart';
import 'package:retroapp/sign_in.dart';
import 'package:retroapp/login_screen.dart';
final String setting = 'Setting';
final String signOut = 'Sign Out';
final String submit = 'Submit';
final Firestore _firestore = Firestore.instance;
const String hintTextAddItem = 'Speak now or forever hold your peace';
const String hintTextEditItem = 'Will delete if your input is empty';
/// Each items inside ListBox is related to its BoxType, init the value to an empty List
Map<BoxType, List<String>> items = {
BoxType.HAPPY: [],
BoxType.SAD: [],
BoxType.IDEA: [],
BoxType.MENTION: []
};
class InputScreen extends StatefulWidget {
static const String routeName = '/input_screen';
final Map event;
InputScreen({Key key, @required this.event}) : super(key: key);
@override
_InputScreenState createState() => _InputScreenState();
}
class _InputScreenState extends State<InputScreen> {
final List<Tab> myTabs = <Tab>[
Tab(icon: Icon(Icons.mood, color: Colors.green[800])),
Tab(icon: Icon(Icons.mood_bad, color: Colors.red[900])),
Tab(icon: Icon(Icons.lightbulb_outline, color: Colors.yellow)),
Tab(icon: Icon(Icons.alternate_email)),
];
Map event;
@override
void initState() {
super.initState();
// throw guest, only authenticated users can access this screen
if (loggedInUser.toString().isEmpty) {
signOutGoogle();
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) {
return LoginScreen();
}), ModalRoute.withName('/'));
}
// assign event from widget
this.event = widget.event;
// if user has already submitted the cards, redirect to LoungeScreen
Future<QuerySnapshot> submittedUsers = getSubmittedUsers();
submittedUsers
.then((submittedUsers) {
if (submittedUsers.documents.isNotEmpty) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LoungeScreen(event: this.event)));
}
})
.timeout(Duration(seconds: 120))
.catchError((err) {
print(err);
});
}
/// if this collection has a value, then the user has already submitted the cards (perhaps from different device)
Future<QuerySnapshot> getSubmittedUsers() async {
QuerySnapshot docRef = await _firestore
.collection('events/' + event['id'] + '/users')
.where('user_id', isEqualTo: loggedInUser.uid)
.getDocuments();
return docRef;
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: myTabs,
),
title: Text(event['name']),
actions: <Widget>[
IconButton(
icon: Icon(Icons.send),
onPressed: () {
buildShowDialog(context, event);
},
),
],
),
body: TabBarView(
children: <Widget>[
ListBox(boxType: BoxType.HAPPY),
ListBox(boxType: BoxType.SAD),
ListBox(boxType: BoxType.IDEA),
ListBox(boxType: BoxType.MENTION),
],
),
),
);
}
Future buildShowDialog(BuildContext context, Map event) {
// checking any empty card
// if (Foundation.kReleaseMode) {
for (var item in items.values) {
if (item.isEmpty) {
return showDialog(
context: context,
builder: (_) {
return AlertDialog(
content: Text('Please fill in all cards'),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('OK'),
)
],
);
},
);
}
}
// }
return showDialog(
context: context,
builder: (_) {
return AlertDialog(
content:
Text('Once submitted, cards can not be edited.\n\nContinue?'),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
onPressed: () {
Navigator.pop(context);
},
),
RaisedButton(
color: Colors.blueAccent,
child: Text('Continue'),
onPressed: () {
_submitCards(event, context);
},
),
],
);
},
barrierDismissible: true,
);
}
/// Submit all cards to firestore
void _submitCards(Map event, BuildContext context) {
// first, check if user has already submitted cards
Future<QuerySnapshot> submittedUsers = getSubmittedUsers();
submittedUsers
.then((submittedUser) {
if (submittedUser.documents.isNotEmpty) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LoungeScreen(event: this.event)));
}
// TODO : add a loading indicator (?)
// we should wait for the checking process to full complete before running the batch
batchProcess();
})
.timeout(Duration(seconds: 120))
.catchError((err) {
print(err);
});
}
//
void batchProcess() {
// start 'transaction'
WriteBatch batch = _firestore.batch();
// add user_id to users collection
batch.setData(
_firestore.collection('events/' + event['id'] + '/users').document(),
{
'user_id': loggedInUser.uid,
'partner_id': null,
'timestamp': Timestamp.now(),
},
);
// decrement the counter of `count_waiting_users`
batch.updateData(_firestore.collection('events').document(event['id']),
{'count_waiting_users': FieldValue.increment(-1)});
// preparing data for submision
items.forEach((key, item) {
for (var index = 0; index < item.length; index++) {
batch.setData(_firestore.collection('cards').document(), {
'detail': item[index],
'type': boxTypes[key].toString(),
'event_id': event['id'],
'position': index,
'user_id': loggedInUser.uid,
'timestamp': Timestamp.now(),
});
}
});
batch.commit().catchError((err) {
print(err);
}).whenComplete(() {
Navigator.pop(context); // will pop the dialog stack
// disable back button on LoungeScreen
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => LoungeScreen(event: event)));
});
}
}
class ListBox extends StatefulWidget {
final BoxType boxType;
ListBox({@required this.boxType});
@override
_ListBoxState createState() => _ListBoxState();
}
class _ListBoxState extends State<ListBox> {
bool isEditing = false;
int currentEditPosition;
IconData messageBarIcon = Icons.add;
String hintText = hintTextAddItem;
Map<BoxType, Color> colorThemes = {
BoxType.HAPPY: Colors.green[100],
BoxType.SAD: Colors.red[200],
BoxType.IDEA: Colors.yellow[100],
BoxType.MENTION: Colors.blueGrey[100]
};
final TextEditingController tfController = TextEditingController();
final ScrollController scController = ScrollController();
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: Container(
color: colorThemes[widget.boxType],
child: ListView.builder(
itemCount: items[widget.boxType].length,
itemBuilder: _buildCardItems,
controller: scController,
),
),
),
Container(
child: messageBar(context),
),
],
);
}
Widget _buildCardItems(BuildContext context, int position) {
// only create the delete button on the last item
if (items[widget.boxType].length - 1 != position)
return Card(
color: (position == currentEditPosition ? Colors.blue[100] : null),
child: ListTile(
title: Text(items[widget.boxType][position]),
onTap: () {
_editItem(items[widget.boxType], position);
},
),
);
return Card(
color: (position == currentEditPosition ? Colors.blue[100] : null),
child: ListTile(
onTap: () {
_editItem(items[widget.boxType], position);
},
title: Text(items[widget.boxType][position]),
// only provide delete button on last item
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.pink),
onPressed: () {
return showDialog(
context: context,
builder: (_) {
return AlertDialog(
content: Text('Are you sure to delete this item?'),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
onPressed: () {
Navigator.pop(context);
},
),
RaisedButton(
color: Colors.blueAccent,
child: Text('Delete'),
onPressed: () {
_removeItem();
Navigator.pop(context);
},
),
],
);
},
barrierDismissible: true,
);
},
),
),
);
}
Widget messageBar(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.0),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: Colors.lightBlueAccent, width: 2.0),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: TextField(
autofocus: true,
textAlignVertical: TextAlignVertical.center,
controller: tfController,
decoration: InputDecoration(
contentPadding: EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 14.0),
hintText: hintText,
border: InputBorder.none,
),
),
),
GestureDetector(
onTap: () {
if (isEditing)
_saveItem(currentEditPosition, tfController.text);
else
_addItem(tfController.text);
},
child: CircleAvatar(
child: Icon(
messageBarIcon,
),
),
)
],
),
);
}
// Add item to the cardItems
void _addItem(String text) {
if (text.trim().isEmpty) return;
setState(() {
items[widget.boxType].add(text);
tfController.clear();
scController.jumpTo(scController.position.maxScrollExtent +
80.0); // TODO: need to change the workaround
});
}
// Save item to the new value
void _saveItem(int position, String newText) {
if (newText.trim().isEmpty)
_removeItem(position);
else
items[widget.boxType][position] = newText;
setState(() {
_resetMode();
});
}
// Editing the item at position
void _editItem(List<String> cardItems, int position) {
// if the same card is tapped again, cancel editing
if (isEditing && position == currentEditPosition)
{
_resetMode();
setState(() {});
return;
}
isEditing = true;
currentEditPosition = position;
hintText = hintTextEditItem;
setState(() {
tfController.text = cardItems[position];
// TODO: find another workaround to put the cursor at the end of the selection
tfController.selection =
TextSelection.collapsed(offset: cardItems[position].length);
messageBarIcon = Icons.refresh;
});
}
// remove item at position. If position is not specified, remove the last one
void _removeItem([int position]) {
if (position == null)
items[widget.boxType].removeLast();
else
items[widget.boxType].removeAt(position);
_resetMode();
setState(() {});
}
// Reset all needed variables to init state
void _resetMode() {
isEditing = false;
currentEditPosition = null;
messageBarIcon = Icons.add;
tfController.clear();
hintText = hintTextAddItem;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment