diff --git a/lib/main.dart b/lib/main.dart index be935c6..935629f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,5 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:intl/intl.dart'; import 'package:steamwar_multitool/src/app.dart'; -final kDateFormat = DateFormat("dd.MM.yyyy HH:mm"); - void main() => runApp(const ProviderScope(child: DevServerStarterApp())); diff --git a/lib/src/app.dart b/lib/src/app.dart index 4d98a5c..6987870 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:steamwar_multitool/src/screens/event.dart'; -import 'package:steamwar_multitool/src/screens/event_fights_graph.dart'; +import 'package:steamwar_multitool/src/screens/event/event.dart'; +import 'package:steamwar_multitool/src/screens/event/event_fights_graph.dart'; -import 'screens/home.dart'; -import 'screens/login.dart'; -import 'screens/userinfo.dart'; +import 'screens/home/home.dart'; +import 'screens/login/login.dart'; +import 'screens/settings/userinfo.dart'; final _routes = GoRouter( routes: [ diff --git a/lib/src/components/components.dart b/lib/src/components/components.dart new file mode 100644 index 0000000..5e279ad --- /dev/null +++ b/lib/src/components/components.dart @@ -0,0 +1,2 @@ +export 'date_time_editor.dart'; +export 'error.dart'; diff --git a/lib/src/components/date_time_editor.dart b/lib/src/components/date_time_editor.dart new file mode 100644 index 0000000..c3a2185 --- /dev/null +++ b/lib/src/components/date_time_editor.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:steamwar_multitool/src/util/constants.dart'; + +class DateTimeEditor extends HookConsumerWidget { + final void Function(DateTime) onChanged; + final DateTime initialDate; + final bool enabled; + final MainAxisAlignment mainAxisAlignment; + const DateTimeEditor(this.onChanged, this.initialDate, this.enabled, + {Key? key, this.mainAxisAlignment = MainAxisAlignment.start}) + : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Row( + mainAxisAlignment: mainAxisAlignment, + children: [ + TextButton( + onPressed: enabled + ? () async { + final date = await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: DateTime(2020), + lastDate: DateTime(2030), + ); + if (date != null) { + onChanged(date); + } + } + : null, + child: Text(kDateFormat.format(initialDate)), + ), + IconButton( + onPressed: enabled + ? () async { + final time = await showTimePicker( + builder: (context, child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + alwaysUse24HourFormat: true, + ), + child: child!, + ); + }, + context: context, + initialTime: TimeOfDay( + hour: initialDate.hour, minute: initialDate.minute), + ); + if (time != null) { + final changed = DateTime( + initialDate.year, + initialDate.month, + initialDate.day, + time.hour, + time.minute); + onChanged(changed); + } + } + : null, + icon: const Icon(Icons.schedule)), + ], + ); + } +} diff --git a/lib/src/screens/components/error.dart b/lib/src/components/error.dart similarity index 100% rename from lib/src/screens/components/error.dart rename to lib/src/components/error.dart diff --git a/lib/src/delegates/delegates.dart b/lib/src/delegates/delegates.dart new file mode 100644 index 0000000..1836dee --- /dev/null +++ b/lib/src/delegates/delegates.dart @@ -0,0 +1,6 @@ +export 'gamemode.dart'; +export 'groups.dart'; +export 'map.dart'; +export 'schematic_type.dart'; +export 'team.dart'; +export 'user.dart'; diff --git a/lib/src/delegates/gamemode.dart b/lib/src/delegates/gamemode.dart new file mode 100644 index 0000000..5b1481c --- /dev/null +++ b/lib/src/delegates/gamemode.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +class GamemodeSearchDelegate extends SearchDelegate { + final List modes; + + GamemodeSearchDelegate(this.modes); + + @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 _buildList(context); + } + + @override + Widget buildSuggestions(BuildContext context) { + return _buildList(context); + } + + Widget _buildList(BuildContext context) { + final out = modes + .where((element) => element.toLowerCase().contains(query.toLowerCase())) + .toList(); + return ListView.builder( + itemCount: out.length, + itemBuilder: (context, index) { + final type = out[index]; + + return ListTile( + title: Text(type), + onTap: () { + close(context, type); + }, + ); + }, + ); + } +} diff --git a/lib/src/delegates/groups.dart b/lib/src/delegates/groups.dart new file mode 100644 index 0000000..4817169 --- /dev/null +++ b/lib/src/delegates/groups.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +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/lib/src/delegates/map.dart b/lib/src/delegates/map.dart new file mode 100644 index 0000000..3f0d064 --- /dev/null +++ b/lib/src/delegates/map.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +class MapSearchDelegate extends SearchDelegate { + final List maps; + + MapSearchDelegate(this.maps); + + @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 _buildList(context); + } + + @override + Widget buildSuggestions(BuildContext context) { + return _buildList(context); + } + + Widget _buildList(BuildContext context) { + final out = maps + .where((element) => element.toLowerCase().contains(query.toLowerCase())) + .toList(); + return ListView.builder( + itemCount: out.length, + itemBuilder: (context, index) { + final type = out[index]; + + return ListTile( + title: Text(type), + onTap: () { + close(context, type); + }, + ); + }, + ); + } +} diff --git a/lib/src/delegates/schematic_type.dart b/lib/src/delegates/schematic_type.dart new file mode 100644 index 0000000..cf57373 --- /dev/null +++ b/lib/src/delegates/schematic_type.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; + +final RESET_TYPE = SchematicType("RESET", "RESET"); + +class SchematicTypeSearchDelegate extends SearchDelegate { + final List types; + + SchematicTypeSearchDelegate(this.types); + + @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 _buildList(context); + } + + @override + Widget buildSuggestions(BuildContext context) { + return _buildList(context); + } + + Widget _buildList(BuildContext context) { + final out = types + .where((element) => element.name.contains(query.toLowerCase())) + .toList(); + return ListView.builder( + itemCount: out.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + return ListTile( + title: const Text("Reset"), + leading: const Icon(Icons.clear), + onTap: () { + close(context, RESET_TYPE); + }, + ); + } + + final type = out[index - 1]; + + return ListTile( + title: Text(type.name), + onTap: () { + close(context, type); + }, + ); + }, + ); + } +} diff --git a/lib/src/delegates/team.dart b/lib/src/delegates/team.dart new file mode 100644 index 0000000..6f0ac09 --- /dev/null +++ b/lib/src/delegates/team.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; + +class TeamSearchDelegate extends SearchDelegate { + final List teams; + + TeamSearchDelegate(this.teams); + + @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 ListView( + children: [ + ListTile( + title: const Text("?"), + onTap: () { + close(context, Team(-1, "?", "?", "8")); + }, + ), + for (final team in teams) + if (team.name.toLowerCase().contains(query.toLowerCase())) + ListTile( + title: Text(team.name), + onTap: () { + close(context, team); + }, + ) + ], + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + return ListView( + children: [ + ListTile( + title: const Text("?"), + onTap: () { + close(context, Team(-1, "?", "?", "8")); + }, + ), + for (final team in teams) + if (team.name.toLowerCase().contains(query.toLowerCase())) + ListTile( + title: Text(team.name), + onTap: () { + close(context, team); + }, + ) + ], + ); + } +} diff --git a/lib/src/delegates/user.dart b/lib/src/delegates/user.dart new file mode 100644 index 0000000..a8969c4 --- /dev/null +++ b/lib/src/delegates/user.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; + +class UserSearchDelegate extends SearchDelegate { + final List users; + + UserSearchDelegate(this.users); + + @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 ListView( + children: [ + for (final team in users) + if (team.name.toLowerCase().contains(query.toLowerCase())) + ListTile( + title: Text(team.name), + onTap: () { + close(context, team); + }, + ) + ], + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + return ListView( + children: [ + for (final team in users) + if (team.name.toLowerCase().contains(query.toLowerCase())) + ListTile( + title: Text(team.name), + onTap: () { + close(context, team); + }, + ) + ], + ); + } +} diff --git a/lib/src/screens/components/event_dialog.dart b/lib/src/dialogs/create_event_dialog.dart similarity index 87% rename from lib/src/screens/components/event_dialog.dart rename to lib/src/dialogs/create_event_dialog.dart index 2d1b8f2..ae425a9 100644 --- a/lib/src/screens/components/event_dialog.dart +++ b/lib/src/dialogs/create_event_dialog.dart @@ -2,12 +2,11 @@ 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/provider/events.dart'; -import '../../provider/events.dart'; -import '../event.dart'; - -class EventDialog extends HookConsumerWidget { - const EventDialog({Key? key}) : super(key: key); +class CreateEventDialog extends HookConsumerWidget { + const CreateEventDialog({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { @@ -69,9 +68,6 @@ class EventDialog extends HookConsumerWidget { final event = await ref.read(eventRepositoryProvider.future).then( (value) => value.createEvent( eventName.text, startTime.value, endTime.value)); - final eventFull = await ref - .read(eventRepositoryProvider.future) - .then((value) => value.getEvent(event.id)); context.go('/event/${event.id}'); }, child: const Text("Create"), diff --git a/lib/src/dialogs/dialogs.dart b/lib/src/dialogs/dialogs.dart new file mode 100644 index 0000000..dbb979f --- /dev/null +++ b/lib/src/dialogs/dialogs.dart @@ -0,0 +1 @@ +export 'create_event_dialog.dart'; diff --git a/lib/src/provider/types.dart b/lib/src/provider/data.dart similarity index 75% rename from lib/src/provider/types.dart rename to lib/src/provider/data.dart index 53ffcec..ea95257 100644 --- a/lib/src/provider/types.dart +++ b/lib/src/provider/data.dart @@ -1,7 +1,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; import 'http.dart'; +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]); + final fightServersProvider = FutureProvider((ref) async { final client = await ref.watch(httpClient.future); final res = await client.get("/data/gamemodes"); @@ -33,25 +40,3 @@ final usersProvider = FutureProvider((ref) async { final res = await client.get("/data/users"); return (res.data as List).map((e) => User.fromJson(e)).toList(); }); - -class User { - final int id; - final String name; - - User(this.id, this.name); - - factory User.fromJson(Map json) { - return User(json["id"] as int, json["name"] as String); - } -} - -class SchematicType { - final String name; - final String db; - - SchematicType(this.name, this.db); - - factory SchematicType.fromJson(Map json) { - return SchematicType(json["name"], json["db"]); - } -} diff --git a/lib/src/provider/events.dart b/lib/src/provider/events.dart index dd50d2c..205dd55 100644 --- a/lib/src/provider/events.dart +++ b/lib/src/provider/events.dart @@ -1,10 +1,6 @@ -import 'dart:ui'; - -import 'package:dio/dio.dart'; -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:steamwar_multitool/src/provider/types.dart'; +import 'package:steamwar_multitool/src/repositories/event.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; import 'http.dart'; @@ -16,277 +12,3 @@ final eventsListProvider = FutureProvider>((ref) async { final repo = await ref.watch(eventRepositoryProvider.future); 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; - - EventRepository(this._client); - - Future> listEvents() async { - final res = await _client.get("/events"); - return (res.data as List).map((e) => ShortEvent.fromJson(e)).toList(); - } - - Future getEvent(int id) async { - final res = await _client.get("/events/$id"); - return EventExtended.fromJson(res.data); - } - - Future updateEvent( - int id, - String name, - DateTime deadline, - DateTime start, - DateTime end, - int maxTeamMembers, - String? schemType, - bool publicOnly, - bool useSpectateSystem) async { - await _client.put("/events/$id", data: { - "name": name, - "deadline": deadline.millisecondsSinceEpoch, - "start": start.millisecondsSinceEpoch, - "end": end.millisecondsSinceEpoch, - "maxTeamMembers": maxTeamMembers, - "schemType": schemType ?? "null", - "publicSchemsOnly": publicOnly, - "spectateSystem": useSpectateSystem, - }); - } - - Future createEvent(String name, DateTime start, DateTime end) async { - final res = await _client.post("/events", data: { - "name": name, - "start": start.millisecondsSinceEpoch, - "end": end.millisecondsSinceEpoch, - }); - return Event.fromJson(res.data); - } - - Future deleteEvent(int id) async { - await _client.delete("/events/$id"); - } - - Future deleteFight(int id) async { - await _client.delete("/fights/$id"); - } - - 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, - "map": map, - "start": start.millisecondsSinceEpoch, - "blueTeam": blue.id, - "redTeam": red.id, - "kampfleiter": referee, - "group": group ?? "null", - }); - } - - Future> listFights(int eventId) async { - final res = await _client.get("/events/$eventId/fights"); - return (res.data as List).map((e) => EventFight.fromJson(e)).toList(); - } - - Future updateFight(int id, DateTime start, String mode, String map, - int referee, Team blue, Team red, String? group) { - return _client.put("/fights/$id", data: { - "spielmodus": mode, - "map": map, - "start": start.millisecondsSinceEpoch, - "kampfleiter": referee, - "blueTeam": blue.id, - "redTeam": red.id, - "group": group ?? "null", - }); - } -} - -class EventFight { - final int id; - final String gameMode; - final String map; - final DateTime start; - final Team redTeam; - 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.group); - - Team get winner { - return getTeamWithContextColor(Colors.white); - } - - Team getTeamWithContextColor(Color color) { - switch (score) { - case 1: - return blueTeam; - case 2: - return redTeam; - case 3: - return Team( - -1, "Tie", "TIE", color.computeLuminance() > 0.5 ? "f" : "0"); - default: - return Team( - -1, "Unknown", "UNK", color.computeLuminance() > 0.5 ? "f" : "0"); - } - } - - factory EventFight.fromJson(Map json) { - return EventFight( - json["id"], - json["spielmodus"], - json["map"], - DateTime.fromMillisecondsSinceEpoch(json["start"]), - Team.fromJson(json["redTeam"]), - Team.fromJson(json["blueTeam"]), - User.fromJson(json["kampfleiter"]), - json["ergebnis"], - json["group"]); - } -} - -class Team { - final int id; - final String name; - final String kuerzel; - final String colorCode; - - Team(this.id, this.name, this.kuerzel, this.colorCode); - - Color get color { - switch (colorCode) { - case "1": - return const Color(0xFF0000AA); - case "2": - return const Color(0xFF00AA00); - case "3": - return const Color(0xFF00AAAA); - case "4": - return const Color(0xFFAA0000); - case "5": - return const Color(0xFFAA00AA); - case "6": - return const Color(0xFFFFAA00); - case "7": - return const Color(0xFFAAAAAA); - case "8": - return const Color(0xFF555555); - case "9": - return const Color(0xFF5555FF); - case "a": - return const Color(0xFF55FF55); - case "b": - return const Color(0xFF55FFFF); - case "c": - return const Color(0xFFFF5555); - case "d": - return const Color(0xFFFF55FF); - case "e": - return const Color(0xFFFFFF55); - case "f": - return const Color(0xFFFFFFFF); - default: - return const Color(0xFF000000); - } - } - - factory Team.fromJson(Map json) { - return Team( - json['id'] as int, - json['name'] as String, - json['kuerzel'] as String, - json['color'] as String, - ); - } -} - -class EventExtended { - final Event event; - final List fights; - final List teams; - - EventExtended(this.event, this.fights, this.teams); - - factory EventExtended.fromJson(Map json) { - return EventExtended( - Event.fromJson(json['event']), - (json['fights'] as List) - .map((e) => EventFight.fromJson(e)) - .toList(), - (json['teams'] as List).map((e) => Team.fromJson(e)).toList(), - ); - } -} - -class Event { - final int id; - final String name; - final DateTime deadline; - final DateTime start; - final DateTime end; - final int maxTeamMembers; - final String? schemType; - final bool publicSchemsOnly; - final bool spectateSystem; - - Event({ - required this.id, - required this.name, - required this.deadline, - required this.start, - required this.end, - required this.maxTeamMembers, - required this.schemType, - required this.publicSchemsOnly, - required this.spectateSystem, - }); - - factory Event.fromJson(Map json) { - return Event( - id: json['id'], - name: json['name'], - deadline: DateTime.fromMillisecondsSinceEpoch(json['deadline']), - start: DateTime.fromMillisecondsSinceEpoch(json['start']), - end: DateTime.fromMillisecondsSinceEpoch(json['end']), - maxTeamMembers: json['maxTeamMembers'], - schemType: json['schemType'], - publicSchemsOnly: json['publicSchemsOnly'], - spectateSystem: json['spectateSystem'], - ); - } -} - -@JsonSerializable() -class ShortEvent { - final String name; - final int id; - final DateTime start; - final DateTime end; - - ShortEvent(this.name, this.id, this.start, this.end); - - get isUpcoming => start.isAfter(DateTime.now()); - - get isCurrent => - start.isBefore(DateTime.now()) && end.isAfter(DateTime.now()); - - factory ShortEvent.fromJson(Map json) { - return ShortEvent( - json["name"], - json["id"], - DateTime.fromMillisecondsSinceEpoch(json["start"]), - DateTime.fromMillisecondsSinceEpoch(json["end"])); - } -} diff --git a/lib/src/provider/mods.dart b/lib/src/provider/mods.dart index 9e7d4ab..df2b738 100644 --- a/lib/src/provider/mods.dart +++ b/lib/src/provider/mods.dart @@ -1,6 +1,7 @@ -import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:steamwar_multitool/src/provider/http.dart'; +import 'package:steamwar_multitool/src/repositories/mod.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; final modRepository = FutureProvider( (ref) async => ModRepository(await ref.read(httpClient.future)), @@ -15,85 +16,3 @@ final mods = FutureProvider>((ref) async { final repo = await ref.watch(modRepository.future); return repo.listMods(); }, dependencies: [modRepository]); - -class ModRepository { - final Dio _client; - - ModRepository(this._client); - - Future> listMods() async { - final res = await _client.get("/mods/all"); - return (res.data as List).map((e) => Mod.fromJson(e)).toList(); - } - - Future> listUnchecked() async { - final res = await _client.get("/mods/unchecked"); - return (res.data as List).map((e) => Mod.fromJson(e)).toList(); - } - - Future updateMod(Platform platform, String name, ModType type) { - return _client.put("/mods/${platform.name}/$name", data: { - "modType": type.name, - }); - } -} - -class Mod { - final Platform platform; - final String name; - final ModType type; - - Mod(this.platform, this.name, this.type); - - factory Mod.fromJson(Map json) { - return Mod( - Platform.fromOrdinal(json["platform"] as int), - json["modName"], - ModType.fromOrdinal(json["modType"] as int), - ); - } -} - -enum Platform { - FORGE, - LABYMOD, - FABRIC; - - static Platform fromOrdinal(int ordinal) { - switch (ordinal) { - case 0: - return FORGE; - case 1: - return LABYMOD; - case 2: - return FABRIC; - default: - throw Exception("Invalid ordinal"); - } - } -} - -enum ModType { - UNKLASSIFIED, - GREEN, - YELLOW, - RED, - YOUTUBER_ONLY; - - static ModType fromOrdinal(int ordinal) { - switch (ordinal) { - case 0: - return UNKLASSIFIED; - case 1: - return GREEN; - case 2: - return YELLOW; - case 3: - return RED; - case 4: - return YOUTUBER_ONLY; - default: - throw Exception("Invalid ordinal"); - } - } -} diff --git a/lib/src/provider/provider.dart b/lib/src/provider/provider.dart new file mode 100644 index 0000000..aafce88 --- /dev/null +++ b/lib/src/provider/provider.dart @@ -0,0 +1,5 @@ +export 'data.dart'; +export 'events.dart'; +export 'http.dart'; +export 'mods.dart'; +export 'user.dart'; diff --git a/lib/src/provider/server.dart b/lib/src/provider/server.dart deleted file mode 100644 index c0e20ac..0000000 --- a/lib/src/provider/server.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'dart:convert'; - -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../screens/console.dart'; - -final serversProvider = FutureProvider>((ref) async { - return []; - //final sftp = await ref.watch(sftpProvider.future); - //final ret = (await sftp.readdir("/servers").first) - // .map((e) => e.filename) - // .where((element) => element != "." && element != "..") - // .toList(); - //ret.sort(); - //return ret; -}); - -final favoriteServersProvider = FutureProvider((ref) async { - final prefs = await SharedPreferences.getInstance(); - final ret = prefs.getStringList("favorite_servers") ?? []; - return ret.map((e) => ServerStartParameters.fromJson(jsonDecode(e))).toList(); -}); diff --git a/lib/src/provider/user.dart b/lib/src/provider/user.dart index fc001fa..1657d5d 100644 --- a/lib/src/provider/user.dart +++ b/lib/src/provider/user.dart @@ -1,5 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; final userDataProvider = FutureProvider((ref) async { final prefs = await SharedPreferences.getInstance(); @@ -11,13 +12,3 @@ final userDataProvider = FutureProvider((ref) async { uneditablePastEvents: uneditablePastEvents, ); }); - -class UserData { - final String key; - final bool uneditablePastEvents; - - UserData({ - required this.key, - required this.uneditablePastEvents, - }); -} diff --git a/lib/src/repositories/event.dart b/lib/src/repositories/event.dart new file mode 100644 index 0000000..a87dcbb --- /dev/null +++ b/lib/src/repositories/event.dart @@ -0,0 +1,89 @@ +import 'package:dio/dio.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; + +class EventRepository { + final Dio _client; + + EventRepository(this._client); + + Future> listEvents() async { + final res = await _client.get("/events"); + return (res.data as List).map((e) => ShortEvent.fromJson(e)).toList(); + } + + Future getEvent(int id) async { + final res = await _client.get("/events/$id"); + return EventExtended.fromJson(res.data); + } + + Future updateEvent( + int id, + String name, + DateTime deadline, + DateTime start, + DateTime end, + int maxTeamMembers, + String? schemType, + bool publicOnly, + bool useSpectateSystem) async { + await _client.put("/events/$id", data: { + "name": name, + "deadline": deadline.millisecondsSinceEpoch, + "start": start.millisecondsSinceEpoch, + "end": end.millisecondsSinceEpoch, + "maxTeamMembers": maxTeamMembers, + "schemType": schemType ?? "null", + "publicSchemsOnly": publicOnly, + "spectateSystem": useSpectateSystem, + }); + } + + Future createEvent(String name, DateTime start, DateTime end) async { + final res = await _client.post("/events", data: { + "name": name, + "start": start.millisecondsSinceEpoch, + "end": end.millisecondsSinceEpoch, + }); + return Event.fromJson(res.data); + } + + Future deleteEvent(int id) async { + await _client.delete("/events/$id"); + } + + Future deleteFight(int id) async { + await _client.delete("/fights/$id"); + } + + 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, + "map": map, + "start": start.millisecondsSinceEpoch, + "blueTeam": blue.id, + "redTeam": red.id, + "kampfleiter": referee, + "group": group ?? "null", + }); + } + + Future> listFights(int eventId) async { + final res = await _client.get("/events/$eventId/fights"); + return (res.data as List).map((e) => EventFight.fromJson(e)).toList(); + } + + Future updateFight(int id, DateTime start, String mode, String map, + int referee, Team blue, Team red, String? group) { + return _client.put("/fights/$id", data: { + "spielmodus": mode, + "map": map, + "start": start.millisecondsSinceEpoch, + "kampfleiter": referee, + "blueTeam": blue.id, + "redTeam": red.id, + "group": group ?? "null", + }); + } +} diff --git a/lib/src/repositories/mod.dart b/lib/src/repositories/mod.dart new file mode 100644 index 0000000..5043ea6 --- /dev/null +++ b/lib/src/repositories/mod.dart @@ -0,0 +1,24 @@ +import 'package:dio/dio.dart'; +import 'package:steamwar_multitool/src/types/mods.dart'; + +class ModRepository { + final Dio _client; + + ModRepository(this._client); + + Future> listMods() async { + final res = await _client.get("/mods/all"); + return (res.data as List).map((e) => Mod.fromJson(e)).toList(); + } + + Future> listUnchecked() async { + final res = await _client.get("/mods/unchecked"); + return (res.data as List).map((e) => Mod.fromJson(e)).toList(); + } + + Future updateMod(Platform platform, String name, ModType type) { + return _client.put("/mods/${platform.name}/$name", data: { + "modType": type.name, + }); + } +} diff --git a/lib/src/screens/components/server_list.dart b/lib/src/screens/components/server_list.dart deleted file mode 100644 index e5363a7..0000000 --- a/lib/src/screens/components/server_list.dart +++ /dev/null @@ -1,290 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../../provider/server.dart'; -import '../console.dart'; -import 'error.dart'; - -class ServerListComponent extends HookConsumerWidget { - const ServerListComponent({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final servers = ref.watch(serversProvider); - final favoriteServers = ref.watch(favoriteServersProvider); - return servers.when( - data: (data) { - return ListView( - children: [ - favoriteServers.when( - data: (data) { - if (data.isEmpty) { - return Container(); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ListTile( - title: Text("Favorites"), - ), - SizedBox( - height: 200, - child: ListView( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - children: [ - for (final server in data) - AspectRatio( - aspectRatio: 1, - child: Card( - child: InkWell( - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) { - return ConsoleScreen( - server, - ); - }, - ), - ); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text(server.name, - style: Theme.of(context) - .textTheme - .headline6), - if (server.world != null) - Text("World: ${server.world!}"), - if (server.plugins != null) - Text("Plugins: ${server.plugins!}"), - if (server.port != null) - Text("Port: ${server.port!}"), - const Spacer(), - Row( - mainAxisAlignment: - MainAxisAlignment.end, - children: [ - IconButton( - onPressed: () async { - final remove = - await showDialog( - context: context, - builder: (contex) { - return AlertDialog( - title: const Text( - "Remove from favorites?"), - actions: [ - TextButton( - onPressed: - () { - Navigator.of( - context) - .pop( - false); - }, - child: const Text( - "Cancel")), - TextButton( - onPressed: - () async { - Navigator.of( - context) - .pop( - true); - }, - child: const Text( - "Remove")), - ], - ); - }); - if (remove) { - final favs = data.toList(); - favs.remove(server); - final prefs = - await SharedPreferences - .getInstance(); - prefs.setStringList( - "favorite_servers", - favs - .map((e) => - jsonEncode( - e.toJson())) - .toList()); - ref.invalidate( - favoriteServersProvider); - } - }, - icon: const Icon(Icons.delete), - ), - ], - ), - ], - ), - ), - ), - ), - ), - ], - ), - ), - ], - ); - }, - error: (err, stack) => ErrorComponent(err, stack), - loading: () => const LinearProgressIndicator()), - for (final server in data) - ListTile( - leading: const Icon(Icons.dns), - title: Text(server), - onTap: () { - showDialog( - context: context, - builder: (context) => - _CustomizeServerStartParameters(server)); - }, - trailing: Tooltip( - message: "Quick Start", - child: IconButton( - icon: const Icon(Icons.play_arrow), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - ConsoleScreen(ServerStartParameters(server)), - ), - ); - }, - ), - ), - ), - ], - ); - }, - error: (err, stack) { - return ErrorComponent(err, stack); - }, - loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, - ); - } -} - -class _CustomizeServerStartParameters extends HookConsumerWidget { - final String server; - const _CustomizeServerStartParameters(this.server, {Key? key}) - : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final worldNameController = useTextEditingController(); - final pluginsController = useTextEditingController(); - final portController = useTextEditingController(); - final saved = useState(false); - - ServerStartParameters constructParameters() { - return ServerStartParameters( - server, - world: - worldNameController.text.isEmpty ? null : worldNameController.text, - plugins: pluginsController.text.isEmpty ? null : pluginsController.text, - port: - portController.text.isEmpty ? null : int.parse(portController.text), - ); - } - - return AlertDialog( - title: Text("Start $server"), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - decoration: const InputDecoration( - labelText: "World Name", - border: OutlineInputBorder(), - ), - controller: worldNameController, - onChanged: (d) => saved.value = false, - ), - const SizedBox( - height: 16, - ), - TextField( - decoration: const InputDecoration( - labelText: "Plugins", - border: OutlineInputBorder(), - ), - controller: pluginsController, - onChanged: (d) => saved.value = false, - ), - const SizedBox( - height: 16, - ), - // Allow only Numbers - TextField( - decoration: const InputDecoration( - labelText: "Port", - border: OutlineInputBorder(), - ), - inputFormatters: [ - FilteringTextInputFormatter.singleLineFormatter, - FilteringTextInputFormatter.digitsOnly - ], - keyboardType: TextInputType.number, - controller: portController, - onChanged: (d) => saved.value = false, - ), - ], - ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text("Cancel"), - ), - TextButton( - onPressed: () async { - final favs = await ref.read(favoriteServersProvider.future); - favs.add(constructParameters()); - final prefs = await SharedPreferences.getInstance(); - await prefs.setStringList( - "favorite_servers", - favs - .map((e) => e.toJson()) - .map((e) => jsonEncode(e)) - .toList()); - ref.invalidate(favoriteServersProvider); - saved.value = true; - }, - child: Text(saved.value ? "Saved!" : "Save")), - TextButton( - onPressed: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ConsoleScreen(constructParameters()), - ), - ); - }, - child: const Text("Start"), - ), - ], - ); - } -} diff --git a/lib/src/screens/console.dart b/lib/src/screens/console.dart deleted file mode 100644 index 5415973..0000000 --- a/lib/src/screens/console.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'dart:typed_data'; - -//import 'package:dartssh2/dartssh2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:xterm/xterm.dart'; - -part 'console.g.dart'; - -class ConsoleScreen extends StatefulHookConsumerWidget { - final ServerStartParameters parameters; - const ConsoleScreen(this.parameters, {Key? key}) : super(key: key); - - @override - ConsumerState createState() => _ConsoleScreenState(); -} - -class _ConsoleScreenState extends ConsumerState { - //late final SSHSession session; - late final Terminal terminal; - final _controller = ScrollController(keepScrollOffset: true); - - @override - Widget build(BuildContext context) { - final inputFieldController = useTextEditingController(); - final inputFieldFocusNode = useFocusNode(); - - return Scaffold( - appBar: AppBar( - title: Text(widget.parameters.name), - actions: [ - IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - setState(() { - terminal.eraseDisplay(); - }); - }, - ) - ], - ), - body: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: TerminalView(terminal, alwaysShowCursor: false), - ), - TextField( - focusNode: inputFieldFocusNode, - onSubmitted: (value) async { - inputFieldController.text = ""; - terminal.write("> $value"); - terminal.nextLine(); - //session.write( - // Uint8List.fromList(value.codeUnits + '\n'.codeUnits)); - inputFieldFocusNode.requestFocus(); - }, - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'Command', - ), - controller: inputFieldController, - ), - ], - ), - ), - ); - } - - @override - void dispose() { - super.dispose(); - _controller.dispose(); - } - - @override - void deactivate() { - super.deactivate(); - //session.kill(SSHSignal.KILL); - //session.close(); - } - - @override - void initState() { - super.initState(); - terminal = Terminal(); - terminal.setLineFeedMode(true); - /*final clientFut = ref.read(sshClientProvider); - final client = clientFut.value!; - client - .execute( - "python3 /binarys/dev.py ${widget.parameters.name} ${widget.parameters.extraArguments}") - .then((value) { - session = value; - session.stdout.listen((event) { - setState(() { - terminal.write(String.fromCharCodes(event)); - }); - }); - session.done.then((value) { - if (mounted) { - Navigator.of(context).pop(); - if (session.exitCode != 0) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text("Exited with code ${session.exitCode}"))); - } - } - }); - });*/ - } -} - -@JsonSerializable() -class ServerStartParameters { - final String name; - final String? plugins; - final String? world; - final int? port; - - ServerStartParameters(this.name, {this.plugins, this.world, this.port}); - - String get extraArguments { - final args = []; - if (plugins != null) { - args.add("-p"); - args.add(plugins!); - } - if (world != null) { - args.add("-w"); - args.add(world!); - } - if (port != null) { - args.add("--port"); - args.add(port.toString()); - } - return args.join(" "); - } - - factory ServerStartParameters.fromJson(Map json) => - _$ServerStartParametersFromJson(json); - - Map toJson() => _$ServerStartParametersToJson(this); -} diff --git a/lib/src/screens/console.g.dart b/lib/src/screens/console.g.dart deleted file mode 100644 index ff6e5ce..0000000 --- a/lib/src/screens/console.g.dart +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'console.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -ServerStartParameters _$ServerStartParametersFromJson( - Map json) => - ServerStartParameters( - json['name'] as String, - plugins: json['plugins'] as String?, - world: json['world'] as String?, - port: json['port'] as int?, - ); - -Map _$ServerStartParametersToJson( - ServerStartParameters instance) => - { - 'name': instance.name, - 'plugins': instance.plugins, - 'world': instance.world, - 'port': instance.port, - }; diff --git a/lib/src/screens/event.dart b/lib/src/screens/event.dart deleted file mode 100644 index 8d67f93..0000000 --- a/lib/src/screens/event.dart +++ /dev/null @@ -1,1137 +0,0 @@ -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'; - -import '../../main.dart'; -import '../provider/events.dart'; -import '../provider/types.dart'; -import 'search_delegates.dart'; - -T? catchToNull(T Function() f) { - try { - return f(); - } catch (e) { - return null; - } -} - -class EditEventScreen extends HookConsumerWidget { - final int eventId; - const EditEventScreen(this.eventId, {Key? key}) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final eventFuture = useMemoized(() => ref - .watch(eventRepositoryProvider.future) - .then((value) => value.getEvent(eventId))); - - return FutureBuilder( - future: eventFuture, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Scaffold( - appBar: AppBar( - title: const Text('Error'), - ), - body: Center( - child: Text('Error: ${snapshot.error}'), - ), - ); - } - if (!snapshot.hasData) { - return Scaffold( - appBar: AppBar( - title: const Text('Loading'), - ), - body: const Center( - child: CircularProgressIndicator(), - ), - ); - } - final eventData = snapshot.data as EventExtended; - - return _EventScreen( - eventData: eventData, - ); - }, - ); - } -} - -class _EventScreen extends HookConsumerWidget { - const _EventScreen({Key? key, required this.eventData}) : super(key: key); - - final EventExtended eventData; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final event = eventData.event; - - final nameController = useTextEditingController(text: event.name); - final deadlineState = useState(event.deadline); - final startDateState = useState(event.start); - final endDateState = useState(event.end); - final maxTeamMembersState = useState(event.maxTeamMembers); - final maxTeamMembersController = - useTextEditingController(text: event.maxTeamMembers.toString()); - final invalidMaxTeamMembers = useState(false); - final schematicTypeState = useState(null); - final publicOnlyState = useState(event.publicSchemsOnly); - final spectateSystemState = useState(event.spectateSystem); - - useMemoized(() { - ref.read(schematicTypesProvider.future).then((value) { - schematicTypeState.value = catchToNull( - () => value.firstWhere((element) => element.db == event.schemType)); - }); - }); - - final changed = useState(false); - return Scaffold( - appBar: AppBar( - title: Text("${"Edit"} ${event.name}"), - actions: [ - IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text("Delete Event"), - content: const Text( - "Are you sure you want to delete this event?"), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text("Cancel")), - TextButton( - onPressed: () { - ref.read(eventRepositoryProvider.future).then( - (value) => value.deleteEvent(event.id)); - ref.invalidate(eventsListProvider); - Navigator.of(context).pop(); - Navigator.of(context).pop(); - }, - child: const Text( - "Delete", - style: TextStyle(color: Colors.red), - )), - ], - )); - }, - icon: const Icon(Icons.delete_outline, color: Colors.red), - ), - IconButton( - onPressed: changed.value - ? () { - ref - .read(eventRepositoryProvider.future) - .then((value) => value.updateEvent( - event.id, - nameController.text, - deadlineState.value, - startDateState.value, - endDateState.value, - maxTeamMembersState.value, - schematicTypeState.value?.db, - publicOnlyState.value, - spectateSystemState.value, - )) - .whenComplete( - () => ref.invalidate(eventsListProvider)); - context.go('/'); - } - : null, - icon: const Icon(Icons.save)), - ], - leading: IconButton( - onPressed: () async { - var canPop = false; - if (changed.value) { - final accepted = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text("Unsaved changes"), - content: const Text( - "You have unsaved changes. Do you want to discard them?"), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text("No")), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: const Text("Yes", - style: TextStyle(color: Colors.red))), - ], - )); - if (accepted != null && accepted) { - canPop = true; - } - } else { - canPop = true; - } - if (canPop) context.go("/"); - }, - icon: const Icon(Icons.arrow_back), - ), - ), - body: Padding( - padding: const EdgeInsets.all(8.0), - child: ListView( - children: [ - const SizedBox(height: 8), - TextField( - controller: nameController, - decoration: const InputDecoration( - labelText: "Name", - border: OutlineInputBorder(), - ), - onChanged: (value) { - changed.value = true; - }, - ), - const SizedBox(height: 8), - Row( - children: [ - const Text("Deadline: "), - DateTimeEditor((p0) { - deadlineState.value = p0; - changed.value = true; - }, deadlineState.value, true), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - const Text("Start: "), - DateTimeEditor((p0) { - startDateState.value = p0; - changed.value = true; - }, startDateState.value, true), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - const Text("End: "), - DateTimeEditor((p0) { - endDateState.value = p0; - changed.value = true; - }, endDateState.value, true), - ], - ), - const SizedBox(height: 8), - TextField( - controller: maxTeamMembersController, - decoration: InputDecoration( - labelText: "Max Team Members", - border: const OutlineInputBorder(), - errorText: invalidMaxTeamMembers.value - ? "Must be a number above 0" - : null), - keyboardType: TextInputType.number, - onChanged: (value) { - if (value.isEmpty || - int.tryParse(value) == null || - int.parse(value) <= 0) { - invalidMaxTeamMembers.value = true; - } else { - invalidMaxTeamMembers.value = false; - maxTeamMembersState.value = int.parse(value); - changed.value = true; - } - }, - ), - Slider( - value: maxTeamMembersState.value.toDouble(), - onChanged: (p0) { - maxTeamMembersState.value = p0.toInt(); - maxTeamMembersController.text = p0.toInt().toString(); - changed.value = true; - }, - min: min(1, maxTeamMembersState.value.toDouble()), - max: max(30, maxTeamMembersState.value.toDouble()), - divisions: - max(30, maxTeamMembersState.value.toDouble()).toInt() - 1, - label: maxTeamMembersState.value.toString(), - ), - const SizedBox(height: 8), - Row( - children: [ - const Text("Schematic Type: "), - TextButton( - onPressed: () async { - final types = - await ref.read(schematicTypesProvider.future); - final out = await showSearch( - context: context, - delegate: SchematicTypeSearchDelegate(types)); - if (out == RESET_TYPE) { - schematicTypeState.value = null; - changed.value = true; - } else { - schematicTypeState.value = - out ?? schematicTypeState.value; - changed.value = true; - } - }, - child: Text(schematicTypeState.value?.name ?? "Select")), - ], - ), - const SizedBox(height: 8), - CheckboxListTile( - value: publicOnlyState.value, - onChanged: (value) { - publicOnlyState.value = value ?? publicOnlyState.value; - changed.value = true; - }, - title: const Text("Public Only"), - ), - const SizedBox(height: 8), - CheckboxListTile( - value: spectateSystemState.value, - onChanged: (value) { - spectateSystemState.value = value ?? spectateSystemState.value; - changed.value = true; - }, - title: const Text("Use Spectate System"), - ), - const SizedBox(height: 8), - Text("Teams (${eventData.teams.length})", - style: Theme.of(context).textTheme.headline6), - Wrap( - children: [ - for (final team in eventData.teams) - Padding( - padding: const EdgeInsets.all(8.0), - child: Chip( - label: Text(team.name, - style: TextStyle( - color: team.color.computeLuminance() > 0.5 - ? Colors.black - : Colors.white)), - backgroundColor: team.color, - ), - ) - ], - ), - const SizedBox(height: 8), - Text("Fights", style: Theme.of(context).textTheme.headline6), - _EventFightList( - eventData: eventData, - ), - ], - ), - ), - ); - } -} - -class _EventFightList extends HookConsumerWidget { - final EventExtended eventData; - - const _EventFightList({Key? key, required this.eventData}) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final event = eventData.event; - final fights = useState>(eventData.fights); - final selected = useState>([]); - - final currentCheckBoxState = useMemoized(() { - if (selected.value.isEmpty) { - return false; - } else if (selected.value.length == fights.value.length) { - return true; - } else { - return null; - } - }, [selected.value, fights.value]); - - void update() async { - final repo = await ref.read(eventRepositoryProvider.future); - fights.value = await repo.listFights(event.id); - } - - return Column( - children: [ - const SizedBox(height: 8), - Row( - children: [ - const SizedBox(width: 16), - Checkbox( - value: currentCheckBoxState, - onChanged: (v) { - if (v == null) { - selected.value = []; - } else if (v) { - selected.value = fights.value - .where((element) => element.start.isAfter(DateTime.now())) - .map((e) => e.id) - .toList(); - if (selected.value.isEmpty) { - selected.value = fights.value.map((e) => e.id).toList(); - } - } else { - selected.value = fights.value.map((e) => e.id).toList(); - } - }, - tristate: true, - ), - const SizedBox(width: 8), - ButtonBar( - children: [ - Tooltip( - message: "Reschedule Fights", - child: IconButton( - onPressed: () { - if (selected.value.isEmpty) { - return; - } - showDialog( - context: context, - builder: (context) { - return HookBuilder(builder: (context) { - final lowest = useMemoized( - () => DateTime.fromMillisecondsSinceEpoch( - fights.value - .where((element) => selected.value - .contains(element.id)) - .map((e) => - e.start.millisecondsSinceEpoch) - .reduce(min)), - [fights.value]); - final date = useState(lowest); - - final offset = useMemoized( - () => date.value.difference(lowest), - [date.value, lowest]); - - return AlertDialog( - title: const Text("Reschedule Fights"), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - DateTimeEditor( - mainAxisAlignment: - MainAxisAlignment.center, - (p0) { - date.value = p0; - }, - date.value, - true, - ), - const SizedBox(height: 8), - Text( - "${offset.isNegative ? "-" : ""}${offset.inHours.abs().toString().padLeft(2, "0")}:${offset.inMinutes.remainder(60).abs().toInt().toString().padLeft(2, "0")}:${offset.inSeconds.remainder(60).abs().toInt().toString().padLeft(2, "0")}"), - ], - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text("Cancel"), - ), - TextButton( - onPressed: () async { - final selectedFights = fights.value - .where((element) => selected.value - .contains(element.id)) - .toList(); - final repo = await ref - .read(eventRepositoryProvider.future); - for (final fight in selectedFights) { - await repo.updateFight( - fight.id, - fight.start.add(offset), - fight.gameMode, - fight.map, - fight.kampfleiter.id, - fight.blueTeam, - fight.redTeam, - fight.group); - } - update(); - Navigator.of(context).pop(); - }, - child: const Text("Reshedule"), - ), - ], - ); - }); - }); - }, - icon: const Icon(Icons.event_note), - ), - ), - Tooltip( - message: "Change Kampfleiter", - child: IconButton( - onPressed: () async { - if (selected.value.isEmpty) { - return; - } - - final kampfleiter = await showSearch( - context: context, - delegate: UserSearchDelegate( - await ref.read(usersProvider.future))); - - if (kampfleiter == 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, - kampfleiter.id, - fight.blueTeam, - fight.redTeam, - fight.group); - } - } - update(); - } - }, - 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.isEmpty ? null : group); - } - } - update(); - } - }, - icon: const Icon(Icons.group)), - ), - Tooltip( - message: "Delete Fights", - child: IconButton( - onPressed: () { - if (selected.value.isEmpty) { - return; - } - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text("Delete Fights"), - content: const Text( - "Do you really want to delete the selected fights?"), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text("Cancel"), - ), - TextButton( - onPressed: () async { - final repo = await ref - .read(eventRepositoryProvider.future); - for (final fight in selected.value) { - await repo.deleteFight(fight); - } - update(); - Navigator.of(context).pop(); - }, - child: const Text("Delete", - style: TextStyle(color: Colors.red)), - ), - ], - ); - }); - }, - icon: const Icon(Icons.delete, color: Colors.red), - ), - ), - ], - ), - const Spacer(), - FloatingActionButton.extended( - onPressed: () { - showDialog( - context: context, - builder: (context) { - return _AddFightWidget( - eventData, - () => update(), - ); - }, - ); - }, - label: const Text("Add Fight"), - icon: const Icon(Icons.add), - ), - const SizedBox(width: 8), - PopupMenuButton( - itemBuilder: (context) { - return const [ - PopupMenuItem( - value: "export", - child: Text("Export Data"), - ), - ]; - }, - onSelected: (value) { - if (value == "export") { - openExportedFights(fights.value, event.name); - } - }, - ), - const SizedBox(width: 8), - ], - ), - const Divider(), - const SizedBox(height: 8), - if (fights.value.isEmpty) - const Center( - child: Text("No fights yet"), - ), - 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( - color: fight - .getTeamWithContextColor( - Theme.of(context).scaffoldBackgroundColor) - .color - .computeLuminance() > - 0.5 - ? Colors.black - : Colors.white), - ), - subtitle: Text( - fight.score == 0 - ? kDateFormat.format(fight.start) - : "Winner: ${fight.winner.name}", - style: TextStyle( - color: fight - .getTeamWithContextColor( - Theme.of(context).scaffoldBackgroundColor) - .color - .computeLuminance() > - 0.5 - ? Colors.black - : Colors.white)), - onTap: () { - showDialog( - context: context, - builder: (context) { - return EditEventFightDialog( - fight: fight, - fightsRefresher: () => update(), - event: eventData, - ); - }); - }, - tileColor: - fight.score != 0 ? fight.winner.color.withOpacity(0.5) : null, - leading: Checkbox( - value: selected.value.contains(fight.id), - onChanged: (value) { - if (value ?? false) { - selected.value = [...selected.value, fight.id]; - } else { - selected.value = selected.value - .where((element) => element != fight.id) - .toList(); - } - }), - ), - ) - ], - ); - } -} - -class EditEventFightDialog extends HookConsumerWidget { - const EditEventFightDialog( - {Key? key, - required this.fight, - required this.fightsRefresher, - required this.event}) - : super(key: key); - - final EventFight fight; - final EventExtended event; - final void Function() fightsRefresher; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final blueTeam = useState(fight.blueTeam); - final redTeam = useState(fight.redTeam); - final start = useState(fight.start); - 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}"), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 8), - const Text("Blue Team"), - _TeamSelector(event.event, blueTeam.value, (p0) { - blueTeam.value = p0; - }, event.teams), - const SizedBox(height: 8), - const Text("Red Team"), - _TeamSelector(event.event, redTeam.value, (p0) { - redTeam.value = p0; - }, event.teams), - const SizedBox(height: 8), - const Text("Start"), - const SizedBox(height: 8), - DateTimeEditor((p0) { - start.value = p0; - }, start.value, true, mainAxisAlignment: MainAxisAlignment.center), - const SizedBox(height: 8), - TextButton( - onPressed: () { - start.value = start.value.add(const Duration(seconds: 30)); - }, - child: const Text("Add 30 Seconds"), - ), - const SizedBox(height: 8), - const Text("Game Mode"), - const SizedBox(height: 8), - ElevatedButton( - onPressed: () async { - final modes = await ref.read(fightServersProvider.future); - final out = await showSearch( - context: context, delegate: GamemodeSearchDelegate(modes)); - if (out != null) { - mode.value = out; - } - }, - child: Text(mode.value), - ), - const SizedBox(height: 8), - const Text("Map"), - const SizedBox(height: 8), - ElevatedButton( - onPressed: () async { - final maps = await ref.read(mapsProvider.future); - final out = await showSearch( - context: context, - delegate: MapSearchDelegate(maps[mode.value] ?? [])); - if (out != null) { - map.value = out; - fightsRefresher(); - } - }, - child: Text(map.value), - ), - const SizedBox(height: 8), - const Text("Kampfleiter"), - const SizedBox(height: 8), - TextButton( - onPressed: () async { - final users = await ref.read(usersProvider.future); - final user = await showSearch( - context: context, delegate: UserSearchDelegate(users)); - if (user != null) { - referrer.value = user; - fightsRefresher(); - } - }, - child: Text(referrer.value.id == 0 - ? "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: [ - TextButton( - onPressed: () async { - final delete = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text("Delete Fight?"), - content: const Text( - "Do you really want to delete this fight?"), - icon: const Icon(Icons.warning), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text("No")), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: const Text("Yes", - style: TextStyle(color: Colors.red))), - ], - )); - if (delete) { - ref.read(eventRepositoryProvider.future).then( - (value) => value.deleteFight(fight.id).then( - (value) { - fightsRefresher(); - }, - ), - ); - Navigator.of(context).pop(); - } - }, - child: const Text("Delete", style: TextStyle(color: Colors.red))), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text("Cancel")), - TextButton( - 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, - group.value.isEmpty ? null : group.value); - fightsRefresher(); - nav.pop(); - }, - child: const Text("Save")), - ], - ); - } -} - -class _AddFightWidget extends HookConsumerWidget { - final void Function() onAdded; - final EventExtended eventData; - const _AddFightWidget( - this.eventData, - this.onAdded, { - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final event = eventData.event; - final gamemode = useState(null); - final map = useState(null); - final blueTeam = useState(null); - final redTeam = useState(null); - final date = useState(event.start); - final referrer = useState(null); - final group = useState(""); - - final canCreate = useMemoized(() { - return gamemode.value != null && - map.value != null && - blueTeam.value != null && - redTeam.value != null; - }, [gamemode.value, map.value, blueTeam.value, redTeam.value]); - - return AlertDialog( - title: const Text("Add Fight"), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text("Gamemode"), - const SizedBox(height: 8), - ElevatedButton( - onPressed: () async { - final modes = await ref.read(fightServersProvider.future); - final mode = await showSearch( - context: context, delegate: GamemodeSearchDelegate(modes)); - if (mode != null) { - gamemode.value = mode; - } - }, - child: Text(gamemode.value ?? "Select"), - ), - const SizedBox(height: 8), - const Text("Map"), - const SizedBox(height: 8), - ElevatedButton( - onPressed: gamemode.value != null - ? () async { - final maps = await ref.read(mapsProvider.future); - final sMap = await showSearch( - context: context, - delegate: MapSearchDelegate(maps[gamemode.value]!)); - if (sMap != null) { - map.value = sMap; - } - } - : null, - child: Text(map.value ?? "Select"), - ), - const SizedBox(height: 8), - const Text("Blue Team"), - _TeamSelector(event, blueTeam.value, (p0) => blueTeam.value = p0, - eventData.teams), - const SizedBox(height: 8), - const Text("Red Team"), - _TeamSelector(event, redTeam.value, (p0) => redTeam.value = p0, - eventData.teams), - const SizedBox(height: 8), - DateTimeEditor((p0) => date.value = p0, date.value, true, - mainAxisAlignment: MainAxisAlignment.center), - const SizedBox(height: 8), - const Text("Kampfleiter"), - const SizedBox(height: 8), - TextButton( - onPressed: () async { - final users = await ref.read(usersProvider.future); - final user = await showSearch( - context: context, delegate: UserSearchDelegate(users)); - if (user != null) { - referrer.value = user; - } - }, - child: Text(referrer.value == null || referrer.value!.id == 0 - ? "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: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text("Cancel")), - TextButton( - onPressed: canCreate - ? () async { - final nav = Navigator.of(context); - final repo = await ref.read(eventRepositoryProvider.future); - await repo.createFight( - event.id, - date.value, - gamemode.value!, - map.value!, - blueTeam.value!, - redTeam.value!, - referrer.value?.id ?? 0, - group.value.isEmpty ? null : group.value); - onAdded.call(); - nav.pop(); - } - : null, - child: const Text("Add")), - ], - ); - } -} - -class _TeamSelector extends HookConsumerWidget { - final Event event; - final Team? selectedTeam; - final void Function(Team) onSelected; - final List teams; - - const _TeamSelector( - this.event, - this.selectedTeam, - this.onSelected, - this.teams, { - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ElevatedButton( - onPressed: () async { - final team = await showSearch( - context: context, delegate: TeamSearchDelegate(teams)); - if (team != null) { - onSelected(team); - } - }, - child: Text(selectedTeam?.name ?? "Select Team"), - ); - } -} - -class DateTimeEditor extends HookConsumerWidget { - final void Function(DateTime) onChanged; - final DateTime initialDate; - final bool enabled; - final MainAxisAlignment mainAxisAlignment; - const DateTimeEditor(this.onChanged, this.initialDate, this.enabled, - {Key? key, this.mainAxisAlignment = MainAxisAlignment.start}) - : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Row( - mainAxisAlignment: mainAxisAlignment, - children: [ - TextButton( - onPressed: enabled - ? () async { - final date = await showDatePicker( - context: context, - initialDate: initialDate, - firstDate: DateTime(2020), - lastDate: DateTime(2030), - ); - if (date != null) { - onChanged(date); - } - } - : null, - child: Text(kDateFormat.format(initialDate)), - ), - IconButton( - onPressed: enabled - ? () async { - final time = await showTimePicker( - builder: (context, child) { - return MediaQuery( - data: MediaQuery.of(context).copyWith( - alwaysUse24HourFormat: true, - ), - child: child!, - ); - }, - context: context, - initialTime: TimeOfDay( - hour: initialDate.hour, minute: initialDate.minute), - ); - if (time != null) { - final changed = DateTime( - initialDate.year, - initialDate.month, - initialDate.day, - time.hour, - time.minute); - onChanged(changed); - } - } - : null, - icon: const Icon(Icons.schedule)), - ], - ); - } -} diff --git a/lib/src/screens/bracket_generator.dart b/lib/src/screens/event/bracket_generator.dart similarity index 100% rename from lib/src/screens/bracket_generator.dart rename to lib/src/screens/event/bracket_generator.dart diff --git a/lib/src/screens/event/components/dialogs/add_fight.dart b/lib/src/screens/event/components/dialogs/add_fight.dart new file mode 100644 index 0000000..c5378b8 --- /dev/null +++ b/lib/src/screens/event/components/dialogs/add_fight.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:steamwar_multitool/src/components/date_time_editor.dart'; +import 'package:steamwar_multitool/src/provider/provider.dart'; +import 'package:steamwar_multitool/src/screens/event/components/team_selector.dart'; +import 'package:steamwar_multitool/src/delegates/delegates.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; + +class AddFightDialog extends HookConsumerWidget { + final void Function() onAdded; + final EventExtended eventData; + const AddFightDialog( + this.eventData, + this.onAdded, { + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final event = eventData.event; + final gamemode = useState(null); + final map = useState(null); + final blueTeam = useState(null); + final redTeam = useState(null); + final date = useState(event.start); + final referrer = useState(null); + final group = useState(""); + + final canCreate = useMemoized(() { + return gamemode.value != null && + map.value != null && + blueTeam.value != null && + redTeam.value != null; + }, [gamemode.value, map.value, blueTeam.value, redTeam.value]); + + return AlertDialog( + title: const Text("Add Fight"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("Gamemode"), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () async { + final modes = await ref.read(fightServersProvider.future); + final mode = await showSearch( + context: context, delegate: GamemodeSearchDelegate(modes)); + if (mode != null) { + gamemode.value = mode; + } + }, + child: Text(gamemode.value ?? "Select"), + ), + const SizedBox(height: 8), + const Text("Map"), + const SizedBox(height: 8), + ElevatedButton( + onPressed: gamemode.value != null + ? () async { + final maps = await ref.read(mapsProvider.future); + final sMap = await showSearch( + context: context, + delegate: MapSearchDelegate(maps[gamemode.value]!)); + if (sMap != null) { + map.value = sMap; + } + } + : null, + child: Text(map.value ?? "Select"), + ), + const SizedBox(height: 8), + const Text("Blue Team"), + TeamSelector(event, blueTeam.value, (p0) => blueTeam.value = p0, + eventData.teams), + const SizedBox(height: 8), + const Text("Red Team"), + TeamSelector(event, redTeam.value, (p0) => redTeam.value = p0, + eventData.teams), + const SizedBox(height: 8), + DateTimeEditor((p0) => date.value = p0, date.value, true, + mainAxisAlignment: MainAxisAlignment.center), + const SizedBox(height: 8), + const Text("Kampfleiter"), + const SizedBox(height: 8), + TextButton( + onPressed: () async { + final users = await ref.read(usersProvider.future); + final user = await showSearch( + context: context, delegate: UserSearchDelegate(users)); + if (user != null) { + referrer.value = user; + } + }, + child: Text(referrer.value == null || referrer.value!.id == 0 + ? "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: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("Cancel")), + TextButton( + onPressed: canCreate + ? () async { + final nav = Navigator.of(context); + final repo = await ref.read(eventRepositoryProvider.future); + await repo.createFight( + event.id, + date.value, + gamemode.value!, + map.value!, + blueTeam.value!, + redTeam.value!, + referrer.value?.id ?? 0, + group.value.isEmpty ? null : group.value); + onAdded.call(); + nav.pop(); + } + : null, + child: const Text("Add")), + ], + ); + } +} diff --git a/lib/src/screens/event/components/dialogs/dialogs.dart b/lib/src/screens/event/components/dialogs/dialogs.dart new file mode 100644 index 0000000..f8ecbe1 --- /dev/null +++ b/lib/src/screens/event/components/dialogs/dialogs.dart @@ -0,0 +1,2 @@ +export 'add_fight.dart'; +export 'edit_fight.dart'; diff --git a/lib/src/screens/event/components/dialogs/edit_fight.dart b/lib/src/screens/event/components/dialogs/edit_fight.dart new file mode 100644 index 0000000..d37fdc6 --- /dev/null +++ b/lib/src/screens/event/components/dialogs/edit_fight.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:steamwar_multitool/src/components/date_time_editor.dart'; +import 'package:steamwar_multitool/src/provider/provider.dart'; +import 'package:steamwar_multitool/src/screens/event/components/team_selector.dart'; +import 'package:steamwar_multitool/src/delegates/delegates.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; + +class EditEventFightDialog extends HookConsumerWidget { + const EditEventFightDialog( + {Key? key, + required this.fight, + required this.fightsRefresher, + required this.event}) + : super(key: key); + + final EventFight fight; + final EventExtended event; + final void Function() fightsRefresher; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final blueTeam = useState(fight.blueTeam); + final redTeam = useState(fight.redTeam); + final start = useState(fight.start); + 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}"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + const Text("Blue Team"), + TeamSelector(event.event, blueTeam.value, (p0) { + blueTeam.value = p0; + }, event.teams), + const SizedBox(height: 8), + const Text("Red Team"), + TeamSelector(event.event, redTeam.value, (p0) { + redTeam.value = p0; + }, event.teams), + const SizedBox(height: 8), + const Text("Start"), + const SizedBox(height: 8), + DateTimeEditor((p0) { + start.value = p0; + }, start.value, true, mainAxisAlignment: MainAxisAlignment.center), + const SizedBox(height: 8), + TextButton( + onPressed: () { + start.value = start.value.add(const Duration(seconds: 30)); + }, + child: const Text("Add 30 Seconds"), + ), + const SizedBox(height: 8), + const Text("Game Mode"), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () async { + final modes = await ref.read(fightServersProvider.future); + final out = await showSearch( + context: context, delegate: GamemodeSearchDelegate(modes)); + if (out != null) { + mode.value = out; + } + }, + child: Text(mode.value), + ), + const SizedBox(height: 8), + const Text("Map"), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () async { + final maps = await ref.read(mapsProvider.future); + final out = await showSearch( + context: context, + delegate: MapSearchDelegate(maps[mode.value] ?? [])); + if (out != null) { + map.value = out; + fightsRefresher(); + } + }, + child: Text(map.value), + ), + const SizedBox(height: 8), + const Text("Kampfleiter"), + const SizedBox(height: 8), + TextButton( + onPressed: () async { + final users = await ref.read(usersProvider.future); + final user = await showSearch( + context: context, delegate: UserSearchDelegate(users)); + if (user != null) { + referrer.value = user; + fightsRefresher(); + } + }, + child: Text(referrer.value.id == 0 + ? "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: [ + TextButton( + onPressed: () async { + final delete = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Delete Fight?"), + content: const Text( + "Do you really want to delete this fight?"), + icon: const Icon(Icons.warning), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text("No")), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text("Yes", + style: TextStyle(color: Colors.red))), + ], + )); + if (delete) { + ref.read(eventRepositoryProvider.future).then( + (value) => value.deleteFight(fight.id).then( + (value) { + fightsRefresher(); + }, + ), + ); + Navigator.of(context).pop(); + } + }, + child: const Text("Delete", style: TextStyle(color: Colors.red))), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("Cancel")), + TextButton( + 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, + group.value.isEmpty ? null : group.value); + fightsRefresher(); + nav.pop(); + }, + child: const Text("Save")), + ], + ); + } +} diff --git a/lib/src/screens/event/components/fight_list.dart b/lib/src/screens/event/components/fight_list.dart new file mode 100644 index 0000000..343c25d --- /dev/null +++ b/lib/src/screens/event/components/fight_list.dart @@ -0,0 +1,408 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.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'; +import 'package:steamwar_multitool/src/provider/provider.dart'; +import 'package:steamwar_multitool/src/delegates/delegates.dart'; +import 'package:steamwar_multitool/src/screens/event/components/dialogs/dialogs.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; +import 'package:steamwar_multitool/src/util/constants.dart'; +import 'package:steamwar_multitool/src/util/event_fight_exporter.dart'; + +class EventFightList extends HookConsumerWidget { + final EventExtended eventData; + + const EventFightList({Key? key, required this.eventData}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final event = eventData.event; + final fights = useState>(eventData.fights); + final selected = useState>([]); + + final currentCheckBoxState = useMemoized(() { + if (selected.value.isEmpty) { + return false; + } else if (selected.value.length == fights.value.length) { + return true; + } else { + return null; + } + }, [selected.value, fights.value]); + + void update() async { + final repo = await ref.read(eventRepositoryProvider.future); + fights.value = await repo.listFights(event.id); + } + + return Column( + children: [ + const SizedBox(height: 8), + Row( + children: [ + const SizedBox(width: 16), + Checkbox( + value: currentCheckBoxState, + onChanged: (v) { + if (v == null) { + selected.value = []; + } else if (v) { + selected.value = fights.value + .where((element) => element.start.isAfter(DateTime.now())) + .map((e) => e.id) + .toList(); + if (selected.value.isEmpty) { + selected.value = fights.value.map((e) => e.id).toList(); + } + } else { + selected.value = fights.value.map((e) => e.id).toList(); + } + }, + tristate: true, + ), + const SizedBox(width: 8), + ButtonBar( + children: [ + Tooltip( + message: "Reschedule Fights", + child: IconButton( + onPressed: () { + if (selected.value.isEmpty) { + return; + } + showDialog( + context: context, + builder: (context) { + return HookBuilder(builder: (context) { + final lowest = useMemoized( + () => DateTime.fromMillisecondsSinceEpoch( + fights.value + .where((element) => selected.value + .contains(element.id)) + .map((e) => + e.start.millisecondsSinceEpoch) + .reduce(min)), + [fights.value]); + final date = useState(lowest); + + final offset = useMemoized( + () => date.value.difference(lowest), + [date.value, lowest]); + + return AlertDialog( + title: const Text("Reschedule Fights"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DateTimeEditor( + mainAxisAlignment: + MainAxisAlignment.center, + (p0) { + date.value = p0; + }, + date.value, + true, + ), + const SizedBox(height: 8), + Text( + "${offset.isNegative ? "-" : ""}${offset.inHours.abs().toString().padLeft(2, "0")}:${offset.inMinutes.remainder(60).abs().toInt().toString().padLeft(2, "0")}:${offset.inSeconds.remainder(60).abs().toInt().toString().padLeft(2, "0")}"), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("Cancel"), + ), + TextButton( + onPressed: () async { + final selectedFights = fights.value + .where((element) => selected.value + .contains(element.id)) + .toList(); + final repo = await ref + .read(eventRepositoryProvider.future); + for (final fight in selectedFights) { + await repo.updateFight( + fight.id, + fight.start.add(offset), + fight.gameMode, + fight.map, + fight.kampfleiter.id, + fight.blueTeam, + fight.redTeam, + fight.group); + } + update(); + Navigator.of(context).pop(); + }, + child: const Text("Reshedule"), + ), + ], + ); + }); + }); + }, + icon: const Icon(Icons.event_note), + ), + ), + Tooltip( + message: "Change Kampfleiter", + child: IconButton( + onPressed: () async { + if (selected.value.isEmpty) { + return; + } + + final kampfleiter = await showSearch( + context: context, + delegate: UserSearchDelegate( + await ref.read(usersProvider.future))); + + if (kampfleiter == 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, + kampfleiter.id, + fight.blueTeam, + fight.redTeam, + fight.group); + } + } + update(); + } + }, + 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.isEmpty ? null : group); + } + } + update(); + } + }, + icon: const Icon(Icons.group)), + ), + Tooltip( + message: "Delete Fights", + child: IconButton( + onPressed: () { + if (selected.value.isEmpty) { + return; + } + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text("Delete Fights"), + content: const Text( + "Do you really want to delete the selected fights?"), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("Cancel"), + ), + TextButton( + onPressed: () async { + final repo = await ref + .read(eventRepositoryProvider.future); + for (final fight in selected.value) { + await repo.deleteFight(fight); + } + update(); + Navigator.of(context).pop(); + }, + child: const Text("Delete", + style: TextStyle(color: Colors.red)), + ), + ], + ); + }); + }, + icon: const Icon(Icons.delete, color: Colors.red), + ), + ), + ], + ), + const Spacer(), + FloatingActionButton.extended( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return AddFightDialog( + eventData, + () => update(), + ); + }, + ); + }, + label: const Text("Add Fight"), + icon: const Icon(Icons.add), + ), + const SizedBox(width: 8), + PopupMenuButton( + itemBuilder: (context) { + return const [ + PopupMenuItem( + value: "export", + child: Text("Export Data"), + ), + ]; + }, + onSelected: (value) { + if (value == "export") { + openExportedFights(fights.value, event.name); + } + }, + ), + const SizedBox(width: 8), + ], + ), + const Divider(), + const SizedBox(height: 8), + if (fights.value.isEmpty) + const Center( + child: Text("No fights yet"), + ), + 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( + color: fight + .getTeamWithContextColor( + Theme.of(context).scaffoldBackgroundColor) + .color + .computeLuminance() > + 0.5 + ? Colors.black + : Colors.white), + ), + subtitle: Text( + fight.score == 0 + ? kDateFormat.format(fight.start) + : "Winner: ${fight.winner.name}", + style: TextStyle( + color: fight + .getTeamWithContextColor( + Theme.of(context).scaffoldBackgroundColor) + .color + .computeLuminance() > + 0.5 + ? Colors.black + : Colors.white)), + onTap: () { + showDialog( + context: context, + builder: (context) { + return EditEventFightDialog( + fight: fight, + fightsRefresher: () => update(), + event: eventData, + ); + }); + }, + tileColor: + fight.score != 0 ? fight.winner.color.withOpacity(0.5) : null, + leading: Checkbox( + value: selected.value.contains(fight.id), + onChanged: (value) { + if (value ?? false) { + selected.value = [...selected.value, fight.id]; + } else { + selected.value = selected.value + .where((element) => element != fight.id) + .toList(); + } + }), + ), + ) + ], + ); + } +} diff --git a/lib/src/screens/event/components/loaded_event.dart b/lib/src/screens/event/components/loaded_event.dart new file mode 100644 index 0000000..6afa7f7 --- /dev/null +++ b/lib/src/screens/event/components/loaded_event.dart @@ -0,0 +1,283 @@ +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/date_time_editor.dart'; +import 'package:steamwar_multitool/src/provider/provider.dart'; +import 'package:steamwar_multitool/src/screens/event/components/fight_list.dart'; +import 'package:steamwar_multitool/src/screens/event/event.dart'; +import 'package:steamwar_multitool/src/delegates/delegates.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; + +class LoadedEventScreen extends HookConsumerWidget { + const LoadedEventScreen({Key? key, required this.eventData}) + : super(key: key); + + final EventExtended eventData; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final event = eventData.event; + + final nameController = useTextEditingController(text: event.name); + final deadlineState = useState(event.deadline); + final startDateState = useState(event.start); + final endDateState = useState(event.end); + final maxTeamMembersState = useState(event.maxTeamMembers); + final maxTeamMembersController = + useTextEditingController(text: event.maxTeamMembers.toString()); + final invalidMaxTeamMembers = useState(false); + final schematicTypeState = useState(null); + final publicOnlyState = useState(event.publicSchemsOnly); + final spectateSystemState = useState(event.spectateSystem); + + useMemoized(() { + ref.read(schematicTypesProvider.future).then((value) { + schematicTypeState.value = catchToNull( + () => value.firstWhere((element) => element.db == event.schemType)); + }); + }); + + final changed = useState(false); + return Scaffold( + appBar: AppBar( + title: Text("${"Edit"} ${event.name}"), + actions: [ + IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Delete Event"), + content: const Text( + "Are you sure you want to delete this event?"), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("Cancel")), + TextButton( + onPressed: () { + ref.read(eventRepositoryProvider.future).then( + (value) => value.deleteEvent(event.id)); + ref.invalidate(eventsListProvider); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + child: const Text( + "Delete", + style: TextStyle(color: Colors.red), + )), + ], + )); + }, + icon: const Icon(Icons.delete_outline, color: Colors.red), + ), + IconButton( + onPressed: changed.value + ? () { + ref + .read(eventRepositoryProvider.future) + .then((value) => value.updateEvent( + event.id, + nameController.text, + deadlineState.value, + startDateState.value, + endDateState.value, + maxTeamMembersState.value, + schematicTypeState.value?.db, + publicOnlyState.value, + spectateSystemState.value, + )) + .whenComplete( + () => ref.invalidate(eventsListProvider)); + context.go('/'); + } + : null, + icon: const Icon(Icons.save)), + ], + leading: IconButton( + onPressed: () async { + var canPop = false; + if (changed.value) { + final accepted = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Unsaved changes"), + content: const Text( + "You have unsaved changes. Do you want to discard them?"), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text("No")), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text("Yes", + style: TextStyle(color: Colors.red))), + ], + )); + if (accepted != null && accepted) { + canPop = true; + } + } else { + canPop = true; + } + if (canPop) context.go("/"); + }, + icon: const Icon(Icons.arrow_back), + ), + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: ListView( + children: [ + const SizedBox(height: 8), + TextField( + controller: nameController, + decoration: const InputDecoration( + labelText: "Name", + border: OutlineInputBorder(), + ), + onChanged: (value) { + changed.value = true; + }, + ), + const SizedBox(height: 8), + Row( + children: [ + const Text("Deadline: "), + DateTimeEditor((p0) { + deadlineState.value = p0; + changed.value = true; + }, deadlineState.value, true), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Text("Start: "), + DateTimeEditor((p0) { + startDateState.value = p0; + changed.value = true; + }, startDateState.value, true), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Text("End: "), + DateTimeEditor((p0) { + endDateState.value = p0; + changed.value = true; + }, endDateState.value, true), + ], + ), + const SizedBox(height: 8), + TextField( + controller: maxTeamMembersController, + decoration: InputDecoration( + labelText: "Max Team Members", + border: const OutlineInputBorder(), + errorText: invalidMaxTeamMembers.value + ? "Must be a number above 0" + : null), + keyboardType: TextInputType.number, + onChanged: (value) { + if (value.isEmpty || + int.tryParse(value) == null || + int.parse(value) <= 0) { + invalidMaxTeamMembers.value = true; + } else { + invalidMaxTeamMembers.value = false; + maxTeamMembersState.value = int.parse(value); + changed.value = true; + } + }, + ), + Slider( + value: maxTeamMembersState.value.toDouble(), + onChanged: (p0) { + maxTeamMembersState.value = p0.toInt(); + maxTeamMembersController.text = p0.toInt().toString(); + changed.value = true; + }, + min: min(1, maxTeamMembersState.value.toDouble()), + max: max(30, maxTeamMembersState.value.toDouble()), + divisions: + max(30, maxTeamMembersState.value.toDouble()).toInt() - 1, + label: maxTeamMembersState.value.toString(), + ), + const SizedBox(height: 8), + Row( + children: [ + const Text("Schematic Type: "), + TextButton( + onPressed: () async { + final types = + await ref.read(schematicTypesProvider.future); + final out = await showSearch( + context: context, + delegate: SchematicTypeSearchDelegate(types)); + if (out == RESET_TYPE) { + schematicTypeState.value = null; + changed.value = true; + } else { + schematicTypeState.value = + out ?? schematicTypeState.value; + changed.value = true; + } + }, + child: Text(schematicTypeState.value?.name ?? "Select")), + ], + ), + const SizedBox(height: 8), + CheckboxListTile( + value: publicOnlyState.value, + onChanged: (value) { + publicOnlyState.value = value ?? publicOnlyState.value; + changed.value = true; + }, + title: const Text("Public Only"), + ), + const SizedBox(height: 8), + CheckboxListTile( + value: spectateSystemState.value, + onChanged: (value) { + spectateSystemState.value = value ?? spectateSystemState.value; + changed.value = true; + }, + title: const Text("Use Spectate System"), + ), + const SizedBox(height: 8), + Text("Teams (${eventData.teams.length})", + style: Theme.of(context).textTheme.headline6), + Wrap( + children: [ + for (final team in eventData.teams) + Padding( + padding: const EdgeInsets.all(8.0), + child: Chip( + label: Text(team.name, + style: TextStyle( + color: team.color.computeLuminance() > 0.5 + ? Colors.black + : Colors.white)), + backgroundColor: team.color, + ), + ) + ], + ), + const SizedBox(height: 8), + Text("Fights", style: Theme.of(context).textTheme.headline6), + EventFightList( + eventData: eventData, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/screens/event/components/team_selector.dart b/lib/src/screens/event/components/team_selector.dart new file mode 100644 index 0000000..48880f4 --- /dev/null +++ b/lib/src/screens/event/components/team_selector.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:steamwar_multitool/src/delegates/delegates.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; + +class TeamSelector extends HookConsumerWidget { + final Event event; + final Team? selectedTeam; + final void Function(Team) onSelected; + final List teams; + + const TeamSelector( + this.event, + this.selectedTeam, + this.onSelected, + this.teams, { + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ElevatedButton( + onPressed: () async { + final team = await showSearch( + context: context, delegate: TeamSearchDelegate(teams)); + if (team != null) { + onSelected(team); + } + }, + child: Text(selectedTeam?.name ?? "Select Team"), + ); + } +} diff --git a/lib/src/screens/event/event.dart b/lib/src/screens/event/event.dart new file mode 100644 index 0000000..2bfb0b2 --- /dev/null +++ b/lib/src/screens/event/event.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:steamwar_multitool/src/provider/events.dart'; +import 'package:steamwar_multitool/src/screens/event/components/loaded_event.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; + +T? catchToNull(T Function() f) { + try { + return f(); + } catch (e) { + return null; + } +} + +class EditEventScreen extends HookConsumerWidget { + final int eventId; + const EditEventScreen(this.eventId, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final eventFuture = useMemoized(() => ref + .watch(eventRepositoryProvider.future) + .then((value) => value.getEvent(eventId))); + + return FutureBuilder( + future: eventFuture, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Scaffold( + appBar: AppBar( + title: const Text('Error'), + ), + body: Center( + child: Text('Error: ${snapshot.error}'), + ), + ); + } + if (!snapshot.hasData) { + return Scaffold( + appBar: AppBar( + title: const Text('Loading'), + ), + body: const Center( + child: CircularProgressIndicator(), + ), + ); + } + final eventData = snapshot.data as EventExtended; + + return LoadedEventScreen( + eventData: eventData, + ); + }, + ); + } +} diff --git a/lib/src/screens/event_fights_graph.dart b/lib/src/screens/event/event_fights_graph.dart similarity index 97% rename from lib/src/screens/event_fights_graph.dart rename to lib/src/screens/event/event_fights_graph.dart index 2387533..feb6d61 100644 --- a/lib/src/screens/event_fights_graph.dart +++ b/lib/src/screens/event/event_fights_graph.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:steamwar_multitool/src/provider/events.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; class EventfightsGraph extends HookConsumerWidget { final int eventId; diff --git a/lib/src/screens/home.dart b/lib/src/screens/home/home.dart similarity index 88% rename from lib/src/screens/home.dart rename to lib/src/screens/home/home.dart index ad7ea51..597b853 100644 --- a/lib/src/screens/home.dart +++ b/lib/src/screens/home/home.dart @@ -6,20 +6,14 @@ 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 '../provider/events.dart'; -import '../provider/server.dart'; -import '../provider/user.dart'; -import 'components/events_list.dart'; -import 'components/mod_list.dart'; +import 'package:steamwar_multitool/src/provider/provider.dart'; +import 'package:steamwar_multitool/src/screens/home/lists/events_list.dart'; class HomeScreen extends HookConsumerWidget { const HomeScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { - final servers = ref.watch(serversProvider); - final userData = ref.watch(userDataProvider); if (userData.hasError) { @@ -44,12 +38,10 @@ class HomeScreen extends HookConsumerWidget { automaticallyImplyLeading: false, actions: [ IconButton( - onPressed: servers.isLoading - ? null - : () { - ref.invalidate(eventsListProvider); - ref.invalidate(uncheckedMods); - }, + onPressed: () { + ref.invalidate(eventsListProvider); + ref.invalidate(uncheckedMods); + }, icon: const Icon(Icons.refresh), ), IconButton( diff --git a/lib/src/screens/components/events_list.dart b/lib/src/screens/home/lists/events_list.dart similarity index 89% rename from lib/src/screens/components/events_list.dart rename to lib/src/screens/home/lists/events_list.dart index 2319a93..059a552 100644 --- a/lib/src/screens/components/events_list.dart +++ b/lib/src/screens/home/lists/events_list.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:steamwar_multitool/main.dart'; +import 'package:steamwar_multitool/src/components/components.dart'; +import 'package:steamwar_multitool/src/dialogs/dialogs.dart'; +import 'package:steamwar_multitool/src/util/constants.dart'; -import '../../provider/events.dart'; -import 'error.dart'; -import 'event_dialog.dart'; +import '../../../provider/events.dart'; class EventListComponent extends HookConsumerWidget { const EventListComponent({Key? key}) : super(key: key); @@ -23,7 +23,8 @@ class EventListComponent extends HookConsumerWidget { FloatingActionButton.extended( onPressed: () { showDialog( - context: context, builder: (context) => const EventDialog()); + context: context, + builder: (context) => const CreateEventDialog()); }, label: const Text("Create Event"), icon: const Icon(Icons.add), diff --git a/lib/src/screens/components/mod_list.dart b/lib/src/screens/home/lists/mod_list.dart similarity index 94% rename from lib/src/screens/components/mod_list.dart rename to lib/src/screens/home/lists/mod_list.dart index d5ad1d9..30a93ff 100644 --- a/lib/src/screens/components/mod_list.dart +++ b/lib/src/screens/home/lists/mod_list.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:steamwar_multitool/src/components/error.dart'; import 'package:steamwar_multitool/src/provider/mods.dart'; -import 'error.dart'; - class ModListComponent extends HookConsumerWidget { const ModListComponent({Key? key}) : super(key: key); diff --git a/lib/src/screens/login.dart b/lib/src/screens/login/login.dart similarity index 98% rename from lib/src/screens/login.dart rename to lib/src/screens/login/login.dart index a40c8a3..a11eff1 100644 --- a/lib/src/screens/login.dart +++ b/lib/src/screens/login/login.dart @@ -7,8 +7,7 @@ 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/http.dart'; - -import '../provider/user.dart'; +import 'package:steamwar_multitool/src/provider/provider.dart'; class LoginScreenWidget extends HookConsumerWidget { const LoginScreenWidget({ diff --git a/lib/src/screens/search_delegates.dart b/lib/src/screens/search_delegates.dart deleted file mode 100644 index 2d6f187..0000000 --- a/lib/src/screens/search_delegates.dart +++ /dev/null @@ -1,385 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../provider/events.dart'; -import '../provider/types.dart'; - -final RESET_TYPE = SchematicType("RESET", "RESET"); - -class SchematicTypeSearchDelegate extends SearchDelegate { - final List types; - - SchematicTypeSearchDelegate(this.types); - - @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 _buildList(context); - } - - @override - Widget buildSuggestions(BuildContext context) { - return _buildList(context); - } - - Widget _buildList(BuildContext context) { - final out = types - .where((element) => element.name.contains(query.toLowerCase())) - .toList(); - return ListView.builder( - itemCount: out.length + 1, - itemBuilder: (context, index) { - if (index == 0) { - return ListTile( - title: const Text("Reset"), - leading: const Icon(Icons.clear), - onTap: () { - close(context, RESET_TYPE); - }, - ); - } - - final type = out[index - 1]; - - return ListTile( - title: Text(type.name), - onTap: () { - close(context, type); - }, - ); - }, - ); - } -} - -class GamemodeSearchDelegate extends SearchDelegate { - final List modes; - - GamemodeSearchDelegate(this.modes); - - @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 _buildList(context); - } - - @override - Widget buildSuggestions(BuildContext context) { - return _buildList(context); - } - - Widget _buildList(BuildContext context) { - final out = modes - .where((element) => element.toLowerCase().contains(query.toLowerCase())) - .toList(); - return ListView.builder( - itemCount: out.length, - itemBuilder: (context, index) { - final type = out[index]; - - return ListTile( - title: Text(type), - onTap: () { - close(context, type); - }, - ); - }, - ); - } -} - -class MapSearchDelegate extends SearchDelegate { - final List maps; - - MapSearchDelegate(this.maps); - - @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 _buildList(context); - } - - @override - Widget buildSuggestions(BuildContext context) { - return _buildList(context); - } - - Widget _buildList(BuildContext context) { - final out = maps - .where((element) => element.toLowerCase().contains(query.toLowerCase())) - .toList(); - return ListView.builder( - itemCount: out.length, - itemBuilder: (context, index) { - final type = out[index]; - - return ListTile( - title: Text(type), - onTap: () { - close(context, type); - }, - ); - }, - ); - } -} - -class UserSearchDelegate extends SearchDelegate { - final List users; - - UserSearchDelegate(this.users); - - @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 ListView( - children: [ - for (final team in users) - if (team.name.toLowerCase().contains(query.toLowerCase())) - ListTile( - title: Text(team.name), - onTap: () { - close(context, team); - }, - ) - ], - ); - } - - @override - Widget buildSuggestions(BuildContext context) { - return ListView( - children: [ - for (final team in users) - if (team.name.toLowerCase().contains(query.toLowerCase())) - ListTile( - title: Text(team.name), - onTap: () { - close(context, team); - }, - ) - ], - ); - } -} - -class TeamSearchDelegate extends SearchDelegate { - final List teams; - - TeamSearchDelegate(this.teams); - - @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 ListView( - children: [ - ListTile( - title: const Text("?"), - onTap: () { - close(context, Team(-1, "?", "?", "8")); - }, - ), - for (final team in teams) - if (team.name.toLowerCase().contains(query.toLowerCase())) - ListTile( - title: Text(team.name), - onTap: () { - close(context, team); - }, - ) - ], - ); - } - - @override - Widget buildSuggestions(BuildContext context) { - return ListView( - children: [ - ListTile( - title: const Text("?"), - onTap: () { - close(context, Team(-1, "?", "?", "8")); - }, - ), - for (final team in teams) - if (team.name.toLowerCase().contains(query.toLowerCase())) - ListTile( - title: Text(team.name), - onTap: () { - close(context, team); - }, - ) - ], - ); - } -} - -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/lib/src/screens/userinfo.dart b/lib/src/screens/settings/userinfo.dart similarity index 96% rename from lib/src/screens/userinfo.dart rename to lib/src/screens/settings/userinfo.dart index 01a7fbb..e73d51a 100644 --- a/lib/src/screens/userinfo.dart +++ b/lib/src/screens/settings/userinfo.dart @@ -3,9 +3,8 @@ 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 '../provider/user.dart'; -import 'components/error.dart'; +import 'package:steamwar_multitool/src/components/error.dart'; +import 'package:steamwar_multitool/src/provider/provider.dart'; class SettingsScreen extends HookConsumerWidget { const SettingsScreen({Key? key}) : super(key: key); diff --git a/lib/src/types/event.dart b/lib/src/types/event.dart new file mode 100644 index 0000000..0e7623e --- /dev/null +++ b/lib/src/types/event.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; + +class EventFight { + final int id; + final String gameMode; + final String map; + final DateTime start; + final Team redTeam; + 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.group); + + Team get winner { + return getTeamWithContextColor(Colors.white); + } + + Team getTeamWithContextColor(Color color) { + switch (score) { + case 1: + return blueTeam; + case 2: + return redTeam; + case 3: + return Team( + -1, "Tie", "TIE", color.computeLuminance() > 0.5 ? "f" : "0"); + default: + return Team( + -1, "Unknown", "UNK", color.computeLuminance() > 0.5 ? "f" : "0"); + } + } + + factory EventFight.fromJson(Map json) { + return EventFight( + json["id"], + json["spielmodus"], + json["map"], + DateTime.fromMillisecondsSinceEpoch(json["start"]), + Team.fromJson(json["redTeam"]), + Team.fromJson(json["blueTeam"]), + User.fromJson(json["kampfleiter"]), + json["ergebnis"], + json["group"]); + } +} + +class Team { + final int id; + final String name; + final String kuerzel; + final String colorCode; + + Team(this.id, this.name, this.kuerzel, this.colorCode); + + Color get color { + switch (colorCode) { + case "1": + return const Color(0xFF0000AA); + case "2": + return const Color(0xFF00AA00); + case "3": + return const Color(0xFF00AAAA); + case "4": + return const Color(0xFFAA0000); + case "5": + return const Color(0xFFAA00AA); + case "6": + return const Color(0xFFFFAA00); + case "7": + return const Color(0xFFAAAAAA); + case "8": + return const Color(0xFF555555); + case "9": + return const Color(0xFF5555FF); + case "a": + return const Color(0xFF55FF55); + case "b": + return const Color(0xFF55FFFF); + case "c": + return const Color(0xFFFF5555); + case "d": + return const Color(0xFFFF55FF); + case "e": + return const Color(0xFFFFFF55); + case "f": + return const Color(0xFFFFFFFF); + default: + return const Color(0xFF000000); + } + } + + factory Team.fromJson(Map json) { + return Team( + json['id'] as int, + json['name'] as String, + json['kuerzel'] as String, + json['color'] as String, + ); + } +} + +class EventExtended { + final Event event; + final List fights; + final List teams; + + EventExtended(this.event, this.fights, this.teams); + + factory EventExtended.fromJson(Map json) { + return EventExtended( + Event.fromJson(json['event']), + (json['fights'] as List) + .map((e) => EventFight.fromJson(e)) + .toList(), + (json['teams'] as List).map((e) => Team.fromJson(e)).toList(), + ); + } +} + +class Event { + final int id; + final String name; + final DateTime deadline; + final DateTime start; + final DateTime end; + final int maxTeamMembers; + final String? schemType; + final bool publicSchemsOnly; + final bool spectateSystem; + + Event({ + required this.id, + required this.name, + required this.deadline, + required this.start, + required this.end, + required this.maxTeamMembers, + required this.schemType, + required this.publicSchemsOnly, + required this.spectateSystem, + }); + + factory Event.fromJson(Map json) { + return Event( + id: json['id'], + name: json['name'], + deadline: DateTime.fromMillisecondsSinceEpoch(json['deadline']), + start: DateTime.fromMillisecondsSinceEpoch(json['start']), + end: DateTime.fromMillisecondsSinceEpoch(json['end']), + maxTeamMembers: json['maxTeamMembers'], + schemType: json['schemType'], + publicSchemsOnly: json['publicSchemsOnly'], + spectateSystem: json['spectateSystem'], + ); + } +} + +class ShortEvent { + final String name; + final int id; + final DateTime start; + final DateTime end; + + ShortEvent(this.name, this.id, this.start, this.end); + + get isUpcoming => start.isAfter(DateTime.now()); + + get isCurrent => + start.isBefore(DateTime.now()) && end.isAfter(DateTime.now()); + + factory ShortEvent.fromJson(Map json) { + return ShortEvent( + json["name"], + json["id"], + DateTime.fromMillisecondsSinceEpoch(json["start"]), + DateTime.fromMillisecondsSinceEpoch(json["end"])); + } +} diff --git a/lib/src/types/mods.dart b/lib/src/types/mods.dart new file mode 100644 index 0000000..bffdd49 --- /dev/null +++ b/lib/src/types/mods.dart @@ -0,0 +1,59 @@ +class Mod { + final Platform platform; + final String name; + final ModType type; + + Mod(this.platform, this.name, this.type); + + factory Mod.fromJson(Map json) { + return Mod( + Platform.fromOrdinal(json["platform"] as int), + json["modName"], + ModType.fromOrdinal(json["modType"] as int), + ); + } +} + +enum Platform { + FORGE, + LABYMOD, + FABRIC; + + static Platform fromOrdinal(int ordinal) { + switch (ordinal) { + case 0: + return FORGE; + case 1: + return LABYMOD; + case 2: + return FABRIC; + default: + throw Exception("Invalid ordinal"); + } + } +} + +enum ModType { + UNKLASSIFIED, + GREEN, + YELLOW, + RED, + YOUTUBER_ONLY; + + static ModType fromOrdinal(int ordinal) { + switch (ordinal) { + case 0: + return UNKLASSIFIED; + case 1: + return GREEN; + case 2: + return YELLOW; + case 3: + return RED; + case 4: + return YOUTUBER_ONLY; + default: + throw Exception("Invalid ordinal"); + } + } +} diff --git a/lib/src/types/schematic.dart b/lib/src/types/schematic.dart new file mode 100644 index 0000000..7558ec6 --- /dev/null +++ b/lib/src/types/schematic.dart @@ -0,0 +1,10 @@ +class SchematicType { + final String name; + final String db; + + SchematicType(this.name, this.db); + + factory SchematicType.fromJson(Map json) { + return SchematicType(json["name"], json["db"]); + } +} diff --git a/lib/src/types/types.dart b/lib/src/types/types.dart new file mode 100644 index 0000000..66870c1 --- /dev/null +++ b/lib/src/types/types.dart @@ -0,0 +1,5 @@ +export 'event.dart'; +export 'user_data.dart'; +export 'mods.dart'; +export 'user.dart'; +export 'schematic.dart'; diff --git a/lib/src/types/user.dart b/lib/src/types/user.dart new file mode 100644 index 0000000..c9a1987 --- /dev/null +++ b/lib/src/types/user.dart @@ -0,0 +1,10 @@ +class User { + final int id; + final String name; + + User(this.id, this.name); + + factory User.fromJson(Map json) { + return User(json["id"] as int, json["name"] as String); + } +} diff --git a/lib/src/types/user_data.dart b/lib/src/types/user_data.dart new file mode 100644 index 0000000..17a113b --- /dev/null +++ b/lib/src/types/user_data.dart @@ -0,0 +1,9 @@ +class UserData { + final String key; + final bool uneditablePastEvents; + + UserData({ + required this.key, + required this.uneditablePastEvents, + }); +} diff --git a/lib/src/util/constants.dart b/lib/src/util/constants.dart new file mode 100644 index 0000000..ed7fcfe --- /dev/null +++ b/lib/src/util/constants.dart @@ -0,0 +1,3 @@ +import 'package:intl/intl.dart'; + +final kDateFormat = DateFormat("dd.MM.yyyy HH:mm"); diff --git a/lib/src/util/event_fight_exporter.dart b/lib/src/util/event_fight_exporter.dart index 386b848..54425b7 100644 --- a/lib/src/util/event_fight_exporter.dart +++ b/lib/src/util/event_fight_exporter.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:html'; import 'package:csv/csv.dart'; -import 'package:steamwar_multitool/src/provider/events.dart'; +import 'package:steamwar_multitool/src/types/types.dart'; /* Start,BlueTeam,RedTeam,WinnerTeam