diff --git a/lib/core/enums.dart b/lib/core/enums.dart index af1f4a6..68752fb 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -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'; + } +} diff --git a/lib/presentation/views/main_menu/create_game/choose_game_view.dart b/lib/presentation/views/main_menu/create_game/choose_game_view.dart new file mode 100644 index 0000000..53a4fcb --- /dev/null +++ b/lib/presentation/views/main_menu/create_game/choose_game_view.dart @@ -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 createState() => _ChooseGameViewState(); +} + +class _ChooseGameViewState extends State { + 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; + } + }); + }, + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/views/main_menu/create_game/choose_group_view.dart b/lib/presentation/views/main_menu/create_game/choose_group_view.dart index c98ce6d..c30d6ef 100644 --- a/lib/presentation/views/main_menu/create_game/choose_group_view.dart +++ b/lib/presentation/views/main_menu/create_game/choose_group_view.dart @@ -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 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 { - late int selectedGroupIndex; + late String selectedGroupId; + final TextEditingController controller = TextEditingController(); + final String hintText = 'Group Name'; + late final List filteredGroups; @override void initState() { - selectedGroupIndex = widget.initialGroupIndex; + selectedGroupId = widget.initialGroupId; + filteredGroups = [...widget.groups]; super.initState(); } @@ -33,34 +39,90 @@ class _ChooseGroupViewState extends State { 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), ), centerTitle: true, ), - body: ListView.builder( - padding: const EdgeInsets.only(bottom: 85), - itemCount: widget.groups.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]); - }); - }, - child: GroupTile( - group: widget.groups[index], - isHighlighted: selectedGroupIndex == index, + 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', + ), + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 85), + itemCount: filteredGroups.length, + itemBuilder: (BuildContext context, int index) { + return GestureDetector( + onTap: () { + setState(() { + if (selectedGroupId != filteredGroups[index].id) { + selectedGroupId = filteredGroups[index].id; + } else { + selectedGroupId = ''; + } + }); + }, + child: GroupTile( + 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()), + ), + ); + } + }); + } } diff --git a/lib/presentation/views/main_menu/create_game/choose_ruleset_view.dart b/lib/presentation/views/main_menu/create_game/choose_ruleset_view.dart index b54f56e..537f749 100644 --- a/lib/presentation/views/main_menu/create_game/choose_ruleset_view.dart +++ b/lib/presentation/views/main_menu/create_game/choose_ruleset_view.dart @@ -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,84 +36,41 @@ class _ChooseRulesetViewState extends State { 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( - padding: const EdgeInsets.only(bottom: 85), - itemCount: widget.rulesets.length, - itemBuilder: (BuildContext context, int index) { - return RulesetListTile( - onPressed: () async { - setState(() { - 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, - isHighlighted: selectedRulesetIndex == index, - ); - }, - ), - const Center( - child: Text( - 'No gametypes available', - style: TextStyle(color: Colors.white70), - ), - ), - ], - ), - ), - ], + body: ListView.builder( + padding: const EdgeInsets.only(bottom: 85), + itemCount: widget.rulesets.length, + itemBuilder: (BuildContext context, int index) { + return TitleDescriptionListTile( + onPressed: () async { + setState(() { + if (selectedRulesetIndex == index) { + selectedRulesetIndex = -1; + } else { + selectedRulesetIndex = index; + } + }); + }, + title: translateRulesetToString(widget.rulesets[index].$1), + description: widget.rulesets[index].$2, + isHighlighted: selectedRulesetIndex == index, + ); + }, ), ), ); diff --git a/lib/presentation/views/main_menu/create_game/create_game_view.dart b/lib/presentation/views/main_menu/create_game/create_game_view.dart index 485118b..eef2de6 100644 --- a/lib/presentation/views/main_menu/create_game/create_game_view.dart +++ b/lib/presentation/views/main_menu/create_game/create_game_view.dart @@ -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 createState() => _CreateGameViewState(); @@ -39,12 +43,18 @@ class _CreateGameViewState extends State { /// List of all players from the database List 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 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 { /// 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? 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(context, listen: false); _allGroupsFuture = db.groupDao.getAllGroups(); @@ -93,6 +114,8 @@ class _CreateGameViewState extends State { groupsList = result[0] as List; playerList = result[1] as List; }); + + filteredPlayerList = List.from(playerList); } @override @@ -113,15 +136,38 @@ class _CreateGameViewState extends State { 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 { selectedRulesetIndex = rulesets.indexWhere( (r) => r.$1 == selectedRuleset, ); + selectedGameIndex = -1; setState(() {}); }, ), @@ -152,28 +199,28 @@ class _CreateGameViewState extends State { 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 { }, ), ), - CustomWidthButton( text: 'Create game', sizeRelativeToWidth: 0.95, @@ -191,42 +237,37 @@ class _CreateGameViewState extends State { 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; } } diff --git a/lib/presentation/views/main_menu/game_history_view.dart b/lib/presentation/views/main_menu/game_history_view.dart index a4b8cca..46cb1db 100644 --- a/lib/presentation/views/main_menu/game_history_view.dart +++ b/lib/presentation/views/main_menu/game_history_view.dart @@ -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 { } 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 { 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 { ), ); } + + void refreshGameList() { + setState(() { + _gameListFuture = db.gameDao.getAllGames(); + }); + } } diff --git a/lib/presentation/views/main_menu/game_result_view.dart b/lib/presentation/views/main_menu/game_result_view.dart index f13553b..6e60410 100644 --- a/lib/presentation/views/main_menu/game_result_view.dart +++ b/lib/presentation/views/main_menu/game_result_view.dart @@ -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 createState() => _GameResultViewState(); } @@ -131,6 +132,7 @@ class _GameResultViewState extends State { winnerId: _selectedPlayer!.id, ); } + widget.onWinnerChanged?.call(); } List getAllPlayers(Game game) { diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 092a613..e2114b2 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -11,12 +11,14 @@ import 'package:provider/provider.dart'; class PlayerSelection extends StatefulWidget { final Function(List value) onChanged; - final List initialPlayers; + final List availablePlayers; + final List? 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 { 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]; diff --git a/lib/presentation/widgets/tiles/ruleset_list_tile.dart b/lib/presentation/widgets/tiles/ruleset_list_tile.dart deleted file mode 100644 index 13eaf82..0000000 --- a/lib/presentation/widgets/tiles/ruleset_list_tile.dart +++ /dev/null @@ -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), - ], - ), - ), - ); - } -} diff --git a/lib/presentation/widgets/tiles/title_description_list_tile.dart b/lib/presentation/widgets/tiles/title_description_list_tile.dart new file mode 100644 index 0000000..7a138a0 --- /dev/null +++ b/lib/presentation/widgets/tiles/title_description_list_tile.dart @@ -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( + 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), + ], + ], + ), + ), + ); + } +}