From 586174c264dc82ad3883ebd59e249f695dc65cb3 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Thu, 6 Jun 2024 08:50:08 +0200 Subject: [PATCH] feat: add all elements from flutter_bottom_alert_dialog to the component --- CHANGELOG.md | 3 + example/lib/main.dart | 351 +++++++++++++++++++++++- example/test/widget_test.dart | 2 +- lib/flutter_dialogs.dart | 2 + lib/src/bottom_alert_dialog.dart | 299 ++++++++++++++++++++ lib/src/bottom_alert_dialog_config.dart | 127 +++++++++ pubspec.yaml | 2 +- 7 files changed, 775 insertions(+), 11 deletions(-) create mode 100644 lib/src/bottom_alert_dialog.dart create mode 100644 lib/src/bottom_alert_dialog_config.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index fef9865..ce6c617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.0.0 +- Flutter_dialogs and flutter_bottom_alert_dialogs combined into one package + ## 0.0.2 - Added CI and linter diff --git a/example/lib/main.dart b/example/lib/main.dart index e64bcff..ad1d3f1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,11 +6,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_dialogs/flutter_dialogs.dart'; void main() { - runApp(const MyApp()); + runApp(const DialogDemoApp()); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class DialogDemoApp extends StatelessWidget { + const DialogDemoApp({super.key}); @override Widget build(BuildContext context) { @@ -19,21 +19,26 @@ class MyApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.blue, ), - home: const MyHomePage(title: 'Flutter Dialogs demo'), + home: const DialogDemoPage(title: 'Flutter Dialogs demo'), + builder: (context, child) { + return BottomAlertDialogConfig( + child: child ?? const SizedBox.shrink(), + ); + }, ); } } -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); +class DialogDemoPage extends StatefulWidget { + const DialogDemoPage({super.key, required this.title}); final String title; @override - State createState() => _MyHomePageState(); + State createState() => _DialogDemoPageState(); } -class _MyHomePageState extends State { +class _DialogDemoPageState extends State { @override void initState() { super.initState(); @@ -103,11 +108,339 @@ class _MyHomePageState extends State { ), const Spacer( flex: 3, - ) + ), + ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return BottomAlertDialog.singleButton( + closeButton: true, + title: const Text('Confirm'), + body: const Text( + 'Click the button to dismiss', + ), + buttonText: 'Ok', + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + }, + child: const Text('BottomAlertDialog.singleButton'), + ), + Container( + height: 10, + ), + ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return BottomAlertDialog.multiButton( + title: const Text('Favorite Color'), + body: const Text( + 'Choose your favorite color', + ), + buttons: [ + BottomAlertDialogAction( + text: 'Red', + onPressed: () { + Navigator.pop(context); + }, + buttonType: ButtonType.primary, + ), + BottomAlertDialogAction( + text: 'Green', + onPressed: () { + Navigator.pop(context); + }, + buttonType: ButtonType.primary, + ), + BottomAlertDialogAction( + text: 'Blue', + onPressed: () { + Navigator.pop(context); + }, + buttonType: ButtonType.primary, + ), + BottomAlertDialogAction( + text: 'Yellow', + onPressed: () { + Navigator.pop(context); + }, + buttonType: ButtonType.primary, + ), + ], + ); + }, + ); + }, + child: const Text('BottomAlertDialog.multiButton'), + ), + Container( + height: 10, + ), + ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return BottomAlertDialog.singleButtonIcon( + closeButton: true, + title: const Text('Confirm'), + body: const Text( + 'Click the button to dismiss', + ), + icon: const Icon( + Icons.info, + color: Colors.blue, + ), + buttonText: 'Ok', + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + }, + child: const Text('BottomAlertDialog.singleButtonIcon'), + ), + Container( + height: 10, + ), + ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return BottomAlertDialog.icon( + closeButton: true, + title: const Text('Favorite Car'), + body: const Text( + 'Choose your favorite car brand', + ), + icon: const Icon( + Icons.car_rental_sharp, + ), + buttons: [ + ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('BMW'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Opel'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Mercedes'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Kia'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Skoda'), + ), + ], + ); + }, + ); + }, + child: const Text('BottomAlertDialog.icon'), + ), + Container( + height: 10, + ), + ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return BottomAlertDialog.yesOrNo( + title: const Text('Question'), + body: const Text( + 'Do you really wanna do this?', + ), + onYes: () { + Navigator.of(context).pop(); + }, + onNo: () { + Navigator.of(context).pop(); + }, + ); + }, + ); + }, + child: const Text('BottomAlertDialog.yesOrNo'), + ), + Container( + height: 10, + ), + ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return BottomAlertDialog.yesOrNoIcon( + title: const Text('Question'), + body: const Text( + 'Do you really wanna do this?', + ), + onYes: () { + Navigator.of(context).pop(); + }, + onNo: () { + Navigator.of(context).pop(); + }, + icon: const Icon( + Icons.question_mark_sharp, + color: Colors.red, + ), + ); + }, + ); + }, + child: const Text('BottomAlertDialog.yesOrNoIcon'), + ), + Container( + height: 10, + ), + ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return BottomAlertDialog.custom( + closeButton: true, + body: SizedBox( + height: 100, + child: Column( + children: [ + const Text('Custom Dialog with PageView'), + Flexible( + child: PageView( + children: [ + Container( + child: + const Center(child: Text('Page 1')), + ), + Container( + child: + const Center(child: Text('Page 2')), + ), + Container( + child: + const Center(child: Text('Page 3')), + ), + ], + ), + ), + ], + ), + ), + buttons: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Ok'), + ), + ], + ); + }, + ); + }, + child: const Text('BottomAlertDialog.custom'), + ), + Container( + height: 10, + ), + ElevatedButton( + child: const Text('Multiple chained dialogs'), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return BottomAlertDialog.yesOrNo( + closeButton: true, + title: const Text('Pokémon'), + body: const Text( + 'Do you want to choose your starter Pokémon?', + ), + onYes: () { + Navigator.pop(context); + showDialog( + context: context, + builder: (context) => BottomAlertDialog.multiButton( + title: const Text('Starter Pokémon'), + body: const Text('Choose a starter Pokémon'), + buttons: [ + BottomAlertDialogAction( + text: 'Turtwig', + buttonType: ButtonType.secondary, + onPressed: () => + _showDoneDialog(context, 'Turtwig'), + ), + BottomAlertDialogAction( + text: 'Chimchar', + buttonType: ButtonType.secondary, + onPressed: () => + _showDoneDialog(context, 'Chimchar'), + ), + BottomAlertDialogAction( + text: 'Piplup', + buttonType: ButtonType.secondary, + onPressed: () => + _showDoneDialog(context, 'Piplup'), + ), + ], + ), + ); + }, + onNo: () => Navigator.pop(context), + ); + }, + ); + }, + ), ], ), ), ), ); } + + void _showDoneDialog(BuildContext context, String name) { + Navigator.pop(context); + showDialog( + context: context, + builder: (context) => BottomAlertDialog.icon( + title: const Text('Good choice!'), + icon: Icon( + Icons.catching_pokemon, + color: Color(name.hashCode).withAlpha(255), + ), + body: Text('You chose $name to be your starter Pokémon.'), + buttons: [ + const CloseButton( + color: Colors.green, + ), + ], + ), + ); + } } diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index ccc4ce3..a67285d 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -17,7 +17,7 @@ import 'package:example/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(const DialogDemoApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); diff --git a/lib/flutter_dialogs.dart b/lib/flutter_dialogs.dart index 843a5f1..2133525 100644 --- a/lib/flutter_dialogs.dart +++ b/lib/flutter_dialogs.dart @@ -8,3 +8,5 @@ export './src/alert_dialogs.dart'; export './src/dialogs.dart'; export './src/popup_parent.dart'; export './src/popup_service.dart'; +export './src/bottom_alert_dialog_config.dart'; +export './src/bottom_alert_dialog.dart'; diff --git a/lib/src/bottom_alert_dialog.dart b/lib/src/bottom_alert_dialog.dart new file mode 100644 index 0000000..d44c321 --- /dev/null +++ b/lib/src/bottom_alert_dialog.dart @@ -0,0 +1,299 @@ +// SPDX-FileCopyrightText: 2022 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter/material.dart'; +import 'package:flutter_dialogs/src/bottom_alert_dialog_config.dart'; + +class BottomAlertDialogAction extends StatelessWidget { + const BottomAlertDialogAction({ + required this.text, + required this.onPressed, + this.buttonType = ButtonType.tertiary, + Key? key, + }) : super(key: key); + final String text; + final ButtonType buttonType; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + var config = BottomAlertDialogConfig.of(context); + var buttonBuilder = config.buttonBuilder; + var translatedText = text; + if (text == 'shell.alertdialog.button.yes') { + translatedText = config.yesText; + } else if (text == 'shell.alertdialog.button.no') { + translatedText = config.noText; + } + return buttonBuilder.call( + context, + text: translatedText, + onPressed: onPressed, + buttonType: buttonType, + ); + } +} + +class BottomAlertDialog extends StatelessWidget { + factory BottomAlertDialog.custom({ + required Widget body, + required List buttons, + List? actions, + bool? closeButton, + }) { + return BottomAlertDialog._( + closeButton: closeButton, + buttons: buttons, + actions: actions, + body: (_) => body, + ); + } + + factory BottomAlertDialog.singleButtonIcon({ + required Widget title, + required Widget body, + required Widget icon, + required String buttonText, + required VoidCallback onPressed, + ButtonType buttonType = ButtonType.tertiary, + bool? closeButton, + }) { + return BottomAlertDialog.icon( + closeButton: closeButton, + title: title, + icon: icon, + body: body, + buttons: [ + BottomAlertDialogAction( + text: buttonText, + buttonType: buttonType, + onPressed: onPressed, + ), + ], + ); + } + + factory BottomAlertDialog.yesOrNoIcon({ + required Widget title, + required Widget body, + required Widget icon, + required VoidCallback onYes, + required VoidCallback onNo, + bool focusYes = true, + bool otherSecondary = false, + bool? closeButton, + }) { + return BottomAlertDialog.icon( + closeButton: closeButton, + title: title, + body: body, + icon: icon, + buttons: _getYesNoDialogButtons(focusYes, otherSecondary, onYes, onNo), + ); + } + + factory BottomAlertDialog.yesOrNo({ + required Widget title, + required Widget body, + required VoidCallback onYes, + required VoidCallback onNo, + bool focusYes = true, + bool otherSecondary = false, + bool? closeButton, + }) { + return BottomAlertDialog.multiButton( + closeButton: closeButton, + title: title, + body: body, + buttons: const [], + actions: _getYesNoDialogButtons(focusYes, otherSecondary, onYes, onNo), + ); + } + + factory BottomAlertDialog.icon({ + required Widget title, + required Widget icon, + required Widget body, + required List buttons, + List? actions, + bool? closeButton, + }) { + return BottomAlertDialog._( + closeButton: closeButton, + buttons: buttons, + actions: actions, + body: (context) => Column( + children: [ + icon, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: title, + ), + Padding( + padding: const EdgeInsets.only(top: 20, left: 20, right: 20), + child: body, + ), + ], + ), + ); + } + + factory BottomAlertDialog.multiButton({ + required Widget title, + required Widget body, + required List buttons, + List? actions, + bool? closeButton, + }) { + return BottomAlertDialog._( + closeButton: closeButton, + buttons: buttons, + actions: actions, + body: (context) => Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + child: title, + ), + Padding( + padding: const EdgeInsets.only(top: 20, left: 20, right: 20), + child: body, + ), + ], + ), + ); + } + factory BottomAlertDialog.singleButton({ + required Widget title, + required Widget body, + required String buttonText, + required VoidCallback onPressed, + ButtonType buttonType = ButtonType.tertiary, + bool? closeButton, + }) { + return BottomAlertDialog.multiButton( + closeButton: closeButton, + title: title, + body: body, + buttons: [ + BottomAlertDialogAction( + text: buttonText, + onPressed: onPressed, + buttonType: buttonType, + ), + ], + ); + } + const BottomAlertDialog._({ + required this.buttons, + required this.body, + this.actions, + this.closeButton = false, + }); + final List buttons; + final WidgetBuilder body; + final bool? closeButton; + final List? actions; + + static List _getYesNoDialogButtons( + bool focusYes, + bool otherSecondary, + VoidCallback onYes, + VoidCallback onNo, + ) { + return [ + if (focusYes) ...[ + BottomAlertDialogAction( + text: 'shell.alertdialog.button.no', + buttonType: + otherSecondary ? ButtonType.secondary : ButtonType.tertiary, + onPressed: onNo, + ), + ], + BottomAlertDialogAction( + text: 'shell.alertdialog.button.yes', + buttonType: focusYes + ? ButtonType.primary + : otherSecondary + ? ButtonType.secondary + : ButtonType.tertiary, + onPressed: onYes, + ), + if (!focusYes) ...[ + BottomAlertDialogAction( + text: 'shell.alertdialog.button.no', + buttonType: ButtonType.primary, + onPressed: onNo, + ), + ], + ]; + } + + @override + Widget build(BuildContext context) { + var config = BottomAlertDialogConfig.of(context); + + return Column( + children: [ + const Spacer(), + AlertDialog( + insetPadding: EdgeInsets.zero, + contentPadding: EdgeInsets.zero, + backgroundColor: + config.backgroundColor ?? Theme.of(context).cardColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10), + ), + ), + content: SizedBox( + width: MediaQuery.of(context).size.width, + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(top: 10), + child: Align( + alignment: Alignment.center, + child: Column( + children: [ + body.call(context), + Padding( + padding: EdgeInsets.only( + top: buttons.isNotEmpty ? 40 : 0, + bottom: 20, + left: 20, + right: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: (actions == null) ? buttons : actions!, + ), + ), + ], + ), + ), + ), + if (closeButton ?? false) ...[ + Padding( + padding: EdgeInsets.zero, + child: Align( + alignment: Alignment.topRight, + child: config.closeButtonBuilder.call( + context, + onPressed: () => Navigator.pop(context), + ), + ), + ), + ], + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/bottom_alert_dialog_config.dart b/lib/src/bottom_alert_dialog_config.dart new file mode 100644 index 0000000..c8465ec --- /dev/null +++ b/lib/src/bottom_alert_dialog_config.dart @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2022 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter/material.dart'; + +enum ButtonType { + primary, + secondary, + tertiary, +} + +typedef ButtonBuilder = Widget Function( + BuildContext context, { + required String text, + required void Function() onPressed, + ButtonType buttonType, +}); + +typedef CloseButtonBuilder = Widget Function( + BuildContext context, { + required void Function() onPressed, +}); + +class BottomAlertDialogConfig extends InheritedWidget { + const BottomAlertDialogConfig({ + required super.child, + ButtonBuilder? buttonBuilder, + CloseButtonBuilder? closeButtonBuilder, + this.yesText = 'Yes', + this.noText = 'No', + this.backgroundColor, + super.key, + }) : _buttonBuilder = buttonBuilder, + _closeButtonBuilder = closeButtonBuilder; + + final ButtonBuilder? _buttonBuilder; + final CloseButtonBuilder? _closeButtonBuilder; + final String yesText; + final String noText; + final Color? backgroundColor; + + ButtonBuilder get buttonBuilder => + _buttonBuilder ?? + ( + context, { + required onPressed, + required text, + buttonType = ButtonType.tertiary, + }) { + var theme = Theme.of(context); + switch (buttonType) { + case ButtonType.primary: + return ElevatedButton( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(theme.colorScheme.primary), + foregroundColor: MaterialStateProperty.all(Colors.black), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + side: BorderSide(color: theme.colorScheme.primary), + ), + ), + ), + onPressed: onPressed, + child: Text(text), + ); + case ButtonType.secondary: + return ElevatedButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(Colors.white), + foregroundColor: MaterialStateProperty.all(Colors.black), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + side: BorderSide(color: theme.colorScheme.primary), + ), + ), + ), + onPressed: onPressed, + child: Text(text), + ); + case ButtonType.tertiary: + return ElevatedButton( + style: ButtonStyle( + shadowColor: MaterialStateProperty.all(Colors.transparent), + backgroundColor: MaterialStateProperty.all(Colors.white), + foregroundColor: MaterialStateProperty.all(Colors.black), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + side: const BorderSide(color: Colors.white), + ), + ), + ), + onPressed: onPressed, + child: Text(text), + ); + } + }; + + CloseButtonBuilder get closeButtonBuilder => + _closeButtonBuilder ?? + (context, {required onPressed}) { + return IconButton( + onPressed: onPressed, + icon: const Icon( + Icons.close, + size: 25, + color: Colors.black, + ), + ); + }; + + static BottomAlertDialogConfig of(BuildContext context) { + var result = + context.dependOnInheritedWidgetOfExactType(); + assert(result != null, 'No BottomAlertDialogConfig found in context'); + return result!; + } + + @override + bool updateShouldNotify(BottomAlertDialogConfig oldWidget) => + buttonBuilder != oldWidget.buttonBuilder || + closeButtonBuilder != oldWidget.closeButtonBuilder; +} diff --git a/pubspec.yaml b/pubspec.yaml index c500562..373e5e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_dialogs description: A new Flutter package project. -version: 0.0.2 +version: 1.0.0 homepage: https://github.com/Iconica-Development/flutter_dialogs environment: