Revert to 4bd2f97
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 45s
Pull Request Pipeline / localizations (pull_request) Successful in 29s
Pull Request Pipeline / test (pull_request) Successful in 1m31s

This commit is contained in:
2026-05-25 19:22:59 +02:00
parent 6679a0f942
commit eaf7822732
70 changed files with 6970 additions and 839 deletions

View File

@@ -51,7 +51,7 @@ class GameTile extends StatelessWidget {
? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white)
: Colors.white;
final gameColor = badgeColor ?? getColorFromGameColor(GameColor.orange);
final gameColor = badgeColor ?? getColorFromAppColor(AppColor.orange);
return GestureDetector(
onTap: () async {

View File

@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/presentation/views/main_menu/player_detail_view.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class GroupTile extends StatefulWidget {
@@ -15,6 +17,7 @@ class GroupTile extends StatefulWidget {
required this.group,
this.isHighlighted = false,
this.onTap,
this.onPlayerChanged,
});
/// The group data to be displayed.
@@ -26,6 +29,9 @@ class GroupTile extends StatefulWidget {
/// Callback function to be executed when the tile is tapped.
final VoidCallback? onTap;
/// Callback function to be executed when the players in the group are changed.
final VoidCallback? onPlayerChanged;
@override
State<GroupTile> createState() => _GroupTileState();
}
@@ -92,6 +98,19 @@ class _GroupTileState extends State<GroupTile> {
text: member.name,
suffixText: getNameCountText(member),
iconEnabled: false,
onTileTap: () {
Navigator.push(
context,
adaptivePageRoute(
builder: (context) => PlayerDetailView(
player: member,
callback: () {
widget.onPlayerChanged?.call();
},
),
),
);
},
),
],
),

View File

@@ -3,11 +3,13 @@ import 'dart:core' hide Match;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/player_detail_view.dart';
import 'package:tallee/presentation/widgets/game_label.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -24,6 +26,7 @@ class MatchTile extends StatefulWidget {
required this.onTap,
this.width,
this.compact = false,
this.onPlayerEdited,
});
/// The match data to be displayed.
@@ -32,6 +35,9 @@ class MatchTile extends StatefulWidget {
/// The callback invoked when the tile is tapped.
final VoidCallback onTap;
/// The callback invoked when the players are edited
final VoidCallback? onPlayerEdited;
/// Optional width for the tile.
final double? width;
@@ -224,6 +230,19 @@ class _MatchTileState extends State<MatchTile> {
text: player.name,
suffixText: getNameCountText(player),
iconEnabled: false,
onTileTap: () {
Navigator.push(
context,
adaptivePageRoute(
builder: (context) => PlayerDetailView(
player: player,
callback: () {
widget.onPlayerEdited?.call();
},
),
),
);
},
);
}).toList(),
),

View File

