diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index a2d9ec1..9e3138b 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,4 +1,9 @@ PODS: + - audio_session (0.0.1): + - Flutter + - camerawesome (0.0.1): + - Flutter + - JPSVolumeButtonHandler - device_info_plus (0.0.1): - Flutter - DKImagePickerController/Core (4.3.4): @@ -54,6 +59,9 @@ PODS: - Flutter - irondash_engine_context (0.0.1): - Flutter + - JPSVolumeButtonHandler (1.0.5) + - just_audio (0.0.1): + - Flutter - libwebp (1.3.1): - libwebp/demux (= 1.3.1) - libwebp/mux (= 1.3.1) @@ -79,6 +87,9 @@ PODS: - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter + - record_darwin (1.0.0): + - Flutter + - FlutterMacOS - SDWebImage (5.17.0): - SDWebImage/Core (= 5.17.0) - SDWebImage/Core (5.17.0) @@ -107,6 +118,8 @@ PODS: - Flutter DEPENDENCIES: + - audio_session (from `.symlinks/plugins/audio_session/ios`) + - camerawesome (from `.symlinks/plugins/camerawesome/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) @@ -115,10 +128,12 @@ DEPENDENCIES: - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) + - just_audio (from `.symlinks/plugins/just_audio/ios`) - open_filex (from `.symlinks/plugins/open_filex/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - record_darwin (from `.symlinks/plugins/record_darwin/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/darwin`) @@ -132,6 +147,7 @@ SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery + - JPSVolumeButtonHandler - libwebp - Mantle - OrderedSet @@ -140,6 +156,10 @@ SPEC REPOS: - SwiftyGif EXTERNAL SOURCES: + audio_session: + :path: ".symlinks/plugins/audio_session/ios" + camerawesome: + :path: ".symlinks/plugins/camerawesome/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" file_picker: @@ -156,6 +176,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker_ios/ios" irondash_engine_context: :path: ".symlinks/plugins/irondash_engine_context/ios" + just_audio: + :path: ".symlinks/plugins/just_audio/ios" open_filex: :path: ".symlinks/plugins/open_filex/ios" package_info_plus: @@ -164,6 +186,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + record_darwin: + :path: ".symlinks/plugins/record_darwin/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: @@ -182,6 +206,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: + audio_session: 4f3e461722055d21515cf3261b64c973c062f345 + camerawesome: 1e06540f60158809bc70f398ed1ac2cf93fe4188 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 @@ -192,6 +218,8 @@ SPEC CHECKSUMS: flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 + JPSVolumeButtonHandler: 53110330c9168ed325def93eabff39f0fe3e8082 + just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa libwebp: 33dc822fbbf4503668d09f7885bbfedc76c45e96 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 @@ -199,6 +227,7 @@ SPEC CHECKSUMS: package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: 036b856153a2b1f61f21030ff725f3e6fece2b78 + record_darwin: 1f6619f2abac4d1ca91d3eeab038c980d76f1517 SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 SDWebImageWebPCoder: af09429398d99d524cae2fe00f6f0f6e491ed102 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 @@ -206,7 +235,7 @@ SPEC CHECKSUMS: sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f - url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 + url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 diff --git a/example/lib/pages/components/button.dart b/example/lib/pages/components/button.dart index 78491eb..78d7e08 100644 --- a/example/lib/pages/components/button.dart +++ b/example/lib/pages/components/button.dart @@ -116,28 +116,28 @@ class _ButtonDemoState extends State { child: const Text('Muted'), ), const SizedBox(height: 10), - CircleIconButton( + ZdsRoundButton( icon: ZetaIcons.end_call_round, label: "Reject", - type: CircleButtonType.negative, + type: ZdsRoundButtonType.negative, onTap: () { print("Tap"); }, ), const SizedBox(height: 10), - CircleIconButton( + ZdsRoundButton( icon: ZetaIcons.phone_round, label: "Accept", - type: CircleButtonType.positive, + type: ZdsRoundButtonType.positive, onTap: () { print("Tap"); }, ), const SizedBox(height: 10), - CircleIconButton( + ZdsRoundButton( icon: ZetaIcons.microphone_round, label: "Mute", - type: CircleButtonType.base, + type: ZdsRoundButtonType.base, activeIcon: ZetaIcons.microphone_off_round, activeLabel: "Un-Mute", onTap: () { @@ -145,10 +145,10 @@ class _ButtonDemoState extends State { }, ), const SizedBox(height: 10), - CircleIconButton( + ZdsRoundButton( icon: ZetaIcons.alert_round, label: "Security", - type: CircleButtonType.alert, + type: ZdsRoundButtonType.alert, onTap: () { print("Tap"); }, diff --git a/example/lib/pages/components/dial_pad.dart b/example/lib/pages/components/dial_pad.dart new file mode 100644 index 0000000..9835d90 --- /dev/null +++ b/example/lib/pages/components/dial_pad.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class DialPadExample extends StatelessWidget { + const DialPadExample({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ZdsButton.filled( + child: Text('ShowDialPad'), + onTap: () { + showZdsDialPadSheet( + context, + subtitle: 'Verified Number', + pages: { + 'Recents': Center(child: Text('Recents list goes here')), + 'Contacts': Center(child: Text('Contacts list goes here')), + }, + ); + }, + ), + const SizedBox(height: ZetaSpacing.xl), + ZdsButton.outlined( + child: Text('ShowDialPad (text)'), + onTap: () { + showZdsDialPadSheet( + context, + subtitle: 'Verified Number', + showText: true, + pages: { + 'Recents': Center(child: Text('Recents list goes here')), + 'Contacts': Center(child: Text('Contacts list goes here')), + }, + ); + }, + ), + ], + ), + ); + } +} diff --git a/example/lib/routes.dart b/example/lib/routes.dart index 904d0a5..d03c87a 100644 --- a/example/lib/routes.dart +++ b/example/lib/routes.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'pages/components/audio_recorder.dart'; import 'home.dart'; import 'pages/assets/animations.dart'; @@ -7,6 +6,7 @@ import 'pages/assets/icons.dart'; import 'pages/assets/images.dart'; import 'pages/components/app_bar.dart'; import 'pages/components/audio_player.dart'; +import 'pages/components/audio_recorder.dart'; import 'pages/components/big_toggle_button.dart'; import 'pages/components/block_table.dart'; import 'pages/components/bottom_bar.dart'; @@ -23,6 +23,7 @@ import 'pages/components/conditional_wrapper.dart'; import 'pages/components/date_picker.dart'; import 'pages/components/day_picker_demo.dart'; import 'pages/components/default_flutter.dart'; +import 'pages/components/dial_pad.dart'; import 'pages/components/empty_list_view.dart'; import 'pages/components/empty_view.dart'; import 'pages/components/expandable.dart'; @@ -122,6 +123,7 @@ final kRoutes = { const DemoRoute(title: 'Toast', wrapper: false, child: ToastDemo()), const DemoRoute(title: 'Toolbar', wrapper: false, child: ToolBarDemo()), const DemoRoute(title: 'Vertical Navigation', child: VerticalNavDemo()), + const DemoRoute(title: 'Dial Pad', child: DialPadExample()), ], 'Assets': [ const DemoRoute(title: 'Animations', child: AnimationsDemo()), diff --git a/example/macos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/macos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/example/macos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/example/pubspec.yaml b/example/pubspec.yaml index fc9e96d..81f458a 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -22,8 +22,6 @@ dependencies: path_provider: ^2.1.2 scrollable_positioned_list: ^0.3.8 url_launcher: ^6.2.5 - zeta_flutter: ^0.4.0 - cross_file: ^0.3.3+8 flutter: uses-material-design: true diff --git a/lib/src/components/atoms.dart b/lib/src/components/atoms.dart index 856639c..9d22e56 100644 --- a/lib/src/components/atoms.dart +++ b/lib/src/components/atoms.dart @@ -6,7 +6,6 @@ export 'atoms/back_button.dart'; export 'atoms/border_clipper.dart'; export 'atoms/button.dart'; export 'atoms/card.dart'; -export 'atoms/circle_icon_button.dart'; export 'atoms/conditional_wrapper.dart'; export 'atoms/dashed_line.dart'; export 'atoms/expandable.dart'; @@ -17,6 +16,7 @@ export 'atoms/interactive_viewer.dart'; export 'atoms/label.dart'; export 'atoms/notification.dart'; export 'atoms/popover.dart'; +export 'atoms/round_button.dart'; export 'atoms/selection_pills.dart'; export 'atoms/shake_animation.dart'; export 'atoms/slidable_widget.dart'; diff --git a/lib/src/components/atoms/circle_icon_button.dart b/lib/src/components/atoms/circle_icon_button.dart deleted file mode 100644 index d054821..0000000 --- a/lib/src/components/atoms/circle_icon_button.dart +++ /dev/null @@ -1,246 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import '../../../zds_flutter.dart'; - -/// Button type for [CircleIconButton] -enum CircleButtonType { - /// Positive button, defaults to green - positive, - - /// Negative button, defaults to red - negative, - - ///Alert button, defaults to white - alert, - - /// Untoggled button, defaults to white - base, - - /// Toggled button, defaults to black - toggled -} - -/// Button size for [CircleIconButton] -enum ButtonSize { - /// Large - large, - - /// Medium - medium, - - /// Small - small -} - -extension on CircleButtonType { - ZetaColorSwatch color(ZetaColors colors) { - switch (this) { - case CircleButtonType.positive: - return colors.positive; - case CircleButtonType.negative: - return colors.negative; - case CircleButtonType.alert: - return ZetaColorSwatch.fromColor(Colors.white); - case CircleButtonType.base: - return ZetaColorSwatch.fromColor(Colors.grey.shade100); - case CircleButtonType.toggled: - return ZetaColorSwatch.fromColor(Colors.black); - } - } - - ZetaColorSwatch borderColor(ZetaColors colors) { - switch (this) { - case CircleButtonType.alert: - return ZetaColorSwatch.fromColor(Colors.red); - case CircleButtonType.base: - return ZetaColorSwatch.fromColor(colors.borderSubtle); - case CircleButtonType.toggled: - return ZetaColorSwatch.fromColor(colors.borderSubtle); - case CircleButtonType.positive: - case CircleButtonType.negative: - return ZetaColorSwatch.fromColor(Colors.white); - } - } - - ZetaColorSwatch foregroundColor(ZetaColors colors) { - switch (this) { - case CircleButtonType.alert: - return ZetaColorSwatch.fromColor(Colors.red); - case CircleButtonType.base: - return ZetaColorSwatch.fromColor(Colors.black); - case CircleButtonType.toggled: - case CircleButtonType.positive: - case CircleButtonType.negative: - return ZetaColorSwatch.fromColor(Colors.white); - } - } - - bool get border => index > 1; -} - -/// Component [CircleIconButton] -class CircleIconButton extends StatefulWidget { - /// Constructor for [CircleIconButton] - - const CircleIconButton({ - super.key, - this.size = ButtonSize.large, - required this.type, - required this.icon, - required this.label, - this.activeIcon, - this.activeLabel, - required this.onTap, - }); - - /// Size for [CircleIconButton] - final ButtonSize size; - - /// Type of [CircleIconButton] - final CircleButtonType type; - - /// Default icon - final IconData icon; - - /// Default label - final String label; - - /// Toggled icon - final IconData? activeIcon; - - /// Toggled label - final String? activeLabel; - - /// Callback function - final VoidCallback onTap; - - @override - State createState() => _CircleIconButton(); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(EnumProperty('size', size)) - ..add(EnumProperty('type', type)) - ..add(DiagnosticsProperty('icon', icon)) - ..add(StringProperty('label', label)) - ..add(DiagnosticsProperty('activeIcon', activeIcon)) - ..add(StringProperty('activeLabel', activeLabel)) - ..add(ObjectFlagProperty.has('onTap', onTap)); - } -} - -class _CircleIconButton extends State { - CircleButtonType type = CircleButtonType.positive; - bool isPressed = false; - - @override - void initState() { - super.initState(); - type = widget.type; - } - - Future handleClick() async { - final bool isToggleable = type == CircleButtonType.toggled || type == CircleButtonType.base; - - //Change style to show button clicking effect - if (!isToggleable) { - setState(() { - isPressed = true; - }); - } - - await Future.delayed(const Duration(milliseconds: 100)); - - if (isToggleable) { - setState(() { - type = (type == CircleButtonType.toggled) ? CircleButtonType.base : CircleButtonType.toggled; - }); - } - - setState(() { - isPressed = false; - }); - - widget.onTap(); - } - - @override - Widget build(BuildContext context) { - final colors = Zeta.of(context).colors; - final bool toggled = type == CircleButtonType.toggled; - - return Column( - children: [ - GestureDetector( - onTap: handleClick, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - decoration: _animatedStyle(colors, isPressed), - child: Icon( - toggled ? widget.activeIcon : widget.icon, - size: _iconSize, - color: type.foregroundColor(colors), - ).padding(_iconPadding), - ), - ), - Text( - toggled ? widget.activeLabel! : widget.label, - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: widget.size == ButtonSize.small ? 14 : 16, - ), - ), - ] - .divide( - const SizedBox( - height: ZetaSpacing.xxs, - ), - ) - .toList(), - ); - } - - double get _iconPadding { - switch (widget.size) { - case ButtonSize.large: - return ZetaSpacing.x5; - case ButtonSize.medium: - return 15; - case ButtonSize.small: - return ZetaSpacing.x1; - } - } - - double get _iconSize { - switch (widget.size) { - case ButtonSize.large: - return ZetaSpacing.x10; - case ButtonSize.medium: - return ZetaSpacing.x7_5; - case ButtonSize.small: - return ZetaSpacing.x5; - } - } - - BoxDecoration _animatedStyle(ZetaColors colors, bool isPressed) { - return BoxDecoration( - color: isPressed ? type.color(colors).shade70 : type.color(colors), - borderRadius: const BorderRadius.all(Radius.circular(360)), - border: type.border - ? Border.all( - color: type.borderColor(colors), - ) - : null, - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(EnumProperty('type', type)) - ..add(DiagnosticsProperty('isPressed', isPressed)); - } -} diff --git a/lib/src/components/atoms/round_button.dart b/lib/src/components/atoms/round_button.dart new file mode 100644 index 0000000..fce469e --- /dev/null +++ b/lib/src/components/atoms/round_button.dart @@ -0,0 +1,244 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zds_flutter.dart'; + +/// Button type for [ZdsRoundButton] +enum ZdsRoundButtonType { + /// Positive button, defaults to green + positive, + + /// Negative button, defaults to red + negative, + + ///Alert button, defaults to white + alert, + + /// Untoggled button, defaults to white + base, + + /// Toggled button, defaults to black + toggled +} + +/// Button size for [ZdsRoundButton] +enum ZdsRoundButtonSize { + /// 100 x 100 + xlarge, + + /// 80 x 80 + large, + + /// 60 x 60 + medium, + + /// 44 x 44 + small +} + +extension on ZdsRoundButtonSize { + TextStyle get textStyle { + switch (this) { + case ZdsRoundButtonSize.xlarge: + case ZdsRoundButtonSize.large: + return ZetaTextStyles.labelLarge; + case ZdsRoundButtonSize.medium: + return ZetaTextStyles.labelMedium; + case ZdsRoundButtonSize.small: + return ZetaTextStyles.labelSmall; + } + } +} + +extension on ZdsRoundButtonType { + Color color(ZetaColors colors) { + switch (this) { + case ZdsRoundButtonType.positive: + return colors.positive; + case ZdsRoundButtonType.negative: + return colors.negative; + case ZdsRoundButtonType.alert: + return colors.surfacePrimary; + case ZdsRoundButtonType.base: + return colors.cool.shade20; + case ZdsRoundButtonType.toggled: + return colors.cool.shade90; + } + } + + Color borderColor(ZetaColors colors) { + switch (this) { + case ZdsRoundButtonType.alert: + return colors.error; + case ZdsRoundButtonType.base: + case ZdsRoundButtonType.toggled: + return colors.borderSubtle; + case ZdsRoundButtonType.positive: + case ZdsRoundButtonType.negative: + return Colors.transparent; + } + } + + bool get border => index > 1; +} + +/// Component [ZdsRoundButton] +class ZdsRoundButton extends StatefulWidget { + /// Constructor for [ZdsRoundButton] + + const ZdsRoundButton({ + super.key, + this.size = ZdsRoundButtonSize.large, + required this.type, + required this.icon, + this.label, + this.activeIcon, + this.activeLabel, + this.onTap, + }); + + /// Size for [ZdsRoundButton] + final ZdsRoundButtonSize size; + + /// Type of [ZdsRoundButton] + final ZdsRoundButtonType type; + + /// Default icon + final IconData icon; + + /// Default label + final String? label; + + /// Toggled icon + final IconData? activeIcon; + + /// Toggled label + final String? activeLabel; + + /// Callback function + final VoidCallback? onTap; + + @override + State createState() => _CircleIconButton(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('size', size)) + ..add(EnumProperty('type', type)) + ..add(DiagnosticsProperty('icon', icon)) + ..add(StringProperty('label', label)) + ..add(DiagnosticsProperty('activeIcon', activeIcon)) + ..add(StringProperty('activeLabel', activeLabel)) + ..add(ObjectFlagProperty.has('onTap', onTap)); + } +} + +class _CircleIconButton extends State { + late ZdsRoundButtonType type; + bool isPressed = false; + + @override + void initState() { + super.initState(); + type = widget.type; + } + + Future handleClick() async { + final bool isToggleable = type == ZdsRoundButtonType.toggled || type == ZdsRoundButtonType.base; + + //Change style to show button clicking effect + if (!isToggleable) { + setState(() { + isPressed = true; + }); + } + + await Future.delayed(const Duration(milliseconds: 100)); + + if (isToggleable) { + setState(() { + type = (type == ZdsRoundButtonType.toggled) ? ZdsRoundButtonType.base : ZdsRoundButtonType.toggled; + }); + } + + setState(() { + isPressed = false; + }); + + widget.onTap?.call(); + } + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + final bool toggled = type == ZdsRoundButtonType.toggled; + final backgroundColor = type.color(colors); + final foregroundColor = type == ZdsRoundButtonType.alert ? colors.negative : backgroundColor.onColor; + + return Column( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: ZetaRadius.full, + border: type.border ? Border.all(color: type.borderColor(colors), width: 2) : null, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: handleClick, + splashColor: Colors.transparent, + borderRadius: ZetaRadius.full, + child: Icon( + toggled ? widget.activeIcon : widget.icon, + size: _iconSize, + color: foregroundColor, + ).padding(_iconPadding), + ), + ), + ), + if (widget.label != null) + Text( + toggled ? widget.activeLabel! : widget.label!, + style: widget.size.textStyle, + ), + ].divide(const SizedBox(height: ZetaSpacing.xxs)).toList(), + ); + } + + double get _iconPadding { + switch (widget.size) { + case ZdsRoundButtonSize.xlarge: + return 25; + case ZdsRoundButtonSize.large: + return ZetaSpacing.x5; + case ZdsRoundButtonSize.medium: + return 15; + case ZdsRoundButtonSize.small: + return 11; + } + } + + double get _iconSize { + switch (widget.size) { + case ZdsRoundButtonSize.xlarge: + return 55; + case ZdsRoundButtonSize.large: + return ZetaSpacing.x10; + case ZdsRoundButtonSize.medium: + return ZetaSpacing.x7_5; + case ZdsRoundButtonSize.small: + return 22; + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('type', type)) + ..add(DiagnosticsProperty('isPressed', isPressed)); + } +} diff --git a/lib/src/components/organisms.dart b/lib/src/components/organisms.dart index db7b594..2909c88 100644 --- a/lib/src/components/organisms.dart +++ b/lib/src/components/organisms.dart @@ -9,6 +9,7 @@ export 'organisms/chat/chat.dart'; export 'organisms/date_range.dart'; export 'organisms/date_range_picker_tile.dart'; export 'organisms/day_picker.dart'; +export 'organisms/dial_pad.dart'; export 'organisms/file_picker/file_picker.dart'; export 'organisms/file_preview.dart'; export 'organisms/fiscal_date_picker.dart'; diff --git a/lib/src/components/organisms/dial_pad.dart b/lib/src/components/organisms/dial_pad.dart new file mode 100644 index 0000000..434b55b --- /dev/null +++ b/lib/src/components/organisms/dial_pad.dart @@ -0,0 +1,327 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// Dial pad gives the user the ability to dial a number and start a call. +/// +/// It also has a quick dial security action and a delete entry action. +class ZdsDialPad extends StatefulWidget { + /// Constructs a [ZdsDialPad]. + const ZdsDialPad({ + super.key, + this.subtitle, + this.subtitleColor, + this.onNumberTapped, + this.onCallTapped, + this.onSecurityTapped, + this.initialValue = '', + this.leftButton, + this.buttonValues, + this.onTextTapped, + this.showText = false, + }); + + /// (Optional) Text that appears above the entered number. + final String? subtitle; + + /// Color of subtitle text that appears above the entered number. + /// + /// Defaults to primary color. + final Color? subtitleColor; + + /// Returns tapped number for each tap. + /// + /// When backspace is selected, `-1` is returned. + final ValueChanged? onNumberTapped; + + /// Returns tapped text for each tap after a short delay + /// + /// When backspace is selected, `-1` is returned. + final ValueChanged? onTextTapped; + + /// Returns full number entered when call button is tapped. + final ValueChanged? onCallTapped; + + /// Returns full number entered when security button is tapped. + final ValueChanged? onSecurityTapped; + + /// Initial value in dial pad. + final String initialValue; + + /// Button to show left of call button, typically a [ZdsRoundButton] with [ZdsRoundButtonSize.medium]. + /// + /// Defaults to Security button using [onSecurityTapped]. + /// + /// To hide this button, pass an empty widget, such as a [SizedBox]. + final Widget? leftButton; + + /// See [ZetaDialPad.buttonValues]. + final Map? buttonValues; + + /// Whether the display should show text or number. + /// + /// Defaults to false (shows numbers). + final bool showText; + + @override + State createState() => _ZdsDialPadState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('subtitle', subtitle)) + ..add(ColorProperty('subtitleColor', subtitleColor)) + ..add(ObjectFlagProperty>.has('onNumberTapped', onNumberTapped)) + ..add(ObjectFlagProperty>.has('onCall', onCallTapped)) + ..add(ObjectFlagProperty>.has('onSecurity', onSecurityTapped)) + ..add(StringProperty('initialValue', initialValue)) + ..add(DiagnosticsProperty?>('buttonValues', buttonValues)) + ..add(ObjectFlagProperty?>.has('onTextTapped', onTextTapped)) + ..add(DiagnosticsProperty('showText', showText)); + } +} + +const Map _defaultButtonValues = { + '1': '', + '2': 'ABC', + '3': 'DEF', + '4': 'GHI', + '5': 'JKL', + '6': 'MNO', + '7': 'PQRS', + '8': 'TUV', + '9': 'WXYZ', + '*': '', + '0': '+', + '#': '', +}; + +class _ZdsDialPadState extends State with AutomaticKeepAliveClientMixin { + late String _number; + late String _text; + String? _lastTapped; + int _tapCounter = 0; + Timer? _debounce; + + @override + void initState() { + super.initState(); + _number = _text = widget.initialValue.toNumber; + } + + Map get _buttonValues => widget.buttonValues ?? _defaultButtonValues; + + void onNumberTap(String added) { + if (widget.showText) { + if (_lastTapped == added) { + _tapCounter++; + final letters = _buttonValues[_lastTapped]; + final List options = letters!.split(''); + final int index = _tapCounter % options.length; + _debounce?.cancel(); + + setState(() { + _text = _text.substring(0, _text.length - 1) + letters[index]; + _debounce = Timer(const Duration(milliseconds: 500), _debounceCallback); + }); + } else { + _debounce?.cancel(); + final letters = _buttonValues[added]; + final List options = letters!.split(''); + + setState(() { + _text += options.first; + _lastTapped = added; + _tapCounter = 0; + _debounce = Timer(const Duration(milliseconds: 500), _debounceCallback); + }); + } + } else { + setState(() => _number += added); + } + widget.onNumberTapped?.call(added); + } + + void _debounceCallback() { + setState(() { + _lastTapped = null; + _tapCounter = 0; + }); + } + + void backSpace() { + if (_number.isNotEmpty) setState(() => _number = _number.substring(0, _number.length - 1)); + if (_text.isNotEmpty) setState(() => _text = _text.substring(0, _text.length - 1)); + widget.onNumberTapped?.call('-1'); + widget.onTextTapped?.call('-1'); + } + + @override + Widget build(BuildContext context) { + super.build(context); + final colors = Zeta.of(context).colors; + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 384), + child: SingleChildScrollView( + child: Column( + children: [ + const SizedBox(height: ZetaSpacing.x4), + Text( + widget.subtitle ?? '', + style: ZetaTextStyles.bodySmall.apply(color: widget.subtitleColor ?? colors.primary), + ), + const SizedBox(height: ZetaSpacing.xs), + Text(widget.showText ? _text : _number, style: ZetaTextStyles.heading1), + const SizedBox(height: ZetaSpacing.m), + ZetaDialPad( + onNumber: onNumberTap, + buttonValues: widget.buttonValues, + onText: widget.onTextTapped, + ), + const SizedBox(height: ZetaSpacing.m), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + (widget.leftButton != null + ? widget.leftButton is ZdsRoundButton + ? ZdsRoundButton( + type: (widget.leftButton! as ZdsRoundButton).type, + icon: (widget.leftButton! as ZdsRoundButton).icon, + activeIcon: (widget.leftButton! as ZdsRoundButton).activeIcon, + activeLabel: (widget.leftButton! as ZdsRoundButton).activeLabel, + label: (widget.leftButton! as ZdsRoundButton).label, + onTap: (widget.leftButton! as ZdsRoundButton).onTap, + size: ZdsRoundButtonSize.medium, + ) + : SizedBox.square(dimension: 60, child: widget.leftButton) + : ZdsRoundButton( + type: ZdsRoundButtonType.alert, + icon: Icons.notifications_active_rounded, + label: ComponentStrings.of(context).get('SECURITY', 'Security'), + size: ZdsRoundButtonSize.medium, + onTap: () => widget.onSecurityTapped?.call(_number), + )) + .paddingTop(ZetaSpacing.x4), + ZdsRoundButton( + type: ZdsRoundButtonType.positive, + icon: ZetaIcons.phone_round, + onTap: () => widget.onCallTapped?.call(_number), + size: ZdsRoundButtonSize.xlarge, + ), + IconButton( + onPressed: backSpace, + icon: const Icon(ZetaIcons.backspace_round), + color: Zeta.of(context).colors.iconDisabled, + ), + ], + ).paddingHorizontal(ZetaSpacing.l), + ], + ).paddingHorizontal(ZetaSpacing.x3), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('number', _number)); + } + + @override + bool get wantKeepAlive => true; +} + +extension on String? { + String get toNumber => this == null ? '' : this!.replaceAll(RegExp('[^0-9+*#]'), ''); +} + +const double _dialPadSheetSizeMax = 680; +const double _dialPadSheetSizeMin = 400; + +/// Helper method to show a [ZdsDialPad] in a Modal bottom sheet. +Future showZdsDialPadSheet( + BuildContext context, { + String? subtitle, + Color? subtitleColor, + ValueChanged? onNumberTapped, + ValueChanged? onCallTapped, + ValueChanged? onSecurityTapped, + String initialValue = '', + Map? pages, + Map? buttonValues, + Widget? leftButton, + ValueChanged? onTextTapped, + bool showText = false, +}) async { + final colors = Zeta.of(context).colors; + await showModalBottomSheet( + context: context, + isScrollControlled: true, + constraints: BoxConstraints( + maxWidth: _dialPadSheetSizeMax, + minWidth: MediaQuery.of(context).size.width.clamp(0, _dialPadSheetSizeMin), + maxHeight: _dialPadSheetSizeMax + MediaQuery.of(context).padding.bottom, + ), + backgroundColor: colors.surfaceTertiary, + builder: (context) { + return SafeArea( + child: DefaultTabController( + length: (pages?.length ?? 0) + 1, + child: Column( + children: [ + Container( + width: ZetaSpacing.x11, + height: ZetaSpacing.x1, + margin: const EdgeInsets.only(top: ZetaSpacing.x5, bottom: ZetaSpacing.x2), + decoration: BoxDecoration( + color: colors.borderDefault, + borderRadius: ZetaRadius.wide, + ), + ), + Expanded( + child: Scaffold( + body: Column( + children: [ + Expanded( + child: TabBarView( + children: [ + ZdsDialPad( + subtitle: subtitle, + subtitleColor: subtitleColor, + onNumberTapped: onNumberTapped, + onCallTapped: onCallTapped, + onSecurityTapped: onSecurityTapped, + initialValue: initialValue, + buttonValues: buttonValues, + leftButton: leftButton, + onTextTapped: onTextTapped, + showText: showText, + ), + if (pages != null && pages.isNotEmpty) + ...List.generate(pages.length, (index) => pages.values.toList()[index]), + ], + ), + ), + const SizedBox(height: ZetaSpacing.x6), + if (pages != null && pages.isNotEmpty) + ZdsTabBar( + tabs: [ + ZdsTab(label: ComponentStrings.of(context).get('DIAL_PAD', 'Dial Pad')), + ...List.generate(pages.length, (index) => ZdsTab(label: pages.keys.toList()[index])), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ); +} diff --git a/pubspec.yaml b/pubspec.yaml index ab9ce86..288955f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,7 +73,7 @@ dependencies: video_compress: ^3.1.0 video_player: ^2.7.2 vsc_quill_delta_to_html: ^1.0.3 - zeta_flutter: ^0.4.0 + zeta_flutter: ^0.5.0 dev_dependencies: flutter_test: