From 84e1f7f4695c66b8e32acd2b2dd42ac9eaba35e9 Mon Sep 17 00:00:00 2001 From: kevin-ee Date: Thu, 10 Oct 2019 23:58:20 +0900 Subject: [PATCH] (#42) Code refactoring after initial release - (#47) solved this issue too --- lib/Dependencies.dart | 2 +- lib/data/AppDatabase.dart | 100 ++++--- lib/data/CategoryRepositoryImpl.dart | 14 +- lib/data/LockRepositoryImpl.dart | 14 +- lib/data/MemoRepositoryImpl.dart | 14 +- lib/data/ToDoRepositoryImpl.dart | 14 +- lib/data/datasource/CategoryDataSource.dart | 9 + lib/data/datasource/LockDataSource.dart | 9 + lib/data/datasource/MemoDataSource.dart | 10 + lib/data/datasource/ToDoDataSource.dart | 8 + lib/domain/entity/DayMemo.dart | 2 + lib/domain/usecase/HomeUsecases.dart | 2 +- lib/presentation/day/DayBloc.dart | 8 +- lib/presentation/day/DayScreen.dart | 308 ++++++++++++-------- lib/presentation/day/DayState.dart | 19 +- lib/presentation/home/HomeBloc.dart | 4 +- lib/presentation/settings/SettingsBloc.dart | 31 +- lib/presentation/week/WeekBloc.dart | 8 +- lib/presentation/week/WeekScreen.dart | 270 ++++++++--------- 19 files changed, 476 insertions(+), 370 deletions(-) create mode 100644 lib/data/datasource/CategoryDataSource.dart create mode 100644 lib/data/datasource/LockDataSource.dart create mode 100644 lib/data/datasource/MemoDataSource.dart create mode 100644 lib/data/datasource/ToDoDataSource.dart diff --git a/lib/Dependencies.dart b/lib/Dependencies.dart index 9881ecf..ec7d5d2 100644 --- a/lib/Dependencies.dart +++ b/lib/Dependencies.dart @@ -27,7 +27,7 @@ final AppPreferences _prefs = AppPreferences(); final DrawerRepository _drawerRepository = DrawerRepositoryImpl(); final MemoRepository _memoRepository = MemoRepositoryImpl(_database); final DateRepository _dateRepository = DateRepositoryImpl(); -final ToDoRepository _toDoRepository = TodoRepositoryImpl(_database); +final ToDoRepository _toDoRepository = ToDoRepositoryImpl(_database); final LockRepository _lockRepository = LockRepositoryImpl(_database); final PrefsRepository _prefsRepository = PrefsRepositoryImpl(_prefs); final CategoryRepository _categoryRepository = CategoryRepositoryImpl(_database); diff --git a/lib/data/AppDatabase.dart b/lib/data/AppDatabase.dart index bb45de5..5347c1d 100644 --- a/lib/data/AppDatabase.dart +++ b/lib/data/AppDatabase.dart @@ -2,6 +2,10 @@ import 'package:path/path.dart'; import 'package:rxdart/rxdart.dart'; import 'package:sqflite/sqflite.dart'; +import 'package:todo_app/data/datasource/CategoryDataSource.dart'; +import 'package:todo_app/data/datasource/LockDataSource.dart'; +import 'package:todo_app/data/datasource/MemoDataSource.dart'; +import 'package:todo_app/data/datasource/ToDoDataSource.dart'; import 'package:todo_app/domain/entity/Category.dart'; import 'package:todo_app/domain/entity/CheckPoint.dart'; import 'package:todo_app/domain/entity/DateInWeek.dart'; @@ -9,7 +13,10 @@ import 'package:todo_app/domain/entity/DayMemo.dart'; import 'package:todo_app/domain/entity/Lock.dart'; import 'package:todo_app/domain/entity/ToDo.dart'; -class AppDatabase { +class AppDatabase implements ToDoDataSource, + MemoDataSource, + LockDataSource, + CategoryDataSource { static const String TABLE_CHECK_POINTS = 'checkpoints'; static const String TABLE_TODOS = 'todos'; static const String TABLE_LOCKS = 'locks'; @@ -110,6 +117,7 @@ class AppDatabase { ); } + @override Future> getToDos(DateTime date) async { final db = await _database.first; final List> maps = await db.query( @@ -126,26 +134,27 @@ class AppDatabase { return result; } - Future setDayMemo(DayMemo dayMemo) async { + @override + Future setToDo(ToDo toDo) async { final db = await _database.first; await db.insert( - TABLE_DAY_MEMOS, - dayMemo.toDatabase(), + TABLE_TODOS, + toDo.toDatabase(), conflictAlgorithm: ConflictAlgorithm.replace, ); } - Future getDayMemo(DateTime date) async { + @override + Future removeToDo(ToDo toDo) async { final db = await _database.first; - final Map map = await db.query( - TABLE_DAY_MEMOS, - where: DayMemo.createWhereQuery(), - whereArgs: DayMemo.createWhereArgs(date), - ).then((l) => l.isEmpty ? null : l[0]); - return map != null ? DayMemo.fromDatabase(map) - : DayMemo(year: date.year, month: date.month, day: date.day); + await db.delete( + TABLE_TODOS, + where: ToDo.createWhereQueryForToDo(), + whereArgs: ToDo.createWhereArgsForToDo(toDo), + ); } + @override Future> getCheckPoints(DateTime date) async { final db = await _database.first; final dateInWeek = DateInWeek.fromDate(date); @@ -167,6 +176,39 @@ class AppDatabase { return checkPoints; } + @override + Future setCheckPoint(CheckPoint checkPoint) async { + final db = await _database.first; + await db.insert( + TABLE_CHECK_POINTS, + checkPoint.toDatabase(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + @override + Future getDayMemo(DateTime date) async { + final db = await _database.first; + final Map map = await db.query( + TABLE_DAY_MEMOS, + where: DayMemo.createWhereQuery(), + whereArgs: DayMemo.createWhereArgs(date), + ).then((l) => l.isEmpty ? null : l[0]); + return map != null ? DayMemo.fromDatabase(map) + : DayMemo(year: date.year, month: date.month, day: date.day); + } + + @override + Future setDayMemo(DayMemo dayMemo) async { + final db = await _database.first; + await db.insert( + TABLE_DAY_MEMOS, + dayMemo.toDatabase(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + @override Future getIsCheckPointsLocked(DateTime date, bool defaultValue) async { final db = await _database.first; final Map map = await db.query( @@ -177,6 +219,7 @@ class AppDatabase { return map != null ? Lock.fromDatabase(map).isLocked : defaultValue; } + @override Future setIsCheckPointsLocked(DateInWeek dateInWeek, bool value) async { final db = await _database.first; final map = { @@ -190,6 +233,7 @@ class AppDatabase { ); } + @override Future setIsDayRecordLocked(DateTime date, bool value) async { final db = await _database.first; final map = { @@ -203,15 +247,7 @@ class AppDatabase { ); } - Future setCheckPoint(CheckPoint checkPoint) async { - final db = await _database.first; - await db.insert( - TABLE_CHECK_POINTS, - checkPoint.toDatabase(), - conflictAlgorithm: ConflictAlgorithm.replace, - ); - } - + @override Future getIsDayRecordLocked(DateTime date, bool defaultValue) async { final db = await _database.first; final Map map = await db.query( @@ -222,24 +258,7 @@ class AppDatabase { return map != null ? Lock.fromDatabase(map).isLocked : defaultValue; } - Future setToDo(ToDo toDo) async { - final db = await _database.first; - await db.insert( - TABLE_TODOS, - toDo.toDatabase(), - conflictAlgorithm: ConflictAlgorithm.replace, - ); - } - - Future removeToDo(ToDo toDo) async { - final db = await _database.first; - await db.delete( - TABLE_TODOS, - where: ToDo.createWhereQueryForToDo(), - whereArgs: ToDo.createWhereArgsForToDo(toDo), - ); - } - + @override Future getCategory(int id) async { final db = await _database.first; Map map = await db.query( @@ -250,6 +269,7 @@ class AppDatabase { return map != null ? Category.fromDatabase(map) : Category(); } + @override Future> getAllCategories() async { final db = await _database.first; List> maps = await db.query( @@ -266,6 +286,7 @@ class AppDatabase { return result; } + @override Future setCategory(Category category) async { if (category.id == Category.ID_DEFAULT) { return Category.ID_DEFAULT; @@ -279,6 +300,7 @@ class AppDatabase { } } + @override Future removeCategory(Category category) async { final db = await _database.first; await db.delete( diff --git a/lib/data/CategoryRepositoryImpl.dart b/lib/data/CategoryRepositoryImpl.dart index 23faabb..24ebe84 100644 --- a/lib/data/CategoryRepositoryImpl.dart +++ b/lib/data/CategoryRepositoryImpl.dart @@ -1,30 +1,30 @@ -import 'package:todo_app/data/AppDatabase.dart'; +import 'package:todo_app/data/datasource/CategoryDataSource.dart'; import 'package:todo_app/domain/entity/Category.dart'; import 'package:todo_app/domain/repository/CategoryRepository.dart'; class CategoryRepositoryImpl implements CategoryRepository { - final AppDatabase _db; + final CategoryDataSource _dataSource; - const CategoryRepositoryImpl(this._db); + const CategoryRepositoryImpl(this._dataSource); @override Future getCategory(int id) async { - return _db.getCategory(id); + return _dataSource.getCategory(id); } @override Future setCategory(Category category) async { - return _db.setCategory(category); + return _dataSource.setCategory(category); } @override Future> getAllCategories() async { - return _db.getAllCategories(); + return _dataSource.getAllCategories(); } @override Future removeCategory(Category category) async { - return _db.removeCategory(category); + return _dataSource.removeCategory(category); } } \ No newline at end of file diff --git a/lib/data/LockRepositoryImpl.dart b/lib/data/LockRepositoryImpl.dart index 4baa2a9..a7358f6 100644 --- a/lib/data/LockRepositoryImpl.dart +++ b/lib/data/LockRepositoryImpl.dart @@ -1,31 +1,31 @@ -import 'package:todo_app/data/AppDatabase.dart'; +import 'package:todo_app/data/datasource/LockDataSource.dart'; import 'package:todo_app/domain/entity/DateInWeek.dart'; import 'package:todo_app/domain/repository/LockRepository.dart'; class LockRepositoryImpl implements LockRepository { - final AppDatabase _db; + final LockDataSource _dataSource; - const LockRepositoryImpl(this._db); + const LockRepositoryImpl(this._dataSource); @override Future getIsCheckPointsLocked(DateTime date, bool defaultValue) async { - return await _db.getIsCheckPointsLocked(date, defaultValue); + return await _dataSource.getIsCheckPointsLocked(date, defaultValue); } @override Future getIsDayRecordLocked(DateTime date, bool defaultValue) async { - return await _db.getIsDayRecordLocked(date, defaultValue); + return await _dataSource.getIsDayRecordLocked(date, defaultValue); } @override void setIsCheckPointsLocked(DateInWeek dateInWeek, bool value) { - _db.setIsCheckPointsLocked(dateInWeek, value); + _dataSource.setIsCheckPointsLocked(dateInWeek, value); } @override void setIsDayRecordLocked(DateTime date, bool value) { - _db.setIsDayRecordLocked(date, value); + _dataSource.setIsDayRecordLocked(date, value); } } \ No newline at end of file diff --git a/lib/data/MemoRepositoryImpl.dart b/lib/data/MemoRepositoryImpl.dart index 669b1e5..16e2ba3 100644 --- a/lib/data/MemoRepositoryImpl.dart +++ b/lib/data/MemoRepositoryImpl.dart @@ -1,32 +1,32 @@ -import 'package:todo_app/data/AppDatabase.dart'; +import 'package:todo_app/data/datasource/MemoDataSource.dart'; import 'package:todo_app/domain/entity/CheckPoint.dart'; import 'package:todo_app/domain/entity/DayMemo.dart'; import 'package:todo_app/domain/repository/MemoRepository.dart'; class MemoRepositoryImpl implements MemoRepository { - final AppDatabase _db; + final MemoDataSource _dataSource; - const MemoRepositoryImpl(this._db); + const MemoRepositoryImpl(this._dataSource); @override Future> getCheckPoints(DateTime date) async { - return await _db.getCheckPoints(date); + return await _dataSource.getCheckPoints(date); } @override Future getDayMemo(DateTime date) async { - return await _db.getDayMemo(date); + return await _dataSource.getDayMemo(date); } @override void setDayMemo(DayMemo dayMemo) { - _db.setDayMemo(dayMemo); + _dataSource.setDayMemo(dayMemo); } @override void setCheckPoint(CheckPoint checkPoint) { - _db.setCheckPoint(checkPoint); + _dataSource.setCheckPoint(checkPoint); } } \ No newline at end of file diff --git a/lib/data/ToDoRepositoryImpl.dart b/lib/data/ToDoRepositoryImpl.dart index 374e0e5..7eaa4c7 100644 --- a/lib/data/ToDoRepositoryImpl.dart +++ b/lib/data/ToDoRepositoryImpl.dart @@ -1,26 +1,26 @@ -import 'package:todo_app/data/AppDatabase.dart'; +import 'package:todo_app/data/datasource/ToDoDataSource.dart'; import 'package:todo_app/domain/entity/ToDo.dart'; import 'package:todo_app/domain/repository/ToDoRepository.dart'; -class TodoRepositoryImpl implements ToDoRepository { - final AppDatabase _db; +class ToDoRepositoryImpl implements ToDoRepository { + final ToDoDataSource _dataSource; - const TodoRepositoryImpl(this._db); + const ToDoRepositoryImpl(this._dataSource); @override Future> getToDos(DateTime date) async { - return await _db.getToDos(date); + return await _dataSource.getToDos(date); } @override void setToDo(ToDo toDo) { - _db.setToDo(toDo); + _dataSource.setToDo(toDo); } @override void removeToDo(ToDo toDo) { - _db.removeToDo(toDo); + _dataSource.removeToDo(toDo); } } \ No newline at end of file diff --git a/lib/data/datasource/CategoryDataSource.dart b/lib/data/datasource/CategoryDataSource.dart new file mode 100644 index 0000000..db77d1d --- /dev/null +++ b/lib/data/datasource/CategoryDataSource.dart @@ -0,0 +1,9 @@ + +import 'package:todo_app/domain/entity/Category.dart'; + +abstract class CategoryDataSource { + Future getCategory(int id); + Future> getAllCategories(); + Future setCategory(Category category); + Future removeCategory(Category category); +} \ No newline at end of file diff --git a/lib/data/datasource/LockDataSource.dart b/lib/data/datasource/LockDataSource.dart new file mode 100644 index 0000000..94cc385 --- /dev/null +++ b/lib/data/datasource/LockDataSource.dart @@ -0,0 +1,9 @@ + +import 'package:todo_app/domain/entity/DateInWeek.dart'; + +abstract class LockDataSource { + Future getIsCheckPointsLocked(DateTime date, bool defaultValue); + void setIsCheckPointsLocked(DateInWeek dateInWeek, bool value); + Future getIsDayRecordLocked(DateTime date, bool defaultValue); + void setIsDayRecordLocked(DateTime date, bool value); +} \ No newline at end of file diff --git a/lib/data/datasource/MemoDataSource.dart b/lib/data/datasource/MemoDataSource.dart new file mode 100644 index 0000000..1974094 --- /dev/null +++ b/lib/data/datasource/MemoDataSource.dart @@ -0,0 +1,10 @@ + +import 'package:todo_app/domain/entity/CheckPoint.dart'; +import 'package:todo_app/domain/entity/DayMemo.dart'; + +abstract class MemoDataSource { + Future> getCheckPoints(DateTime date); + void setCheckPoint(CheckPoint checkPoint); + Future getDayMemo(DateTime date); + void setDayMemo(DayMemo dayMemo); +} \ No newline at end of file diff --git a/lib/data/datasource/ToDoDataSource.dart b/lib/data/datasource/ToDoDataSource.dart new file mode 100644 index 0000000..59ba713 --- /dev/null +++ b/lib/data/datasource/ToDoDataSource.dart @@ -0,0 +1,8 @@ + +import 'package:todo_app/domain/entity/ToDo.dart'; + +abstract class ToDoDataSource { + Future> getToDos(DateTime date); + void setToDo(ToDo toDo); + void removeToDo(ToDo toDo); +} \ No newline at end of file diff --git a/lib/domain/entity/DayMemo.dart b/lib/domain/entity/DayMemo.dart index 3131d76..0943586 100644 --- a/lib/domain/entity/DayMemo.dart +++ b/lib/domain/entity/DayMemo.dart @@ -29,6 +29,8 @@ class DayMemo { final String hint; final bool isExpanded; + String get key => '$year-$month-$day'; + const DayMemo({ this.year = 0, this.month = 0, diff --git a/lib/domain/usecase/HomeUsecases.dart b/lib/domain/usecase/HomeUsecases.dart index c2937b4..213f0a2 100644 --- a/lib/domain/usecase/HomeUsecases.dart +++ b/lib/domain/usecase/HomeUsecases.dart @@ -13,7 +13,7 @@ class HomeUsecases { final screenItems = _drawerRepository.getDrawerScreenItems(); final List allDrawerItems = [childScreenItems, screenItems].expand((l) => l).toList(); - // 4번째 아이템 뒤에 공백을 넣음 + // insert Spacer after 2nd item if (allDrawerItems.length >= 2) { allDrawerItems.insert(2, DrawerSpacerItem()); } diff --git a/lib/presentation/day/DayBloc.dart b/lib/presentation/day/DayBloc.dart index 4f34071..53d722c 100644 --- a/lib/presentation/day/DayBloc.dart +++ b/lib/presentation/day/DayBloc.dart @@ -44,20 +44,20 @@ class DayBloc { )); } - bool onWillPopScope() { + bool handleBackPress() { final editorState = _state.value.editorState; if (editorState == EditorState.SHOWN_CATEGORY) { _state.add(_state.value.buildNew( editorState: EditorState.SHOWN_TODO, )); - return false; + return true; } else if (editorState == EditorState.SHOWN_TODO) { _state.add(_state.value.buildNew( editorState: EditorState.HIDDEN, )); - return false; + return true; } - return true; + return false; } void onBackArrowClicked(BuildContext context) { diff --git a/lib/presentation/day/DayScreen.dart b/lib/presentation/day/DayScreen.dart index 1ad0422..6df32a3 100644 --- a/lib/presentation/day/DayScreen.dart +++ b/lib/presentation/day/DayScreen.dart @@ -18,9 +18,7 @@ import 'package:todo_app/presentation/widgets/AppTextField.dart'; class DayScreen extends StatefulWidget { final DateTime date; - DayScreen({ - @required this.date, - }); + DayScreen(this.date); @override State createState() => _DayScreenState(); @@ -29,6 +27,8 @@ class DayScreen extends StatefulWidget { class _DayScreenState extends State { DayBloc _bloc; ScrollController _toDoScrollController; + // todo: is this the right way to use focusnodes? + final Map _focusNodes = {}; @override void initState() { @@ -42,6 +42,9 @@ class _DayScreenState extends State { super.dispose(); _bloc.dispose(); _toDoScrollController.dispose(); + + _focusNodes.forEach((key, focusNode) => focusNode.dispose()); + _focusNodes.clear(); } @override @@ -56,87 +59,110 @@ class _DayScreenState extends State { } Widget _buildUI(DayState state) { - if (state.scrollToBottomEvent) { - SchedulerBinding.instance.addPostFrameCallback((duration) { - if (_toDoScrollController.hasClients) { - _toDoScrollController.position.jumpTo( - _toDoScrollController.position.maxScrollExtent, - ); - } - }); - } + if (state.scrollToBottomEvent) { + SchedulerBinding.instance.addPostFrameCallback((duration) { + if (_toDoScrollController.hasClients) { + _toDoScrollController.position.jumpTo( + _toDoScrollController.position.maxScrollExtent, + ); + } + }); + } - if (state.scrollToToDoListEvent) { - Future.delayed(const Duration(milliseconds: 500), () { - if (_toDoScrollController.hasClients) { - final double targetPixel = state.isMemoExpanded ? 170 : 70; - if (_toDoScrollController.position.pixels < targetPixel) { - _toDoScrollController.position.animateTo( - targetPixel, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } + if (state.scrollToToDoListEvent) { + // 500.. magic number.. + Future.delayed(const Duration(milliseconds: 500), () { + if (_toDoScrollController.hasClients) { + final double targetPixel = state.isMemoExpanded ? 170 : 70; + if (_toDoScrollController.position.pixels < targetPixel) { + _toDoScrollController.position.animateTo( + targetPixel, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); } - }); - } + } + }); + } return WillPopScope( - onWillPop: () async => _bloc.onWillPopScope(), + onWillPop: () async => !_bloc.handleBackPress() && !_unfocusTextFieldIfAny(), child: SafeArea( - child: Material( - child: Scaffold( - floatingActionButton: state.isFabVisible ? _FAB( - bloc: _bloc, - ) : null, - body: Stack( - fit: StackFit.expand, - children: [ - Column( - children: [ - _Header( - bloc: _bloc, - title: AppLocalizations.of(context).getDayScreenTitle(state.month, state.day, state.weekday), - ), - Expanded( - child: Stack( - children: [ - state.toDoRecords.length == 0 ? _EmptyToDoListView( - bloc: _bloc, - dayMemo: state.dayMemo, - ) : _ToDoListView( - bloc: _bloc, - dayMemo: state.dayMemo, - toDoRecords: state.toDoRecords, - scrollController: _toDoScrollController, - ), - _HeaderShadow( - scrollController: _toDoScrollController, - ), - ], - ), + child: Scaffold( + floatingActionButton: state.isFabVisible ? _FAB( + bloc: _bloc, + ) : null, + body: Stack( + fit: StackFit.expand, + children: [ + Column( + children: [ + _Header( + bloc: _bloc, + title: AppLocalizations.of(context).getDayScreenTitle(state.month, state.day, state.weekday), + ), + Expanded( + child: Stack( + children: [ + state.toDoRecords.length == 0 ? _EmptyToDoListView( + bloc: _bloc, + dayMemo: state.dayMemo, + focusNodeProvider: _getOrCreateFocusNode, + ) : _ToDoListView( + bloc: _bloc, + dayMemo: state.dayMemo, + toDoRecords: state.toDoRecords, + scrollController: _toDoScrollController, + focusNodeProvider: _getOrCreateFocusNode, + ), + _HeaderShadow( + scrollController: _toDoScrollController, + ), + ], ), - ], - ), - // 키보드 위 입력창 - state.editorState == EditorState.HIDDEN ? const SizedBox.shrink() - : state.editorState == EditorState.SHOWN_TODO ? _ToDoEditorContainer( - bloc: _bloc, - editingToDoRecord: state.editingToDoRecord, - ) : _CategoryEditorContainer( - bloc: _bloc, - allCategories: state.allCategories, - editingCategory: state.editingCategory, - categoryPickers: state.categoryPickers, - selectedPickerIndex: state.selectedPickerIndex, - ) - ], - ), + ), + ], + ), + // editor positioned just above the keyboard + state.editorState == EditorState.HIDDEN ? const SizedBox.shrink() + : state.editorState == EditorState.SHOWN_TODO ? _ToDoEditorContainer( + bloc: _bloc, + editingToDoRecord: state.editingToDoRecord, + focusNodeProvider: _getOrCreateFocusNode, + ) : _CategoryEditorContainer( + bloc: _bloc, + allCategories: state.allCategories, + editingCategory: state.editingCategory, + categoryPickers: state.categoryPickers, + selectedPickerIndex: state.selectedPickerIndex, + focusNodeProvider: _getOrCreateFocusNode, + ), + ], ), ), ), ); } + + FocusNode _getOrCreateFocusNode(String key) { + if (_focusNodes.containsKey(key)) { + return _focusNodes[key]; + } else { + final newFocusNode = FocusNode(); + _focusNodes[key] = newFocusNode; + return newFocusNode; + } + } + + bool _unfocusTextFieldIfAny() { + for (FocusNode focusNode in _focusNodes.values) { + if (focusNode.hasPrimaryFocus) { + focusNode.unfocus(); + return true; + } + } + return false; + } } class _FAB extends StatelessWidget { @@ -199,10 +225,12 @@ class _Header extends StatelessWidget { class _EmptyToDoListView extends StatelessWidget { final DayBloc bloc; final DayMemo dayMemo; + final FocusNode Function(String key) focusNodeProvider; _EmptyToDoListView({ @required this.bloc, @required this.dayMemo, + @required this.focusNodeProvider, }); @override @@ -213,6 +241,7 @@ class _EmptyToDoListView extends StatelessWidget { _DayMemo( bloc: bloc, dayMemo: dayMemo, + focusNodeProvider: focusNodeProvider, ), Padding( padding: EdgeInsets.only(left: 18, top: 20), @@ -243,10 +272,12 @@ class _EmptyToDoListView extends StatelessWidget { class _DayMemo extends StatelessWidget { final DayBloc bloc; final DayMemo dayMemo; + final FocusNode Function(String key) focusNodeProvider; _DayMemo({ @required this.bloc, @required this.dayMemo, + @required this.focusNodeProvider, }); @override @@ -292,6 +323,7 @@ class _DayMemo extends StatelessWidget { child: SizedBox( height: 93, child: AppTextField( + focusNode: focusNodeProvider(dayMemo.key), text: dayMemo.text, textSize: 12, textColor: AppColors.TEXT_WHITE, @@ -315,12 +347,14 @@ class _ToDoListView extends StatelessWidget { final DayMemo dayMemo; final List toDoRecords; final ScrollController scrollController; + final FocusNode Function(String key) focusNodeProvider; _ToDoListView({ @required this.bloc, @required this.dayMemo, @required this.toDoRecords, @required this.scrollController, + @required this.focusNodeProvider, }); @override @@ -333,6 +367,7 @@ class _ToDoListView extends StatelessWidget { return _DayMemo( bloc: bloc, dayMemo: dayMemo, + focusNodeProvider: focusNodeProvider, ); } else if (index == 1) { return Padding( @@ -620,10 +655,12 @@ class _ColorCategoryThumbnail extends StatelessWidget { class _ToDoEditorContainer extends StatelessWidget { final DayBloc bloc; final ToDoRecord editingToDoRecord; + final FocusNode Function(String key) focusNodeProvider; _ToDoEditorContainer({ @required this.bloc, @required this.editingToDoRecord, + @required this.focusNodeProvider, }); @override @@ -657,6 +694,7 @@ class _ToDoEditorContainer extends StatelessWidget { _ToDoEditor( bloc: bloc, editingToDoRecord: editingToDoRecord, + focusNodeProvider: focusNodeProvider, ), _ToDoEditorCategoryButton( bloc: bloc, @@ -674,10 +712,14 @@ class _ToDoEditorContainer extends StatelessWidget { class _ToDoEditor extends StatelessWidget { final DayBloc bloc; final ToDoRecord editingToDoRecord; + final FocusNode Function(String key) focusNodeProvider; + + final _focusNodeKey = 'toDoEditor'; _ToDoEditor({ @required this.bloc, @required this.editingToDoRecord, + @required this.focusNodeProvider, }); @override @@ -699,6 +741,7 @@ class _ToDoEditor extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(vertical: 15), child: AppTextField( + focusNode: focusNodeProvider(_focusNodeKey), text: toDo.text, textSize: 14, textColor: AppColors.TEXT_BLACK, @@ -757,7 +800,8 @@ class _ToDoEditorCategoryButton extends StatelessWidget { color: AppColors.PRIMARY, ), child: Text( - '${AppLocalizations.of(context).category}: ${categoryName.isEmpty ? AppLocalizations.of(context).categoryNone : categoryName}', + '${AppLocalizations.of(context).category}: ' + '${categoryName.isEmpty ? AppLocalizations.of(context).categoryNone : categoryName}', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -777,6 +821,7 @@ class _CategoryEditorContainer extends StatelessWidget { final Category editingCategory; final List categoryPickers; final int selectedPickerIndex; + final FocusNode Function(String key) focusNodeProvider; _CategoryEditorContainer({ @required this.bloc, @@ -784,6 +829,7 @@ class _CategoryEditorContainer extends StatelessWidget { @required this.editingCategory, @required this.categoryPickers, @required this.selectedPickerIndex, + @required this.focusNodeProvider, }); @override @@ -814,6 +860,7 @@ class _CategoryEditorContainer extends StatelessWidget { _CategoryEditor( bloc: bloc, category: editingCategory, + focusNodeProvider: focusNodeProvider, ), Padding( padding: const EdgeInsets.only(bottom: 8), @@ -872,48 +919,11 @@ class _CategoryEditorCategoryList extends StatelessWidget { itemCount: allCategories.length, itemBuilder: (context, index) { final category = allCategories[index]; - return Column( - children: [ - index == 0 ? SizedBox(height: 4,) : Container(), - InkWell( - onTap: () => bloc.onCategoryEditorCategoryClicked(category), - onLongPress: () => bloc.onCategoryEditorCategoryLongClicked(context, category), - child: Row( - children: [ - SizedBox(width: 8,), - Padding( - padding: const EdgeInsets.all(6), - child: _CategoryThumbnail( - category: category, - width: 24, - height: 24, - fontSize: 14), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), - child: Text( - category.name.isEmpty ? AppLocalizations.of(context).categoryNone : category.name, - style: TextStyle( - color: AppColors.TEXT_BLACK, - fontSize: 14, - ), - ), - ), - ) - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Container( - width: double.infinity, - height: 1, - color: AppColors.DIVIDER, - ), - ), - index == allCategories.length - 1 ? SizedBox(height: 4,) : const SizedBox.shrink(), - ], + return _CategoryListItem( + bloc: bloc, + category: category, + isFirst: index == 0, + isLast: index == allCategories.length - 1, ); }, ), @@ -922,13 +932,78 @@ class _CategoryEditorCategoryList extends StatelessWidget { } } +class _CategoryListItem extends StatelessWidget { + final DayBloc bloc; + final Category category; + final bool isFirst; + final bool isLast; + + _CategoryListItem({ + @required this.bloc, + @required this.category, + @required this.isFirst, + @required this.isLast, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + isFirst ? SizedBox(height: 4,) : Container(), + InkWell( + onTap: () => bloc.onCategoryEditorCategoryClicked(category), + onLongPress: () => bloc.onCategoryEditorCategoryLongClicked(context, category), + child: Row( + children: [ + SizedBox(width: 8,), + Padding( + padding: const EdgeInsets.all(6), + child: _CategoryThumbnail( + category: category, + width: 24, + height: 24, + fontSize: 14), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), + child: Text( + category.name.isEmpty ? AppLocalizations.of(context).categoryNone : category.name, + style: TextStyle( + color: AppColors.TEXT_BLACK, + fontSize: 14, + ), + ), + ), + ) + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Container( + width: double.infinity, + height: 1, + color: AppColors.DIVIDER, + ), + ), + isLast ? SizedBox(height: 4,) : const SizedBox.shrink(), + ], + ); + } +} + class _CategoryEditor extends StatelessWidget { final DayBloc bloc; final Category category; + final FocusNode Function(String key) focusNodeProvider; + + final _focusNodeKey = 'categoryEditor'; _CategoryEditor({ @required this.bloc, @required this.category, + @required this.focusNodeProvider, }); @override @@ -952,6 +1027,7 @@ class _CategoryEditor extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(vertical: 14), child: AppTextField( + focusNode: focusNodeProvider(_focusNodeKey), text: category.name, textSize: 14, textColor: AppColors.TEXT_BLACK, diff --git a/lib/presentation/day/DayState.dart b/lib/presentation/day/DayState.dart index eb5fde5..d0dcf93 100644 --- a/lib/presentation/day/DayState.dart +++ b/lib/presentation/day/DayState.dart @@ -85,6 +85,7 @@ class DayState { allCategories: allCategories ?? this.allCategories, selectedPickerIndex: selectedPickerIndex ?? this.selectedPickerIndex, + // these are one-time events, so default to false if not given to "true" as parameter scrollToBottomEvent: scrollToBottomEvent ?? false, scrollToToDoListEvent: scrollToToDoListEvent ?? false, ); @@ -98,24 +99,6 @@ class DayState { } return buildNew(toDoRecords: newRecords); } - - String _toWeekDayString(int weekDay) { - if (weekDay == DateTime.monday) { - return '월요일'; - } else if (weekDay == DateTime.tuesday) { - return '화요일'; - } else if (weekDay == DateTime.wednesday) { - return '수요일'; - } else if (weekDay == DateTime.thursday) { - return '목요일'; - } else if (weekDay == DateTime.friday) { - return '금요일'; - } else if (weekDay == DateTime.saturday) { - return '토요일'; - } else { - return '일요일'; - } - } } enum EditorState { diff --git a/lib/presentation/home/HomeBloc.dart b/lib/presentation/home/HomeBloc.dart index 2d4bbf0..f712886 100644 --- a/lib/presentation/home/HomeBloc.dart +++ b/lib/presentation/home/HomeBloc.dart @@ -30,7 +30,7 @@ class HomeBloc { } void onDrawerChildScreenItemClicked(BuildContext context, DrawerChildScreenItem item) { - Navigator.of(context).pop(); + Navigator.pop(context); setCurrentDrawerChildScreenItem(item.key); } @@ -56,7 +56,7 @@ class HomeBloc { } void _dispatchSettingsChangedEvent() { - for (var listener in _settingsChangedEventListeners) { + for (final listener in _settingsChangedEventListeners) { listener(); } } diff --git a/lib/presentation/settings/SettingsBloc.dart b/lib/presentation/settings/SettingsBloc.dart index c9769ac..69ce571 100644 --- a/lib/presentation/settings/SettingsBloc.dart +++ b/lib/presentation/settings/SettingsBloc.dart @@ -14,6 +14,7 @@ import 'package:todo_app/presentation/createpassword/CreatePasswordScreen.dart'; import 'package:todo_app/presentation/inputpassword/InputPasswordScreen.dart'; class SettingsBloc { + static const _SENDGRID_SEND_API_ENDPOINT = 'https://api.sendgrid.com/v3/mail/send'; final SettingsUsecases _usecases = dependencies.settingsUsecases; void Function() _needUpdateListener; @@ -32,7 +33,9 @@ class SettingsBloc { _usecases.setDefaultLocked(false); _showCreatePasswordDialog(context, scaffoldState); } else if (!isDefaultLocked) { - _usecases.setDefaultLocked(true); // 비번 입력하기 전에는 바뀌면 안되므로 일단은 다시 되돌려버림 + // set it to true forcefully instantly. + // will set to false when user inputs password correctly + _usecases.setDefaultLocked(true); Utils.showBottomSheet(scaffoldState, (context) => InputPasswordScreen(onSuccess: () { _usecases.setDefaultLocked(false); @@ -175,27 +178,33 @@ class SettingsBloc { } Future _onConfirmSendTempPasswordOkClicked(BuildContext context, ScaffoldState scaffoldState) async { + Navigator.pop(context); + final mailSentMsg = AppLocalizations.of(context).tempPasswordMailSent; final mailSendFailedMsg = AppLocalizations.of(context).tempPasswordMailSendFailed; final failedToSaveTempPasswordMsg = AppLocalizations.of(context).failedToSaveTempPasswordByUnknownError; - Navigator.pop(context); + final prevPassword = await _usecases.getUserPassword(); - await _usecases.setUserPassword(_generateRandomPassword()); + await _usecases.setUserPassword(_createRandomPassword()); final changedPassword = await _usecases.getUserPassword(); + if (changedPassword.isNotEmpty && prevPassword != changedPassword) { final mailTitle = AppLocalizations.of(context).tempPasswordMailSubject; final mailBody = AppLocalizations.of(context).tempPasswordMailBody; final recoveryEmail = await _usecases.getRecoveryEmail(); - final body = '{"personalizations":[{"to":[{"email":"$recoveryEmail"}],"subject":"$mailTitle"}],"content": [{"type": "text/plain", "value": "$mailBody$changedPassword"}],"from":{"email":"giantsol64@gmail.com","name":"Blue Diary Developer"}}'; + final body = _createEmailBodyJson( + targetEmail: recoveryEmail, + title: mailTitle, + body: '$mailBody$changedPassword'); final response = await http.post( - 'https://api.sendgrid.com/v3/mail/send', + _SENDGRID_SEND_API_ENDPOINT, headers: { HttpHeaders.authorizationHeader: SENDGRID_AUTHORIZATION, HttpHeaders.contentTypeHeader: 'application/json', }, body: body, ); - if ([200, 201, 202].contains(response.statusCode)) { + if (response.statusCode.toString().startsWith('2')) { Utils.showSnackBar(scaffoldState, mailSentMsg, _snackBarDuration); } else { Utils.showSnackBar(scaffoldState, mailSendFailedMsg, _snackBarDuration); @@ -205,11 +214,19 @@ class SettingsBloc { } } - String _generateRandomPassword() { + String _createRandomPassword() { final rand = Random(); return '${rand.nextInt(10)}${rand.nextInt(10)}${rand.nextInt(10)}${rand.nextInt(10)}'; } + String _createEmailBodyJson({ + @required String targetEmail, + @required String title, + @required String body, + }) { + return '{"personalizations":[{"to":[{"email":"$targetEmail"}],"subject":"$title"}],"content": [{"type": "text/plain", "value": "$body"}],"from":{"email":"giantsol64@gmail.com","name":"Blue Diary Developer"}}'; + } + Future onResetPasswordClicked(BuildContext context, ScaffoldState scaffoldState) async { final prevPassword = await _usecases.getUserPassword(); if (prevPassword.isEmpty) { diff --git a/lib/presentation/week/WeekBloc.dart b/lib/presentation/week/WeekBloc.dart index 2854744..3ecf453 100644 --- a/lib/presentation/week/WeekBloc.dart +++ b/lib/presentation/week/WeekBloc.dart @@ -83,7 +83,6 @@ class WeekBloc { InputPasswordScreen(onSuccess: () { final updatedWeekRecord = weekRecord.buildNew(isCheckPointsLocked: false); _state.add(_state.value.buildNewWeekRecordUpdated(updatedWeekRecord)); - _usecases.setCheckPointsLocked(weekRecord.dateInWeek, false); }, onFail: () { delegator.showSnackBar(AppLocalizations.of(context).unlockFail, _snackBarDuration); @@ -165,7 +164,7 @@ class WeekBloc { await Navigator.push( context, MaterialPageRoute( - builder: (context) => DayScreen(date: dayRecord.date), + builder: (context) => DayScreen(dayRecord.date), ), ); _initState(); @@ -177,7 +176,7 @@ class WeekBloc { await Navigator.push( context, MaterialPageRoute( - builder: (context) => DayScreen(date: dayRecord.date), + builder: (context) => DayScreen(dayRecord.date), ), ); _initState(); @@ -214,7 +213,7 @@ class WeekBloc { } void _onCreatePasswordCancelClicked(BuildContext context) { - Navigator.of(context).pop(); + Navigator.pop(context); } void _onCreatePasswordOkClicked(BuildContext context) { @@ -235,7 +234,6 @@ class WeekBloc { void dispose() { _state.close(); - delegator.removeSettingsChangedListener(_settingsChangedListener); } } \ No newline at end of file diff --git a/lib/presentation/week/WeekScreen.dart b/lib/presentation/week/WeekScreen.dart index daff7f7..aab8bd5 100644 --- a/lib/presentation/week/WeekScreen.dart +++ b/lib/presentation/week/WeekScreen.dart @@ -48,11 +48,11 @@ class _WeekScreenState extends State { void dispose() { super.dispose(); _bloc.dispose(); + _weekRecordPageController.dispose(); + _scrollController.dispose(); _focusNodes.forEach((key, focusNode) => focusNode.dispose()); _focusNodes.clear(); - - _scrollController.dispose(); } @override @@ -71,48 +71,44 @@ class _WeekScreenState extends State { onWillPop: () async { return !_unfocusTextFieldIfAny(); }, - child: GestureDetector( - onTapDown: (_) => _unfocusTextFieldIfAny(), - behavior: HitTestBehavior.translucent, - child: Column( - children: [ - _Header( - bloc: _bloc, - displayYear: state.year.toString(), - displayMonthAndWeek: AppLocalizations.of(context).getMonthAndNthWeek(state.month, state.nthWeek), - ), - Expanded( - child: Stack( - children: [ - InfinityPageView( - controller: _weekRecordPageController, - itemCount: state.weekRecords.length, - itemBuilder: (context, index) { - final weekRecords = state.weekRecords; - if (weekRecords.isEmpty || weekRecords[index] == null) { - return null; - } - return _WeekRecord( - bloc: _bloc, - weekRecord: weekRecords[index], - focusNodeProvider: _getOrCreateFocusNode, - scrollController: _scrollController, - ); - }, - onPageChanged: (changedIndex) { - _headerShadowState.currentState.updateShadowVisibility(false); - _bloc.onWeekRecordPageChanged(changedIndex); - }, - ), - _HeaderShadow( - key: _headerShadowState, - scrollController: _scrollController, - ), - ], - ), + child: Column( + children: [ + _Header( + bloc: _bloc, + displayYear: state.year.toString(), + displayMonthAndWeek: AppLocalizations.of(context).getMonthAndNthWeek(state.month, state.nthWeek), + ), + Expanded( + child: Stack( + children: [ + InfinityPageView( + controller: _weekRecordPageController, + itemCount: state.weekRecords.length, + itemBuilder: (context, index) { + final weekRecords = state.weekRecords; + if (weekRecords.isEmpty || weekRecords[index] == null) { + return null; + } + return _WeekRecord( + bloc: _bloc, + weekRecord: weekRecords[index], + focusNodeProvider: _getOrCreateFocusNode, + scrollController: _scrollController, + ); + }, + onPageChanged: (changedIndex) { + _headerShadowState.currentState.updateShadowVisibility(false); + _bloc.onWeekRecordPageChanged(changedIndex); + }, + ), + _HeaderShadow( + key: _headerShadowState, + scrollController: _scrollController, + ), + ], ), - ] - ), + ), + ] ), ); } @@ -190,7 +186,7 @@ class _Header extends StatelessWidget { class _WeekRecord extends StatelessWidget { final WeekBloc bloc; final WeekRecord weekRecord; - final FocusNodeProvider focusNodeProvider; + final FocusNode Function(String key) focusNodeProvider; final ScrollController scrollController; _WeekRecord({ @@ -232,7 +228,7 @@ class _WeekRecord extends StatelessWidget { class _CheckPointsBox extends StatelessWidget { final WeekBloc bloc; final WeekRecord weekRecord; - final FocusNodeProvider focusNodeProvider; + final FocusNode Function(String key) focusNodeProvider; _CheckPointsBox({ @required this.bloc, @@ -273,7 +269,8 @@ class _CheckPointsBox extends StatelessWidget { ], ), ), - !weekRecord.isCheckPointsLocked ? Padding( + weekRecord.isCheckPointsLocked ? const SizedBox.shrink() + : Padding( padding: const EdgeInsets.only(bottom: 9), child: Column( children: List.generate(weekRecord.checkPoints.length, (index) { @@ -285,7 +282,7 @@ class _CheckPointsBox extends StatelessWidget { ); }), ), - ) : const SizedBox.shrink(), + ), ], ), ), @@ -303,31 +300,24 @@ class _LockedIcon extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - width: 38, - height: 38, - child: Stack( - children: [ - Center( - child: Container( - decoration: BoxDecoration( - color: AppColors.SECONDARY, - shape: BoxShape.circle, - ), - width: 28, - height: 28, - alignment: Alignment.center, - child: Image.asset('assets/ic_lock_on.png'), + return InkWell( + customBorder: CircleBorder(), + onTap: onTap, + child: SizedBox( + width: 38, + height: 38, + child: Center( + child: Container( + decoration: BoxDecoration( + color: AppColors.SECONDARY, + shape: BoxShape.circle, ), + width: 28, + height: 28, + alignment: Alignment.center, + child: Image.asset('assets/ic_lock_on.png'), ), - Material( - type: MaterialType.transparency, - child: InkWell( - customBorder: CircleBorder(), - onTap: onTap, - ), - ) - ] + ), ), ); } @@ -342,31 +332,24 @@ class _UnlockedIcon extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - width: 38, - height: 38, - child: Stack( - children: [ - Center( - child: Container( - decoration: BoxDecoration( - color: AppColors.BACKGROUND_GREY, - shape: BoxShape.circle, - ), - width: 28, - height: 28, - alignment: Alignment.center, - child: Image.asset('assets/ic_lock_off.png'), + return InkWell( + customBorder: CircleBorder(), + onTap: onTap, + child: SizedBox( + width: 38, + height: 38, + child: Center( + child: Container( + decoration: BoxDecoration( + color: AppColors.BACKGROUND_GREY, + shape: BoxShape.circle, ), + width: 28, + height: 28, + alignment: Alignment.center, + child: Image.asset('assets/ic_lock_off.png'), ), - Material( - type: MaterialType.transparency, - child: InkWell( - customBorder: CircleBorder(), - onTap: onTap, - ), - ) - ] + ), ), ); } @@ -376,7 +359,7 @@ class _CheckPointItem extends StatelessWidget { final WeekBloc bloc; final WeekRecord weekRecord; final CheckPoint checkPoint; - final FocusNodeProvider focusNodeProvider; + final FocusNode Function(String key) focusNodeProvider; _CheckPointItem({ @required this.bloc, @@ -429,8 +412,6 @@ class _CheckPointItem extends StatelessWidget { } } -typedef FocusNodeProvider = FocusNode Function(String key); - class _DayPreviewItem extends StatelessWidget { final WeekBloc bloc; final WeekRecord weekRecord; @@ -477,62 +458,53 @@ class _DayPreviewItemContent extends StatelessWidget { return Row( children: [ Expanded( - child: IntrinsicHeight( - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(left: 15), - child: Row( - children: [ - _DayPreviewItemThumbnail( - hasBorder: dayRecord.hasBorder, - filledRatio: dayRecord.filledRatio, - text: dayRecord.thumbnailString, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 18, top: 4, bottom: 4,), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - dayRecord.isToday == true ? _DayPreviewItemTodayText() : const SizedBox.shrink(), - Text( - AppLocalizations.of(context).getDayRecordTitle(dayRecord.month, dayRecord.day), - style: TextStyle( - fontSize: 18, - color: AppColors.TEXT_BLACK, - ), - ), - SizedBox(width: 12), - dayRecord.filledRatio == 1.0 ? _DayPreviewItemCompleteText() : const SizedBox.shrink(), - ], - ), - Padding( - padding: const EdgeInsets.only(top: 2,), - child: Text( - dayRecord.subtitle, - style: TextStyle( - fontSize: 14, - color: dayRecord.subtitleColor, - ), + child: InkWell( + onTap: () => bloc.onDayPreviewClicked(context, dayRecord), + child: Padding( + padding: const EdgeInsets.only(left: 15), + child: Row( + children: [ + _DayPreviewItemThumbnail( + hasBorder: dayRecord.hasBorder, + filledRatio: dayRecord.filledRatio, + text: dayRecord.thumbnailString, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 18, top: 4, bottom: 4,), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + dayRecord.isToday == true ? _DayPreviewItemTodayText() : const SizedBox.shrink(), + Text( + AppLocalizations.of(context).getDayRecordTitle(dayRecord.month, dayRecord.day), + style: TextStyle( + fontSize: 18, + color: AppColors.TEXT_BLACK, ), ), + SizedBox(width: 12), + dayRecord.filledRatio == 1.0 ? _DayPreviewItemCompleteText() : const SizedBox.shrink(), ], ), - ), - ) - ], - ), - ), - Material( - type: MaterialType.transparency, - child: InkWell( - onTap: () => bloc.onDayPreviewClicked(context, dayRecord), - ), - ), - ] + Padding( + padding: const EdgeInsets.only(top: 2,), + child: Text( + dayRecord.subtitle, + style: TextStyle( + fontSize: 14, + color: dayRecord.subtitleColor, + ), + ), + ), + ], + ), + ), + ) + ], + ), ), ), ), @@ -786,4 +758,4 @@ class _HeaderShadowState extends State<_HeaderShadow> { ), ); } -} +} \ No newline at end of file