CreateGameView erstellen #67

Merged
sneeex merged 35 commits from feature/3-creategameview-erstellen into development 2025-12-10 20:43:54 +00:00
10 changed files with 440 additions and 223 deletions

View File

@@ -27,5 +27,19 @@ enum ExportResult { success, canceled, unknownException }
/// - [Ruleset.singleWinner]: The game is won by a single player
/// - [Ruleset.singleLoser]: The game is lost by a single player
/// - [Ruleset.mostPoints]: The player with the most points wins.
/// - [Ruleset.lastPoints]: The player with the fewest points wins.
enum Ruleset { singleWinner, singleLoser, mostPoints, lastPoints }
/// - [Ruleset.leastPoints]: The player with the fewest points wins.
enum Ruleset { singleWinner, singleLoser, mostPoints, leastPoints }
/// Translates a [Ruleset] enum value to its corresponding string representation.
String translateRulesetToString(Ruleset ruleset) {
switch (ruleset) {
case Ruleset.singleWinner:
return 'Single Winner';
case Ruleset.singleLoser:
return 'Single Loser';
case Ruleset.mostPoints:
return 'Most Points';
case Ruleset.leastPoints:
return 'Least Points';
}
}

View File

@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart';
import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart';
class ChooseGameView extends StatefulWidget {
final List<(String, String, Ruleset)> games;
final int initialGameIndex;
const ChooseGameView({
super.key,
required this.games,
required this.initialGameIndex,
});
@override
State<ChooseGameView> createState() => _ChooseGameViewState();
}
class _ChooseGameViewState extends State<ChooseGameView> {
late int selectedGameIndex;
final TextEditingController searchBarController = TextEditingController();
@override
void initState() {
selectedGameIndex = widget.initialGameIndex;
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(selectedGameIndex);
},
),
title: const Text(
'Choose Game',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: CustomSearchBar(
controller: searchBarController,
hintText: 'Game Name',
),
),
const SizedBox(height: 5),
Expanded(
child: ListView.builder(
itemCount: widget.games.length,
itemBuilder: (BuildContext context, int index) {
return TitleDescriptionListTile(
title: widget.games[index].$1,
description: widget.games[index].$2,
badgeText: translateRulesetToString(widget.games[index].$3),
isHighlighted: selectedGameIndex == index,
onPressed: () async {
setState(() {
if (selectedGameIndex == index) {
selectedGameIndex = -1;
} else {
selectedGameIndex = index;
}
});
},
);
},
),
),
],
),
);
}
}

View File

