Add Group Bracket Generator
Dieser Commit ist enthalten in:
Ursprung
6b8f631235
Commit
1a515603b0
@ -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 [
|
||||
|
@ -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,
|
||||
|
@ -17,10 +17,18 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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,27 +55,148 @@ 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<String?>(null);
|
||||
final map = useState<String?>(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<List<List<List<int>>>> groupFights = [];
|
||||
final random = Random();
|
||||
for (final group in groups.value) {
|
||||
int rounds = group.length - 1;
|
||||
List<List<List<int>>> groupFight = [];
|
||||
for (int i = 0; i < rounds; i++) {
|
||||
final availableTeams = group.toList();
|
||||
if (group.length % 2 == 1) availableTeams.removeAt(i);
|
||||
final List<List<int>> 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([]);
|
||||
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();
|
||||
},
|
||||
child: Icon(Icons.navigate_next),
|
||||
),
|
||||
body: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
],
|
||||
),
|
||||
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>[]);
|
||||
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;
|
||||
},
|
||||
),
|
||||
],
|
||||
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,
|
||||
@ -94,13 +223,17 @@ class GroupBracketGenerator extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: DragTarget<Team>(
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
return SizedBox(
|
||||
width: 200,
|
||||
height: 200,
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 200,
|
||||
maxWidth: 200,
|
||||
minHeight: 200,
|
||||
),
|
||||
child: Card(
|
||||
child: Column(
|
||||
children: [
|
||||
Text('Group ${groups.value.indexOf(group) + 1}'),
|
||||
Text(
|
||||
'Group ${groups.value.indexOf(group) + 1}'),
|
||||
Wrap(
|
||||
children: [
|
||||
for (final teamId in group)
|
||||
@ -113,7 +246,8 @@ class GroupBracketGenerator extends HookConsumerWidget {
|
||||
),
|
||||
onTap: () {
|
||||
groups.value = [
|
||||
for (final addGroup in groups.value)
|
||||
for (final addGroup
|
||||
in groups.value)
|
||||
if (addGroup.contains(teamId))
|
||||
addGroup
|
||||
.where((element) =>
|
||||
@ -175,8 +309,107 @@ class GroupBracketGenerator extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
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}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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");
|
||||
|
26
lib/src/util/functions.dart
Normale Datei
26
lib/src/util/functions.dart
Normale Datei
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
int wrapAround(int value, int max) {
|
||||
if (value < max) {
|
||||
return value;
|
||||
} else {
|
||||
return value % max;
|
||||
}
|
||||
}
|
In neuem Issue referenzieren
Einen Benutzer sperren