1
0
Dieser Commit ist enthalten in:
Chaoscaot 2023-01-21 10:52:05 +01:00
Ursprung 9fc40df916
Commit cdca1c8562
9 geänderte Dateien mit 482 neuen und 30 gelöschten Zeilen

Datei anzeigen

@ -1,6 +1,7 @@
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 'screens/home.dart';
import 'screens/login.dart';
@ -23,6 +24,16 @@ final _routes = GoRouter(
}
return EditEventScreen(eventId);
}),
GoRoute(
path: "/event/:eventId/graph",
builder: (context, state) {
final eventId = int.tryParse(state.params['eventId']!);
if (eventId == null) {
context.go("/");
return const SizedBox();
}
return EventfightsGraph(eventId);
}),
],
initialLocation: "/login",
);

99
lib/src/provider/mods.dart Normale Datei
Datei anzeigen

@ -0,0 +1,99 @@
import 'package:dio/dio.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:steamwar_multitool/src/provider/http.dart';
final modRepository = FutureProvider(
(ref) async => ModRepository(await ref.read(httpClient.future)),
dependencies: [httpClient]);
final uncheckedMods = FutureProvider<List<Mod>>((ref) async {
final repo = await ref.watch(modRepository.future);
return repo.listUnchecked();
}, dependencies: [modRepository]);
final mods = FutureProvider<List<Mod>>((ref) async {
final repo = await ref.watch(modRepository.future);
return repo.listMods();
}, dependencies: [modRepository]);
class ModRepository {
final Dio _client;
ModRepository(this._client);
Future<List<Mod>> listMods() async {
final res = await _client.get("/mods/all");
return (res.data as List).map((e) => Mod.fromJson(e)).toList();
}
Future<List<Mod>> listUnchecked() async {
final res = await _client.get("/mods/unchecked");
return (res.data as List).map((e) => Mod.fromJson(e)).toList();
}
Future<void> 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<String, dynamic> 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");
}
}
}

Datei anzeigen