@@ -1,16 +1,18 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart';
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
class ChooseGroupView extends StatefulWidget {
final List<Group> groups;
final int initialGroupIndex;
final String initialGroupId;
const ChooseGroupView({
super.key,
required this.groups,
required this.initialGroupIndex,
required this.initialGroupId,
});
@override
@@ -18,11 +20,15 @@ class ChooseGroupView extends StatefulWidget {
}
class _ChooseGroupViewState extends State<ChooseGroupView> {
late int selectedGroupIndex;
late String selectedGroupId;
final TextEditingController controller = TextEditingController();
final String hintText = 'Group Name';
late final List<Group> filteredGroups;
@override
void initState() {
selectedGroupIndex = widget.initialGroupIndex;
selectedGroupId = widget.initialGroupId;
filteredGroups = [...widget.groups];
super.initState();
}
@@ -33,34 +39,90 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(
selectedGroupId == ''
? null
: widget.groups.firstWhere(
(group) => group.id == selectedGroupId,
),
);
},
),
title: const Text(
'Choose Group',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
flixcoo marked this conversation as resolved
Review

Warum lässt sich in Choose Game View suchen, im Choose Group View aber nicht? Können beides ja potenziell sehr lange Listen sein

Warum lässt sich in Choose Game View suchen, im Choose Group View aber nicht? Können beides ja potenziell sehr lange Listen sein
Review

Erledigt (So würde dass dann auch für die Games funktionieren)

Erledigt (So würde dass dann auch für die Games funktionieren)
),
centerTitle: true,
),
body: ListView.builder(
body: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: CustomSearchBar(
controller: controller,
hintText: hintText,
onChanged: (value) {
setState(() {
filterGroups(value);
});
},
),
),
Expanded(
child: Visibility(
visible: filteredGroups.isNotEmpty,
replacement: const TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: 'There is no group matching your search',
),
flixcoo marked this conversation as resolved
Review

Hier bitte ohne Punkt, das hatten wir ja gesagt, und wir müssen uns darauf einigen ob der Titel infos erhält, oder nur keywords wie info/error, weil ich das bis jetzt so gemacht habe.
Sonst passt so

Hier bitte ohne Punkt, das hatten wir ja gesagt, und wir müssen uns darauf einigen ob der Titel infos erhält, oder nur keywords wie info/error, weil ich das bis jetzt so gemacht habe. Sonst passt so
Review

Wenn der Titel keine Infos enthalten soll, würd ich daraus n enum Attribut machen damit der je nach enum gesetzt wird (Icon und Titel)

Wenn der Titel keine Infos enthalten soll, würd ich daraus n enum Attribut machen damit der je nach enum gesetzt wird (Icon und Titel)
Review

ja findste mit oder ohne richtigen Title besser?

ja findste mit oder ohne richtigen Title besser?
Review

ist mir auch egal, lass es so oder nicht. Wenn du es mit Info/Error im Titel willst, mach am besten das einmal temporär in den Titel und dann nen Issue für Änderung der anderen Titel in eine enum Version oder halt eine Version mit aussagekräftigem Titel.

Aber auf jeden Fall den Punkt weg machen am Ende von Message
dann approve ich

ist mir auch egal, lass es so oder nicht. Wenn du es mit Info/Error im Titel willst, mach am besten das einmal temporär in den Titel und dann nen Issue für Änderung der anderen Titel in eine enum Version oder halt eine Version mit aussagekräftigem Titel. **Aber auf jeden Fall den Punkt weg machen am Ende von Message** dann approve ich
Review

erledigt

erledigt
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: widget.groups.length,
itemCount: filteredGroups.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () {
setState(() {
selectedGroupIndex = index;
});
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
Navigator.of(context).pop(widget.groups[index]);
if (selectedGroupId != filteredGroups[index].id) {
selectedGroupId = filteredGroups[index].id;
} else {
selectedGroupId = '';
}
});
},
child: GroupTile(
group: widget.groups[index],
isHighlighted: selectedGroupIndex == index,
group: filteredGroups[index],
isHighlighted:
selectedGroupId == filteredGroups[index].id,
),
);
},
),
),
),
],
),
);
}
/// Filters the groups based on the search query.
/// TODO: Maybe implement also targetting player names?
void filterGroups(String query) {
setState(() {
if (query.isEmpty) {
filteredGroups.clear();
filteredGroups.addAll(widget.groups);
} else {
filteredGroups.clear();
filteredGroups.addAll(
widget.groups.where(
(group) => group.name.toLowerCase().contains(query.toLowerCase()),
),
);
}
});
}
}

View File

