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,76 +63,76 @@ 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)) {
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(),
) ||
currentGame['title'].toString().toLowerCase().contains(
value.toLowerCase(),
) ||
currentGame['group'].toString().toLowerCase().contains(
value.toLowerCase(),
);
});
setState(() {
suggestedGameData.clear();
suggestedGameData.addAll(suggestions);
});
},
),
),
],
), ),
); );
} }
}
Widget gameHistoryListView(allGameData, suggestedGameData) { final List<Game> games = (isLoading
if (suggestedGameData.isEmpty && allGameData.isEmpty) { ? skeletonData
return const TopCenteredMessage( : (snapshot.data ?? [])
icon: Icons.info, ..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
title: 'Info', .toList();
message: 'Keine Spiele erstellt',
); return AppSkeleton(
} else if (suggestedGameData.isEmpty) { enabled: isLoading,
return const TopCenteredMessage( child: ListView.builder(
icon: Icons.search, padding: const EdgeInsets.only(bottom: 85),
title: 'Info', itemCount: games.length + 1,
message: 'Kein Spiel mit den Suchparametern gefunden.', itemBuilder: (BuildContext context, int index) {
if (index == games.length) {
return SizedBox(
height: MediaQuery.paddingOf(context).bottom - 80,
); );
} }
return ListView.builder( 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(); }); } ```
itemCount: suggestedGameData.length, },
itemBuilder: (context, index) { ),
final currentGame = suggestedGameData[index];
return doubleRowInfoTile(
currentGame['game'] + ': ',
currentGame['title'],
"${currentGame['players']} Spieler",
currentGame['group'],
currentGame['date'],
); );
}, },
),
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(() {
_gameListFuture = db.gameDao.getAllGames();
});
},
),
),
],
),
);
}
} }

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: