made alertDialog Confirm Button deactivate based on input, fix app skeleton alignment issue, implement correct nameCount Display
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 42s
Pull Request Pipeline / lint (pull_request) Failing after 51s

This commit is contained in:
2026-05-21 09:47:49 +02:00
parent 679e869229
commit b61a93328f
5 changed files with 133 additions and 75 deletions

View File

@@ -67,18 +67,26 @@ class _PlayerDetailViewState extends State<PlayerDetailView> {
),
);
TextEditingController nameController = TextEditingController();
@override
void initState() {
super.initState();
_player = widget.player;
db = Provider.of<AppDatabase>(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<PlayerDetailView> {
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<PlayerDetailView> {
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<PlayerDetailView> {
icon: Icons.bar_chart,
content: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
child: Column(
children: [
_buildStatRow(
@@ -227,44 +252,57 @@ class _PlayerDetailViewState extends State<PlayerDetailView> {
text: "Edit player",
icon: Icons.edit,
onPressed: () async {
final controller = TextEditingController(text: _player.name);
nameController.text = _player.name;
showDialog<bool>(
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<PlayerDetailView> {
),
);
}
bool isConfirmButtonEnabled() {
return nameController.text.trim().isNotEmpty;
}
}

View File

@@ -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<AppSkeleton> createState() => _AppSkeletonState();
}
@@ -45,13 +50,14 @@ class _AppSkeletonState extends State<AppSkeleton> {
layoutBuilder: !widget.fixLayoutBuilder
? AnimatedSwitcher.defaultLayoutBuilder
: (Widget? currentChild, List<Widget> previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: [...previousChildren, ?currentChild],
);
final children = <Widget>[...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,
);
}
}

View File

@@ -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<AnimatedDialogButton> {
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,
),
),
),
),
),

View File

@@ -19,7 +19,6 @@ class CustomAlertDialog extends StatelessWidget {
final String title;
final Widget content;
final List<CustomDialogAction> actions;
@override
Widget build(BuildContext context) {
return AlertDialog(

View File

@@ -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,