Finished the new event screen
This commit is contained in:
parent
2eeed6b287
commit
560b40ca3b
10 changed files with 262 additions and 21 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
72
lib/src/blocs/new_event_bloc.dart
Normal file
72
lib/src/blocs/new_event_bloc.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
68
lib/src/screens/new_event_screen.dart
Normal file
68
lib/src/screens/new_event_screen.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
67
lib/src/widgets/ocurrance_selector.dart
Normal file
67
lib/src/widgets/ocurrance_selector.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue