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) {
|
if (change.after.exists && change.before.exists) {
|
||||||
/// Exit the funciton if case this is an update operation and the
|
/// Exit the funciton if case this is an update operation and the
|
||||||
/// event and priority of the taks haven't changed.
|
/// 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');
|
console.log('Nothing to update, exiting function');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'screens/events_screen.dart';
|
||||||
import 'screens/home_screen.dart';
|
import 'screens/home_screen.dart';
|
||||||
import 'screens/initial_loading_screen.dart';
|
import 'screens/initial_loading_screen.dart';
|
||||||
import 'screens/login_screen.dart';
|
import 'screens/login_screen.dart';
|
||||||
|
import 'screens/new_event_screen.dart';
|
||||||
import 'screens/new_image_screen.dart';
|
import 'screens/new_image_screen.dart';
|
||||||
import 'screens/task_screen.dart';
|
import 'screens/task_screen.dart';
|
||||||
|
|
||||||
|
|
@ -84,6 +85,12 @@ class App extends StatelessWidget {
|
||||||
return EventsScreen();
|
return EventsScreen();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} else if (routeTokens.first == 'newEvent') {
|
||||||
|
return MaterialPageRoute(
|
||||||
|
builder: (BuildContext contex) {
|
||||||
|
return NewEventScreen();
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Default route.
|
// Default route.
|
||||||
return MaterialPageRoute(
|
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.
|
/// Emits an error if the string added is empty.
|
||||||
Observable<String> get taskText =>
|
Observable<String> get taskText =>
|
||||||
_taskText.stream.transform(validateStringNotEmpty);
|
_taskText.stream.transform(stringNotEmptyValidator);
|
||||||
|
|
||||||
/// An observable of the submit enabled flag.
|
/// An observable of the submit enabled flag.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart' hide AppBar;
|
import 'package:flutter/material.dart' hide AppBar;
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
|
||||||
import '../blocs/events_bloc.dart';
|
import '../blocs/events_bloc.dart';
|
||||||
import '../models/event_model.dart';
|
import '../models/event_model.dart';
|
||||||
|
|
@ -34,6 +35,10 @@ class _EventsScreenState extends State<EventsScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
child: Icon(FontAwesomeIcons.plus),
|
||||||
|
onPressed: () => Navigator.of(context).pushNamed('newEvent/'),
|
||||||
|
),
|
||||||
drawer: PopulatedDrawer(
|
drawer: PopulatedDrawer(
|
||||||
userAvatarUrl: userAvatarUrl,
|
userAvatarUrl: userAvatarUrl,
|
||||||
userDisplayName: userDisplayName,
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -63,12 +63,14 @@ class _TaskScreenState extends State<TaskScreen> {
|
||||||
textFieldInitialValue = snapshot.data;
|
textFieldInitialValue = snapshot.data;
|
||||||
}
|
}
|
||||||
return BigTextInput(
|
return BigTextInput(
|
||||||
initialValue:
|
initialValue: widget.isEdit ? textFieldInitialValue : '',
|
||||||
widget.isEdit ? textFieldInitialValue : '',
|
|
||||||
height: 95,
|
height: 95,
|
||||||
onChanged: bloc.changeTaskText,
|
onChanged: bloc.changeTaskText,
|
||||||
|
maxCharacters: 220,
|
||||||
|
hint: 'Do something...',
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 15,
|
height: 15,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,9 @@ Color getColorFromEvent(EventModel event) {
|
||||||
return kLowPriorityColor;
|
return kLowPriorityColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Validators {
|
mixin Validators {
|
||||||
final validateStringNotEmpty = StreamTransformer<String, String>.fromHandlers(
|
final stringNotEmptyValidator =
|
||||||
|
StreamTransformer<String, String>.fromHandlers(
|
||||||
handleData: (String string, EventSink<String> sink) {
|
handleData: (String string, EventSink<String> sink) {
|
||||||
if (string.isEmpty) {
|
if (string.isEmpty) {
|
||||||
sink.addError('Text cannot be empty');
|
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.
|
/// Returns a stream transformer that sorts tasks by priority.
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,21 @@ class BigTextInput extends StatefulWidget {
|
||||||
/// The initial value for the input.
|
/// The initial value for the input.
|
||||||
final String initialValue;
|
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({
|
BigTextInput({
|
||||||
@required this.onChanged,
|
@required this.onChanged,
|
||||||
this.height,
|
this.height,
|
||||||
this.width,
|
this.width,
|
||||||
this.elevated = true,
|
this.elevated = true,
|
||||||
this.initialValue = '',
|
this.initialValue = '',
|
||||||
});
|
this.hint = '',
|
||||||
|
@required this.maxCharacters,
|
||||||
|
}) : assert(maxCharacters != null);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_BigTextInputState createState() => _BigTextInputState();
|
_BigTextInputState createState() => _BigTextInputState();
|
||||||
|
|
@ -81,7 +89,7 @@ class _BigTextInputState extends State<BigTextInput> {
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
maxLength: 220,
|
maxLength: widget.maxCharacters,
|
||||||
maxLengthEnforced: true,
|
maxLengthEnforced: true,
|
||||||
cursorColor: Theme.of(context).cursorColor,
|
cursorColor: Theme.of(context).cursorColor,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
|
|
@ -93,7 +101,7 @@ class _BigTextInputState extends State<BigTextInput> {
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding: EdgeInsets.only(left: 5.0, right: 5.0, top: 5.0),
|
contentPadding: EdgeInsets.only(left: 5.0, right: 5.0, top: 5.0),
|
||||||
counterStyle: TextStyle(color: Colors.white),
|
counterStyle: TextStyle(color: Colors.white),
|
||||||
hintText: 'Do something...',
|
hintText: widget.hint,
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: Theme.of(context).cursorColor,
|
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