@ -11,11 +11,13 @@ class ErrorComponent extends StatelessWidget {
return Container(
color: Colors.red,
child: Center(
child: Column(
children: [
SelectableText(error.toString()),
if (stackTrace != null) Text(stackTrace.toString()),
],
child: SingleChildScrollView(
child: Column(
children: [
SelectableText(error.toString()),
if (stackTrace != null) Text(stackTrace.toString()),
],
),
),
),
);

Datei anzeigen

@ -22,7 +22,8 @@ class EventListComponent extends HookConsumerWidget {
children: [
FloatingActionButton.extended(
onPressed: () {
showDialog(context: context, builder: (context) => EventDialog());
showDialog(
context: context, builder: (context) => const EventDialog());
},
label: const Text("Create Event"),
icon: const Icon(Icons.add),

Datei anzeigen

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:steamwar_multitool/src/provider/mods.dart';
import 'error.dart';
class ModListComponent extends HookConsumerWidget {
const ModListComponent({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final list = ref.watch(uncheckedMods);
return list.when(
data: (data) {
return ListView(
children: [
Center(
child: Text("WIP",
style: Theme.of(context).textTheme.headline5)),
...data
.map(
(e) => ListTile(
title: Text(e.name),
subtitle: Text(e.platform.name),
),
)
.toList()
],
);
},
error: (err, stack) => ErrorComponent(err, stack),
loading: () => const Center(child: CircularProgressIndicator()));
}
}

Datei anzeigen

@ -4,6 +4,7 @@ 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:intl/intl.dart';
import 'package:steamwar_multitool/src/util/event_fight_exporter.dart';
import '../../main.dart';
@ -302,7 +303,8 @@ class _EventScreen extends HookConsumerWidget {
title: const Text("Use Spectate System"),
),
const SizedBox(height: 8),
Text("Teams", style: Theme.of(context).textTheme.headline6),
Text("Teams (${eventData.teams.length})",
style: Theme.of(context).textTheme.headline6),
Wrap(
children: [
for (final team in eventData.teams)
@ -340,6 +342,17 @@ class _EventFightList extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final event = eventData.event;
final fights = useState<List<EventFight>>(eventData.fights);
final selected = useState<List<int>>([]);
final currentCheckBoxState = useMemoized<bool?>(() {
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);
@ -351,30 +364,213 @@ class _EventFightList extends HookConsumerWidget {
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: FloatingActionButton.extended(
onPressed: () {
showDialog(
context: context,
builder: (context) {
return _AddFightWidget(
eventData,
() => update(),
);
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>(
() => DateTime.fromMillisecondsSinceEpoch(
fights.value
.where((element) => selected.value
.contains(element.id))
.map((e) =>
e.start.millisecondsSinceEpoch)
.reduce(min)),
[fights.value]);
final date = useState<DateTime>(lowest);
final offset = useMemoized(
() => date.value.difference(lowest),
[date.value, lowest]);
return AlertDialog(
title: const Text("Reshedule 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) {
repo.updateFight(
fight.id,
fight.start.add(offset),
fight.gameMode,
fight.map,
fight.kampfleiter.id,
fight.blueTeam,
fight.redTeam);
}
update();
Navigator.of(context).pop();
},
child: const Text("Reshedule"),
),
],
);
});
});
},
);
},
label: const Text("Add Fight"),
icon: const Icon(Icons.add),
),
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)) {
repo.updateFight(
fight.id,
fight.start,
fight.gameMode,
fight.map,
kampfleiter.id,
fight.blueTeam,
fight.redTeam);
}
}
update();
}
},
icon: const Icon(Icons.person)),
),
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) {
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 [
return const [
PopupMenuItem(
child: const Text("Export Data"),
value: "export",
child: Text("Export Data"),
),
];
},
@ -386,7 +582,12 @@ class _EventFightList extends HookConsumerWidget {
)
],
),
const Divider(),
const SizedBox(height: 8),
if (fights.value.isEmpty)
const Center(
child: Text("No fights yet"),
),
for (final fight in fights.value)
ListTile(
title: Text(
@ -427,6 +628,17 @@ class _EventFightList extends HookConsumerWidget {
},
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();
}
}),
)
],
);
@ -474,7 +686,7 @@ class EditEventFightDialog extends HookConsumerWidget {
const SizedBox(height: 8),
DateTimeEditor((p0) {
start.value = p0;
}, start.value, true),
}, start.value, true, mainAxisAlignment: MainAxisAlignment.center),
const SizedBox(height: 8),
TextButton(
onPressed: () {
@ -540,6 +752,7 @@ class EditEventFightDialog extends HookConsumerWidget {
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),
@ -561,7 +774,7 @@ class EditEventFightDialog extends HookConsumerWidget {
Navigator.of(context).pop();
}
},
child: const Text("Delete")),
child: const Text("Delete", style: TextStyle(color: Colors.red))),
TextButton(
onPressed: () {
Navigator.of(context).pop();
@ -652,7 +865,8 @@ class _AddFightWidget extends HookConsumerWidget {
_TeamSelector(event, redTeam.value, (p0) => redTeam.value = p0,
eventData.teams),
const SizedBox(height: 8),
DateTimeEditor((p0) => date.value = p0, date.value, true),
DateTimeEditor((p0) => date.value = p0, date.value, true,
mainAxisAlignment: MainAxisAlignment.center),
const SizedBox(height: 8),
const Text("Kampfleiter"),
const SizedBox(height: 8),
@ -733,13 +947,15 @@ 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})
{Key? key, this.mainAxisAlignment = MainAxisAlignment.start})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Row(
mainAxisAlignment: mainAxisAlignment,
children: [
TextButton(
onPressed: enabled

Datei anzeigen

@ -0,0 +1,79 @@
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';
class EventfightsGraph extends HookConsumerWidget {
final int eventId;
const EventfightsGraph(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(
builder: (context, snapshot) {
if (snapshot.hasData) {
return _EventFightsGraph(snapshot.data!);
} else if (snapshot.hasError) {
return const Text("Error");
} else {
return const Center(child: CircularProgressIndicator());
}
},
future: eventFuture);
}
}
class EventFightPosition {
final Offset position;
final int fightId;
EventFightPosition(this.position, this.fightId);
}
class _EventFightsGraph extends HookConsumerWidget {
final EventExtended event;
const _EventFightsGraph(this.event, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
//final positions = useState(List.generate(event.fights.length,
// (index) => EventFightPosition(Offset.zero, event.fights[index].id)));
final positions = useState([
EventFightPosition(Offset.zero, 0),
EventFightPosition(Offset.zero, 1)
]);
return Scaffold(
appBar: AppBar(
title: Text(event.event.name),
),
body: Stack(
children: [
for (final fight in positions.value)
Positioned(
top: fight.position.dy,
left: fight.position.dx,
child: GestureDetector(
onPanUpdate: (details) {
positions.value = [
...positions.value
.where((element) => element.fightId != fight.fightId),
EventFightPosition(
fight.position + (details.delta * 2), fight.fightId)
];
},
child: Container(
color: Colors.red,
width: 100,
height: 100,
),
),
)
],
),
);
}
}

Datei anzeigen

@ -5,11 +5,13 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:steamwar_multitool/src/provider/mods.dart';
import '../provider/events.dart';
import '../provider/server.dart';
import '../provider/user.dart';
import 'components/events_list.dart';
import 'components/mod_list.dart';
class HomeScreen extends HookConsumerWidget {
const HomeScreen({Key? key}) : super(key: key);
@ -46,6 +48,7 @@ class HomeScreen extends HookConsumerWidget {
? null
: () {
ref.invalidate(eventsListProvider);
ref.invalidate(uncheckedMods);
},
icon: const Icon(Icons.refresh),
),
@ -91,8 +94,8 @@ class HomeScreen extends HookConsumerWidget {
label: Text('Events'),
),
NavigationRailDestination(
icon: Icon(Icons.dns),
label: Text('Server'),
icon: Icon(Icons.developer_mode),
label: Text('Mods'),
),
],
labelType: NavigationRailLabelType.selected,
@ -114,6 +117,7 @@ class HomeScreen extends HookConsumerWidget {
//ServerListComponent(),
EventListComponent(),
Placeholder(),
//ModListComponent(),
],
),
),

Datei anzeigen

@ -1,5 +1,6 @@
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:shared_preferences/shared_preferences.dart';
@ -46,6 +47,10 @@ class SettingsScreen extends HookConsumerWidget {
icon: const Icon(Icons.save),
),
],
leading: IconButton(
onPressed: () => context.go("/"),
icon: Icon(Icons.arrow_back),
),
),
body: Padding(
padding: const EdgeInsets.all(16.0),