Merge remote-tracking branch 'origin/development' into bug/162-text-overflows-durch-tastatur
# Conflicts: # pubspec.yaml
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
import 'package:game_tracker/data/db/database.dart';
|
||||
import 'package:game_tracker/data/dto/group.dart';
|
||||
import 'package:game_tracker/data/dto/match.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
import 'package:game_tracker/l10n/generated/app_localizations.dart';
|
||||
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
|
||||
import 'package:game_tracker/presentation/widgets/buttons/animated_dialog_button.dart';
|
||||
import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart';
|
||||
import 'package:game_tracker/presentation/widgets/colored_icon_container.dart';
|
||||
import 'package:game_tracker/presentation/widgets/custom_alert_dialog.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class GroupProfileView extends StatefulWidget {
|
||||
/// A view that displays the profile of a group
|
||||
/// - [group]: The group to display
|
||||
const GroupProfileView({
|
||||
super.key,
|
||||
required this.group,
|
||||
required this.callback,
|
||||
});
|
||||
|
||||
/// The group to display
|
||||
final Group group;
|
||||
|
||||
final VoidCallback callback;
|
||||
|
||||
@override
|
||||
State<GroupProfileView> createState() => _GroupProfileViewState();
|
||||
}
|
||||
|
||||
class _GroupProfileViewState extends State<GroupProfileView> {
|
||||
late final AppDatabase db;
|
||||
bool isLoading = true;
|
||||
|
||||
/// Total matches played in this group
|
||||
int totalMatches = 0;
|
||||
|
||||
String bestPlayer = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
_loadStatistics();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text(loc.group_profile),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () async {
|
||||
showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => CustomAlertDialog(
|
||||
title: '${loc.delete_group}?',
|
||||
content: loc.this_cannot_be_undone,
|
||||
actions: [
|
||||
AnimatedDialogButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(
|
||||
loc.cancel,
|
||||
style: const TextStyle(color: CustomTheme.textColor),
|
||||
),
|
||||
),
|
||||
AnimatedDialogButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(
|
||||
loc.delete,
|
||||
style: TextStyle(color: CustomTheme.secondaryColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((confirmed) async {
|
||||
if (confirmed! && context.mounted) {
|
||||
await db.groupDao.deleteGroup(groupId: widget.group.id);
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
widget.callback.call();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ListView(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 20,
|
||||
bottom: 100,
|
||||
),
|
||||
children: [
|
||||
const Center(
|
||||
child: ColoredIconContainer(
|
||||
icon: Icons.group,
|
||||
containerSize: 55,
|
||||
iconSize: 38,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
widget.group.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(widget.group.createdAt)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
InfoTile(
|
||||
title: loc.members,
|
||||
icon: Icons.people,
|
||||
horizontalAlignment: CrossAxisAlignment.start,
|
||||
content: Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: widget.group.members.map((member) {
|
||||
return TextIconTile(
|
||||
text: member.name,
|
||||
iconEnabled: false,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
InfoTile(
|
||||
title: loc.statistics,
|
||||
icon: Icons.bar_chart,
|
||||
content: AppSkeleton(
|
||||
enabled: isLoading,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildStatRow(
|
||||
loc.members,
|
||||
widget.group.members.length.toString(),
|
||||
),
|
||||
_buildStatRow(
|
||||
loc.played_matches,
|
||||
totalMatches.toString(),
|
||||
),
|
||||
_buildStatRow(loc.best_player, bestPlayer),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: MediaQuery.paddingOf(context).bottom,
|
||||
child: MainMenuButton(
|
||||
text: loc.edit_group,
|
||||
icon: Icons.edit,
|
||||
onPressed: () {
|
||||
// TODO: Uncomment when GroupDetailView is implemented
|
||||
/*
|
||||
await Navigator.push(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) {
|
||||
|
||||
return const GroupDetailView();
|
||||
},
|
||||
),
|
||||
);*/
|
||||
print('Edit Group pressed');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a single statistic row with a label and value
|
||||
/// - [label]: The label of the statistic
|
||||
/// - [value]: The value of the statistic
|
||||
Widget _buildStatRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads statistics for this group
|
||||
Future<void> _loadStatistics() async {
|
||||
final matches = await db.matchDao.getAllMatches();
|
||||
final groupMatches = matches
|
||||
.where((match) => match.group?.id == widget.group.id)
|
||||
.toList();
|
||||
|
||||
setState(() {
|
||||
totalMatches = groupMatches.length;
|
||||
bestPlayer = _getBestPlayer(groupMatches);
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// Determines the best player in the group based on match wins
|
||||
String _getBestPlayer(List<Match> matches) {
|
||||
final bestPlayerCounts = <Player, int>{};
|
||||
|
||||
// Count wins for each player
|
||||
for (var match in matches) {
|
||||
if (match.winner != null) {
|
||||
bestPlayerCounts.update(
|
||||
match.winner!,
|
||||
(value) => value + 1,
|
||||
ifAbsent: () => 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort players by win count
|
||||
final sortedPlayers = bestPlayerCounts.entries.toList()
|
||||
..sort((a, b) => b.value.compareTo(a.value));
|
||||
|
||||
// Get the best player
|
||||
bestPlayer = sortedPlayers.isNotEmpty ? sortedPlayers.first.key.name : '-';
|
||||
|
||||
return bestPlayer;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:game_tracker/data/dto/group.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
import 'package:game_tracker/l10n/generated/app_localizations.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/group_view/create_group_view.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/group_view/group_profile_view.dart';
|
||||
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
|
||||
import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
|
||||
@@ -74,7 +75,22 @@ class _GroupsViewState extends State<GroupsView> {
|
||||
height: MediaQuery.paddingOf(context).bottom - 20,
|
||||
);
|
||||
}
|
||||
return GroupTile(group: groups[index]);
|
||||
return GroupTile(
|
||||
group: groups[index],
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) {
|
||||
return GroupProfileView(
|
||||
group: groups[index],
|
||||
callback: loadGroups,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -105,6 +121,9 @@ class _GroupsViewState extends State<GroupsView> {
|
||||
}
|
||||
|
||||
void loadGroups() {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
Future.wait([
|
||||
db.groupDao.getAllGroups(),
|
||||
Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
import 'package:game_tracker/core/enums.dart';
|
||||
import 'package:game_tracker/l10n/generated/app_localizations.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart';
|
||||
|
||||
class ChooseRulesetView extends StatefulWidget {
|
||||
/// A view that allows the user to choose a ruleset from a list of available rulesets
|
||||
/// - [rulesets]: A list of tuples containing the ruleset and its description
|
||||
/// - [initialRulesetIndex]: The index of the initially selected ruleset
|
||||
const ChooseRulesetView({
|
||||
super.key,
|
||||
required this.rulesets,
|
||||
required this.initialRulesetIndex,
|
||||
});
|
||||
|
||||
/// A list of tuples containing the ruleset and its description
|
||||
final List<(Ruleset, String)> rulesets;
|
||||
|
||||
/// The index of the initially selected ruleset
|
||||
final int initialRulesetIndex;
|
||||
|
||||
@override
|
||||
State<ChooseRulesetView> createState() => _ChooseRulesetViewState();
|
||||
}
|
||||
|
||||
class _ChooseRulesetViewState extends State<ChooseRulesetView> {
|
||||
/// Currently selected ruleset index
|
||||
late int selectedRulesetIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
selectedRulesetIndex = widget.initialRulesetIndex;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
initialIndex: 0,
|
||||
child: Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(
|
||||
selectedRulesetIndex == -1
|
||||
? null
|
||||
: widget.rulesets[selectedRulesetIndex].$1,
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text(loc.choose_ruleset),
|
||||
),
|
||||
body: PopScope(
|
||||
// This fixes that the Android Back Gesture didn't return the
|
||||
// selectedRulesetIndex and therefore the selected Ruleset wasn't saved
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (bool didPop, Object? result) {
|
||||
if (didPop) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop(
|
||||
selectedRulesetIndex == -1
|
||||
? null
|
||||
: widget.rulesets[selectedRulesetIndex].$1,
|
||||
);
|
||||
},
|
||||
child: 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,
|
||||
context,
|
||||
),
|
||||
description: widget.rulesets[index].$2,
|
||||
isHighlighted: selectedRulesetIndex == index,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import 'package:game_tracker/data/dto/player.dart';
|
||||
import 'package:game_tracker/l10n/generated/app_localizations.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_game_view.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_group_view.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart';
|
||||
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
|
||||
import 'package:game_tracker/presentation/widgets/player_selection.dart';
|
||||
@@ -58,13 +57,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
/// the [ChooseGroupView]
|
||||
String selectedGroupId = '';
|
||||
|
||||
/// The currently selected ruleset
|
||||
Ruleset? selectedRuleset;
|
||||
|
||||
/// The index of the currently selected ruleset in [rulesets] to mark it in
|
||||
/// the [ChooseRulesetView]
|
||||
int selectedRulesetIndex = -1;
|
||||
|
||||
/// The index of the currently selected game in [games] to mark it in
|
||||
/// the [ChooseGameView]
|
||||
int selectedGameIndex = -1;
|
||||
@@ -72,9 +64,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
/// The currently selected players
|
||||
List<Player>? selectedPlayers;
|
||||
|
||||
/// List of available rulesets with their localized string representations
|
||||
late final List<(Ruleset, String)> _rulesets;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -107,15 +96,8 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
super.didChangeDependencies();
|
||||
final loc = AppLocalizations.of(context);
|
||||
hintText ??= loc.match_name;
|
||||
_rulesets = [
|
||||
(Ruleset.singleWinner, loc.ruleset_single_winner),
|
||||
(Ruleset.singleLoser, loc.ruleset_single_loser),
|
||||
(Ruleset.mostPoints, loc.ruleset_most_points),
|
||||
(Ruleset.leastPoints, loc.ruleset_least_points),
|
||||
];
|
||||
}
|
||||
|
||||
// TODO: Replace when games are implemented
|
||||
List<(String, String, Ruleset)> games = [
|
||||
('Example Game 1', 'This is a description', Ruleset.leastPoints),
|
||||
('Example Game 2', '', Ruleset.singleWinner),
|
||||
@@ -158,39 +140,12 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
setState(() {
|
||||
if (selectedGameIndex != -1) {
|
||||
hintText = games[selectedGameIndex].$1;
|
||||
selectedRuleset = games[selectedGameIndex].$3;
|
||||
selectedRulesetIndex = _rulesets.indexWhere(
|
||||
(r) => r.$1 == selectedRuleset,
|
||||
);
|
||||
} else {
|
||||
hintText = loc.match_name;
|
||||
selectedRuleset = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
ChooseTile(
|
||||
title: loc.ruleset,
|
||||
trailingText: selectedRuleset == null
|
||||
? loc.none
|
||||
: translateRulesetToString(selectedRuleset!, context),
|
||||
onPressed: () async {
|
||||
selectedRuleset = await Navigator.of(context).push(
|
||||
adaptivePageRoute(
|
||||
builder: (context) => ChooseRulesetView(
|
||||
rulesets: _rulesets,
|
||||
initialRulesetIndex: selectedRulesetIndex,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
selectedRulesetIndex = _rulesets.indexWhere(
|
||||
(r) => r.$1 == selectedRuleset,
|
||||
);
|
||||
selectedGameIndex = -1;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
ChooseTile(
|
||||
title: loc.group,
|
||||
trailingText: selectedGroup == null
|
||||
@@ -275,7 +230,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
/// - Either a group is selected OR at least 2 players are selected
|
||||
bool _enableCreateGameButton() {
|
||||
return (selectedGroup != null ||
|
||||
(selectedPlayers != null && selectedPlayers!.length > 1)) &&
|
||||
selectedRuleset != null;
|
||||
(selectedPlayers != null && selectedPlayers!.length > 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
import 'package:game_tracker/l10n/generated/app_localizations.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart';
|
||||
import 'package:game_tracker/presentation/widgets/colored_icon_container.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class LicenseDetailView extends StatelessWidget {
|
||||
@@ -29,19 +30,11 @@ class LicenseDetailView extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsetsGeometry.only(right: 15),
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.primaryColor.withAlpha(40),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.description,
|
||||
color: CustomTheme.primaryColor,
|
||||
size: 30,
|
||||
),
|
||||
const ColoredIconContainer(
|
||||
icon: Icons.description,
|
||||
margin: EdgeInsetsGeometry.only(right: 15),
|
||||
containerSize: 60,
|
||||
iconSize: 30,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
57
lib/presentation/widgets/colored_icon_container.dart
Normal file
57
lib/presentation/widgets/colored_icon_container.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
|
||||
class ColoredIconContainer extends StatelessWidget {
|
||||
/// A customizable container widget that displays an icon with a colored background.
|
||||
/// - [icon]: The icon to be displayed inside the container.
|
||||
/// - [containerSize]: The size of the container (width and height).
|
||||
/// - [iconSize]: The size of the icon inside the container.
|
||||
/// - [margin]: Optional margin around the container.
|
||||
/// - [padding]: Optional padding inside the container.
|
||||
const ColoredIconContainer({
|
||||
super.key,
|
||||
required this.icon,
|
||||
this.containerSize = 44,
|
||||
this.iconSize = 28,
|
||||
this.margin,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
/// The icon to be displayed inside the container.
|
||||
final IconData icon;
|
||||
|
||||
/// The size of the container (width and height).
|
||||
final double containerSize;
|
||||
|
||||
/// The size of the icon inside the container.
|
||||
final double iconSize;
|
||||
|
||||
/// Optional margin around the container.
|
||||
final EdgeInsetsGeometry? margin;
|
||||
|
||||
/// Optional padding inside the container.
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: containerSize,
|
||||
height: containerSize,
|
||||
margin: margin,
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.primaryColor.withAlpha(40),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: iconSize,
|
||||
color: CustomTheme.primaryColor.withGreen(40),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,17 @@ import 'package:game_tracker/core/custom_theme.dart';
|
||||
import 'package:game_tracker/data/dto/group.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
class GroupTile extends StatelessWidget {
|
||||
class GroupTile extends StatefulWidget {
|
||||
/// A tile widget that displays information about a group, including its name and members.
|
||||
/// - [group]: The group data to be displayed.
|
||||
/// - [isHighlighted]: Whether the tile should be highlighted.
|
||||
const GroupTile({super.key, required this.group, this.isHighlighted = false});
|
||||
/// - [onTap]: Callback function to be executed when the tile is tapped.
|
||||
const GroupTile({
|
||||
super.key,
|
||||
required this.group,
|
||||
this.isHighlighted = false,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
/// The group data to be displayed.
|
||||
final Group group;
|
||||
@@ -15,61 +21,72 @@ class GroupTile extends StatelessWidget {
|
||||
/// Whether the tile should be highlighted.
|
||||
final bool isHighlighted;
|
||||
|
||||
/// Callback function to be executed when the tile is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
State<GroupTile> createState() => _GroupTileState();
|
||||
}
|
||||
|
||||
class _GroupTileState extends State<GroupTile> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
margin: CustomTheme.standardMargin,
|
||||
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
|
||||
decoration: isHighlighted
|
||||
? CustomTheme.highlightedBoxDecoration
|
||||
: CustomTheme.standardBoxDecoration,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
group.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'${group.members.length}',
|
||||
return GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: AnimatedContainer(
|
||||
margin: CustomTheme.standardMargin,
|
||||
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
|
||||
decoration: widget.isHighlighted
|
||||
? CustomTheme.highlightedBoxDecoration
|
||||
: CustomTheme.standardBoxDecoration,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.group.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
const Icon(Icons.group, size: 22),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 12.0,
|
||||
runSpacing: 8.0,
|
||||
children: <Widget>[
|
||||
for (var member in [
|
||||
...group.members,
|
||||
]..sort((a, b) => a.name.compareTo(b.name)))
|
||||
TextIconTile(text: member.name, iconEnabled: false),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2.5),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'${widget.group.members.length}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
const Icon(Icons.group, size: 22),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 12.0,
|
||||
runSpacing: 8.0,
|
||||
children: <Widget>[
|
||||
for (var member in [
|
||||
...widget.group.members,
|
||||
]..sort((a, b) => a.name.compareTo(b.name)))
|
||||
TextIconTile(text: member.name, iconEnabled: false),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2.5),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ class InfoTile extends StatefulWidget {
|
||||
this.padding,
|
||||
this.height,
|
||||
this.width,
|
||||
this.horizontalAlignment = CrossAxisAlignment.center,
|
||||
});
|
||||
|
||||
/// The title text displayed on the tile.
|
||||
@@ -37,6 +38,9 @@ class InfoTile extends StatefulWidget {
|
||||
/// Optional width for the tile.
|
||||
final double? width;
|
||||
|
||||
/// The main axis alignment for the content.
|
||||
final CrossAxisAlignment horizontalAlignment;
|
||||
|
||||
@override
|
||||
State<InfoTile> createState() => _InfoTileState();
|
||||
}
|
||||
@@ -51,7 +55,7 @@ class _InfoTileState extends State<InfoTile> {
|
||||
decoration: CustomTheme.standardBoxDecoration,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: widget.horizontalAlignment,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart';
|
||||
import 'package:game_tracker/presentation/widgets/colored_icon_container.dart';
|
||||
|
||||
class LicenseTile extends StatelessWidget {
|
||||
/// A tile widget that displays information about a software package license.
|
||||
@@ -29,18 +30,10 @@ class LicenseTile extends StatelessWidget {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.primaryColor.withAlpha(40),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.description,
|
||||
color: CustomTheme.primaryColor,
|
||||
size: 32,
|
||||
),
|
||||
const ColoredIconContainer(
|
||||
icon: Icons.description,
|
||||
containerSize: 50,
|
||||
iconSize: 32,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
|
||||
@@ -230,7 +230,7 @@ class _MatchTileState extends State<MatchTile> {
|
||||
} else if (difference.inDays < 7) {
|
||||
return loc.days_ago(difference.inDays);
|
||||
} else {
|
||||
return DateFormat('MMM d, yyyy').format(dateTime);
|
||||
return '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(dateTime)}';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
import 'package:game_tracker/presentation/widgets/colored_icon_container.dart';
|
||||
|
||||
class SettingsListTile extends StatelessWidget {
|
||||
/// A customizable settings list tile widget that displays an icon, title, and an optional suffix widget.
|
||||
@@ -46,18 +47,10 @@ class SettingsListTile extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.primaryColor.withAlpha(40),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 28,
|
||||
color: CustomTheme.primaryColor.withGreen(40),
|
||||
),
|
||||
ColoredIconContainer(
|
||||
icon: icon,
|
||||
containerSize: 44,
|
||||
iconSize: 28,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(title, style: const TextStyle(fontSize: 18)),
|
||||
|
||||
@@ -73,7 +73,6 @@ class TitleDescriptionListTile extends StatelessWidget {
|
||||
const Spacer(),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 115),
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 2,
|
||||
horizontal: 6,
|
||||
|
||||
Reference in New Issue
Block a user