implement group edit view
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Failing after 2m3s

This commit is contained in:
2026-01-10 20:14:37 +01:00
parent 8791b5296e
commit 45a419cae7
12 changed files with 395 additions and 211 deletions

View File

@@ -70,6 +70,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
suggestedPlayers = skeletonData;
selectedPlayers = widget.initialSelectedPlayers ?? [];
loadPlayerList();
}
@@ -99,7 +100,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
if (value.isEmpty) {
// If the search is empty, it shows all unselected players.
suggestedPlayers = allPlayers.where((player) {
return !selectedPlayers.contains(player);
return !selectedPlayers.any((p) => p.id == player.id);
}).toList();
} else {
// If there is input, it filters by name match (case-insensitive) and ensures
@@ -108,9 +109,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
final bool nameMatches = player.name.toLowerCase().contains(
value.toLowerCase(),
);
final bool isNotSelected = !selectedPlayers.contains(
player,
);
final bool isNotSelected = !selectedPlayers.any((p) => p.id == player.id);
return nameMatches && isNotSelected;
}).toList();
}
@@ -125,46 +124,49 @@ class _PlayerSelectionState extends State<PlayerSelection> {
const SizedBox(height: 10),
SizedBox(
height: 50,
child: selectedPlayers.isEmpty
? Center(child: Text(loc.no_players_selected))
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (var player in selectedPlayers)
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: TextIconTile(
text: player.name,
onIconTap: () {
setState(() {
// Removes the player from the selection and notifies the parent.
selectedPlayers.remove(player);
widget.onChanged([...selectedPlayers]);
child: AppSkeleton(
enabled: isLoading,
child: selectedPlayers.isEmpty
? Center(child: Text(loc.no_players_selected))
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (var player in selectedPlayers)
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: TextIconTile(
text: player.name,
onIconTap: () {
setState(() {
// Removes the player from the selection and notifies the parent.
selectedPlayers.remove(player);
widget.onChanged([...selectedPlayers]);
// Get the current search query
final currentSearch = _searchBarController
.text
.toLowerCase();
// Get the current search query
final currentSearch = _searchBarController
.text
.toLowerCase();
// If the player matches the current search query (or search is empty),
// they are added back to the `suggestedPlayers` and the list is re-sorted.
if (currentSearch.isEmpty ||
player.name.toLowerCase().contains(
currentSearch,
)) {
suggestedPlayers.add(player);
suggestedPlayers.sort(
(a, b) => a.name.compareTo(b.name),
);
}
});
},
// If the player matches the current search query (or search is empty),
// they are added back to the `suggestedPlayers` and the list is re-sorted.
if (currentSearch.isEmpty ||
player.name.toLowerCase().contains(
currentSearch,
)) {
suggestedPlayers.add(player);
suggestedPlayers.sort(
(a, b) => a.name.compareTo(b.name),
);
}
});
},
),
),
),
],
],
),
),
),
),
),
const SizedBox(height: 10),
Text(
@@ -243,7 +245,21 @@ class _PlayerSelectionState extends State<PlayerSelection> {
// Otherwise, use the loaded players from the database.
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...loadedPlayers];
suggestedPlayers = [...loadedPlayers];
if (widget.initialSelectedPlayers != null) {
// Excludes already selected players from the suggested players list.
suggestedPlayers = loadedPlayers.where((p) => !widget.initialSelectedPlayers!.any((ip) => ip.id == p.id)).toList();
// Ensures that only players available for selection are pre-selected.
selectedPlayers = widget.initialSelectedPlayers!
.where(
(p) => allPlayers.any(
(available) => available.id == p.id,
),
)
.toList();
} else {
// If no initial selection, all loaded players are suggested.
suggestedPlayers = [...loadedPlayers];
}
}
isLoading = false;
});

View File

@@ -6,8 +6,9 @@ import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
/// 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.
/// - [onTap]: An optional callback function to handle tap events.
class GroupTile extends StatelessWidget {
const GroupTile({super.key, required this.group, this.isHighlighted = false});
const GroupTile({super.key, required this.group, this.isHighlighted = false, this.onTap});
/// The group data to be displayed.
final Group group;
@@ -15,61 +16,67 @@ class GroupTile extends StatelessWidget {
/// Whether the tile should be highlighted.
final bool isHighlighted;
/// Callback function to handle tap events.
final VoidCallback? onTap;
@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: onTap,
child: 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.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(
'${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 [
...group.members,
]..sort((a, b) => a.name.compareTo(b.name)))
TextIconTile(text: member.name, iconEnabled: false),
],
),
const SizedBox(height: 2.5),
],
),
),
);
}