GameResultView erstellen #62

Merged
flixcoo merged 31 commits from feature/48-game-result-view-erstellen into development 2025-12-06 14:06:29 +00:00
5 changed files with 311 additions and 118 deletions

View File

@@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/db/database.dart';
@@ -5,6 +6,7 @@ import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/presentation/views/main_menu/create_group_view.dart';
import 'package:game_tracker/presentation/views/main_menu/game_result_view.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/tiles/game_history_tile.dart';
@@ -21,7 +23,6 @@ class GameHistoryView extends StatefulWidget {
class _GameHistoryViewState extends State<GameHistoryView> {
late Future<List<Game>> _gameListFuture;
late final AppDatabase db;
late bool isLoading = true;
late final List<Game> skeletonData = List.filled(
4,
@@ -46,14 +47,10 @@ class _GameHistoryViewState extends State<GameHistoryView> {
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
_gameListFuture = db.gameDao.getAllGames();
Future.wait([_gameListFuture]).then((result) async {
await Future.delayed(const Duration(milliseconds: 250));
setState(() {
isLoading = false;
});
});
_gameListFuture = Future.delayed(
const Duration(milliseconds: 250),
() => db.gameDao.getAllGames(),
);
}
@override
@@ -81,19 +78,19 @@ class _GameHistoryViewState extends State<GameHistoryView> {
return const Center(
child: TopCenteredMessage(
icon: Icons.report,
title: 'Error',
message: 'No Games Available',
title: 'Info',
sneeex marked this conversation as resolved Outdated

Ich finde wir sollten für solche generischen Statusmeldungen nicht komplette Sätze mit Punkt machen oder?
Finde No games created yet passt da besser rein als die Version mit Punkt am Ende

Ich finde wir sollten für solche generischen Statusmeldungen nicht komplette Sätze mit Punkt machen oder? Finde `No games created yet` passt da besser rein als die Version mit Punkt am Ende
message: 'No games created yet',
),
);
}
final bool isLoading =
snapshot.connectionState == ConnectionState.waiting;
final List<Game> games =
(isLoading ? skeletonData : (snapshot.data ?? [])
..sort(
(a, b) => b.createdAt.compareTo(a.createdAt),
))
.toList();
return AppSkeleton(
enabled: isLoading,
child: ListView.builder(
@@ -106,8 +103,21 @@ class _GameHistoryViewState extends State<GameHistoryView> {
);
}
return GameHistoryTile(
onTap: () async {
await Navigator.push(
context,
sneeex marked this conversation as resolved
Review

ich fände hier noch `fullscreenDialog: true' cool, ich finde das passt besser zur Navigation, also:

onTap: () async {
  await Navigator.push(
    context,
    MaterialPageRoute(
      fullscreenDialog: true,
      builder: (context) =>
          GameResultView(game: games[index]),
    ),
  );
  setState(() {
    _gameListFuture = db.gameDao.getAllGames();
  });
}
ich fände hier noch `fullscreenDialog: true' cool, ich finde das passt besser zur Navigation, also: ```dart onTap: () async { await Navigator.push( context, MaterialPageRoute( fullscreenDialog: true, builder: (context) => GameResultView(game: games[index]), ), ); setState(() { _gameListFuture = db.gameDao.getAllGames(); }); } ```
Review

aber finde die animation dann komisch, das fadet so ein und sehe da jetzt keinen großen unterschied, also warum fullscreendialog und nicht das normale?

aber finde die animation dann komisch, das fadet so ein und sehe da jetzt keinen großen unterschied, also warum fullscreendialog und nicht das normale?
Review

Korrektur:

 onTap: () async {
  await Navigator.push(
    context,
    CupertinoPageRoute(
      fullscreenDialog: true,
      builder: (context) =>
          GameResultView(game: games[index]),
    ),
  );
  setState(() {
    _gameListFuture = db.gameDao.getAllGames();
  });
}
Korrektur: ```dart onTap: () async { await Navigator.push( context, CupertinoPageRoute( fullscreenDialog: true, builder: (context) => GameResultView(game: games[index]), ), ); setState(() { _gameListFuture = db.gameDao.getAllGames(); }); } ```
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) =>
GameResultView(game: games[index]),
),
);
setState(() {
_gameListFuture = db.gameDao.getAllGames();
});
},
game: games[index],
); // Placeholder
);
},
),
);

View File

@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/presentation/widgets/tiles/custom_radio_list_tile.dart';
import 'package:provider/provider.dart';
class GameResultView extends StatefulWidget {
final Game game;
const GameResultView({super.key, required this.game});
@override
State<GameResultView> createState() => _GameResultViewState();
}
class _GameResultViewState extends State<GameResultView> {
late final List<Player> allPlayers;
late final AppDatabase db;
Player? _selectedPlayer;
@override
sneeex marked this conversation as resolved Outdated

Umbenennen in selectedPlayer oder currentWinner o.Ä.

Umbenennen in `selectedPlayer` oder `currentWinner` o.Ä.
void initState() {
db = Provider.of<AppDatabase>(context, listen: false);
allPlayers = getAllPlayers(widget.game);
if (widget.game.winner != null) {
_selectedPlayer = allPlayers.firstWhere(
(p) => p.id == widget.game.winner!.id,
);
}
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
title: Text(
widget.game.name,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis,
),
),
centerTitle: true,
),
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.boxBorder),
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Select Winner:',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
sneeex marked this conversation as resolved Outdated

Würde height mindestens auf 10 erhöhen

