feat: basic integration of teams

This commit is contained in:
2026-05-17 21:29:16 +02:00
parent badf5ea311
commit a957408c7e
20 changed files with 1325 additions and 325 deletions

View File

@@ -16,7 +16,7 @@ class MainMenuButton extends StatefulWidget {
});
/// The callback to be invoked when the button is pressed.
final void Function() onPressed;
final void Function()? onPressed;
/// The icon of the button.
final IconData icon;
@@ -31,9 +31,11 @@ class MainMenuButton extends StatefulWidget {
}
class _MainMenuButtonState extends State<MainMenuButton>
with SingleTickerProviderStateMixin {
with TickerProviderStateMixin {
late AnimationController _animationController;
late AnimationController _disabledAnimationController;
late Animation<double> _scaleAnimation;
late Animation<double> _disabledScaleAnimation;
/// How long the button needs to be pressed to register it as long press
Timer? _longPressTimer;
@@ -52,37 +54,59 @@ class _MainMenuButtonState extends State<MainMenuButton>
vsync: this,
);
_disabledAnimationController = AnimationController(
duration: const Duration(milliseconds: 100),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
_disabledScaleAnimation = Tween<double>(begin: 1.0, end: 0.98).animate(
CurvedAnimation(
parent: _disabledAnimationController,
curve: Curves.easeInOut,
),
);
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scaleAnimation,
scale: widget.onPressed == null
? _disabledScaleAnimation
: _scaleAnimation,
child: GestureDetector(
onTapDown: (_) {
_animationController.forward();
if (widget.onLongPressed != null) {
_longPressTimer = Timer(const Duration(milliseconds: 400), () {
_isLongPressing = true;
widget.onLongPressed?.call();
_repeatTimer = Timer.periodic(
const Duration(milliseconds: 250),
(_) => widget.onLongPressed?.call(),
);
});
if (widget.onPressed == null) {
_disabledAnimationController.forward();
} else {
_animationController.forward();
if (widget.onLongPressed != null) {
_longPressTimer = Timer(const Duration(milliseconds: 400), () {
_isLongPressing = true;
widget.onLongPressed?.call();
_repeatTimer = Timer.periodic(
const Duration(milliseconds: 250),
(_) => widget.onLongPressed?.call(),
);
});
}
}
},
onTapUp: (_) async {
_cancelTimers();
if (mounted && !_isLongPressing) {
widget.onPressed();
if (widget.onPressed == null) {
_disabledAnimationController.reverse();
} else {
_cancelTimers();
if (mounted && !_isLongPressing) {
widget.onPressed?.call();
}
_isLongPressing = false;
await Future.delayed(const Duration(milliseconds: 100));
await _animationController.reverse();
}
_isLongPressing = false;
await Future.delayed(const Duration(milliseconds: 100));
await _animationController.reverse();
},
onTapCancel: () {
_isLongPressing = false;
@@ -91,7 +115,7 @@ class _MainMenuButtonState extends State<MainMenuButton>
},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
color: widget.onPressed == null ? Colors.grey : Colors.white,
borderRadius: BorderRadius.circular(30),
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
@@ -122,6 +146,7 @@ class _MainMenuButtonState extends State<MainMenuButton>
void dispose() {
_cancelTimers();
_animationController.dispose();
_disabledAnimationController.dispose();
super.dispose();
}

View File

@@ -57,7 +57,6 @@ class TextInputField extends StatelessWidget {
filled: true,
fillColor: CustomTheme.boxColor,
hintText: hintText,
hintStyle: const TextStyle(fontSize: 18),
counterText: showCounterText ? null : '',
enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),

View File

@@ -8,13 +8,13 @@ class CustomRadioListTile<T> extends StatelessWidget {
/// - [onContainerTap]: The callback invoked when the container is tapped.
const CustomRadioListTile({
super.key,
required this.text,
required this.content,
required this.value,
required this.onContainerTap,
});
/// The text to display next to the radio button.
final String text;
final Widget content;
/// The value associated with the radio button.
final T value;
@@ -37,16 +37,7 @@ class CustomRadioListTile<T> extends StatelessWidget {
child: Row(
children: [
Radio<T>(value: value, toggleable: true),
Expanded(
child: Text(
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
Expanded(child: content),
],
),
),

View File

@@ -5,36 +5,34 @@ import 'package:tallee/l10n/generated/app_localizations.dart';
class ScoreListTile extends StatelessWidget {
/// A custom list tile widget that has a text field for inputting a score.
/// - [text]: The leading text to be displayed.
/// - [content]: The leading Widget to be displayed.
/// - [controller]: The controller for the text field to input the score.
const ScoreListTile({
super.key,
required this.text,
required this.content,
required this.controller,
this.horizontalPadding = 20,
});
/// The text to display next to the radio button.
final String text;
final Widget content;
final TextEditingController controller;
final double horizontalPadding;
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 20),
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
decoration: const BoxDecoration(color: CustomTheme.boxColor),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500),
),
content,
SizedBox(
width: 100,
height: 40,

View File

@@ -128,6 +128,12 @@ class _MatchTileState extends State<MatchTile> {
const SizedBox(height: 12),
Text(
'team match: ${match.isTeamMatch}',
style: const TextStyle(fontSize: 14, color: Colors.white),
),
const SizedBox(height: 12),
// Winner / In Progress Info
if (match.mvp.isNotEmpty) ...[
Container(

View File

@@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class TeamCreationTile extends StatefulWidget {
const TeamCreationTile({
super.key,
required this.color,
required this.controller,
required this.players,
required this.hintText,
this.onDelete,
this.onColorSelection,
this.onPlayerTap,
});
final GameColor color;
final List<Player> players;
final TextEditingController controller;
final String hintText;
final VoidCallback? onDelete;
final ValueChanged<GameColor>? onColorSelection;
final void Function(Player player)? onPlayerTap;
@override
State<TeamCreationTile> createState() => _TeamCreationTileState();
}
class _TeamCreationTileState extends State<TeamCreationTile> {
@override
Widget build(BuildContext context) {
return Container(
margin: CustomTheme.standardMargin,
padding: const EdgeInsets.all(12),
decoration: CustomTheme.standardBoxDecoration,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: TextInputField(
controller: widget.controller,
hintText: widget.hintText,
maxLength: Constants.MAX_TEAM_NAME_LENGTH,
),
),
const SizedBox(width: 8),
IconButton(
onPressed: () => widget.onDelete?.call(),
icon: const Icon(Icons.delete, size: 24),
),
],
),
const SizedBox(height: 8),
const Text(
'Color',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: GameColor.values.map((color) {
final isSelected = widget.color == color;
return GestureDetector(
onTap: () {
widget.onColorSelection?.call(color);
},
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: getColorFromGameColor(color),
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? Colors.white : Colors.transparent,
width: 3,
),
),
child: isSelected
? const Icon(Icons.check, size: 18, color: Colors.white)
: null,
),
);
}).toList(),
),
const SizedBox(height: 12),
const Text(
'Players',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor,
),
),
const SizedBox(height: 8),
if (widget.players.isEmpty)
const Text(
'Keine Spieler:innen zugewiesen',
style: TextStyle(color: CustomTheme.hintColor),
)
else
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.players
.map(
(player) => GestureDetector(
onTap: () => widget.onPlayerTap?.call(player),
child: TextIconTile(
text: player.name,
suffixText: getNameCountText(player),
iconEnabled: widget.onPlayerTap != null,
onIconTap: () => widget.onPlayerTap?.call(player),
),
),
)
.toList(),
),
],
),
);
}
}