@@ -1,11 +1,12 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/presentation/widgets/tiles/ruleset_list_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart';
class ChooseRulesetView extends StatefulWidget {
final List<(Ruleset, String, String)> rulesets;
final List<(Ruleset, String)> rulesets;
final int initialRulesetIndex;
const ChooseRulesetView({
super.key,
required this.rulesets,
@@ -35,85 +36,42 @@ class _ChooseRulesetViewState extends State<ChooseRulesetView> {
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(
selectedRulesetIndex == -1
? null
: widget.rulesets[selectedRulesetIndex].$1,
);
},
),
title: const Text(
'Choose Ruleset',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: Column(
children: [
Container(
color: CustomTheme.backgroundColor,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: TabBar(
padding: const EdgeInsets.symmetric(horizontal: 5),
// Label Settings
labelStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
labelColor: Colors.white,
unselectedLabelStyle: const TextStyle(fontSize: 14),
unselectedLabelColor: Colors.white70,
// Indicator Settings
indicator: CustomTheme.standardBoxDecoration,
indicatorSize: TabBarIndicatorSize.tab,
indicatorWeight: 1,
indicatorPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 0,
),
// Divider Settings
dividerHeight: 0,
tabs: const [
Tab(text: 'Rulesets'),
Tab(text: 'Gametypes'),
],
),
),
const Divider(
indent: 30,
endIndent: 30,
thickness: 3,
radius: BorderRadius.all(Radius.circular(12)),
),
Expanded(
child: TabBarView(
children: [
ListView.builder(
body: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: widget.rulesets.length,
itemBuilder: (BuildContext context, int index) {
return RulesetListTile(
return TitleDescriptionListTile(
onPressed: () async {
setState(() {
if (selectedRulesetIndex == index) {
selectedRulesetIndex = -1;
} else {
selectedRulesetIndex = index;
});
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
Navigator.of(
context,
).pop(widget.rulesets[index].$1);
}
});
},
title: widget.rulesets[index].$2,
description: widget.rulesets[index].$3,
title: translateRulesetToString(widget.rulesets[index].$1),
description: widget.rulesets[index].$2,
isHighlighted: selectedRulesetIndex == index,
);
},
),
const Center(
child: Text(
'No gametypes available',
style: TextStyle(color: Colors.white70),
),
),
],
),
),
],
),
),
);
}

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/core/enums.dart';
@@ -5,8 +6,10 @@ 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_game/choose_game_view.dart';
import 'package:game_tracker/presentation/views/main_menu/create_game/choose_group_view.dart';
import 'package:game_tracker/presentation/views/main_menu/create_game/choose_ruleset_view.dart';
import 'package:game_tracker/presentation/views/main_menu/game_result_view.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/player_selection.dart';
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
@@ -14,7 +17,8 @@ import 'package:game_tracker/presentation/widgets/tiles/choose_tile.dart';
import 'package:provider/provider.dart';
class CreateGameView extends StatefulWidget {
const CreateGameView({super.key});
final VoidCallback? onWinnerChanged;
const CreateGameView({super.key, this.onWinnerChanged});
@override
State<CreateGameView> createState() => _CreateGameViewState();
@@ -39,12 +43,18 @@ class _CreateGameViewState extends State<CreateGameView> {
/// List of all players from the database
List<Player> playerList = [];
/// List of players filtered based on the selected group
/// If a group is selected, this list contains all players from [playerList]
/// who are not members of the selected group. If no group is selected,
/// this list is identical to [playerList].
List<Player> filteredPlayerList = [];
/// The currently selected group
Group? selectedGroup;
/// The index of the currently selected group in [groupsList] to mark it in
/// the [ChooseGroupView]
int selectedGroupIndex = -1;
String selectedGroupId = '';
/// The currently selected ruleset
Ruleset? selectedRuleset;
@@ -53,37 +63,48 @@ class _CreateGameViewState extends State<CreateGameView> {
/// the [ChooseRulesetView]
int selectedRulesetIndex = -1;
/// The index of the currently selected game in [games] to mark it in
/// the [ChooseGameView]
int selectedGameIndex = -1;
/// The currently selected players
List<Player>? selectedPlayers;
/// List of available rulesets with their display names and descriptions
/// as tuples of (Ruleset, String, String)
List<(Ruleset, String, String)> rulesets = [
/// List of available rulesets with their descriptions
/// as tuples of (Ruleset, String)
/// TODO: Replace when rulesets are implemented
List<(Ruleset, String)> rulesets = [
(
Ruleset.singleWinner,
'Single Winner',
'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.',
),
(
Ruleset.singleLoser,
'Single Loser',
'Exactly one loser is determined; last place receives the penalty or consequence.',
),
(
Ruleset.mostPoints,
'Most Points',
'Traditional ruleset: the player with the most points wins.',
),
(
Ruleset.lastPoints,
'Least Points',
Ruleset.leastPoints,
'Inverse scoring: the player with the fewest points wins.',
),
];
// TODO: Replace when games are implemented
List<(String, String, Ruleset)> games = [
('Example Game 1', 'This is a discription', Ruleset.leastPoints),
('Example Game 2', '', Ruleset.singleWinner),
];
@override
void initState() {
super.initState();
_gameNameController.addListener(() {
setState(() {});
});
db = Provider.of<AppDatabase>(context, listen: false);
_allGroupsFuture = db.groupDao.getAllGroups();
@@ -93,6 +114,8 @@ class _CreateGameViewState extends State<CreateGameView> {
groupsList = result[0] as List<Group>;
playerList = result[1] as List<Player>;
});
filteredPlayerList = List.from(playerList);
}
@override
@@ -113,15 +136,38 @@ class _CreateGameViewState extends State<CreateGameView> {
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
child: TextInputField(
controller: _gameNameController,
hintText: 'Game name',
onChanged: (value) {
setState(() {});
},
),
),
ChooseTile(
title: 'Game',
trailingText: selectedGameIndex == -1
? 'None'
: games[selectedGameIndex].$1,
onPressed: () async {
selectedGameIndex = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChooseGameView(
games: games,
initialGameIndex: selectedGameIndex,
),
),
);
setState(() {
if (selectedGameIndex != -1) {
selectedRuleset = games[selectedGameIndex].$3;
selectedRulesetIndex = rulesets.indexWhere(
(r) => r.$1 == selectedRuleset,
);
} else {
selectedRuleset = null;
}
});
},
),
ChooseTile(
title: 'Ruleset',
trailingText: selectedRuleset == null
@@ -139,6 +185,7 @@ class _CreateGameViewState extends State<CreateGameView> {
selectedRulesetIndex = rulesets.indexWhere(
(r) => r.$1 == selectedRuleset,
);
selectedGameIndex = -1;
setState(() {});
},
),
@@ -152,28 +199,28 @@ class _CreateGameViewState extends State<CreateGameView> {
MaterialPageRoute(
builder: (context) => ChooseGroupView(
groups: groupsList,
initialGroupIndex: selectedGroupIndex,
initialGroupId: selectedGroupId,
),
),
);
selectedGroupIndex = groupsList.indexWhere(
(g) => g.id == selectedGroup?.id,
);
selectedGroupId = selectedGroup?.id ?? '';
if (selectedGroup != null) {
filteredPlayerList = playerList
.where(
(p) => !selectedGroup!.members.any((m) => m.id == p.id),
)
.toList();
} else {
filteredPlayerList = List.from(playerList);
}
setState(() {});
},
),
Expanded(
child: PlayerSelection(
key: ValueKey(selectedGroup?.id ?? 'no_group'),
initialPlayers: selectedGroup == null
? playerList
: playerList
.where(
(p) => !selectedGroup!.members.any(
(m) => m.id == p.id,
),
)
.toList(),
initialSelectedPlayers: selectedPlayers ?? [],
availablePlayers: filteredPlayerList,
onChanged: (value) {
setState(() {
selectedPlayers = value;
@@ -181,7 +228,6 @@ class _CreateGameViewState extends State<CreateGameView> {
},
),
),
CustomWidthButton(
text: 'Create game',
sizeRelativeToWidth: 0.95,
@@ -191,42 +237,37 @@ class _CreateGameViewState extends State<CreateGameView> {
Game game = Game(
name: _gameNameController.text.trim(),
createdAt: DateTime.now(),
group: selectedGroup!,
group: selectedGroup,
players: selectedPlayers,
);
// TODO: Replace with navigation to GameResultView()
print('Created game: $game');
Navigator.pop(context);
await db.gameDao.addGame(game: game);
if (context.mounted) {
Navigator.pushReplacement(
context,
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) => GameResultView(
game: game,
onWinnerChanged: widget.onWinnerChanged,
),
),
);
}
}
: null,
),
const SizedBox(height: 20),
],
),
),
);
}
/// Translates a [Ruleset] enum value to its corresponding string representation.
String translateRulesetToString(Ruleset ruleset) {
switch (ruleset) {
case Ruleset.singleWinner:
return 'Single Winner';
case Ruleset.singleLoser:
return 'Single Loser';
case Ruleset.mostPoints:
return 'Most Points';
case Ruleset.lastPoints:
return 'Least Points';
}
}
/// Determines whether the "Create Game" button should be enabled based on
/// the current state of the input fields.
bool _enableCreateGameButton() {
return _gameNameController.text.isNotEmpty &&
(selectedGroup != null ||
(selectedPlayers != null && selectedPlayers!.isNotEmpty)) &&
(selectedPlayers != null && selectedPlayers!.length > 1)) &&
selectedRuleset != null;
}
}