Würde `height` mindestens auf 10 erhöhen
Expanded(
child: RadioGroup<Player>(
groupValue: _selectedPlayer,
sneeex marked this conversation as resolved Outdated

Macht das Sinn überhaupt zu implementieren? Gibt es Fälle in denen ein Spiel keine Spieler hat?

Macht das Sinn überhaupt zu implementieren? Gibt es Fälle in denen ein Spiel keine Spieler hat?

Eigentlich nicht, soll ich's weglassen dann?

Eigentlich nicht, soll ich's weglassen dann?
onChanged: (Player? value) async {
setState(() {
_selectedPlayer = value;
});
await _handleWinnerSaving();
},
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);
}
});
await _handleWinnerSaving();
},
);
},
),
),
),
],
),
),
),
],
),
),
);
}
Future<void> _handleWinnerSaving() async {
if (_selectedPlayer == null) {
await db.gameDao.removeWinner(gameId: widget.game.id);
} else {
await db.gameDao.setWinner(
sneeex marked this conversation as resolved Outdated

Hier ist auch kein Punkt :)
Würde ich aber auch so lassen höhö

Hier ist auch kein Punkt :) Würde ich aber auch so lassen höhö
gameId: widget.game.id,
winnerId: _selectedPlayer!.id,
);
}
}
List<Player> getAllPlayers(Game game) {
if (game.group == null && game.players != null) {
return [...game.players!];
} else if (game.group != null && game.players != null) {
return [...game.players!, ...game.group!.members];
}
return [...game.group!.members];
}
}

View File

@@ -56,7 +56,7 @@ class _GroupsViewState extends State<GroupsView> {
child: TopCenteredMessage(
icon: Icons.report,
title: 'Error',
message: 'Group data couldn\'t\nbe loaded.',
message: 'Group data couldn\'t\nbe loaded',
),
);
}
@@ -66,7 +66,7 @@ class _GroupsViewState extends State<GroupsView> {
child: TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: 'No groups created yet.',
message: 'No groups created yet',
),
);
}

View File

@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class CustomRadioListTile<T> extends StatelessWidget {
final String text;
final T value;
final ValueChanged<T> onContainerTap;
const CustomRadioListTile({
sneeex marked this conversation as resolved
Review

Würde Umbennenen in RadioListTile, weil das gibt es bisher noch nicht, oder? Würde Custom[Widget] immer nur verwenden, wenn es [Widget] bereits gibt (z.B. SearchBar)

Würde Umbennenen in `RadioListTile`, weil das gibt es bisher noch nicht, oder? Würde `Custom[Widget]` immer nur verwenden, wenn es [Widget] bereits gibt (z.B. `SearchBar`)
Review

Ja fair aber dann Guck vielleicht mal was es gibt und was nicht xD https://api.flutter.dev/flutter/material/RadioListTile-class.html

Ja fair aber dann Guck vielleicht mal was es gibt und was nicht xD https://api.flutter.dev/flutter/material/RadioListTile-class.html
super.key,
required this.text,
required this.value,
required this.onContainerTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onContainerTap(value),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Radio<T>(
value: value,
activeColor: CustomTheme.primaryColor,
toggleable: true,
),
Expanded(
child: Text(
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
);
}
}

View File

@@ -6,142 +6,132 @@ import 'package:intl/intl.dart';
class GameHistoryTile extends StatefulWidget {
final Game game;
final VoidCallback onTap;
const GameHistoryTile({
super.key,
required this.game,
});
const GameHistoryTile({super.key, required this.game, required this.onTap});
@override
State<GameHistoryTile> createState() => _GameHistoryTileState();
}
class _GameHistoryTileState extends State<GameHistoryTile> {
@override
Widget build(BuildContext context) {
final group = widget.game.group;
final winner = widget.game.winner;
final allPlayers = _getAllPlayers();
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
widget.game.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
Text(
_formatDate(widget.game.createdAt),
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
const SizedBox(height: 8),
if (group != null) ...[
return GestureDetector(
onTap: widget.onTap,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(
Icons.group,
size: 16,
color: Colors.grey,
),
const SizedBox(width: 6),
Expanded(
child: Text(
group.name,
widget.game.name,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
fontSize: 18,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
Text(
_formatDate(widget.game.createdAt),
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
const SizedBox(height: 12),
],
if (winner != null) ...[
Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.green.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
const SizedBox(height: 8),
if (group != null) ...[
Row(
children: [
const Icon(
Icons.emoji_events,
size: 20,
color: Colors.amber,
),
const SizedBox(width: 8),
const Icon(Icons.group, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Expanded(
child: Text(
'Winner: ${winner.name}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
group.name,
style: const TextStyle(fontSize: 14, color: Colors.grey),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 12),
],
const SizedBox(height: 12),
],
if (allPlayers.isNotEmpty) ...[
const Text(
'Players',
style: TextStyle(
fontSize: 13,
color: Colors.grey,
fontWeight: FontWeight.w500,
if (winner != null) ...[
Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.green.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
const Icon(
Icons.emoji_events,
size: 20,
color: Colors.amber,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Winner: ${winner.name}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
const SizedBox(height: 6),
Wrap(
spacing: 6,
runSpacing: 6,
children: allPlayers.map((player) {
return TextIconTile(
text: player.name,
iconEnabled: false,
);
}).toList(),
),
const SizedBox(height: 12),
],
if (allPlayers.isNotEmpty) ...[
const Text(
'Players',
style: TextStyle(
fontSize: 13,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
Wrap(
spacing: 6,
runSpacing: 6,
children: allPlayers.map((player) {
return TextIconTile(text: player.name, iconEnabled: false);
}).toList(),
),
],
],
],
),
),
);
}
@@ -187,5 +177,4 @@ class _GameHistoryTileState extends State<GameHistoryTile> {
return allPlayers;
}
}
}