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';
gelbeinhalb marked this conversation as resolved Outdated

Umbennenung zu GameView stand mal im Raum oder?

Umbennenung zu `GameView` stand mal im Raum oder?

Ist nicht ein GameView das was sich öffnet wenn man auf ein Game klickt?

Ist nicht ein GameView das was sich öffnet wenn man auf ein Game klickt?

@mathiskir hat die jetzt GameResultView genannt, aber quatsch ihr euch da sonst nochmal ab

@mathiskir hat die jetzt `GameResultView` genannt, aber quatsch ihr euch da sonst nochmal ab

Ich finde wir sollten beides von Game....View in Match...View umbenennen.

Game = Das Spiel (Brettspiel, Kartenspiel)
Match = Eine Partie des Spiels

@flixcoo @mathiskir

Ich finde wir sollten beides von `Game....View` in `Match...View` umbenennen. Game = Das Spiel (Brettspiel, Kartenspiel) Match = Eine Partie des Spiels @flixcoo @mathiskir

Ja fand ich glaub ich sinnvoll. Dann müssten wir aber auch die entsprechenden Klassen umbenennen. Ich würde vorschlagen dass gesammelt in einem Ticket zu machen und deins so zu mergen

Ja fand ich glaub ich sinnvoll. Dann müssten wir aber auch die entsprechenden Klassen umbenennen. Ich würde vorschlagen dass gesammelt in einem Ticket zu machen und deins so zu mergen

Ja würde ich auch sagen, weil dann ja sogar die Datenbank implementation geändert werden muss noch. Und auch unsere test json

Ja würde ich auch sagen, weil dann ja sogar die Datenbank implementation geändert werden muss noch. Und auch unsere test json

ja, lass das hier erstmal rauslassen

ja, lass das hier erstmal rauslassen
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> {
gelbeinhalb marked this conversation as resolved Outdated

Ich würde die Skeleton-Daten so anpassen, dass du nicht mehr machst als auf den Screen passen. Also bei mir sind das glaub ich so 5

Ich würde die Skeleton-Daten so anpassen, dass du nicht mehr machst als auf den Screen passen. Also bei mir sind das glaub ich so 5

ja save :) hab 10 eingestellt zum testen und dann vergessen zu ändern

ja save :) hab 10 eingestellt zum testen und dann vergessen zu ändern
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,41 +63,70 @@ 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(),
FutureBuilder<List<Game>>(
future: _gameListFuture,
builder: (BuildContext context, AsyncSnapshot<List<Game>> snapshot) {
gelbeinhalb marked this conversation as resolved Outdated

Errormessage anders, handelt sich ja nicht um recent games

Errormessage anders, handelt sich ja nicht um recent games

jo

jo

und besser standartisierte top_centered_message wie in GroupsView benutzen.
Außerdem sieht das Alignement komisch aus:
grafik.png

und besser standartisierte `top_centered_message` wie in GroupsView benutzen. Außerdem sieht das Alignement komisch aus: ![grafik.png](/attachments/51e0ad5c-7df8-43e1-8401-dd459be4abbb)
if (snapshot.hasError) {
return const Center(
child: TopCenteredMessage(
icon: Icons.report,
gelbeinhalb marked this conversation as resolved Outdated

Die Skeleton-Zeit ist viel kürzer als bei allen anderen Screens. Ich glaube das liegt an dieser Zeile, weil du hier die isLoading Variable setzt und nicht nach dem oben angegebenen 250ms delay.

Die Skeleton-Zeit ist viel kürzer als bei allen anderen Screens. Ich glaube das liegt an dieser Zeile, weil du hier die `isLoading` Variable setzt und nicht nach dem oben angegebenen 250ms delay.

okay danke :) guck ich mir an

okay danke :) guck ich mir an
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',
),
);
}
gelbeinhalb marked this conversation as resolved Outdated

Bitte das Custom Widget AppSkeleton aus development hier noch Implementieren

Bitte das Custom Widget `AppSkeleton` aus `development` hier noch Implementieren
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) {
gelbeinhalb marked this conversation as resolved Outdated

padding zu groß, zu viel platz unten ohne button, mit button evtl passend

padding zu groß, zu viel platz unten ohne button, mit button evtl passend
if (index == games.length) {
return SizedBox(
height: MediaQuery.paddingOf(context).bottom - 80,
);
});
}
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(() {
suggestedGameData.clear();
suggestedGameData.addAll(suggestions);
_gameListFuture = db.gameDao.getAllGames();
});
},
),
@@ -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';
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> {
gelbeinhalb marked this conversation as resolved Outdated

Funktionen eines Widgets immer nach der build() Methode, diese soll in einem Widget immer als erstes kommen (nach den Variabel-Deklarationen)

Funktionen eines Widgets immer nach der `build()` Methode, diese soll in einem Widget immer als erstes kommen (nach den Variabel-Deklarationen)

👍

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

Entweder die beiden Group Widgets in ein Visibility bzw Offstage Widget wrappen, oder in ein If-Statement zusammenführen:

if(group != null)
[...SizedBox(), Row()]
Entweder die beiden Group Widgets in ein `Visibility` bzw `Offstage` Widget wrappen, oder in ein If-Statement zusammenführen: ```dart if(group != null) [...SizedBox(), Row()] ```

Von Docs: "Offstage can be used to measure the dimensions of a widget without bringing it on screen (yet). To hide a widget from view while it is not needed, prefer removing the widget from the tree entirely rather than keeping it alive in an Offstage subtree" also eher das if

Von Docs: "Offstage can be used to measure the dimensions of a widget without bringing it on screen (yet). To hide a widget from view while it is not needed, prefer removing the widget from the tree entirely rather than keeping it alive in an Offstage subtree" also eher das if

Ja dann mach das lieber mit If

Ja dann mach das lieber mit If
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',
gelbeinhalb marked this conversation as resolved Outdated

Ggf. ohne Doppelpunkt? Da dass ja nur so ne Zwischenüberschrift ist und die Spielernamen ja nicht dahinter in der gleichen Zeile stehen

Ggf. ohne Doppelpunkt? Da dass ja nur so ne Zwischenüberschrift ist und die Spielernamen ja nicht dahinter in der gleichen Zeile stehen
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(),
),
],
],
),
);
}
gelbeinhalb marked this conversation as resolved Outdated

renderoverflow bei langen namen

renderoverflow bei langen namen
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);
}
}
gelbeinhalb marked this conversation as resolved Outdated

is winenr wird nicht genutzt

is winenr wird nicht genutzt
}
// 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: