From 3cfdbbcdbdf2112220504a3b045becf162675f8f Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Sat, 21 Jan 2023 22:29:54 +0100 Subject: [PATCH] Add Group Frontend --- lib/src/provider/events.dart | 27 +++-- lib/src/screens/event.dart | 156 +++++++++++++++++++++++--- lib/src/screens/search_delegates.dart | 66 +++++++++++ pubspec.lock | 7 ++ pubspec.yaml | 1 + 5 files changed, 232 insertions(+), 25 deletions(-) diff --git a/lib/src/provider/events.dart b/lib/src/provider/events.dart index e3bf099..dd50d2c 100644 --- a/lib/src/provider/events.dart +++ b/lib/src/provider/events.dart @@ -17,6 +17,12 @@ final eventsListProvider = FutureProvider>((ref) async { return repo.listEvents(); }, dependencies: [eventRepositoryProvider]); +final groupsProvider = FutureProvider.autoDispose>((ref) async { + return (await ref.watch(httpClient.future)).get("/data/groups").then((value) { + return (value.data as List).map((e) => e as String).toList(); + }); +}, dependencies: [httpClient]); + class EventRepository { final Dio _client; @@ -71,15 +77,8 @@ class EventRepository { await _client.delete("/fights/$id"); } - Future createFight( - int eventId, - DateTime start, - String mode, - String map, - Team blue, - Team red, - int referee, - ) { + Future createFight(int eventId, DateTime start, String mode, String map, + Team blue, Team red, int referee, String? group) { return _client.post("/fights", data: { "event": eventId, "spielmodus": mode, @@ -88,6 +87,7 @@ class EventRepository { "blueTeam": blue.id, "redTeam": red.id, "kampfleiter": referee, + "group": group ?? "null", }); } @@ -97,7 +97,7 @@ class EventRepository { } Future updateFight(int id, DateTime start, String mode, String map, - int referee, Team blue, Team red) { + int referee, Team blue, Team red, String? group) { return _client.put("/fights/$id", data: { "spielmodus": mode, "map": map, @@ -105,6 +105,7 @@ class EventRepository { "kampfleiter": referee, "blueTeam": blue.id, "redTeam": red.id, + "group": group ?? "null", }); } } @@ -118,9 +119,10 @@ class EventFight { final Team blueTeam; final User kampfleiter; final int score; + final String? group; EventFight(this.id, this.gameMode, this.map, this.start, this.redTeam, - this.blueTeam, this.kampfleiter, this.score); + this.blueTeam, this.kampfleiter, this.score, this.group); Team get winner { return getTeamWithContextColor(Colors.white); @@ -150,7 +152,8 @@ class EventFight { Team.fromJson(json["redTeam"]), Team.fromJson(json["blueTeam"]), User.fromJson(json["kampfleiter"]), - json["ergebnis"]); + json["ergebnis"], + json["group"]); } } diff --git a/lib/src/screens/event.dart b/lib/src/screens/event.dart index 17c2f98..16848b1 100644 --- a/lib/src/screens/event.dart +++ b/lib/src/screens/event.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; +import 'package:grouped_list/grouped_list.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:steamwar_multitool/src/util/event_fight_exporter.dart'; @@ -414,7 +415,7 @@ class _EventFightList extends HookConsumerWidget { [date.value, lowest]); return AlertDialog( - title: const Text("Reshedule Fights"), + title: const Text("Reschedule Fights"), content: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -448,14 +449,15 @@ class _EventFightList extends HookConsumerWidget { final repo = await ref .read(eventRepositoryProvider.future); for (final fight in selectedFights) { - repo.updateFight( + await repo.updateFight( fight.id, fight.start.add(offset), fight.gameMode, fight.map, fight.kampfleiter.id, fight.blueTeam, - fight.redTeam); + fight.redTeam, + fight.group); } update(); Navigator.of(context).pop(); @@ -490,14 +492,15 @@ class _EventFightList extends HookConsumerWidget { await ref.read(eventRepositoryProvider.future); for (final fight in fights.value) { if (selected.value.contains(fight.id)) { - repo.updateFight( + await repo.updateFight( fight.id, fight.start, fight.gameMode, fight.map, kampfleiter.id, fight.blueTeam, - fight.redTeam); + fight.redTeam, + fight.group); } } update(); @@ -505,6 +508,42 @@ class _EventFightList extends HookConsumerWidget { }, icon: const Icon(Icons.person)), ), + Tooltip( + message: "Change Group", + child: IconButton( + onPressed: () async { + if (selected.value.isEmpty) { + return; + } + + final group = await showSearch( + context: context, + delegate: GroupSearchDelegate( + await ref.read(groupsProvider.future))); + + if (group == null) { + return; + } else { + final repo = + await ref.read(eventRepositoryProvider.future); + for (final fight in fights.value) { + if (selected.value.contains(fight.id)) { + await repo.updateFight( + fight.id, + fight.start, + fight.gameMode, + fight.map, + fight.kampfleiter.id, + fight.blueTeam, + fight.redTeam, + group); + } + } + update(); + } + }, + icon: const Icon(Icons.group)), + ), Tooltip( message: "Delete Fights", child: IconButton( @@ -531,7 +570,7 @@ class _EventFightList extends HookConsumerWidget { final repo = await ref .read(eventRepositoryProvider.future); for (final fight in selected.value) { - repo.deleteFight(fight); + await repo.deleteFight(fight); } update(); Navigator.of(context).pop(); @@ -579,7 +618,8 @@ class _EventFightList extends HookConsumerWidget { openExportedFights(fights.value, event.name); } }, - ) + ), + const SizedBox(width: 8), ], ), const Divider(), @@ -588,8 +628,51 @@ class _EventFightList extends HookConsumerWidget { const Center( child: Text("No fights yet"), ), - for (final fight in fights.value) - ListTile( + GroupedListView( + elements: fights.value, + groupBy: (fight) => fight.group ?? "Ungrouped", + shrinkWrap: true, + itemComparator: (fight1, fight2) => + fight1.start.compareTo(fight2.start), + groupComparator: (group1, group2) => group1.compareTo(group2), + floatingHeader: true, + groupSeparatorBuilder: (group) => InkWell( + onTap: () { + final g = group == "Ungrouped" ? null : group; + final isAllSelected = selected.value + .where((element) => + fights.value + .firstWhere((element2) => element2.id == element) + .group == + g) + .length == + fights.value.where((element) => element.group == g).length; + if (isAllSelected) { + selected.value = selected.value + .where((element) => + fights.value + .firstWhere((element2) => element2.id == element) + .group != + g) + .toList(); + } else { + selected.value = [ + ...selected.value, + ...fights.value + .where((element) => element.group == g) + .map((e) => e.id) + ]; + } + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + group, + style: Theme.of(context).textTheme.headline6, + ), + ), + ), + itemBuilder: (context, fight) => ListTile( title: Text( "${fight.blueTeam.name} vs ${fight.redTeam.name}", style: TextStyle( @@ -639,7 +722,8 @@ class _EventFightList extends HookConsumerWidget { .toList(); } }), - ) + ), + ) ], ); } @@ -665,6 +749,7 @@ class EditEventFightDialog extends HookConsumerWidget { final mode = useState(fight.gameMode); final map = useState(fight.map); final referrer = useState(fight.kampfleiter); + final group = useState(fight.group ?? ""); return AlertDialog( title: Text("${fight.blueTeam.name} vs ${fight.redTeam.name}"), @@ -741,6 +826,24 @@ class EditEventFightDialog extends HookConsumerWidget { ? "None" : "${referrer.value.name} (${referrer.value!.id})"), ), + const SizedBox(height: 8), + const Text("Group"), + const SizedBox(height: 8), + TextButton( + onPressed: () async { + final selGroup = await showSearch( + context: context, + delegate: GroupSearchDelegate( + await ref.read(groupsProvider.future))); + if (selGroup != null) { + if (selGroup.isEmpty) { + group.value = ""; + } else { + group.value = selGroup; + } + } + }, + child: Text(group.value.isEmpty ? "None" : group.value)), ], ), actions: [ @@ -784,8 +887,15 @@ class EditEventFightDialog extends HookConsumerWidget { onPressed: () async { var nav = Navigator.of(context); final repo = await ref.read(eventRepositoryProvider.future); - await repo.updateFight(fight.id, start.value, mode.value, - map.value, referrer.value.id, blueTeam.value, redTeam.value); + await repo.updateFight( + fight.id, + start.value, + mode.value, + map.value, + referrer.value.id, + blueTeam.value, + redTeam.value, + group.value.isEmpty ? null : group.value); fightsRefresher(); nav.pop(); }, @@ -813,6 +923,7 @@ class _AddFightWidget extends HookConsumerWidget { final redTeam = useState(null); final date = useState(event.start); final referrer = useState(null); + final group = useState(""); final canCreate = useMemoized(() { return gamemode.value != null && @@ -883,6 +994,24 @@ class _AddFightWidget extends HookConsumerWidget { ? "None" : "${referrer.value!.name} (${referrer.value!.id})"), ), + const SizedBox(height: 8), + const Text("Group"), + const SizedBox(height: 8), + TextButton( + onPressed: () async { + final selGroup = await showSearch( + context: context, + delegate: GroupSearchDelegate( + await ref.read(groupsProvider.future))); + if (selGroup != null) { + if (selGroup.isEmpty) { + group.value = ""; + } else { + group.value = selGroup; + } + } + }, + child: Text(group.value.isEmpty ? "None" : group.value)), ], ), actions: [ @@ -903,7 +1032,8 @@ class _AddFightWidget extends HookConsumerWidget { map.value!, blueTeam.value!, redTeam.value!, - referrer.value?.id ?? 0); + referrer.value?.id ?? 0, + group.value.isEmpty ? null : group.value); onAdded.call(); nav.pop(); } diff --git a/lib/src/screens/search_delegates.dart b/lib/src/screens/search_delegates.dart index 4513afd..2d6f187 100644 --- a/lib/src/screens/search_delegates.dart +++ b/lib/src/screens/search_delegates.dart @@ -317,3 +317,69 @@ class TeamSearchDelegate extends SearchDelegate { ); } } + +class GroupSearchDelegate extends SearchDelegate { + final List groups; + + GroupSearchDelegate(this.groups); + + @override + List? buildActions(BuildContext context) { + return [ + IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + query = ""; + }, + ) + ]; + } + + @override + Widget? buildLeading(BuildContext context) { + return IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + close(context, null); + }, + ); + } + + @override + Widget buildResults(BuildContext context) { + return buildSuggestions(context); + } + + @override + Widget buildSuggestions(BuildContext context) { + return ListView( + children: [ + ListTile( + title: Text(query), + onTap: query.isEmpty + ? null + : () { + close(context, query); + }, + subtitle: const Text("Create new group"), + leading: const Icon(Icons.add), + ), + ListTile( + leading: const Icon(Icons.clear), + title: const Text("Reset"), + onTap: () { + close(context, ""); + }, + ), + for (final group in groups) + if (group.toLowerCase().contains(query.toLowerCase())) + ListTile( + title: Text(group), + onTap: () { + close(context, group); + }, + ) + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index f770dc3..0ecb465 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -282,6 +282,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.0" + grouped_list: + dependency: "direct main" + description: + name: grouped_list + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.2" hooks_riverpod: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d65260f..3bede96 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: go_router: ^6.0.0 intl: ^0.18.0 csv: ^5.0.1 + grouped_list: ^5.1.2 dev_dependencies: