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
4 changed files with 296 additions and 243 deletions
Showing only changes of commit 09b407eba8 - Show all commits

View File

@@ -1,7 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/presentation/widgets/tiles/double_row_info_tile.dart'; import 'package:game_tracker/data/db/database.dart';
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/widgets/tiles/game_history_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:provider/provider.dart';
class GameHistoryView extends StatefulWidget { class GameHistoryView extends StatefulWidget {
const GameHistoryView({super.key}); const GameHistoryView({super.key});
@@ -11,119 +19,43 @@ class GameHistoryView extends StatefulWidget {
} }
class _GameHistoryViewState extends State<GameHistoryView> { class _GameHistoryViewState extends State<GameHistoryView> {
final allGameData = [ late Future<List<Game>> _gameListFuture;
{ late final AppDatabase db;
'game': 'Schach', late bool isLoading = true;
'title': 'Abendpartie',
'players': 2, late final List<Game> skeletonData = List.filled(
'group': 'Familie', 4,
'date': '01.06.2024', Game(
}, name: 'Skeleton Game',
{ group: Group(
'game': 'Monopoly', name: 'Skeleton Group',
'title': 'Wochenendspaß mit Gras du Saas', members: [
'players': 4, Player(name: 'Player 1'),
'group': 'Freunde', Player(name: 'Player 2'),
'date': '28.05.2024', Player(name: 'Player 3'),
}, Player(name: 'Long Name Player 4'),
{ Player(name: 'Player 5'),
'game': 'Catan', ],
'title': 'Strategieabend', ),
'players': 3, winner: Player(name: 'Skeleton Player 1'),
'group': 'Brettspieler', players: [
'date': '25.05.2024', Player(name: 'Skeleton Player 6')
}, ],
{ ),
'game': 'Uno', );
'title': 'Schnelle Runde',
'players': 5,
'group': 'Kollegen',
'date': '22.05.2024',
},
{
'game': 'Poker',
'title': 'Freitagspoker',
'players': 6,
'group': 'Pokerclub',
'date': '20.05.2024',
},
{
'game': 'Scrabble',
'title': 'Wortschlacht',
'players': 4,
'group': 'Familie',
'date': '18.05.2024',
},
{
'game': 'Risiko',
'title': 'Weltherrschaft',
'players': 5,
'group': 'Strategiegruppe',
'date': '15.05.2024',
},
{
'game': 'Zug um Zug',
'title': 'Zug-Abenteuer',
'players': 4,
'group': 'Reisende',
'date': '12.05.2024',
},
{
'game': 'Carcassonne',
'title': 'Plättchenlegen',
'players': 3,
'group': 'Brettspieler',
'date': '10.05.2024',
},
{
'game': 'Pandemie',
'title': 'Welt retten',
'players': 4,
'group': 'Koop-Team',
'date': '08.05.2024',
},
{
'game': 'Cluedo',
'title': 'Krimiabend',
'players': 6,
'group': 'Detektive',
'date': '05.05.2024',
},
{
'game': 'Dixit',
'title': 'Fantasiespiel',
'players': 5,
'group': 'Künstler',
'date': '02.05.2024',
},
{
'game': 'Azul',
'title': 'Plättchenmeister',
'players': 4,
'group': 'Familie',
'date': '30.04.2024',
},
{
'game': 'Splendor',
'title': 'Edelsteinhändler',
'players': 3,
'group': 'Freunde',
'date': '28.04.2024',
},
{
'game': '7 Wonders',
'title': 'Antike Reiche',
'players': 7,
'group': 'Geschichtsfreunde',
'date': '25.04.2024',
},
];
late List<Map<String, dynamic>> suggestedGameData;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
suggestedGameData = List.from(allGameData); 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;
});
});
} }
@override @override
@@ -131,41 +63,70 @@ class _GameHistoryViewState extends State<GameHistoryView> {
return Scaffold( return Scaffold(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
body: Stack( body: Stack(
alignment: Alignment.center,
children: [ children: [
Column( FutureBuilder<List<Game>>(
children: [ future: _gameListFuture,
Container(margin: const EdgeInsets.only(bottom: 75)), builder: (BuildContext context, AsyncSnapshot<List<Game>> snapshot) {
Expanded( if (snapshot.hasError) {
child: gameHistoryListView(allGameData, suggestedGameData), return const Center(
), child: TopCenteredMessage(
], icon: Icons.report,
), title: 'Error',
Container( message: 'Game data could not be loaded',
margin: const EdgeInsets.only(top: 10, bottom: 10, left: 10, right: 10), ),
child: SearchBar( );
leading: const Icon(Icons.search), }
onChanged: (value) { if (snapshot.connectionState == ConnectionState.done &&
if (value.isEmpty) { (!snapshot.hasData || snapshot.data!.isEmpty)) {
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
setState(() { return const Center(
suggestedGameData.clear(); child: TopCenteredMessage(
suggestedGameData.addAll(allGameData); icon: Icons.report,
}); title: 'Error',
return; message: 'No Games Available',
} ),
final suggestions = allGameData.where((currentGame) { );
return currentGame['game'].toString().toLowerCase().contains( }
value.toLowerCase(),
) || final List<Game> games = (isLoading
currentGame['title'].toString().toLowerCase().contains( ? skeletonData
value.toLowerCase(), : (snapshot.data ?? [])
) || ..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
currentGame['group'].toString().toLowerCase().contains( .toList();
value.toLowerCase(),
return AppSkeleton(
enabled: isLoading,
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: games.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == games.length) {
return SizedBox(
height: MediaQuery.paddingOf(context).bottom - 80,
); );
}); }
return GameHistoryTile(game: games[index]); // Placeholder
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(); }); } ```
},
),
);
},
),
Positioned(
bottom: MediaQuery.paddingOf(context).bottom,
child: CustomWidthButton(
text: 'Create Game',
sizeRelativeToWidth: 0.90,
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const CreateGroupView();
},
),
);
setState(() { setState(() {
suggestedGameData.clear(); _gameListFuture = db.gameDao.getAllGames();
suggestedGameData.addAll(suggestions);
}); });
}, },
), ),
@@ -174,33 +135,4 @@ class _GameHistoryViewState extends State<GameHistoryView> {
), ),
); );
} }
} }
Widget gameHistoryListView(allGameData, suggestedGameData) {
if (suggestedGameData.isEmpty && allGameData.isEmpty) {
return const TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: 'Keine Spiele erstellt',
);
} else if (suggestedGameData.isEmpty) {
return const TopCenteredMessage(
icon: Icons.search,
title: 'Info',
message: 'Kein Spiel mit den Suchparametern gefunden.',
);
}
return ListView.builder(
itemCount: suggestedGameData.length,
itemBuilder: (context, index) {
final currentGame = suggestedGameData[index];
return doubleRowInfoTile(
currentGame['game'] + ': ',
currentGame['title'],
"${currentGame['players']} Spieler",
currentGame['group'],
currentGame['date'],
);
},
);
}

View File

@@ -1,71 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
Widget doubleRowInfoTile(
String titleOneUpperLeft,
String titleTwoUpperLeft,
String titleUpperRight,
String titleLowerLeft,
String titleLowerRight,
) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: CustomTheme.secondaryColor,
),
child: Column(
children: [
Row(
children: [
Expanded(
flex: 10,
child: Text(
'$titleOneUpperLeft $titleTwoUpperLeft',
style: const TextStyle(fontSize: 20),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
const Spacer(),
Expanded(
flex: 3,
child: Text(
titleUpperRight,
style: const TextStyle(fontSize: 20),
overflow: TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.end,
),
),
],
),
Row(
children: [
Expanded(
flex: 10,
child: Text(
titleLowerLeft,
style: const TextStyle(fontSize: 20),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
const Spacer(),
Expanded(
flex: 4,
child: Text(
titleLowerRight,
style: const TextStyle(fontSize: 20),
overflow: TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.end,
),
),
],
),
],
),
);
}

View File

@@ -0,0 +1,191 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
import 'package:intl/intl.dart';
class GameHistoryTile extends StatefulWidget {
final Game game;
const GameHistoryTile({
super.key,
required this.game,
});
@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) ...[
Row(
children: [
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,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
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(
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: 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(),
),
],
],
),
);
}
String _formatDate(DateTime dateTime) {
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inDays == 0) {
return 'Today at ${DateFormat('HH:mm').format(dateTime)}';
} else if (difference.inDays == 1) {
return 'Yesterday at ${DateFormat('HH:mm').format(dateTime)}';
} else if (difference.inDays < 7) {
return '${difference.inDays} days ago';
} else {
return DateFormat('MMM d, yyyy').format(dateTime);
}
}
List<dynamic> _getAllPlayers() {
final allPlayers = <dynamic>[];
final playerIds = <String>{};
// Add players from game.players
if (widget.game.players != null) {
for (var player in widget.game.players!) {
if (!playerIds.contains(player.id)) {
allPlayers.add(player);
playerIds.add(player.id);
}
}
}
// Add players from game.group.players
if (widget.game.group?.members != null) {
for (var player in widget.game.group!.members) {
if (!playerIds.contains(player.id)) {
allPlayers.add(player);
playerIds.add(player.id);
}
}
}
return allPlayers;
}
}

View File

@@ -24,6 +24,7 @@ dependencies:
json_schema: ^5.2.2 json_schema: ^5.2.2
file_saver: ^0.3.1 file_saver: ^0.3.1
clock: ^1.1.2 clock: ^1.1.2
intl: ^0.18.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: