Merge branch 'development' into feature/206-Neuer-Regelsatz-Platzierung
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 46s
Pull Request Pipeline / lint (pull_request) Successful in 48s

# Conflicts:
#	lib/l10n/generated/app_localizations.dart
#	lib/presentation/views/main_menu/match_view/match_detail_view.dart
#	lib/presentation/views/main_menu/match_view/match_result_view.dart
This commit is contained in:
2026-05-09 23:04:11 +02:00
14 changed files with 485 additions and 522 deletions

View File

@@ -240,6 +240,9 @@ class _MatchDetailViewState extends State<MatchDetailView> {
match: match,
onWinnerChanged: () {
widget.onMatchUpdate.call();
setState(() {
updateScoresForCurrentMatch();
});
},
),
),
@@ -428,4 +431,10 @@ class _MatchDetailViewState extends State<MatchDetailView> {
return '${number}th';
}
}
void updateScoresForCurrentMatch() {
db.scoreEntryDao
.getAllMatchScores(matchId: match.id)
.then((scores) => match.scores = scores);
}
}

View File

@@ -8,8 +8,9 @@ import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/score_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/match_result_view/score_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_list_tile.dart';
class MatchResultView extends StatefulWidget {
@@ -31,6 +32,8 @@ class MatchResultView extends StatefulWidget {
class _MatchResultViewState extends State<MatchResultView> {
late final AppDatabase db;
bool isLiveEditMode = false;
late final Ruleset ruleset;
/// List of all players who participated in the match
@@ -39,6 +42,7 @@ class _MatchResultViewState extends State<MatchResultView> {
/// List of text controllers for score entry, one for each player
late final List<TextEditingController> controller;
/// Flag to indicate if the save button should be enabled
late bool canSave;
/// Currently selected winner player
@@ -58,6 +62,7 @@ class _MatchResultViewState extends State<MatchResultView> {
(index) => TextEditingController()..addListener(() => onTextEnter()),
);
// Prefill fields
if (widget.match.mvp.isNotEmpty) {
if (rulesetSupportsWinnerSelection()) {
_selectedPlayer = allPlayers.firstWhere(
@@ -108,186 +113,232 @@ class _MatchResultViewState extends State<MatchResultView> {
child: Column(
children: [
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 10,
),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorderColor),
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${getTitleForRuleset(loc)}:',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
if (rulesetSupportsWinnerSelection())
Expanded(
child: RadioGroup<Player>(
groupValue: _selectedPlayer,
onChanged: (Player? value) async {
child: isLiveEditMode
// Live Edit Mode
? ListView.builder(
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return LiveEditListTile(
title: allPlayers[index].name,
onChanged: (value) {
setState(() {
_selectedPlayer = value;
controller[index].text = value.toString();
});
},
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return CustomRadioListTile(
text: allPlayers[index].name,
value: allPlayers[index],
onContainerTap: (value) async {
value: int.tryParse(controller[index].text) ?? 0,
);
},
)
// Normal Container
: Container(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 10,
),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorderColor),
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
getTitleForRuleset(loc),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
// Show player selection
if (rulesetSupportsWinnerSelection())
Expanded(
child: RadioGroup<Player>(
groupValue: _selectedPlayer,
onChanged: (Player? value) async {
setState(() {
// Check if the already selected player is the same as the newly tapped player.
if (_selectedPlayer == value) {
// If yes deselected the player by setting it to null.
_selectedPlayer = null;
} else {
// If no assign the newly tapped player to the selected player.
(_selectedPlayer = value);
}
_selectedPlayer = value;
});
},
);
},
),
),
),
if (rulesetSupportsScoreEntry())
Expanded(
child: ListView.separated(
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return ScoreListTile(
text: allPlayers[index].name,
controller: controller[index],
);
},
separatorBuilder: (BuildContext context, int index) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Divider(indent: 20),
);
},
),
),
if (rulesetSupportsPlacement())
Expanded(
child: Row(
children: [
// Placement indicators
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Column(
children: [
for (int i = 0; i < allPlayers.length; i++)
Container(
alignment: Alignment.center,
height: 60,
child: Container(
decoration: BoxDecoration(
color: CustomTheme.boxBorderColor,
borderRadius: CustomTheme
.standardBorderRadiusAll,
),
alignment: Alignment.center,
height: 50,
width: 50,
child: Text(
' #${i + 1} ',
style: const TextStyle(
color: CustomTheme.textColor,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
],
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return CustomRadioListTile(
text: allPlayers[index].name,
value: allPlayers[index],
onContainerTap: (value) async {
setState(() {
// Check if the already selected player is the same as the newly tapped player.
if (_selectedPlayer == value) {
// If yes deselected the player by setting it to null.
_selectedPlayer = null;
} else {
// If no assign the newly tapped player to the selected player.
(_selectedPlayer = value);
}
});
},
);
},
),
),
),
// Drag list
// Show score entry
if (rulesetSupportsScoreEntry())
Expanded(
child: ReorderableListView.builder(
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
proxyDecorator: (child, index, animation) {
return AnimatedBuilder(
animation: animation,
child: child,
builder: (context, child) {
final alpha =
(Curves.easeInOut.transform(
animation.value,
) *
40)
.toInt();
return Stack(
children: [
child!,
Positioned.fill(
left: 4,
top: 4,
right: 4,
bottom: 4,
child: DecoratedBox(
child: ListView.separated(
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return ScoreListTile(
text: allPlayers[index].name,
controller: controller[index],
);
},
separatorBuilder:
(BuildContext context, int index) {
return const Padding(
padding: EdgeInsets.symmetric(
vertical: 8.0,
),
child: Divider(indent: 20),
);
},
),
),
// Show draggable placement list
if (rulesetSupportsPlacement())
Expanded(
child: Row(
children: [
// Placement indicators
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Column(
children: [
for (
int i = 0;
i < allPlayers.length;
i++
)
Container(
alignment: Alignment.center,
height: 60,
child: Container(
decoration: BoxDecoration(
color: Colors.white.withAlpha(
alpha,
),
color:
CustomTheme.boxBorderColor,
borderRadius: CustomTheme
.standardBorderRadiusAll,
),
alignment: Alignment.center,
height: 50,
width: 50,
child: Text(
' #${i + 1} ',
style: const TextStyle(
color: CustomTheme.textColor,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
],
);
},
);
},
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final Player item = allPlayers.removeAt(
oldIndex,
);
allPlayers.insert(newIndex, item);
});
},
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return TextIconListTile(
key: ValueKey(allPlayers[index].id),
text: allPlayers[index].name,
icon: Icons.drag_handle,
);
},
],
),
),
// Drag list
Expanded(
child: ReorderableListView.builder(
physics:
const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
proxyDecorator: (child, index, animation) {
return AnimatedBuilder(
animation: animation,
child: child,
builder: (context, child) {
final alpha =
(Curves.easeInOut.transform(
animation.value,
) *
40)
.toInt();
return Stack(
children: [
child!,
Positioned.fill(
left: 4,
top: 4,
right: 4,
bottom: 4,
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white
.withAlpha(alpha),
borderRadius: CustomTheme
.standardBorderRadiusAll,
),
),
),
],
);
},
);
},
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final Player item = allPlayers
.removeAt(oldIndex);
allPlayers.insert(newIndex, item);
});
},
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return TextIconListTile(
key: ValueKey(allPlayers[index].id),
text: allPlayers[index].name,
icon: Icons.drag_handle,
);
},
),
),
],
),
),
],
),
],
),
],
),
),
),
),
if (rulesetSupportsScoreEntry())
// Button to switch to live edit mode
...[
CustomWidthButton(
text: isLiveEditMode ? loc.exit_view : loc.live_edit_mode,
sizeRelativeToWidth: 0.95,
buttonType: ButtonType.secondary,
onPressed: () => setState(() {
isLiveEditMode = !isLiveEditMode;
}),
),
const SizedBox(height: 10),
],
// Save Changes Button
CustomWidthButton(
text: loc.save_changes,
sizeRelativeToWidth: 0.95,