@@ -1,8 +1,12 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:fluttericon/rpg_awesome_icons.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
@@ -21,8 +25,11 @@ class StatisticsTile extends StatelessWidget {
required this.title,
required this.width,
required this.values,
required this.itemCount,
required this.barColor,
required this.displayCount,
this.selectedGroups,
this.selectedGames,
this.showAllValues = false,
});
/// The icon displayed next to the title.
@@ -37,12 +44,16 @@ class StatisticsTile extends StatelessWidget {
/// A list of tuples containing labels and their corresponding numeric values.
final List<(Player, num)> values;
/// The maximum number of items to display.
final int itemCount;
/// The color of the bars representing the values.
final Color barColor;
// statistic data
final int displayCount;
final List<Group>? selectedGroups;
final List<Game>? selectedGames;
final bool showAllValues;
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
@@ -52,91 +63,202 @@ class StatisticsTile extends StatelessWidget {
title: title,
icon: icon,
content: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Visibility(
visible: values.isNotEmpty,
// No data avaiable message
replacement: Center(
heightFactor: 4,
child: Text(loc.no_data_available),
),
// Bar chart
child: LayoutBuilder(
builder: (context, constraints) {
final maxBarWidth = constraints.maxWidth * 0.65;
final maxBarWidth = constraints.maxWidth * 0.8;
// If displayCount wasnt provided, take all values
final valuesShown = showAllValues
? values.length
: min(values.length, displayCount);
final displayValues = values.take(valuesShown).toList();
final maxVal = displayValues.isNotEmpty
? displayValues.fold<num>(
0,
(currentMax, entry) =>
entry.$2 > currentMax ? entry.$2 : currentMax,
)
: 0;
return Column(
children: List.generate(min(values.length, itemCount), (index) {
/// The maximum wins among all players
final maxMatches = values.isNotEmpty ? values[0].$2 : 0;
children: [
// Bars
...List.generate(valuesShown, (index) {
/// Fraction of wins
final double fraction = (maxVal > 0)
? (displayValues[index].$2 / maxVal)
: 0.0;
/// Fraction of wins
final double fraction = (maxMatches > 0)
? (values[index].$2 / maxMatches)
: 0.0;
/// Calculated width for current the bar
final double barWidth = (maxBarWidth * fraction).clamp(
0.0,
maxBarWidth,
);
/// Calculated width for current the bar
final double barWidth = maxBarWidth * fraction;
final barClr = index >= displayCount
? barColor.withAlpha(150)
: barColor;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Stack(
children: [
Container(
height: 24,
width: barWidth,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: barColor,
),
),
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
TextSpan(
text: values[index].$1.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: getNameCountText(values[index].$1),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor.withAlpha(
150,
),
),
),
],
var textClr = barColor.computeLuminance() > 0.5
? const Color(0xFF101010)
: CustomTheme.textColor;
textClr = textClr.withAlpha(
index >= displayCount ? 220 : 255,
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(
width: maxBarWidth,
child: Stack(
clipBehavior: Clip.hardEdge,
children: [
// Bar
Container(
height: 24,
width: barWidth,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: barClr,
),
),
),
),
],
),
const Spacer(),
Center(
child: Text(
values[index].$2 <= 1 && values[index].$2 is double
? values[index].$2.toStringAsFixed(2)
: values[index].$2.toString(),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
// Player
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: RichText(
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
TextSpan(
text: displayValues[index].$1.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: textClr,
),
),
TextSpan(
text: getNameCountText(
displayValues[index].$1,
),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color:
(barColor ==
getColorFromAppColor(
AppColor.yellow,
)
? const Color(
0xFF101010,
)
: CustomTheme.textColor)
.withAlpha(150),
),
),
],
),
),
),
],
),
),
),
],
const Spacer(),
// Value
Center(
child: Text(
displayValues[index].$2 <= 1 &&
displayValues[index].$2 is double
? displayValues[index].$2.toStringAsFixed(2)
: displayValues[index].$2.toString(),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}),
// Group & Game info
if (hasGame || hasGroup)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 4,
runSpacing: 4,
children: [
// Game
if (hasGroup)
Row(
spacing: 8,
children: [
const Icon(
RpgAwesome.clovers_card,
color: CustomTheme.hintColor,
size: 20,
),
Text(
getGameText(selectedGames!),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: CustomTheme.hintColor,
),
overflow: TextOverflow.ellipsis,
),
],
),
if (hasGroup && hasGame) const SizedBox(width: 20),
// Group
if (hasGroup)
Row(
spacing: 8,
children: [
const Icon(
Icons.groups,
color: CustomTheme.hintColor,
),
Text(
getGroupText(selectedGroups!),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: CustomTheme.hintColor,
),
overflow: TextOverflow.ellipsis,
),
],
),
],
),
),
);
}),
],
);
},
),
@@ -144,4 +266,24 @@ class StatisticsTile extends StatelessWidget {
),
);
}
String getGroupText(List<Group> groups) {
var text = groups[0].name;
if (groups.length > 1) {
return '$text + ${groups.length - 1}';
}
return text;
}
String getGameText(List<Game> games) {
var text = games[0].name;
if (games.length > 1) {
return '$text + ${games.length - 1}';
}
return text;
}
bool get hasGroup => selectedGroups != null && selectedGroups!.isNotEmpty;
bool get hasGame => selectedGames != null && selectedGames!.isNotEmpty;
}

View File

@@ -12,6 +12,7 @@ class TextIconTile extends StatelessWidget {
this.suffixText = '',
this.iconEnabled = true,
this.onIconTap,
this.onTileTap,
});
/// The text to display in the tile.
@@ -25,52 +26,58 @@ class TextIconTile extends StatelessWidget {
/// The callback to be invoked when the icon is tapped.
final VoidCallback? onIconTap;
/// The callback to be invoked when the tile is tapped.
final VoidCallback? onTileTap;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: CustomTheme.onBoxColor,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: [
if (iconEnabled) const SizedBox(width: 3),
Flexible(
child: RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
TextSpan(
text: text,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
return GestureDetector(
onTap: onTileTap,
child: Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: CustomTheme.onBoxColor,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: [
if (iconEnabled) const SizedBox(width: 3),
Flexible(
child: RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
TextSpan(
text: text,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
TextSpan(
text: suffixText,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: CustomTheme.textColor.withAlpha(120),
TextSpan(
text: suffixText,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: CustomTheme.textColor.withAlpha(120),
),
),
),
],
],
),
),
),
),
if (iconEnabled) ...<Widget>[
const SizedBox(width: 3),
GestureDetector(
onTap: onIconTap,
child: const Icon(Icons.close, size: 20),
),
if (iconEnabled) ...<Widget>[
const SizedBox(width: 3),
GestureDetector(
onTap: onIconTap,
child: const Icon(Icons.close, size: 20),
),
],
],
],
),
),
);
}