From c4f3b967f6c398a09310fd09d25bd5b824cdd1c8 Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 8 Apr 2026 01:36:19 +0800 Subject: [PATCH] feat: add interactive widgets (Dialog, BottomSheet, Picker) with PageStandards --- .../interactive/standard_bottom_sheet.dart | 76 ++++++++++++ .../widgets/interactive/standard_dialog.dart | 78 ++++++++++++ .../widgets/interactive/standard_picker.dart | 115 ++++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 lib/src/widgets/interactive/standard_bottom_sheet.dart create mode 100644 lib/src/widgets/interactive/standard_dialog.dart create mode 100644 lib/src/widgets/interactive/standard_picker.dart diff --git a/lib/src/widgets/interactive/standard_bottom_sheet.dart b/lib/src/widgets/interactive/standard_bottom_sheet.dart new file mode 100644 index 0000000..fa7fd53 --- /dev/null +++ b/lib/src/widgets/interactive/standard_bottom_sheet.dart @@ -0,0 +1,76 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +class StandardBottomSheet extends StatelessWidget { + final String? title; + final Widget child; + final double? height; + + const StandardBottomSheet({ + super.key, + this.title, + required this.child, + this.height, + }); + + static Future show({ + required BuildContext context, + String? title, + required Widget child, + double? height, + bool isScrollControlled = false, + }) { + return showModalBottomSheet( + context: context, + isScrollControlled: isScrollControlled, + backgroundColor: Colors.transparent, + builder: (context) => StandardBottomSheet( + title: title, + child: child, + height: height, + ), + ); + } + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + return Container( + height: height, + decoration: BoxDecoration( + color: standards.backgroundColor, + borderRadius: BorderRadius.vertical( + top: Radius.circular(standards.scaledRadius(20)), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: EdgeInsets.only(top: standards.scaledHeight(8)), + width: standards.scaledWidth(36), + height: standards.scaledHeight(4), + decoration: BoxDecoration( + color: standards.textColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(standards.scaledRadius(2)), + ), + ), + if (title != null) + Padding( + padding: standards.scaledPadding(EdgeInsets.all(16)), + child: Text( + title!, + style: standards.textStyle.copyWith( + fontSize: standards.fontSize + 4, + fontWeight: FontWeight.w600, + ), + ), + ), + Flexible(child: child), + ], + ), + ); + } +} diff --git a/lib/src/widgets/interactive/standard_dialog.dart b/lib/src/widgets/interactive/standard_dialog.dart new file mode 100644 index 0000000..1a956d1 --- /dev/null +++ b/lib/src/widgets/interactive/standard_dialog.dart @@ -0,0 +1,78 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +class StandardDialog extends StatelessWidget { + final String title; + final String? message; + final String? confirmText; + final String? cancelText; + final VoidCallback? onConfirm; + final VoidCallback? onCancel; + final bool isDestructive; + + const StandardDialog({ + super.key, + required this.title, + this.message, + this.confirmText, + this.cancelText, + this.onConfirm, + this.onCancel, + this.isDestructive = false, + }); + + static Future show( + BuildContext context, { + required String title, + String? message, + String? confirmText, + String? cancelText, + bool isDestructive = false, + }) { + return showCupertinoDialog( + context: context, + builder: (context) => StandardDialog( + title: title, + message: message, + confirmText: confirmText, + cancelText: cancelText, + isDestructive: isDestructive, + ), + ); + } + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + final l10n = standards.l10n; + + return CupertinoAlertDialog( + title: Text(title, style: standards.textStyle), + content: message != null + ? Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(message!, style: standards.textStyle), + ) + : null, + actions: [ + if (cancelText != null || onCancel != null) + CupertinoDialogAction( + onPressed: () { + Navigator.pop(context, false); + onCancel?.call(); + }, + child: Text(cancelText ?? l10n.cancel), + ), + CupertinoDialogAction( + isDestructiveAction: isDestructive, + onPressed: () { + Navigator.pop(context, true); + onConfirm?.call(); + }, + child: Text(confirmText ?? l10n.confirm), + ), + ], + ); + } +} diff --git a/lib/src/widgets/interactive/standard_picker.dart b/lib/src/widgets/interactive/standard_picker.dart new file mode 100644 index 0000000..75cc3bc --- /dev/null +++ b/lib/src/widgets/interactive/standard_picker.dart @@ -0,0 +1,115 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +class StandardPickerItem { + final T value; + final String label; + + StandardPickerItem({ + required this.value, + required this.label, + }); +} + +class StandardPicker extends StatelessWidget { + final List> items; + final T? selectedValue; + final ValueChanged? onChanged; + final String? title; + + const StandardPicker({ + super.key, + required this.items, + this.selectedValue, + this.onChanged, + this.title, + }); + + static Future show({ + required BuildContext context, + required List> items, + T? selectedValue, + String? title, + }) async { + final standards = PageStandards.of(context); + + return await showCupertinoModalPopup( + context: context, + builder: (context) => Container( + height: standards.scaledHeight(300), + decoration: BoxDecoration( + color: standards.backgroundColor, + borderRadius: BorderRadius.vertical( + top: Radius.circular(standards.scaledRadius(20)), + ), + ), + child: Column( + children: [ + if (title != null) + Container( + padding: standards.scaledPadding(EdgeInsets.all(16)), + child: Text( + title!, + style: standards.textStyle.copyWith( + fontSize: standards.fontSize + 4, + fontWeight: FontWeight.w600, + ), + ), + ), + Expanded( + child: CupertinoPicker( + itemExtent: standards.scaledHeight(40), + scrollController: FixedExtentScrollController( + initialItem: selectedValue != null + ? items.indexWhere((e) => e.value == selectedValue) + : 0, + ), + onSelectedItemChanged: (index) { + Navigator.pop(context, items[index].value); + }, + children: items + .map((item) => Center( + child: Text( + item.label, + style: standards.textStyle, + ), + )) + .toList(), + ), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + return Container( + height: standards.scaledHeight(200), + color: standards.backgroundColor, + child: CupertinoPicker( + itemExtent: standards.scaledHeight(40), + scrollController: FixedExtentScrollController( + initialItem: selectedValue != null + ? items.indexWhere((e) => e.value == selectedValue) + : 0, + ), + onSelectedItemChanged: (index) { + onChanged?.call(items[index].value); + }, + children: items + .map((item) => Center( + child: Text( + item.label, + style: standards.textStyle, + ), + )) + .toList(), + ), + ); + } +}