View File

@@ -5,7 +5,7 @@ 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/views/main_menu/create_game/create_game_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';
@@ -98,17 +98,16 @@ class _GameHistoryViewState extends State<GameHistoryView> {
}
return GameHistoryTile(
onTap: () async {
await Navigator.push(
Navigator.push(
context,
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) =>
GameResultView(game: games[index]),
builder: (context) => GameResultView(
game: games[index],
onWinnerChanged: refreshGameList,
),
),
);
setState(() {
_gameListFuture = db.gameDao.getAllGames();
});
},
game: games[index],
);
@@ -123,17 +122,13 @@ class _GameHistoryViewState extends State<GameHistoryView> {
text: 'Create Game',
sizeRelativeToWidth: 0.90,
onPressed: () async {
await Navigator.push(
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const CreateGroupView();
},
builder: (context) =>
CreateGameView(onWinnerChanged: refreshGameList),
),
);
setState(() {
_gameListFuture = db.gameDao.getAllGames();
});
},
),
),
@@ -141,4 +136,10 @@ class _GameHistoryViewState extends State<GameHistoryView> {
),
);
}
void refreshGameList() {
setState(() {
_gameListFuture = db.gameDao.getAllGames();
});
}
}

View File

@@ -9,8 +9,9 @@ import 'package:provider/provider.dart';
class GameResultView extends StatefulWidget {
final Game game;
const GameResultView({super.key, required this.game});
final VoidCallback? onWinnerChanged;
const GameResultView({super.key, required this.game, this.onWinnerChanged});
@override
State<GameResultView> createState() => _GameResultViewState();
}
@@ -131,6 +132,7 @@ class _GameResultViewState extends State<GameResultView> {
winnerId: _selectedPlayer!.id,
);
}
widget.onWinnerChanged?.call();
}
List<Player> getAllPlayers(Game game) {

View File

@@ -11,12 +11,14 @@ import 'package:provider/provider.dart';
class PlayerSelection extends StatefulWidget {
final Function(List<Player> value) onChanged;
final List<Player> initialPlayers;
final List<Player> availablePlayers;
final List<Player>? initialSelectedPlayers;
const PlayerSelection({
super.key,
required this.onChanged,
this.initialPlayers = const [],
this.availablePlayers = const [],
this.initialSelectedPlayers,
});
@override
@@ -51,10 +53,24 @@ class _PlayerSelectionState extends State<PlayerSelection> {
suggestedPlayers = skeletonData;
_allPlayersFuture.then((loadedPlayers) {
setState(() {
if (widget.initialPlayers.isNotEmpty) {
allPlayers = [...widget.initialPlayers];
suggestedPlayers = [...widget.initialPlayers];
// If a list of available players is provided, use that list.
if (widget.availablePlayers.isNotEmpty) {
widget.availablePlayers.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...widget.availablePlayers];
suggestedPlayers = [...allPlayers];
if (widget.initialSelectedPlayers != null) {
// Ensures that only players available for selection are pre-selected.
selectedPlayers = widget.initialSelectedPlayers!
.where(
(p) => widget.availablePlayers.any(
(available) => available.id == p.id,
),
)
.toList();
}
} else {
// Otherwise, use the loaded players from the database.
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...loadedPlayers];
suggestedPlayers = [...loadedPlayers];

View File

@@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class RulesetListTile extends StatelessWidget {
final String title;
final String description;
final VoidCallback? onPressed;
final bool isHighlighted;
const RulesetListTile({
super.key,
required this.title,
required this.description,
this.onPressed,
this.isHighlighted = false,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressed,
child: AnimatedContainer(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
decoration: isHighlighted
? CustomTheme.highlightedBoxDecoration
: CustomTheme.standardBoxDecoration,
duration: const Duration(milliseconds: 200),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
title,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
],
),
const SizedBox(height: 5),
Text(description, style: const TextStyle(fontSize: 14)),
const SizedBox(height: 2.5),
],
),
),
);
}
}

View File

@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class TitleDescriptionListTile extends StatelessWidget {
final String title;
final String description;
final VoidCallback? onPressed;
final bool isHighlighted;
final String? badgeText;
final Color? badgeColor;
const TitleDescriptionListTile({
super.key,
required this.title,
required this.description,
this.onPressed,
this.isHighlighted = false,
this.badgeText,
this.badgeColor,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressed,
child: AnimatedContainer(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
decoration: isHighlighted
? CustomTheme.highlightedBoxDecoration
: CustomTheme.standardBoxDecoration,
duration: const Duration(milliseconds: 200),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 230,
child: Text(
title,
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
if (badgeText != null) ...[
const Spacer(),
Container(
constraints: const BoxConstraints(maxWidth: 100),
margin: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.symmetric(
vertical: 2,
horizontal: 6,
),
decoration: BoxDecoration(
color: badgeColor ?? CustomTheme.primaryColor,
borderRadius: BorderRadius.circular(4),
),
child: Text(
flixcoo marked this conversation as resolved
Review

badgetext nicht overflow sicher
grafik.png

badgetext nicht overflow sicher ![grafik.png](/attachments/44ff847b-617f-4a67-9a09-1547fe967e1c)
badgeText!,
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
if (description.isNotEmpty) ...[
const SizedBox(height: 5),
Text(description, style: const TextStyle(fontSize: 14)),
const SizedBox(height: 2.5),
],
],
),
),
);
}
}