diff --git a/lib/ui/components/widgets/card_wrapper_widget.dart b/lib/ui/components/widgets/card_wrapper_widget.dart new file mode 100644 index 0000000..b1d81d1 --- /dev/null +++ b/lib/ui/components/widgets/card_wrapper_widget.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class CardWrapperWidget extends StatelessWidget { + final Color backgroundColor; + final Function()? onTap; + final Widget child; + + const CardWrapperWidget( + {super.key, + required this.child, + required this.onTap, + required this.backgroundColor}); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 2, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + color: backgroundColor, + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: onTap, + child: child, + ), + ); + } +} diff --git a/lib/ui/components/widgets/checklist_form_widget.dart b/lib/ui/components/widgets/checklist/checklist_form_widget.dart similarity index 63% rename from lib/ui/components/widgets/checklist_form_widget.dart rename to lib/ui/components/widgets/checklist/checklist_form_widget.dart index cb540c1..e87850c 100644 --- a/lib/ui/components/widgets/checklist_form_widget.dart +++ b/lib/ui/components/widgets/checklist/checklist_form_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:todoapp/ui/components/form_validator.dart'; +import 'package:todoapp/ui/components/widgets/form/form_field_widget.dart'; class ChecklistFormWidget extends StatelessWidget { final Key formKey; @@ -24,21 +25,15 @@ class ChecklistFormWidget extends StatelessWidget { autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( children: [ - TextFormField( - autofocus: true, - controller: checklistEditingController, - decoration: InputDecoration( + FormFieldWidget( labelText: checklistLabel, - labelStyle: Theme.of(context).textTheme.titleMedium, - border: const OutlineInputBorder(), - ), - validator: (value) { - if (formScreenValidator.validateValue(value) == false) { - return checklistErrorMessage; - } - return null; - }, - ), + controller: checklistEditingController, + validator: (value) { + if (formScreenValidator.validateValue(value) == false) { + return checklistErrorMessage; + } + return null; + }), ], ), ); diff --git a/lib/ui/components/widgets/checklist/checklist_full_widget.dart b/lib/ui/components/widgets/checklist/checklist_full_widget.dart index 0b58924..cace041 100644 --- a/lib/ui/components/widgets/checklist/checklist_full_widget.dart +++ b/lib/ui/components/widgets/checklist/checklist_full_widget.dart @@ -75,6 +75,17 @@ class ChecklistsListFullWidgetState extends State { } } + void onSortTasks() { + _tasksViewModel.onSort(); + } + + Future onShareTasks() async { + final checklistName = selected?.title; + if (checklistName != null) { + await _tasksViewModel.shareTasks(checklistName: checklistName); + } + } + Widget _buildTaskList(BuildContext context) { final localizations = AppLocalizations.of(context)!; return Row( diff --git a/lib/ui/components/widgets/checklist/checklist_item_widget.dart b/lib/ui/components/widgets/checklist/checklist_item_widget.dart index d8311d9..04caadf 100644 --- a/lib/ui/components/widgets/checklist/checklist_item_widget.dart +++ b/lib/ui/components/widgets/checklist/checklist_item_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:todoapp/data/model/checklist.dart'; +import 'package:todoapp/ui/components/widgets/card_wrapper_widget.dart'; class ChecklistItemWidget extends StatelessWidget { final Checklist checklist; @@ -43,27 +44,10 @@ class ChecklistItemWidget extends StatelessWidget { backgroundColor = Theme.of(context).colorScheme.tertiaryContainer; } - return Card( - elevation: 2, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - color: backgroundColor, - clipBehavior: Clip.hardEdge, - child: InkWell( - onTap: () => onSelectChecklist(checklist), - child: Padding( - padding: const EdgeInsets.only( - left: 8, - right: 8, - top: 16, - bottom: 16, - ), - child: _internalContent(context, checklist), - ), - ), + return CardWrapperWidget( + onTap: () => onSelectChecklist(checklist), + backgroundColor: backgroundColor, + child: _internalContent(context, checklist), ); } } diff --git a/lib/ui/components/widgets/checklist/checklist_navigation_rail_menu.dart b/lib/ui/components/widgets/checklist/checklist_navigation_rail_menu.dart deleted file mode 100644 index 582c56d..0000000 --- a/lib/ui/components/widgets/checklist/checklist_navigation_rail_menu.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; - -class ChecklistNavigationRailMenu extends StatelessWidget { - final IconData newChecklistIcon; - final String newChecklistLabel; - final IconData newTaskIcon; - final String newTaskLabel; - final VoidCallback onNewChecklistPressed; - final VoidCallback onNewTaskPressed; - - const ChecklistNavigationRailMenu({ - super.key, - required this.newChecklistIcon, - required this.newChecklistLabel, - required this.newTaskIcon, - required this.newTaskLabel, - required this.onNewChecklistPressed, - required this.onNewTaskPressed, - }); - - @override - Widget build(BuildContext context) { - return NavigationRail( - destinations: [ - NavigationRailDestination( - icon: IconButton( - icon: Icon(newChecklistIcon), - onPressed: onNewChecklistPressed, - ), - label: Text(newChecklistLabel), - ), - NavigationRailDestination( - icon: IconButton( - icon: Icon(newTaskIcon), - onPressed: onNewTaskPressed, - ), - label: Text(newTaskLabel), - ), - ], - labelType: NavigationRailLabelType.all, - selectedIndex: null, - ); - } -} diff --git a/lib/ui/components/widgets/checklist/fabmenu/checklist_expandable_fab_menu.dart b/lib/ui/components/widgets/checklist/fabmenu/checklist_expandable_fab_menu.dart new file mode 100644 index 0000000..86af53f --- /dev/null +++ b/lib/ui/components/widgets/checklist/fabmenu/checklist_expandable_fab_menu.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; +import 'package:todoapp/ui/components/widgets/checklist/fabmenu/checklist_fabmenu_item.dart'; +import 'package:todoapp/ui/l10n/app_localizations.dart'; + +class ChecklistExpandableFabMenu extends StatefulWidget { + final Function() onNewChecklistPressed; + final Function() onNewTaskPressed; + final Function() onSharePressed; + final Function() onSortPressed; + + const ChecklistExpandableFabMenu({ + super.key, + required this.onNewChecklistPressed, + required this.onNewTaskPressed, + required this.onSharePressed, + required this.onSortPressed, + }); + + @override + State createState() => + _ChecklistExpandableFabMenuState(); +} + +class _ChecklistExpandableFabMenuState + extends State { + final _key = GlobalKey(); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + return ExpandableFab( + key: _key, + type: ExpandableFabType.side, + overlayStyle: const ExpandableFabOverlayStyle( + blur: 1.0, + ), + children: [ + ChecklistFabMenuItem( + heroTag: 'new_task', + onPressed: () => { + _key.currentState?.close(), + widget.onNewTaskPressed(), + }, + label: localizations.task, + icon: Icons.add_task, + ), + ChecklistFabMenuItem( + heroTag: 'new_checklist', + onPressed: () => { + _key.currentState?.close(), + widget.onNewChecklistPressed(), + }, + label: localizations.checklist, + icon: Icons.plus_one, + ), + ChecklistFabMenuItem( + heroTag: 'share', + onPressed: () => { + _key.currentState?.close(), + widget.onSharePressed(), + }, + label: localizations.share, + icon: Icons.share, + ), + ChecklistFabMenuItem( + heroTag: 'sort', + onPressed: () => { + _key.currentState?.close(), + widget.onSortPressed(), + }, + label: localizations.sort, + icon: Icons.sort, + ) + ], + ); + } +} diff --git a/lib/ui/components/widgets/checklist/fabmenu/checklist_fabmenu_item.dart b/lib/ui/components/widgets/checklist/fabmenu/checklist_fabmenu_item.dart new file mode 100644 index 0000000..06ec999 --- /dev/null +++ b/lib/ui/components/widgets/checklist/fabmenu/checklist_fabmenu_item.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class ChecklistFabMenuItem extends StatelessWidget { + final String heroTag; + final String label; + final IconData icon; + final Function() onPressed; + + const ChecklistFabMenuItem( + {super.key, + required this.label, + required this.onPressed, + required this.icon, + required this.heroTag}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + FloatingActionButton.small( + heroTag: heroTag, + onPressed: onPressed, + child: Icon(icon), + ), + const SizedBox(height: 8), + Text(label), + ], + ); + } +} diff --git a/lib/ui/components/widgets/form/form_field_widget.dart b/lib/ui/components/widgets/form/form_field_widget.dart new file mode 100644 index 0000000..2315f7a --- /dev/null +++ b/lib/ui/components/widgets/form/form_field_widget.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class FormFieldWidget extends StatelessWidget { + final String labelText; + final TextEditingController controller; + final String? Function(String?)? validator; + + const FormFieldWidget({ + super.key, + required this.labelText, + required this.controller, + this.validator, + }); + + @override + Widget build(BuildContext context) { + return TextFormField( + autofocus: true, + maxLines: 3, + controller: controller, + decoration: InputDecoration( + labelText: labelText, + labelStyle: Theme.of(context).textTheme.titleMedium, + border: const OutlineInputBorder(), + ), + validator: validator, + ); + } +} diff --git a/lib/ui/components/widgets/task/task_cell_widget.dart b/lib/ui/components/widgets/task/task_cell_widget.dart index e0094b5..9a0d4b5 100644 --- a/lib/ui/components/widgets/task/task_cell_widget.dart +++ b/lib/ui/components/widgets/task/task_cell_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:todoapp/data/model/task.dart'; +import 'package:todoapp/ui/components/widgets/card_wrapper_widget.dart'; import 'package:todoapp/ui/components/widgets/task/task_title_widget.dart'; class TaskCellWidget extends StatelessWidget { @@ -20,13 +21,9 @@ class TaskCellWidget extends StatelessWidget { @override Widget build(BuildContext context) { - const cardShape = RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(8)), - ); - - return Card( - elevation: 4, - shape: cardShape, + return CardWrapperWidget( + onTap: null, + backgroundColor: Theme.of(context).colorScheme.surfaceBright, child: ListTile( leading: IconButton( onPressed: () => {onRemoveTask(task)}, diff --git a/lib/ui/components/widgets/task_form_widget.dart b/lib/ui/components/widgets/task/task_form_widget.dart similarity index 79% rename from lib/ui/components/widgets/task_form_widget.dart rename to lib/ui/components/widgets/task/task_form_widget.dart index d561bcb..d99b53d 100644 --- a/lib/ui/components/widgets/task_form_widget.dart +++ b/lib/ui/components/widgets/task/task_form_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:todoapp/ui/components/form_validator.dart'; +import 'package:todoapp/ui/components/widgets/form/form_field_widget.dart'; class TaskFormWidget extends StatelessWidget { final Key formKey; @@ -25,14 +26,9 @@ class TaskFormWidget extends StatelessWidget { autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( children: [ - TextFormField( - autofocus: true, + FormFieldWidget( + labelText: taskLabel, controller: taskEditingController, - decoration: InputDecoration( - labelText: taskLabel, - labelStyle: Theme.of(context).textTheme.titleMedium, - border: const OutlineInputBorder(), - ), validator: (value) { if (formScreenValidator.validateValue(value) == false) { return taskErrorMessage; diff --git a/lib/ui/l10n/app_en.arb b/lib/ui/l10n/app_en.arb index 884a97d..c2149c8 100644 --- a/lib/ui/l10n/app_en.arb +++ b/lib/ui/l10n/app_en.arb @@ -18,5 +18,7 @@ "checklist": "Checklist", "checklist_name": "Checklist name", "remove": "Remove", - "sort_message": "It is sorted" + "sort_message": "It is sorted", + "share": "Share", + "sort": "Sort" } \ No newline at end of file diff --git a/lib/ui/l10n/app_pt.arb b/lib/ui/l10n/app_pt.arb index b962663..89250c7 100644 --- a/lib/ui/l10n/app_pt.arb +++ b/lib/ui/l10n/app_pt.arb @@ -18,5 +18,7 @@ "checklist": "Checklist", "checklist_name": "Nome do Checklist", "remove": "Deletar", - "sort_message": "Está ordenado" + "sort_message": "Está ordenado", + "share": "Compartilhar", + "sort": "Ordenar" } \ No newline at end of file diff --git a/lib/ui/screens/checklist/checklist_screen.dart b/lib/ui/screens/checklist/checklist_screen.dart index e370dcd..6f3e895 100644 --- a/lib/ui/screens/checklist/checklist_screen.dart +++ b/lib/ui/screens/checklist/checklist_screen.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:todoapp/ui/components/form_validator.dart'; -import 'package:todoapp/ui/components/widgets/checklist_form_widget.dart'; +import 'package:todoapp/ui/components/widgets/checklist/checklist_form_widget.dart'; import 'package:todoapp/ui/components/widgets/custom_app_bar_widget.dart'; import 'package:todoapp/ui/l10n/app_localizations.dart'; import 'package:todoapp/ui/screens/checklist/checklist_viewmodel.dart'; diff --git a/lib/ui/screens/checklists/checklists_screen.dart b/lib/ui/screens/checklists/checklists_screen.dart index ccb64eb..a128080 100644 --- a/lib/ui/screens/checklists/checklists_screen.dart +++ b/lib/ui/screens/checklists/checklists_screen.dart @@ -1,10 +1,11 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; import 'package:todoapp/data/model/checklist.dart'; import 'package:todoapp/ui/components/widgets/checklist/checklist_full_widget.dart'; -import 'package:todoapp/ui/components/widgets/checklist/checklist_navigation_rail_menu.dart'; import 'package:todoapp/ui/components/widgets/checklist/checklists_list_widget.dart'; +import 'package:todoapp/ui/components/widgets/checklist/fabmenu/checklist_expandable_fab_menu.dart'; import 'package:todoapp/ui/components/widgets/confirmation_alert_dialog_widget.dart'; import 'package:todoapp/ui/components/widgets/custom_app_bar_widget.dart'; import 'package:todoapp/ui/l10n/app_localizations.dart'; @@ -44,6 +45,7 @@ class ChecklistsScaffold extends StatelessWidget { final Function(Checklist) onRemoveChecklist; final Function updateChecklists; final NavigatorProvider navigatorProvider; + /// Use key to access a specific internal behavior of TaskViewModel /// to update the task list through ChecklistFullWidget. final GlobalKey _checklistFullKey = @@ -59,13 +61,32 @@ class ChecklistsScaffold extends StatelessWidget { Widget _buildFloatingActionButton({ required bool isBigSize, - required Function() onPressed, + required BuildContext context, }) { if (isBigSize) { - return const SizedBox.shrink(); + return ChecklistExpandableFabMenu( + onNewChecklistPressed: () async { + await _addNewChecklistEvent(context); + }, + onSharePressed: () async { + await _checklistFullKey.currentState?.onShareTasks(); + }, + onSortPressed: () => { + _checklistFullKey.currentState?.onSortTasks() + }, + onNewTaskPressed: () async { + /// Use key to access a specific internal behavior of + /// TaskViewModel to update the task list through + /// ChecklistFullWidget. + final currentChecklistFullState = _checklistFullKey.currentState; + currentChecklistFullState?.addNewTaskToExistingChecklist(context); + }, + ); } else { return FloatingActionButton( - onPressed: onPressed, + onPressed: () async { + await _addNewChecklistEvent(context); + }, child: const Icon(Icons.plus_one), ); } @@ -77,28 +98,20 @@ class ChecklistsScaffold extends StatelessWidget { final localizations = AppLocalizations.of(context)!; return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - appBar: CustomAppBarWidget( - title: localizations.checklists, - ), - floatingActionButton: _buildFloatingActionButton( - isBigSize: isBigSize, - onPressed: () async { - await _addNewChecklistEvent(context); - }, - ), - body: Row( - children: [ - _buildNavigationRails(context: context, isBigSize: isBigSize), - _buildVerticalSeparator(context: context, isBigSize: isBigSize), - Expanded( - child: _buildCheckListWidget( - context: context, - isBigSize: isBigSize, - ), - ), - ], - )); + floatingActionButtonLocation: isBigSize ? ExpandableFab.location : null, + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: CustomAppBarWidget( + title: localizations.checklists, + ), + floatingActionButton: _buildFloatingActionButton( + isBigSize: isBigSize, + context: context, + ), + body: _buildCheckListWidget( + context: context, + isBigSize: isBigSize, + ), + ); } Widget _buildCheckListWidget({ @@ -137,46 +150,6 @@ class ChecklistsScaffold extends StatelessWidget { ); } - Widget _buildNavigationRails({ - required BuildContext context, - required bool isBigSize, - }) { - if (isBigSize) { - return ChecklistNavigationRailMenu( - newChecklistIcon: Icons.plus_one, - newChecklistLabel: 'New Checklist', - newTaskIcon: Icons.add_task, - newTaskLabel: 'New task', - onNewTaskPressed: () async { - /// Use key to access a specific internal behavior of TaskViewModel - /// to update the task list through ChecklistFullWidget. - final currentChecklistFullState = _checklistFullKey.currentState; - currentChecklistFullState?.addNewTaskToExistingChecklist( - context - ); - }, - onNewChecklistPressed: () async { - await _addNewChecklistEvent(context); - }, - ); - } else { - return const SizedBox.shrink(); - } - } - - Widget _buildVerticalSeparator({ - required BuildContext context, - required bool isBigSize, - }) { - if (isBigSize) { - return const VerticalDivider( - thickness: 0.2, - ); - } else { - return const SizedBox.shrink(); - } - } - void _showConfirmationDialogToRemoveChecklist( BuildContext context, Checklist checklist, diff --git a/lib/ui/screens/task/task_screen.dart b/lib/ui/screens/task/task_screen.dart index b2fe8d4..2bb9c71 100644 --- a/lib/ui/screens/task/task_screen.dart +++ b/lib/ui/screens/task/task_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:todoapp/data/model/task.dart'; import 'package:todoapp/ui/components/form_validator.dart'; import 'package:todoapp/ui/components/widgets/custom_app_bar_widget.dart'; -import 'package:todoapp/ui/components/widgets/task_form_widget.dart'; +import 'package:todoapp/ui/components/widgets/task/task_form_widget.dart'; import 'package:todoapp/ui/l10n/app_localizations.dart'; import 'package:todoapp/ui/screens/task/task_viewmodel.dart'; import 'package:todoapp/util/di/dependency_startup_launcher.dart'; diff --git a/pubspec.lock b/pubspec.lock index 7b2a0ea..dc52c22 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -294,6 +294,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.1" + flutter_expandable_fab: + dependency: "direct main" + description: + name: flutter_expandable_fab + sha256: "2a488600924fd2a041679ad889807ee5670414a7a518cf11d4854b9898b3504f" + url: "https://pub.dev" + source: hosted + version: "2.5.2" flutter_lints: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 839822d..dd51b27 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ dependencies: path: ^1.9.1 share_plus: 12.0.1 auto_route: ^10.1.0+1 + flutter_expandable_fab: ^2.5.2 dev_dependencies: flutter_test: