Finished the new event screen

This commit is contained in:
Mariano Uvalle 2019-04-27 21:47:00 -05:00
parent 2eeed6b287
commit 560b40ca3b
10 changed files with 262 additions and 21 deletions

View file

@ -93,7 +93,7 @@ export const pendingTasksUpdater: functions.CloudFunction<functions.Change<Fireb
if (change.after.exists && change.before.exists) {
/// Exit the funciton if case this is an update operation and the
/// event and priority of the taks haven't changed.
if (before.get('priority') === after.get('priority') || before.get('event') === after.get('event') || before.get('done') === after.get('done')) {
if (before.get('priority') === after.get('priority') && before.get('event') === after.get('event') && before.get('done') === after.get('done')) {
console.log('Nothing to update, exiting function');
return true;
}

View file

@ -5,6 +5,7 @@ import 'screens/events_screen.dart';
import 'screens/home_screen.dart';
import 'screens/initial_loading_screen.dart';
import 'screens/login_screen.dart';
import 'screens/new_event_screen.dart';
import 'screens/new_image_screen.dart';
import 'screens/task_screen.dart';
@ -84,6 +85,12 @@ class App extends StatelessWidget {
return EventsScreen();
},
);
} else if (routeTokens.first == 'newEvent') {
return MaterialPageRoute(
builder: (BuildContext contex) {
return NewEventScreen();
},
);
}
// Default route.
return MaterialPageRoute(

View file

@ -0,0 +1,72 @@
import 'dart:async';
import 'package:rxdart/rxdart.dart';
import '../utils.dart' show Validators;
import '../models/event_model.dart';
import '../models/user_model.dart';
import '../resources/firestore_provider.dart';
import '../services/auth_service.dart';
/// Business loginc componente that manages the state of the new event screen.
class NewEventBloc extends Object with Validators {
/// An instance of the auth service.
final AuthService _auth = authService;
/// An instance of the firebase repository.
final FirestoreProvider _firestore = firestoreProvider;
/// A subject of the name of the event.
final _eventName = BehaviorSubject<String>();
/// A subject of the encoded ocurrance of this event.
final _ocurrance = BehaviorSubject<List<bool>>();
// Streams getters.
/// An observable of the name of the event.
Observable<String> get eventName =>
_eventName.stream.transform(stringNotEmptyValidator);
/// An observable of the encoded ocurrance of this event.
Observable<List<bool>> get ocurrance =>
_ocurrance.stream.transform(occuranceArrayValidator);
/// An observable of the submit enabled flag.
///
/// Only emits true when the event occurs at least once a week and the event
/// name is not empty.
Observable<bool> get submitEnabled =>
Observable.combineLatest2(eventName, ocurrance, (a, b) => true);
//Sinks getters.
/// Changes the current task event name.
Function(String) get changeEventName => _eventName.sink.add;
/// Change the current ocurrance.
Function(List<bool>) get changeOcurrance => _ocurrance.sink.add;
//TODO: use a transaction to make the updates in firestore be atomic.
/// Adds the event to the database.
Future<void> submit() async {
final UserModel userModel = await _auth.getCurrentUserModel();
final event = EventModel(
when: _ocurrance.value,
name: _eventName.value,
pendigTasks: 0,
mediumPriority: 0,
highPriority: 0,
lowPriority: 0,
media: <String>[],
);
await _firestore.updateUser(userModel.id,
events: <String>[_eventName.value]..addAll(userModel.events));
return _firestore.addEvent(userModel.id, event);
}
dispose() async {
await _eventName.drain();
_eventName.close();
await _ocurrance.drain();
_ocurrance.close();
}
}

View file

@ -55,7 +55,7 @@ class TaskBloc extends Object with Validators {
///
/// Emits an error if the string added is empty.
Observable<String> get taskText =>
_taskText.stream.transform(validateStringNotEmpty);
_taskText.stream.transform(stringNotEmptyValidator);
/// An observable of the submit enabled flag.
///

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart' hide AppBar;
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import '../blocs/events_bloc.dart';
import '../models/event_model.dart';
@ -34,6 +35,10 @@ class _EventsScreenState extends State<EventsScreen> {
}
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(FontAwesomeIcons.plus),
onPressed: () => Navigator.of(context).pushNamed('newEvent/'),
),
drawer: PopulatedDrawer(
userAvatarUrl: userAvatarUrl,
userDisplayName: userDisplayName,

View file

@ -0,0 +1,68 @@
import 'package:flutter/material.dart' hide AppBar;
import '../utils.dart' show kSmallTextStyle;
import '../blocs/new_event_bloc.dart';
import '../widgets/app_bar.dart';
import '../widgets/big_text_input.dart';
import '../widgets/gradient_touchable_container.dart';
import '../widgets/ocurrance_selector.dart';
class NewEventScreen extends StatefulWidget {
_NewEventScreenState createState() => _NewEventScreenState();
}
class _NewEventScreenState extends State<NewEventScreen> {
/// An instance of the bloc corresponding to this screen.
final bloc = NewEventBloc();
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: 'New Event',
),
body: SafeArea(
child: Padding(
padding: EdgeInsets.only(top: 15.0, left: 20.0, right: 20.0),
child: Column(
children: <Widget>[
BigTextInput(
onChanged: bloc.changeEventName,
maxCharacters: 16,
hint: 'My event...',
),
SizedBox(
height: 15,
),
OcurranceSelector(
onChange: bloc.changeOcurrance,
),
SizedBox(
height: 15,
),
StreamBuilder(
stream: bloc.submitEnabled,
builder: (context, submitSnap) {
return GradientTouchableContainer(
height: 40,
radius: 8,
isExpanded: true,
enabled: submitSnap.hasData,
onTap: () => onSubmit(context),
child: Text(
'Submit',
style: kSmallTextStyle,
),
);
},
),
],
),
),
),
);
}
void onSubmit(BuildContext context) async {
await bloc.submit();
Navigator.of(context).pop();
}
}

