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 @override
void initState() { void initState() {
super.initState(); super.initState();
_player = widget.player; _player = widget.player;
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
playerNameCount = getNameCountText(_player);
_loadData(); _loadData();
} }
@override
void dispose() {
nameController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
playerNameCount = getNameCountText(_player);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@@ -173,14 +181,22 @@ class _PlayerDetailViewState extends State<PlayerDetailView> {
title: "Matches part of (${totalMatches})", title: "Matches part of (${totalMatches})",
icon: Icons.sports_esports, icon: Icons.sports_esports,
horizontalAlignment: CrossAxisAlignment.start, horizontalAlignment: CrossAxisAlignment.start,
content: Wrap( content: AppSkeleton(
alignment: WrapAlignment.start, enabled: isLoading,
crossAxisAlignment: WrapCrossAlignment.start, fixLayoutBuilder: true,
spacing: 12, alignment: Alignment.topLeft,
runSpacing: 8, child: Wrap(
children: playerMatches.map((match) { alignment: WrapAlignment.start,
return TextIconTile(text: match.name, iconEnabled: false); crossAxisAlignment: WrapCrossAlignment.start,
}).toList(), spacing: 12,
runSpacing: 8,
children: playerMatches.map((match) {
return TextIconTile(
text: match.name,
iconEnabled: false,
);
}).toList(),
),
), ),
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
@@ -188,14 +204,22 @@ class _PlayerDetailViewState extends State<PlayerDetailView> {
title: "Groups part of (${totalGroups})", title: "Groups part of (${totalGroups})",
icon: Icons.people, icon: Icons.people,
horizontalAlignment: CrossAxisAlignment.start, horizontalAlignment: CrossAxisAlignment.start,
content: Wrap( content: AppSkeleton(
alignment: WrapAlignment.start, enabled: isLoading,
crossAxisAlignment: WrapCrossAlignment.start, fixLayoutBuilder: true,
spacing: 12, alignment: Alignment.topLeft,
runSpacing: 8, child: Wrap(
children: playerGroups.map((group) { alignment: WrapAlignment.start,
return TextIconTile(text: group.name, iconEnabled: false); crossAxisAlignment: WrapCrossAlignment.start,
}).toList(), spacing: 12,
runSpacing: 8,
children: playerGroups.map((group) {
return TextIconTile(
text: group.name,
iconEnabled: false,
);
}).toList(),
),
), ),
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
@@ -204,6 +228,7 @@ class _PlayerDetailViewState extends State<PlayerDetailView> {
icon: Icons.bar_chart, icon: Icons.bar_chart,
content: AppSkeleton( content: AppSkeleton(
enabled: isLoading, enabled: isLoading,
fixLayoutBuilder: true,
child: Column( child: Column(
children: [ children: [
_buildStatRow( _buildStatRow(
@@ -227,44 +252,57 @@ class _PlayerDetailViewState extends State<PlayerDetailView> {
text: "Edit player", text: "Edit player",
icon: Icons.edit, icon: Icons.edit,
onPressed: () async { onPressed: () async {
final controller = TextEditingController(text: _player.name); nameController.text = _player.name;
showDialog<bool>( showDialog<bool>(
context: context, context: context,
builder: (context) => CustomAlertDialog( builder: (context) => StatefulBuilder(
title: "Change Name", builder: (context, setDialogState) {
content: TextInputField( return CustomAlertDialog(
controller: controller, title: "Change Name",
hintText: 'Set a player name', content: TextInputField(
), controller: nameController,
actions: [ hintText: 'Set a player name',
CustomDialogAction( onChanged: (_) => setDialogState(() {}),
onPressed: () => Navigator.of(context).pop(true), ),
text: "Confirm", actions: [
), CustomDialogAction(
CustomDialogAction( onPressed: isConfirmButtonEnabled()
onPressed: () => Navigator.of(context).pop(false), ? () => Navigator.of(context).pop(true)
buttonType: ButtonType.secondary, : null,
text: loc.cancel, text: "Confirm",
), ),
], CustomDialogAction(
onPressed: () => Navigator.of(context).pop(false),
buttonType: ButtonType.secondary,
text: loc.cancel,
),
],
);
},
), ),
).then((confirmed) async { ).then((confirmed) async {
if (confirmed! && context.mounted) { 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( await db.playerDao.updatePlayerName(
playerId: _player.id, playerId: _player.id,
name: controller.text, name: newName,
); );
widget.callback.call(); widget.callback.call();
setState(() { setState(() {
_player = Player( _player = Player(
name: controller.text, name: newName,
createdAt: _player.createdAt, createdAt: _player.createdAt,
id: _player.id, id: _player.id,
nameCount: _player.nameCount, nameCount: _player.nameCount,
description: _player.description, 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. /// - [child]: The widget tree to apply the skeleton effect to.
/// - [enabled]: A boolean to enable or disable the skeleton effect. /// - [enabled]: A boolean to enable or disable the skeleton effect.
/// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher. /// - [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({ const AppSkeleton({
super.key, super.key,
required this.child, required this.child,
this.enabled = true, this.enabled = true,
this.fixLayoutBuilder = false, this.fixLayoutBuilder = false,
this.alignment = Alignment.center,
}); });
/// The widget tree to apply the skeleton effect to. /// 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. /// A boolean to fix the layout builder for AnimatedSwitcher.
final bool fixLayoutBuilder; final bool fixLayoutBuilder;
/// The alignment used for the custom layout builder and optional Align wrapper
final Alignment alignment;
@override @override
State<AppSkeleton> createState() => _AppSkeletonState(); State<AppSkeleton> createState() => _AppSkeletonState();
} }
@@ -45,13 +50,14 @@ class _AppSkeletonState extends State<AppSkeleton> {
layoutBuilder: !widget.fixLayoutBuilder layoutBuilder: !widget.fixLayoutBuilder
? AnimatedSwitcher.defaultLayoutBuilder ? AnimatedSwitcher.defaultLayoutBuilder
: (Widget? currentChild, List<Widget> previousChildren) { : (Widget? currentChild, List<Widget> previousChildren) {
return Stack( final children = <Widget>[...previousChildren];
alignment: Alignment.topCenter, if (currentChild != null) children.add(currentChild);
children: [...previousChildren, ?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({ const AnimatedDialogButton({
super.key, super.key,
required this.buttonText, required this.buttonText,
required this.onPressed, this.onPressed,
this.buttonConstraints, this.buttonConstraints,
this.buttonType = ButtonType.primary, this.buttonType = ButtonType.primary,
this.isDescructive = false, this.isDescructive = false,
@@ -19,7 +19,7 @@ class AnimatedDialogButton extends StatefulWidget {
final String buttonText; final String buttonText;
final VoidCallback onPressed; final VoidCallback? onPressed;
final BoxConstraints? buttonConstraints; final BoxConstraints? buttonConstraints;
@@ -38,28 +38,38 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textStyling = _getTextStyling(); final textStyling = _getTextStyling();
final buttonDecoration = _getButtonDecoration(); final buttonDecoration = _getButtonDecoration();
bool _isDisabled = widget.onPressed == null;
return GestureDetector( return IgnorePointer(
onTapDown: (_) => setState(() => _isPressed = true), ignoring: _isDisabled,
onTapUp: (_) => setState(() => _isPressed = false), child: Opacity(
onTapCancel: () => setState(() => _isPressed = false), opacity: _isDisabled ? 0.5 : 1.0,
onTap: widget.onPressed, child: GestureDetector(
child: AnimatedScale( onTapDown: (_) => setState(() => _isPressed = true),
scale: _isPressed ? 0.95 : 1.0, onTapUp: (_) => setState(() => _isPressed = false),
duration: const Duration(milliseconds: 100), onTapCancel: () => setState(() => _isPressed = false),
child: AnimatedOpacity( onTap: widget.onPressed,
opacity: _isPressed ? 0.6 : 1.0, child: AnimatedScale(
duration: const Duration(milliseconds: 100), scale: _isPressed ? 0.95 : 1.0,
child: Center( duration: const Duration(milliseconds: 100),
child: Container( child: AnimatedOpacity(
constraints: widget.buttonConstraints, opacity: _isPressed ? 0.6 : 1.0,
decoration: buttonDecoration, duration: const Duration(milliseconds: 100),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Center(
margin: const EdgeInsets.symmetric(vertical: 8), child: Container(
child: Text( constraints: widget.buttonConstraints,
widget.buttonText, decoration: buttonDecoration,
style: textStyling, padding: const EdgeInsets.symmetric(
textAlign: TextAlign.center, 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 String title;
final Widget content; final Widget content;
final List<CustomDialogAction> actions; final List<CustomDialogAction> actions;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(

View File

@@ -10,7 +10,7 @@ class CustomDialogAction extends StatelessWidget {
/// - [onPressed]: Callback function that is triggered when the button is pressed. /// - [onPressed]: Callback function that is triggered when the button is pressed.
const CustomDialogAction({ const CustomDialogAction({
super.key, super.key,
required this.onPressed, this.onPressed,
required this.text, required this.text,
this.buttonType = ButtonType.primary, this.buttonType = ButtonType.primary,
this.isDestructive = false, this.isDestructive = false,
@@ -20,17 +20,18 @@ class CustomDialogAction extends StatelessWidget {
final ButtonType buttonType; final ButtonType buttonType;
final VoidCallback onPressed; final VoidCallback? onPressed;
final bool isDestructive; final bool isDestructive;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedDialogButton( return AnimatedDialogButton(
onPressed: () async { onPressed: onPressed != null
await HapticFeedback.selectionClick(); ? () async {
onPressed.call(); await HapticFeedback.selectionClick();
}, onPressed?.call();
}
: null,
buttonText: text, buttonText: text,
buttonType: buttonType, buttonType: buttonType,
isDescructive: isDestructive, isDescructive: isDestructive,