diff --git a/functions/src/pending_tasks_updater.ts b/functions/src/pending_tasks_updater.ts index 73f9a64..5da21c9 100644 --- a/functions/src/pending_tasks_updater.ts +++ b/functions/src/pending_tasks_updater.ts @@ -93,7 +93,7 @@ export const pendingTasksUpdater: functions.CloudFunction(); + + /// A subject of the encoded ocurrance of this event. + final _ocurrance = BehaviorSubject>(); + + // Streams getters. + /// An observable of the name of the event. + Observable get eventName => + _eventName.stream.transform(stringNotEmptyValidator); + + /// An observable of the encoded ocurrance of this event. + Observable> 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 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) get changeOcurrance => _ocurrance.sink.add; + + //TODO: use a transaction to make the updates in firestore be atomic. + /// Adds the event to the database. + Future 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: [], + ); + await _firestore.updateUser(userModel.id, + events: [_eventName.value]..addAll(userModel.events)); + return _firestore.addEvent(userModel.id, event); + } + + dispose() async { + await _eventName.drain(); + _eventName.close(); + await _ocurrance.drain(); + _ocurrance.close(); + } +} diff --git a/lib/src/blocs/task_bloc.dart b/lib/src/blocs/task_bloc.dart index 208f0bd..546ed44 100644 --- a/lib/src/blocs/task_bloc.dart +++ b/lib/src/blocs/task_bloc.dart @@ -55,7 +55,7 @@ class TaskBloc extends Object with Validators { /// /// Emits an error if the string added is empty. Observable get taskText => - _taskText.stream.transform(validateStringNotEmpty); + _taskText.stream.transform(stringNotEmptyValidator); /// An observable of the submit enabled flag. /// diff --git a/lib/src/screens/events_screen.dart b/lib/src/screens/events_screen.dart index ef2b84d..b070045 100644 --- a/lib/src/screens/events_screen.dart +++ b/lib/src/screens/events_screen.dart @@ -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 { } return Scaffold( + floatingActionButton: FloatingActionButton( + child: Icon(FontAwesomeIcons.plus), + onPressed: () => Navigator.of(context).pushNamed('newEvent/'), + ), drawer: PopulatedDrawer( userAvatarUrl: userAvatarUrl, userDisplayName: userDisplayName, diff --git a/lib/src/screens/new_event_screen.dart b/lib/src/screens/new_event_screen.dart new file mode 100644 index 0000000..66df7c1 --- /dev/null +++ b/lib/src/screens/new_event_screen.dart @@ -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 { + /// 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: [ + 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(); + } +} diff --git a/lib/src/screens/task_screen.dart b/lib/src/screens/task_screen.dart index 7e89714..482e7f5 100644 --- a/lib/src/screens/task_screen.dart +++ b/lib/src/screens/task_screen.dart @@ -55,20 +55,22 @@ class _TaskScreenState extends State { child: Column( children: [ StreamBuilder( - stream: bloc.textInitialvalue, - builder: - (BuildContext context, AsyncSnapshot 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 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, ), diff --git a/lib/src/utils.dart b/lib/src/utils.dart index f670845..636c45a 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -66,8 +66,9 @@ Color getColorFromEvent(EventModel event) { return kLowPriorityColor; } -class Validators { - final validateStringNotEmpty = StreamTransformer.fromHandlers( +mixin Validators { + final stringNotEmptyValidator = + StreamTransformer.fromHandlers( handleData: (String string, EventSink sink) { if (string.isEmpty) { sink.addError('Text cannot be empty'); @@ -76,6 +77,17 @@ class Validators { } }, ); + + final occuranceArrayValidator = + StreamTransformer, List>.fromHandlers( + handleData: (List array, EventSink> 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. diff --git a/lib/src/widgets/big_text_input.dart b/lib/src/widgets/big_text_input.dart index 019ceac..cebf889 100644 --- a/lib/src/widgets/big_text_input.dart +++ b/lib/src/widgets/big_text_input.dart @@ -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 { 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 { 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, ), diff --git a/lib/src/widgets/ocurrance_selector.dart b/lib/src/widgets/ocurrance_selector.dart new file mode 100644 index 0000000..60e097e --- /dev/null +++ b/lib/src/widgets/ocurrance_selector.dart @@ -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) onChange; + + OcurranceSelector({@required this.onChange}); + _OcurranceSelectorState createState() => _OcurranceSelectorState(); +} + +class _OcurranceSelectorState extends State { + /// 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 ocurrance = List.filled(5, false); + + Widget build(BuildContext context) { + List 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); + } +}