View file

@ -55,20 +55,22 @@ class _TaskScreenState extends State<TaskScreen> {
child: Column(
children: <Widget>[
StreamBuilder(
stream: bloc.textInitialvalue,
builder:
(BuildContext context, AsyncSnapshot<String> snapshot) {
String textFieldInitialValue = '';
if (snapshot.hasData) {
textFieldInitialValue = snapshot.data;
}
return BigTextInput(
initialValue:
widget.isEdit ? textFieldInitialValue : '',
height: 95,
onChanged: bloc.changeTaskText,
);
}),
stream: bloc.textInitialvalue,
builder:
(BuildContext context, AsyncSnapshot<String> snapshot) {
String textFieldInitialValue = '';
if (snapshot.hasData) {
textFieldInitialValue = snapshot.data;
}
return BigTextInput(
initialValue: widget.isEdit ? textFieldInitialValue : '',
height: 95,
onChanged: bloc.changeTaskText,
maxCharacters: 220,
hint: 'Do something...',
);
},
),
SizedBox(
height: 15,
),

View file

@ -66,8 +66,9 @@ Color getColorFromEvent(EventModel event) {
return kLowPriorityColor;
}
class Validators {
final validateStringNotEmpty = StreamTransformer<String, String>.fromHandlers(
mixin Validators {
final stringNotEmptyValidator =
StreamTransformer<String, String>.fromHandlers(
handleData: (String string, EventSink<String> sink) {
if (string.isEmpty) {
sink.addError('Text cannot be empty');
@ -76,6 +77,17 @@ class Validators {
}
},
);
final occuranceArrayValidator =
StreamTransformer<List<bool>, List<bool>>.fromHandlers(
handleData: (List<bool> array, EventSink<List<bool>> sink) {
if (array.length == 5 && array.contains(true)) {
sink.add(array);
} else {
sink.addError('Event has to ocurr at least once');
}
},
);
}
/// Returns a stream transformer that sorts tasks by priority.

View file

@ -17,13 +17,21 @@ class BigTextInput extends StatefulWidget {
/// The initial value for the input.
final String initialValue;
/// Hint to be shown in the input.
final String hint;
/// The maximum amount of character to be allowed on the input.
final int maxCharacters;
BigTextInput({
@required this.onChanged,
this.height,
this.width,
this.elevated = true,
this.initialValue = '',
});
this.hint = '',
@required this.maxCharacters,
}) : assert(maxCharacters != null);
@override
_BigTextInputState createState() => _BigTextInputState();
@ -81,7 +89,7 @@ class _BigTextInputState extends State<BigTextInput> {
child: TextField(
controller: _controller,
maxLines: 3,
maxLength: 220,
maxLength: widget.maxCharacters,
maxLengthEnforced: true,
cursorColor: Theme.of(context).cursorColor,
textInputAction: TextInputAction.done,
@ -93,7 +101,7 @@ class _BigTextInputState extends State<BigTextInput> {
border: InputBorder.none,
contentPadding: EdgeInsets.only(left: 5.0, right: 5.0, top: 5.0),
counterStyle: TextStyle(color: Colors.white),
hintText: 'Do something...',
hintText: widget.hint,
hintStyle: TextStyle(
color: Theme.of(context).cursorColor,
),

View file

@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
/// A widget that lets you select the ocurrance of an event.
class OcurranceSelector extends StatefulWidget {
/// Function to be called when the selected ocurrance changes.
final Function(List<bool>) onChange;
OcurranceSelector({@required this.onChange});
_OcurranceSelectorState createState() => _OcurranceSelectorState();
}
class _OcurranceSelectorState extends State<OcurranceSelector> {
/// Strings corresponding to every day of the week.
static const kDayLetters = ['M', 'T', 'W', 'Th', 'F'];
/// Encoded occurance.
///
/// True means the event occurs in that day.
List<bool> ocurrance = List<bool>.filled(5, false);
Widget build(BuildContext context) {
List<Widget> rowChildren = [];
for (int i = 0; i < 5; i++) {
rowChildren.add(
GestureDetector(
onTap: () => onDayTap(i),
child: Container(
height: 40,
width: 40,
decoration: BoxDecoration(
color: ocurrance[i] ? Colors.white : Colors.grey,
borderRadius: BorderRadius.circular(3),
),
child: Center(
child: Text(
kDayLetters[i],
style: TextStyle(
fontWeight: FontWeight.w500,
),
),
),
),
),
);
if (i != 4) {
rowChildren.add(
SizedBox(
width: 10,
),
);
}
}
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: rowChildren,
);
}
void onDayTap(int index) {
setState(() {
ocurrance[index] = !ocurrance[index];
});
widget.onChange(ocurrance);
}
}