View File

@@ -11,6 +11,7 @@ class TextIconListTile extends StatelessWidget {
required this.text,
this.suffixText = '',
this.icon,
this.color,
this.onPressed,
});
@@ -23,6 +24,8 @@ class TextIconListTile extends StatelessWidget {
/// The icon to display in the tile.
final IconData? icon;
final Color? color;
/// The callback to be invoked when the icon is pressed.
final VoidCallback? onPressed;
@@ -31,7 +34,17 @@ class TextIconListTile extends StatelessWidget {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 15),
decoration: CustomTheme.standardBoxDecoration,
decoration: BoxDecoration(
color:
Color.lerp(CustomTheme.onBoxColor, color?.withAlpha(10), 0.1) ??
CustomTheme.boxColor,
border: Border.all(
color: color ?? CustomTheme.boxBorderColor,
width: color != null ? 2 : 1,
strokeAlign: BorderSide.strokeAlignCenter,
),
borderRadius: CustomTheme.standardBorderRadiusAll,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,

View File

@@ -6,12 +6,14 @@ class TextIconTile extends StatelessWidget {
/// - [text]: The text to display in the tile.
/// - [iconEnabled]: A boolean to determine if the icon should be displayed.
/// - [onIconTap]: The callback to be invoked when the icon is tapped.
/// - [icon]: Optional custom icon. Defaults to [Icons.close].
const TextIconTile({
super.key,
required this.text,
this.suffixText = '',
this.iconEnabled = true,
this.onIconTap,
this.icon = Icons.close,
});
/// The text to display in the tile.
@@ -25,6 +27,9 @@ class TextIconTile extends StatelessWidget {
/// The callback to be invoked when the icon is tapped.
final VoidCallback? onIconTap;
/// The icon to display. Defaults to [Icons.close].
final IconData icon;
@override
Widget build(BuildContext context) {
return Container(
@@ -65,10 +70,7 @@ class TextIconTile extends StatelessWidget {
),
if (iconEnabled) ...<Widget>[
const SizedBox(width: 3),
GestureDetector(
onTap: onIconTap,
child: const Icon(Icons.close, size: 20),
),
GestureDetector(onTap: onIconTap, child: Icon(icon, size: 20)),
],
],
),