GameHistoryView anpassen #20

Merged
flixcoo merged 27 commits from feature/2-gamehistoryview-anpassen into development 2025-11-30 15:59:25 +00:00
4 changed files with 296 additions and 243 deletions

View File

@@ -1,7 +1,15 @@
import 'package:flutter/material.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/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:provider/provider.dart';
class GameHistoryView extends StatefulWidget {
const GameHistoryView({super.key});
@@ -11,119 +19,43 @@ class GameHistoryView extends StatefulWidget {
}
class _GameHistoryViewState extends State<GameHistoryView> {
final allGameData = [
{
'game': 'Schach',
'title': 'Abendpartie',
'players': 2,
'group': 'Familie',
'date': '01.06.2024',
},
{
'game': 'Monopoly',
'title': 'Wochenendspaß mit Gras du Saas',
'players': 4,
'group': 'Freunde',
'date': '28.05.2024',
},
{
'game': 'Catan',
'title': 'Strategieabend',
'players': 3,
'group': 'Brettspieler',
'date': '25.05.2024',
},
{
'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;
late Future<List<Game>> _gameListFuture;
late final AppDatabase db;
late bool isLoading = true;
late final List<Game> skeletonData = List.filled(
4,
Game(
name: 'Skeleton Game',
group: Group(
name: 'Skeleton Group',
members: [
Player(name: 'Player 1'),
Player(name: 'Player 2'),
Player(name: 'Player 3'),
Player(name: 'Long Name Player 4'),
Player(name: 'Player 5'),
],
),
winner: Player(name: 'Skeleton Player 1'),
players: [
Player(name: 'Skeleton Player 6')
],
),
);
@override
void 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
@@ -131,76 +63,76 @@ class _GameHistoryViewState extends State<GameHistoryView> {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
body: Stack(
alignment: Alignment.center,
children: [
Column(
children: [
Container(margin: const EdgeInsets.only(bottom: 75)),
Expanded(
child: gameHistoryListView(allGameData, suggestedGameData),
),
],
),
Container(
margin: const EdgeInsets.only(top: 10, bottom: 10, left: 10, right: 10),
child: SearchBar(
leading: const Icon(Icons.search),
onChanged: (value) {
if (value.isEmpty) {
setState(() {
suggestedGameData.clear();
suggestedGameData.addAll(allGameData);
});
return;
}
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);
});
},
),
),
],
FutureBuilder<List<Game>>(
future: _gameListFuture,
builder: (BuildContext context, AsyncSnapshot<List<Game>> snapshot) {
if (snapshot.hasError) {
return const Center(
child: TopCenteredMessage(
icon: Icons.report,
title: 'Error',
message: 'Game data could not be loaded',
),
);
}
if (snapshot.connectionState == ConnectionState.done &&
(!snapshot.hasData || snapshot.data!.isEmpty)) {
return const Center(
child: TopCenteredMessage(
icon: Icons.report,
title: 'Error',
message: 'No Games Available',
),
);
}
}
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.',
final List<Game> games = (isLoading
? skeletonData
: (snapshot.data ?? [])
..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
.toList();
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 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'],
return GameHistoryTile(game: games[index]); // Placeholder
},
),
);
},
),
gelbeinhalb marked this conversation as resolved
Review

Es fehlt noch der Create Game Button

Es fehlt noch der Create Game Button
Review

Der um ein Match zu erstellen? oder der um ein Game zu erstellen?

Der um ein Match zu erstellen? oder der um ein Game zu erstellen?
Review

Digga xD der Button der zur CreateGameView führt

Digga xD der Button der zur `CreateGameView` führt
Review

Bitte customwidthbutton mit breite 0,9 wie bei groupsview machen

Bitte customwidthbutton mit breite 0,9 wie bei groupsview machen
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';
sneeex marked this conversation as resolved
Review

Wofür intl?

Wofür intl?
Review

Zur berechnung vom datum :) idk ob das der optimalste weg ist

Zur berechnung vom datum :) idk ob das der optimalste weg ist
Review

Aber nutzt du da nicht nur datetime?

Aber nutzt du da nicht nur datetime?
Review

Die DateFormat klasse kommt daher:

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);
    }
  }
Die `DateFormat` klasse kommt daher: ```dart 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); } } ```
class GameHistoryTile extends StatefulWidget {
gelbeinhalb marked this conversation as resolved
Review

(Code-Zeile ist falsch, weil die Datei wird nicht angezeigt) doubleRowInfoTile entfernen, wird nicht mehr genutzt

(Code-Zeile ist falsch, weil die Datei wird nicht angezeigt) `doubleRowInfoTile` entfernen, wird nicht mehr genutzt
Review

check ich nicht. wo wird das denn verwendet?

check ich nicht. wo wird das denn verwendet?
Review

Nirgendwo, deswegen kann es ja weg. Wurde vorher in GameHistoryView verwendet

Nirgendwo, deswegen kann es ja weg. Wurde vorher in `GameHistoryView` verwendet
Review

nirgendwo in meinem code steht doubleRowInfoTile 😭 oder ich bin blöd und finde es nicht

nirgendwo in meinem code steht `doubleRowInfoTile` 😭 oder ich bin blöd und finde es nicht
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(
gelbeinhalb marked this conversation as resolved
Review

Hier auch hinter buid() Methode schieben

Hier auch hinter `buid()` Methode schieben
Review

👍

👍
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,
gelbeinhalb marked this conversation as resolved
Review

Hier auch zusammenfassen oder Visibility bzw. Offstage Widget nutzen

Hier auch zusammenfassen oder `Visibility` bzw. `Offstage` Widget nutzen
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
file_saver: ^0.3.1
clock: ^1.1.2
intl: ^0.18.0
sneeex marked this conversation as resolved
Review

Wofür intl?

Wofür intl?
Review
siehe https://git.yannick-weigert.de/liquid-development/game-tracker/pulls/20#issuecomment-1170
dev_dependencies:
flutter_test: