Live-Edit Modus #207

Merged
sneeex merged 20 commits from feature/202-live-edit-modus into development 2026-05-09 17:58:38 +00:00
3 changed files with 241 additions and 96 deletions
Showing only changes of commit ea5577c288 - Show all commits

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';
class MatchResultView extends StatefulWidget {
/// A view that allows selecting and saving the winner of a match
@@ -30,6 +31,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
@@ -88,115 +91,159 @@ class _MatchResultViewState extends State<MatchResultView> {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
widget.onWinnerChanged?.call();
Navigator.of(context).pop(_selectedPlayer);
},
),
leading: isLiveEditMode
? IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
setState(() {
isLiveEditMode = false;
});
},
)
: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
widget.onWinnerChanged?.call();
Navigator.of(context).pop(_selectedPlayer);
},
),
title: Text(widget.match.name),
),
body: SafeArea(
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 && rulesetSupportsScoreEntry()
? 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(
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,
);
},
)
: 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 {
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;
});
},
);
},
),
),
child: ListView.builder(
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);
}
});
},
);
},
),
),
),
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 (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 (!isLiveEditMode) ...[
if (rulesetSupportsScoreEntry())
// Button to switch to live edit mode
...[
CustomWidthButton(
text: 'Live-Edit Modus',
sizeRelativeToWidth: 0.95,
buttonType: ButtonType.secondary,
onPressed: () => setState(() {
isLiveEditMode = true;
}),
),
const SizedBox(height: 10),
],
// Save Changes Button
CustomWidthButton(
text: loc.save_changes,
sizeRelativeToWidth: 0.95,
onPressed: canSave
? () async {
final ending = DateTime.now();
await db.matchDao.updateMatchEndedAt(
matchId: widget.match.id,
endedAt: ending,
);
await _handleSaving();
if (!context.mounted) return;
Navigator.of(context).pop(_selectedPlayer);
}
: null,
),
),
CustomWidthButton(
text: loc.save_changes,
sizeRelativeToWidth: 0.95,
onPressed: canSave
? () async {
final ending = DateTime.now();
await db.matchDao.updateMatchEndedAt(
matchId: widget.match.id,
endedAt: ending,
);
await _handleSaving();
if (!context.mounted) return;
Navigator.of(context).pop(_selectedPlayer);
}
: null,
),
],
],
),
),

View File

@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:flutter_numeric_text/flutter_numeric_text.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
class LiveEditListTile extends StatefulWidget {
const LiveEditListTile({
super.key,
required this.title,
required this.value,
this.onChanged,
});
final String title;
final int value;
final void Function(int newValue)? onChanged;
@override
State<LiveEditListTile> createState() => _LiveEditListTileState();
}
class _LiveEditListTileState extends State<LiveEditListTile> {
int _score = 0;
final int maxScore = 9999;
final int minScore = -9999;
@override
void initState() {
_score = widget.value;
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 10),
margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
decoration: CustomTheme.standardBoxDecoration,
child: Column(
children: [
Text(
widget.title,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
Padding(
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MainMenuButton(
onPressed: () => _score > minScore
? {
setState(() {
_score--;
if (widget.onChanged != null) {
widget.onChanged!(_score);
}
}),
}
: null,
icon: Icons.remove_rounded,
),
SizedBox(
width: 150,
child: NumericText(
_score.toString(),
maxLines: 1,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.w600,
),
),
),
MainMenuButton(
onPressed: () => _score < maxScore
? {
setState(() {
_score++;
if (widget.onChanged != null) {
widget.onChanged!(_score);
}
}),
}
: null,
icon: Icons.add_rounded,
),
],
),
),
],
),
);
}
}

View File

@@ -17,6 +17,7 @@ dependencies:
sdk: flutter
flutter_localizations:
sdk: flutter
flutter_numeric_text: ^1.3.3
fluttericon: ^2.0.0
font_awesome_flutter: ^11.0.0
intl: any