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',
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
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),
Expanded(
child: RadioGroup<Player>(
groupValue: _selectedPlayer,
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(
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,25 +6,24 @@ 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(
return GestureDetector(
onTap: widget.onTap,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
@@ -50,10 +49,7 @@ class _GameHistoryTileState extends State<GameHistoryTile> {
),
Text(
_formatDate(widget.game.createdAt),
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
@@ -63,19 +59,12 @@ class _GameHistoryTileState extends State<GameHistoryTile> {
if (group != null) ...[
Row(
children: [
const Icon(
Icons.group,
size: 16,
color: Colors.grey,
),
const Icon(Icons.group, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Expanded(
child: Text(
group.name,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
style: const TextStyle(fontSize: 14, color: Colors.grey),
overflow: TextOverflow.ellipsis,
),
),
@@ -86,7 +75,10 @@ class _GameHistoryTileState extends State<GameHistoryTile> {
if (winner != null) ...[
Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
@@ -134,15 +126,13 @@ class _GameHistoryTileState extends State<GameHistoryTile> {
spacing: 6,
runSpacing: 6,
children: allPlayers.map((player) {
return TextIconTile(
text: player.name,
iconEnabled: false,
);
return TextIconTile(text: player.name, iconEnabled: false);
}).toList(),
),
],
],
),
),
);
}
@@ -187,5 +177,4 @@ class _GameHistoryTileState extends State<GameHistoryTile> {
return allPlayers;
}
}