diff --git a/lib/src/screens/event/components/fight_list.dart b/lib/src/screens/event/components/fight_list.dart
index a10adaf..83745bb 100644
--- a/lib/src/screens/event/components/fight_list.dart
+++ b/lib/src/screens/event/components/fight_list.dart
@@ -22,6 +22,7 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.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:steamwar_multitool/src/components/date_time_editor.dart';
@@ -204,7 +205,7 @@ class EventFightList extends HookConsumerWidget {
update();
}
},
- icon: const Icon(Icons.person)),
+ icon: const Icon(Icons.gavel)),
),
Tooltip(
message: "Change Group",
@@ -302,6 +303,13 @@ class EventFightList extends HookConsumerWidget {
icon: const Icon(Icons.add),
),
const SizedBox(width: 8),
+ FilledButton.icon(
+ onPressed: () {
+ context.go("/event/${event.id}/generator");
+ },
+ label: const Text("Generate Fight"),
+ icon: const Icon(Icons.shuffle)),
+ const SizedBox(width: 8),
PopupMenuButton(
itemBuilder: (context) {
return const [
diff --git a/lib/src/screens/event/components/loaded_event.dart b/lib/src/screens/event/components/loaded_event.dart
index 4e2f4cc..abace7a 100644
--- a/lib/src/screens/event/components/loaded_event.dart
+++ b/lib/src/screens/event/components/loaded_event.dart
@@ -118,7 +118,7 @@ class LoadedEventScreen extends HookConsumerWidget {
: null,
icon: const Icon(Icons.save)),
],
- leading: IconButton(
+ leading: BackButton(
onPressed: () async {
var canPop = false;
if (changed.value) {
@@ -147,7 +147,6 @@ class LoadedEventScreen extends HookConsumerWidget {
}
if (canPop) context.go("/");
},
- icon: const Icon(Icons.arrow_back),
),
),
body: Padding(
@@ -283,7 +282,7 @@ class LoadedEventScreen extends HookConsumerWidget {
side: const BorderSide(color: Colors.transparent),
label: Text(team.name,
style: TextStyle(
- color: team.color.computeLuminance() > 0.5
+ color: team.color.computeLuminance() > 0.4
? Colors.black
: Colors.white)),
backgroundColor: team.color,
diff --git a/lib/src/screens/generator/generators/group_generator.dart b/lib/src/screens/generator/generators/group_generator.dart
index f4ced38..f46b2b6 100644
--- a/lib/src/screens/generator/generators/group_generator.dart
+++ b/lib/src/screens/generator/generators/group_generator.dart
@@ -17,10 +17,18 @@
* along with this program. If not, see .
*/
+import 'dart:math';
+
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:steamwar_multitool/src/components/components.dart';
+import 'package:steamwar_multitool/src/delegates/delegates.dart';
+import 'package:steamwar_multitool/src/provider/data.dart';
+import 'package:steamwar_multitool/src/provider/events.dart';
import 'package:steamwar_multitool/src/types/types.dart';
+import 'package:steamwar_multitool/src/util/constants.dart';
class GroupBracketGenerator extends HookConsumerWidget {
final EventExtended event;
@@ -47,49 +55,233 @@ class GroupBracketGenerator extends HookConsumerWidget {
}
}, [groups.value]);
+ Team getTeam(int id) =>
+ event.teams.firstWhere((element) => element.id == id);
+
+ final startTime = useState(event.event.start);
+ final roundTime = useState(const Duration(minutes: 30));
+ final startDelay = useState(const Duration(seconds: 30));
+
+ final gamemode = useState(null);
+ final map = useState(null);
+
final canProceed = useMemoized(
() =>
groups.value.every((element) => element.length >= 2) &&
- groups.value.reduce((value, element) => value + element).length ==
- event.teams.length,
- [groups.value, notInGroupTeams]);
+ groups.value.isNotEmpty &&
+ gamemode.value != null &&
+ map.value != null,
+ [groups.value, notInGroupTeams, gamemode.value, map.value]);
+
+ final fights = useMemoized(() {
+ List>>> groupFights = [];
+ final random = Random();
+ for (final group in groups.value) {
+ int rounds = group.length - 1;
+ List>> groupFight = [];
+ for (int i = 0; i < rounds; i++) {
+ final availableTeams = group.toList();
+ if (group.length % 2 == 1) availableTeams.removeAt(i);
+ final List> roundFights = [];
+ while (availableTeams.isNotEmpty) {
+ final team1 = availableTeams.removeAt(0);
+ final team2 = availableTeams.removeAt(i % availableTeams.length);
+ final fight = [team1, team2];
+ fight.shuffle(random);
+ roundFights.add(fight);
+ }
+ groupFight.add(roundFights);
+ }
+ groupFights.add(groupFight);
+ }
+ return groupFights;
+ }, [groups.value]);
return Scaffold(
appBar: AppBar(
- title: Text('Group Bracket Generator'),
- ),
- floatingActionButton: FloatingActionButton(
- onPressed: !canProceed
- ? null
- : () {
- groups.value.add([]);
- },
- child: Icon(Icons.navigate_next),
- ),
- body: Column(
- mainAxisSize: MainAxisSize.max,
- children: [
- Wrap(
- crossAxisAlignment: WrapCrossAlignment.start,
- alignment: WrapAlignment.start,
- children: [
- for (final team in notInGroupTeams)
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: Draggable(
- data: team,
- feedback: Material(
- color: Colors.transparent, child: _TeamChip(team)),
- child: _TeamChip(team),
- ),
- ),
- ],
+ title: const Text('Group Bracket Generator'),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.sync),
+ tooltip: "Auto Generate",
+ onPressed: () async {
+ final size = await showDialog(
+ context: context,
+ builder: (context) {
+ return HookBuilder(builder: (context) {
+ final groupsCount = useState(event.teams.length ~/ 2);
+ return AlertDialog(
+ title: const Text("Groups"),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text("Groups: ${groupsCount.value}"),
+ Slider(
+ value: groupsCount.value.toDouble(),
+ min: 1,
+ max: event.teams.length / 2,
+ divisions: (event.teams.length ~/ 2) - 1,
+ label: groupsCount.value.toString(),
+ onChanged: (value) {
+ groupsCount.value = value.toInt();
+ },
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(0),
+ child: const Text("Cancel"),
+ ),
+ FilledButton(
+ onPressed: () =>
+ Navigator.of(context).pop(groupsCount.value),
+ child: const Text("Ok"),
+ ),
+ ],
+ );
+ });
+ });
+ if (size == 0) return;
+ final random = Random();
+ final teams = event.teams.toList();
+ teams.shuffle(random);
+ final groupsNew = List.generate(size, (index) => []);
+ int k = 0;
+ for (int i = 0; i < teams.length; i++) {
+ groupsNew[k].add(teams[i].id);
+ k++;
+ if (k >= size) k = 0;
+ }
+ groups.value = groupsNew;
+ },
),
- Wrap(
- crossAxisAlignment: WrapCrossAlignment.start,
- alignment: WrapAlignment.start,
- children: [
- for (final group in groups.value)
+ ],
+ leading: BackButton(
+ onPressed: () => context.go('/event/${event.event.id}'),
+ ),
+ ),
+ floatingActionButton: FloatingActionButton.extended(
+ icon: const Icon(Icons.check),
+ onPressed: canProceed
+ ? () async {
+ final repo = await ref.read(eventRepositoryProvider.future);
+ for (final group in fights) {
+ for (final round in group) {
+ for (final fight in round) {
+ final blue = getTeam(fight[0]);
+ final red = getTeam(fight[1]);
+ final start = startTime.value.add(
+ roundTime.value * group.indexOf(round) +
+ startDelay.value *
+ (round.indexOf(fight) +
+ (fights.indexOf(group) * round.length)),
+ );
+ await repo.createFight(
+ event.event.id,
+ start,
+ gamemode.value!,
+ map.value!,
+ blue,
+ red,
+ 0,
+ "Gruppe ${fights.indexOf(group) + 1}");
+ }
+ }
+ }
+ context.go("/events/${event.event.id}");
+ }
+ : null,
+ label: const Text("Generate")),
+ body: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: ListView(
+ children: [
+ Wrap(
+ crossAxisAlignment: WrapCrossAlignment.start,
+ alignment: WrapAlignment.start,
+ children: [
+ for (final team in notInGroupTeams)
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Draggable(
+ data: team,
+ feedback: Material(
+ color: Colors.transparent, child: _TeamChip(team)),
+ child: _TeamChip(team),
+ ),
+ ),
+ ],
+ ),
+ Wrap(
+ crossAxisAlignment: WrapCrossAlignment.start,
+ alignment: WrapAlignment.start,
+ children: [
+ for (final group in groups.value)
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: DragTarget(
+ builder: (context, candidateData, rejectedData) {
+ return ConstrainedBox(
+ constraints: const BoxConstraints(
+ minWidth: 200,
+ maxWidth: 200,
+ minHeight: 200,
+ ),
+ child: Card(
+ child: Column(
+ children: [
+ Text(
+ 'Group ${groups.value.indexOf(group) + 1}'),
+ Wrap(
+ children: [
+ for (final teamId in group)
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: InkWell(
+ child: _TeamChip(
+ event.teams.firstWhere((element) =>
+ element.id == teamId),
+ ),
+ onTap: () {
+ groups.value = [
+ for (final addGroup
+ in groups.value)
+ if (addGroup.contains(teamId))
+ addGroup
+ .where((element) =>
+ element != teamId)
+ .toList()
+ else
+ addGroup
+ ];
+ },
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ onWillAccept: (data) => true,
+ onAccept: (data) {
+ groups.value = [
+ for (final addGroup in groups.value)
+ if (addGroup.contains(data.id))
+ addGroup
+ .where((element) => element != data.id)
+ .toList()
+ else if (addGroup == group)
+ [...addGroup, data.id]
+ else
+ addGroup
+ ];
+ },
+ ),
+ ),
Padding(
padding: const EdgeInsets.all(8.0),
child: DragTarget(
@@ -99,34 +291,8 @@ class GroupBracketGenerator extends HookConsumerWidget {
height: 200,
child: Card(
child: Column(
- children: [
- Text('Group ${groups.value.indexOf(group) + 1}'),
- Wrap(
- children: [
- for (final teamId in group)
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: InkWell(
- child: _TeamChip(
- event.teams.firstWhere((element) =>
- element.id == teamId),
- ),
- onTap: () {
- groups.value = [
- for (final addGroup in groups.value)
- if (addGroup.contains(teamId))
- addGroup
- .where((element) =>
- element != teamId)
- .toList()
- else
- addGroup
- ];
- },
- ),
- ),
- ],
- ),
+ children: const [
+ Text('New Group'),
],
),
),
@@ -135,47 +301,114 @@ class GroupBracketGenerator extends HookConsumerWidget {
onWillAccept: (data) => true,
onAccept: (data) {
groups.value = [
- for (final addGroup in groups.value)
- if (addGroup.contains(data.id))
- addGroup
- .where((element) => element != data.id)
- .toList()
- else if (addGroup == group)
- [...addGroup, data.id]
- else
- addGroup
+ ...groups.value,
+ [data.id],
];
},
),
),
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: DragTarget(
- builder: (context, candidateData, rejectedData) {
- return SizedBox(
- width: 200,
- height: 200,
- child: Card(
- child: Column(
- children: const [
- Text('New Group'),
+ ],
+ ),
+ Divider(),
+ Column(
+ children: [
+ Row(
+ children: [
+ const Text('Start Time:'),
+ DateTimeEditor((p0) {
+ startTime.value = p0;
+ }, startTime.value, true),
+ ],
+ ),
+ Text('Round Time: ${roundTime.value.inMinutes} min'),
+ Slider(
+ value: roundTime.value.inMinutes.toDouble(),
+ onChanged: (t) =>
+ roundTime.value = Duration(minutes: t.round()),
+ min: 5,
+ max: 60,
+ divisions: 60 - 5,
+ label: '${roundTime.value.inMinutes} min'),
+ Text('Start Delay: ${startDelay.value.inSeconds} sec'),
+ Slider(
+ value: startDelay.value.inSeconds.toDouble(),
+ onChanged: (t) =>
+ startDelay.value = Duration(seconds: t.round()),
+ min: 0,
+ max: 30,
+ divisions: 30,
+ label: '${startDelay.value.inSeconds} sec'),
+ Row(
+ children: [
+ const Text('Gamemode:'),
+ TextButton(
+ onPressed: () async {
+ final newmode = await showSearch(
+ context: context,
+ delegate: GamemodeSearchDelegate(
+ await ref.read(fightServersProvider.future)));
+ gamemode.value = newmode;
+ },
+ child: Text(gamemode.value ?? 'None')),
+ ],
+ ),
+ Row(
+ children: [
+ const Text('Map:'),
+ TextButton(
+ onPressed: gamemode.value != null
+ ? () async {
+ final newmap = await showSearch(
+ context: context,
+ delegate: MapSearchDelegate(await ref
+ .read(mapsProvider.future)
+ .then((value) =>
+ value[gamemode.value]!)));
+ map.value = newmap;
+ }
+ : null,
+ child: Text(map.value ?? 'None')),
+ ],
+ ),
+ const Divider(),
+ for (final group in fights)
+ Column(
+ children: [
+ Text('Group ${fights.indexOf(group) + 1}',
+ style: Theme.of(context).textTheme.headlineMedium),
+ for (final round in group)
+ Column(
+ children: [
+ Text('Round ${group.indexOf(round) + 1}',
+ style:
+ Theme.of(context).textTheme.headlineSmall),
+ const Divider(),
+ for (final fight in round)
+ ListTile(
+ leading: Chip(
+ label: Text(
+ kDateFormat.format(
+ startTime.value.add(
+ roundTime.value * group.indexOf(round) +
+ startDelay.value *
+ (round.indexOf(fight) +
+ (fights.indexOf(group) *
+ round.length)),
+ ),
+ ),
+ ),
+ ),
+ title: Text(
+ '${getTeam(fight[0]).name} vs ${getTeam(fight[1]).name}'),
+ ),
],
),
- ),
- );
- },
- onWillAccept: (data) => true,
- onAccept: (data) {
- groups.value = [
- ...groups.value,
- [data.id],
- ];
- },
- ),
- ),
- ],
- ),
- ],
+ ],
+ )
+ ],
+ ),
+ ],
+ ),
),
);
}
diff --git a/lib/src/screens/home/home.dart b/lib/src/screens/home/home.dart
index 7902dd2..daa42a8 100644
--- a/lib/src/screens/home/home.dart
+++ b/lib/src/screens/home/home.dart
@@ -24,7 +24,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
-import 'package:steamwar_multitool/src/provider/mods.dart';
import 'package:steamwar_multitool/src/provider/provider.dart';
import 'package:steamwar_multitool/src/screens/home/lists/events_list.dart';
diff --git a/lib/src/util/constants.dart b/lib/src/util/constants.dart
index b5a408c..fefbba0 100644
--- a/lib/src/util/constants.dart
+++ b/lib/src/util/constants.dart
@@ -19,4 +19,4 @@
import 'package:intl/intl.dart';
-final kDateFormat = DateFormat("dd.MM.yyyy HH:mm");
+final kDateFormat = DateFormat("dd.MM.yyyy HH:mm:ss");
diff --git a/lib/src/util/functions.dart b/lib/src/util/functions.dart
new file mode 100644
index 0000000..7f6c0ba
--- /dev/null
+++ b/lib/src/util/functions.dart
@@ -0,0 +1,26 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2023 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+int wrapAround(int value, int max) {
+ if (value < max) {
+ return value;
+ } else {
+ return value % max;
+ }
+}