From b61a93328ffb85f1f74dae9adbe63abd39688d0d Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Thu, 21 May 2026 09:47:49 +0200 Subject: [PATCH] made alertDialog Confirm Button deactivate based on input, fix app skeleton alignment issue, implement correct nameCount Display --- .../views/main_menu/player_detail_view.dart | 120 ++++++++++++------ lib/presentation/widgets/app_skeleton.dart | 16 ++- .../buttons/animated_dialog_button.dart | 56 ++++---- .../widgets/dialog/custom_alert_dialog.dart | 1 - .../widgets/dialog/custom_dialog_action.dart | 15 ++- 5 files changed, 133 insertions(+), 75 deletions(-) diff --git a/lib/presentation/views/main_menu/player_detail_view.dart b/lib/presentation/views/main_menu/player_detail_view.dart index 75c2ff6..512bdfd 100644 --- a/lib/presentation/views/main_menu/player_detail_view.dart +++ b/lib/presentation/views/main_menu/player_detail_view.dart @@ -67,18 +67,26 @@ class _PlayerDetailViewState extends State { ), ); + TextEditingController nameController = TextEditingController(); + @override void initState() { super.initState(); _player = widget.player; db = Provider.of(context, listen: false); + playerNameCount = getNameCountText(_player); _loadData(); } + @override + void dispose() { + nameController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); - playerNameCount = getNameCountText(_player); return Scaffold( appBar: AppBar( @@ -173,14 +181,22 @@ class _PlayerDetailViewState extends State { title: "Matches part of (${totalMatches})", icon: Icons.sports_esports, horizontalAlignment: CrossAxisAlignment.start, - content: Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 12, - runSpacing: 8, - children: playerMatches.map((match) { - return TextIconTile(text: match.name, iconEnabled: false); - }).toList(), + content: AppSkeleton( + enabled: isLoading, + fixLayoutBuilder: true, + alignment: Alignment.topLeft, + child: Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: playerMatches.map((match) { + return TextIconTile( + text: match.name, + iconEnabled: false, + ); + }).toList(), + ), ), ), const SizedBox(height: 15), @@ -188,14 +204,22 @@ class _PlayerDetailViewState extends State { title: "Groups part of (${totalGroups})", icon: Icons.people, horizontalAlignment: CrossAxisAlignment.start, - content: Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 12, - runSpacing: 8, - children: playerGroups.map((group) { - return TextIconTile(text: group.name, iconEnabled: false); - }).toList(), + content: AppSkeleton( + enabled: isLoading, + fixLayoutBuilder: true, + alignment: Alignment.topLeft, + child: Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: playerGroups.map((group) { + return TextIconTile( + text: group.name, + iconEnabled: false, + ); + }).toList(), + ), ), ), const SizedBox(height: 15), @@ -204,6 +228,7 @@ class _PlayerDetailViewState extends State { icon: Icons.bar_chart, content: AppSkeleton( enabled: isLoading, + fixLayoutBuilder: true, child: Column( children: [ _buildStatRow( @@ -227,44 +252,57 @@ class _PlayerDetailViewState extends State { text: "Edit player", icon: Icons.edit, onPressed: () async { - final controller = TextEditingController(text: _player.name); + nameController.text = _player.name; showDialog( context: context, - builder: (context) => CustomAlertDialog( - title: "Change Name", - content: TextInputField( - controller: controller, - hintText: 'Set a player name', - ), - actions: [ - CustomDialogAction( - onPressed: () => Navigator.of(context).pop(true), - text: "Confirm", - ), - CustomDialogAction( - onPressed: () => Navigator.of(context).pop(false), - buttonType: ButtonType.secondary, - text: loc.cancel, - ), - ], + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) { + return CustomAlertDialog( + title: "Change Name", + content: TextInputField( + controller: nameController, + hintText: 'Set a player name', + onChanged: (_) => setDialogState(() {}), + ), + actions: [ + CustomDialogAction( + onPressed: isConfirmButtonEnabled() + ? () => Navigator.of(context).pop(true) + : null, + text: "Confirm", + ), + CustomDialogAction( + onPressed: () => Navigator.of(context).pop(false), + buttonType: ButtonType.secondary, + text: loc.cancel, + ), + ], + ); + }, ), ).then((confirmed) async { if (confirmed! && context.mounted) { - if (controller.text != _player.name) { + final newName = nameController.text.trim(); + + if (newName != _player.name) { + final fetchedPlayerNameCount = await db.playerDao + .getNameCount(name: newName); await db.playerDao.updatePlayerName( playerId: _player.id, - name: controller.text, + name: newName, ); widget.callback.call(); setState(() { _player = Player( - name: controller.text, + name: newName, createdAt: _player.createdAt, id: _player.id, nameCount: _player.nameCount, description: _player.description, ); - playerNameCount = getNameCountText(_player); + playerNameCount = fetchedPlayerNameCount != null + ? ' #${fetchedPlayerNameCount + 1}' + : ''; }); } } @@ -330,4 +368,8 @@ class _PlayerDetailViewState extends State { ), ); } + + bool isConfirmButtonEnabled() { + return nameController.text.trim().isNotEmpty; + } } diff --git a/lib/presentation/widgets/app_skeleton.dart b/lib/presentation/widgets/app_skeleton.dart index 8a21320..abdfb8d 100644 --- a/lib/presentation/widgets/app_skeleton.dart +++ b/lib/presentation/widgets/app_skeleton.dart @@ -6,11 +6,13 @@ class AppSkeleton extends StatefulWidget { /// - [child]: The widget tree to apply the skeleton effect to. /// - [enabled]: A boolean to enable or disable the skeleton effect. /// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher. + /// - [alignment]: The alignment used for the custom layout builder and optional Align wrapper. Defaults to [Alignment.center]. const AppSkeleton({ super.key, required this.child, this.enabled = true, this.fixLayoutBuilder = false, + this.alignment = Alignment.center, }); /// The widget tree to apply the skeleton effect to. @@ -22,6 +24,9 @@ class AppSkeleton extends StatefulWidget { /// A boolean to fix the layout builder for AnimatedSwitcher. final bool fixLayoutBuilder; + /// The alignment used for the custom layout builder and optional Align wrapper + final Alignment alignment; + @override State createState() => _AppSkeletonState(); } @@ -45,13 +50,14 @@ class _AppSkeletonState extends State { layoutBuilder: !widget.fixLayoutBuilder ? AnimatedSwitcher.defaultLayoutBuilder : (Widget? currentChild, List previousChildren) { - return Stack( - alignment: Alignment.topCenter, - children: [...previousChildren, ?currentChild], - ); + final children = [...previousChildren]; + if (currentChild != null) children.add(currentChild); + return Stack(alignment: widget.alignment, children: children); }, ), - child: widget.child, + child: widget.fixLayoutBuilder + ? Align(alignment: widget.alignment, child: widget.child) + : widget.child, ); } } diff --git a/lib/presentation/widgets/buttons/animated_dialog_button.dart b/lib/presentation/widgets/buttons/animated_dialog_button.dart index 8c8765e..62960ff 100644 --- a/lib/presentation/widgets/buttons/animated_dialog_button.dart +++ b/lib/presentation/widgets/buttons/animated_dialog_button.dart @@ -11,7 +11,7 @@ class AnimatedDialogButton extends StatefulWidget { const AnimatedDialogButton({ super.key, required this.buttonText, - required this.onPressed, + this.onPressed, this.buttonConstraints, this.buttonType = ButtonType.primary, this.isDescructive = false, @@ -19,7 +19,7 @@ class AnimatedDialogButton extends StatefulWidget { final String buttonText; - final VoidCallback onPressed; + final VoidCallback? onPressed; final BoxConstraints? buttonConstraints; @@ -38,28 +38,38 @@ class _AnimatedDialogButtonState extends State { Widget build(BuildContext context) { final textStyling = _getTextStyling(); final buttonDecoration = _getButtonDecoration(); + bool _isDisabled = widget.onPressed == null; - return GestureDetector( - onTapDown: (_) => setState(() => _isPressed = true), - onTapUp: (_) => setState(() => _isPressed = false), - onTapCancel: () => setState(() => _isPressed = false), - onTap: widget.onPressed, - child: AnimatedScale( - scale: _isPressed ? 0.95 : 1.0, - duration: const Duration(milliseconds: 100), - child: AnimatedOpacity( - opacity: _isPressed ? 0.6 : 1.0, - duration: const Duration(milliseconds: 100), - child: Center( - child: Container( - constraints: widget.buttonConstraints, - decoration: buttonDecoration, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - margin: const EdgeInsets.symmetric(vertical: 8), - child: Text( - widget.buttonText, - style: textStyling, - textAlign: TextAlign.center, + return IgnorePointer( + ignoring: _isDisabled, + child: Opacity( + opacity: _isDisabled ? 0.5 : 1.0, + child: GestureDetector( + onTapDown: (_) => setState(() => _isPressed = true), + onTapUp: (_) => setState(() => _isPressed = false), + onTapCancel: () => setState(() => _isPressed = false), + onTap: widget.onPressed, + child: AnimatedScale( + scale: _isPressed ? 0.95 : 1.0, + duration: const Duration(milliseconds: 100), + child: AnimatedOpacity( + opacity: _isPressed ? 0.6 : 1.0, + duration: const Duration(milliseconds: 100), + child: Center( + child: Container( + constraints: widget.buttonConstraints, + decoration: buttonDecoration, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + margin: const EdgeInsets.symmetric(vertical: 8), + child: Text( + widget.buttonText, + style: textStyling, + textAlign: TextAlign.center, + ), + ), ), ), ), diff --git a/lib/presentation/widgets/dialog/custom_alert_dialog.dart b/lib/presentation/widgets/dialog/custom_alert_dialog.dart index 606fc49..7dfba98 100644 --- a/lib/presentation/widgets/dialog/custom_alert_dialog.dart +++ b/lib/presentation/widgets/dialog/custom_alert_dialog.dart @@ -19,7 +19,6 @@ class CustomAlertDialog extends StatelessWidget { final String title; final Widget content; final List actions; - @override Widget build(BuildContext context) { return AlertDialog( diff --git a/lib/presentation/widgets/dialog/custom_dialog_action.dart b/lib/presentation/widgets/dialog/custom_dialog_action.dart index 26dc40d..0c0b2e0 100644 --- a/lib/presentation/widgets/dialog/custom_dialog_action.dart +++ b/lib/presentation/widgets/dialog/custom_dialog_action.dart @@ -10,7 +10,7 @@ class CustomDialogAction extends StatelessWidget { /// - [onPressed]: Callback function that is triggered when the button is pressed. const CustomDialogAction({ super.key, - required this.onPressed, + this.onPressed, required this.text, this.buttonType = ButtonType.primary, this.isDestructive = false, @@ -20,17 +20,18 @@ class CustomDialogAction extends StatelessWidget { final ButtonType buttonType; - final VoidCallback onPressed; + final VoidCallback? onPressed; final bool isDestructive; - @override Widget build(BuildContext context) { return AnimatedDialogButton( - onPressed: () async { - await HapticFeedback.selectionClick(); - onPressed.call(); - }, + onPressed: onPressed != null + ? () async { + await HapticFeedback.selectionClick(); + onPressed?.call(); + } + : null, buttonText: text, buttonType: buttonType, isDescructive: isDestructive,