1
0

Some™ Changes

Dieser Commit ist enthalten in:
Chaoscaot 2022-12-22 10:21:51 +01:00
Ursprung be8295747f
Commit 907eda9d63
19 geänderte Dateien mit 2234 neuen und 76 gelöschten Zeilen

Datei anzeigen

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:dev_server_starter/src/app.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -5,3 +7,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
void main() {
runApp(const ProviderScope(child: DevServerStarterApp()));
}
String? get userHome =>
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];

Datei anzeigen

@ -1,5 +1,5 @@
import 'package:dev_server_starter/src/screens/console.dart';
import 'package:dev_server_starter/src/screens/login.dart';
import 'package:dev_server_starter/src/screens/userinfo.dart';
import 'package:flutter/material.dart';
import 'screens/home.dart';
@ -13,16 +13,17 @@ class DevServerStarterApp extends StatelessWidget {
routes: {
"/": (context) => const LoginScreenWidget(),
"/home": (context) => const HomeScreen(),
"/settings": (context) => const SettingsScreen(),
},
title: 'Dev Server Starter',
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: Colors.amber[400],
colorSchemeSeed: const Color(0xFFFFFF55),
),
darkTheme: ThemeData(
brightness: Brightness.dark,
useMaterial3: true,
colorSchemeSeed: Colors.amber[400],
colorSchemeSeed: const Color(0xFFFFFF55),
),
debugShowCheckedModeBanner: false,
);

303
lib/src/provider/events.dart Normale Datei
Datei anzeigen

@ -0,0 +1,303 @@
import 'dart:io';
import 'dart:ui';
import 'package:dev_server_starter/src/provider/user.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:mysql_client/mysql_client.dart';
final portProvider = Provider<int>((ref) {
return 49507;
});
final portForwardProvider = FutureProvider<int>((ref) async {
final client = await ref.watch(sshClientProvider.future);
final port = ref.watch(portProvider);
final serverSocket = await ServerSocket.bind("127.0.0.1", port);
serverSocket.listen((socket) async {
final forward = await client.forwardLocal('127.0.0.1', 3306);
forward.stream.cast<List<int>>().pipe(socket);
socket.pipe(forward.sink);
});
ref.onDispose(() async {
try {
await serverSocket.close();
} catch (e) {
print(e);
}
});
return serverSocket.port;
});
final mysqlClientProvider = FutureProvider<MySQLConnection>((ref) async {
final port = await ref.watch(portForwardProvider.future);
final userData = await ref.watch(userDataProvider.future);
final conn = await MySQLConnection.createConnection(
host: "localhost",
port: port,
userName: userData.sqlUserName!,
password: userData.sqlPassword!,
secure: false,
databaseName: userData.useDevDB ? "developer" : "core",
);
await conn.connect(timeoutMs: 200).catchError((err) {
ref.invalidate(portForwardProvider);
});
ref.onDispose(() {
if (conn.connected) conn.close();
});
return conn;
}, dependencies: [portForwardProvider, userDataProvider]);
final eventRepositoryProvider = FutureProvider<EventRepository>((ref) async {
return EventRepository(await ref.watch(mysqlClientProvider.future));
}, dependencies: [mysqlClientProvider]);
final eventsListProvider = FutureProvider<List<ShortEvent>>((ref) async {
final repo = await ref.watch(eventRepositoryProvider.future);
return repo.listEvents();
}, dependencies: [eventRepositoryProvider]);
class EventRepository {
final MySQLConnection _connection;
final _statements = <int, Future<PreparedStmt>>{};
EventRepository(this._connection);
Future<PreparedStmt> getStatement(String sql) async {
return _statements.putIfAbsent(
sql.hashCode, () => _connection.prepare(sql));
}
Future<List<ShortEvent>> listEvents() async {
final result = await _connection.execute(
"SELECT EventName, EventID, Start, End FROM Event ORDER BY Start DESC");
return result.rows
.map((e) => ShortEvent(
e.colByName("EventName")!,
e.typedColByName<int>("EventID")!,
DateTime.parse(e.colByName("Start")!),
DateTime.parse(e.colByName("End")!)))
.toList();
}
Future<Event> getEvent(int id) async {
final result = await getStatement("SELECT * FROM Event WHERE EventID = ?")
.then((value) => value.execute([id]));
return result.rows.map((e) => eventFromResult(e)).first;
}
Future<List<ShortTeam>> getTeams(int eventId) async {
final result = await getStatement(
"SELECT Team.TeamID, TeamName, TeamColor FROM TeamTeilnahme JOIN Team ON TeamTeilnahme.TeamID = Team.TeamID WHERE EventID = ?")
.then((value) => value.execute([eventId]));
return result.rows
.map((e) => ShortTeam(e.typedColByName<int>("TeamID")!,
e.colByName("TeamName")!, e.colByName("TeamColor")!))
.toList();
}
Future<void> updateEvent(
int id,
String name,
DateTime deadline,
DateTime start,
DateTime end,
int maxTeamMembers,
String? schemType,
bool publicOnly,
bool useSpectateSystem) async {
await getStatement(""
"UPDATE `Event` SET `EventName`=?,`Deadline`=?,`Start`=?,`End`=?,`MaximumTeamMembers`=?,`SchemType`=?,`PublicSchemsOnly`=?,`SpectateSystem`=? WHERE `EventID` = ?")
.then((value) => value.execute([
name,
deadline.toIso8601String(),
start.toIso8601String(),
end.toIso8601String(),
maxTeamMembers,
schemType,
publicOnly ? 1 : 0,
useSpectateSystem ? 1 : 0,
id
]));
}
Event eventFromResult(ResultSetRow e) {
return Event(
id: e.typedColByName<int>("EventID")!,
name: e.colByName("EventName")!,
deadline: DateTime.parse(e.colByName("Deadline")!),
start: DateTime.parse(e.colByName("Start")!),
end: DateTime.parse(e.colByName("End")!),
maxTeamMembers: e.typedColByName<int>("MaximumTeamMembers")!,
schemType: e.colByName("SchemType"),
publicOnly: e.typedColByName<bool>("PublicSchemsOnly")!,
useSpectateSystem: e.typedColByName<bool>("SpectateSystem")!,
);
}
Future<Event> createEvent(String name, DateTime start, DateTime end) async {
await getStatement(
"INSERT INTO `Event` (`EventName`, `Start`, `End`, `MaximumTeamMembers`, `PublicSchemsOnly`) VALUES (?, ?, ?, 5, 0)")
.then((value) => value
.execute([name, start.toIso8601String(), end.toIso8601String()]));
final result = await getStatement("SELECT * FROM Event WHERE EventName = ?")
.then((value) => value.execute([name]));
return result.rows.map((e) => eventFromResult(e)).first;
}
Future<void> deleteEvent(int id) async {
await getStatement("DELETE FROM Event WHERE EventID = ?")
.then((value) => value.execute([id]));
}
Future<List<EventFight>> getFightOfEvent(int id) async {
final result = await getStatement(
"SELECT FightID, StartTime, Spielmodus, Map, tb.TeamID as BlueTeamID, tb.TeamColor as BlueTeamColor, tb.Teamname as BlueTeamName, tr.TeamID as RedTeamID, tr.TeamColor as RedTeamColor, tr.Teamname as RedTeamName, Kampfleiter, Ergebnis FROM EventFight JOIN Team tb ON tb.TeamID = EventFight.TeamBlue JOIN Team tr ON tr.TeamID = EventFight.TeamRed WHERE EventID = ?")
.then((value) => value.execute([id]));
return result.rows.map((e) => EventFight.fromResult(e)).toList();
}
Future<void> deleteFight(int id) async {
await getStatement("DELETE FROM EventFight WHERE FightID = ?")
.then((value) => value.execute([id]));
}
Future<void> createFight(int eventId, DateTime start, String mode, String map,
ShortTeam blue, ShortTeam red) {
return getStatement(
"INSERT INTO `EventFight` (`EventID`, `StartTime`, `Spielmodus`, `Map`, `TeamBlue`, `TeamRed`, `Kampfleiter`) VALUES (?, ?, ?, ?, ?, ?, 0)")
.then((value) => value.execute(
[eventId, start.toIso8601String(), mode, map, blue.id, red.id]));
}
Future<void> updateFight(
int id, DateTime start, String mode, String map, int referee) {
return getStatement(
"UPDATE `EventFight` SET `StartTime`=?,`Spielmodus`=?,`Map`=?,`Kampfleiter`=? WHERE `FightID` = ?")
.then((value) =>
value.execute([start.toIso8601String(), mode, map, referee, id]));
}
}
class EventFight {
final int id;
final String gameMode;
final String map;
final DateTime start;
final ShortTeam redTeam;
final ShortTeam blueTeam;
final int fightLeaderId;
final int score;
EventFight(this.id, this.gameMode, this.map, this.start, this.redTeam,
this.blueTeam, this.fightLeaderId, this.score);
ShortTeam get winner {
switch (score) {
case 1:
return blueTeam;
case 2:
return redTeam;
case 3:
return ShortTeam(-1, "Tie", "7");
default:
return ShortTeam(-1, "Unknown", "7");
}
}
factory EventFight.fromResult(ResultSetRow e) {
return EventFight(
e.typedColByName<int>("FightID")!,
e.colByName("Spielmodus")!,
e.colByName("Map")!,
DateTime.parse(e.colByName("StartTime")!),
ShortTeam(e.typedColByName<int>("RedTeamID")!,
e.colByName("RedTeamName")!, e.colByName("RedTeamColor")!),
ShortTeam(e.typedColByName<int>("BlueTeamID")!,
e.colByName("BlueTeamName")!, e.colByName("BlueTeamColor")!),
e.typedColByName<int>("Kampfleiter")!,
e.typedColByName<int>("Ergebnis")!,
);
}
}
class ShortTeam {
final int id;
final String name;
final String colorCode;
ShortTeam(this.id, this.name, 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);
}
}
}
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 publicOnly;
final bool useSpectateSystem;
Event({
required this.id,
required this.name,
required this.deadline,
required this.start,
required this.end,
required this.maxTeamMembers,
required this.schemType,
required this.publicOnly,
required this.useSpectateSystem,
});
}
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());
}

Datei anzeigen

@ -1,5 +1,9 @@
import 'dart:convert';
import 'package:dartssh2/dartssh2.dart';
import 'package:dev_server_starter/src/screens/console.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'user.dart';
@ -19,3 +23,9 @@ final sftpProvider = FutureProvider<SftpClient>((ref) async {
ref.onDispose(sftp.close);
return sftp;
});
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();
});

61
lib/src/provider/types.dart Normale Datei
Datei anzeigen

@ -0,0 +1,61 @@
import 'package:dev_server_starter/src/provider/events.dart';
import 'package:dev_server_starter/src/provider/server.dart';
import 'package:dev_server_starter/src/provider/user.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:yaml/yaml.dart';
final fightServersProvider = FutureProvider((ref) async {
final client = await ref.watch(sftpProvider.future);
return await client.listdir("/configs/GameModes").then(
(value) => value
.map((e) => e.filename)
.where((element) => element != "." && element != "..")
.where((element) =>
element.endsWith(".yml") && !element.endsWith(".kits.yml"))
.map((e) => e.substring(0, e.length - 4))
.toList(),
);
});
final schematicTypesProvider = FutureProvider((ref) async {
final client = await ref.watch(sshClientProvider.future);
final result = await client
.run("grep ' Type: ' /configs/GameModes/*.yml")
.then((value) => String.fromCharCodes(value));
return result
.split("\n")
.where((element) => element.isNotEmpty)
.map((e) => e.split(": ")[2])
.toList();
});
final mapsProvider = FutureProvider<Map<String, List<String>>>((ref) async {
final client = await ref.watch(sshClientProvider.future);
final gamemodes = await ref.read(fightServersProvider.future);
final maps = <String, List<String>>{};
for (var gm in gamemodes) {
final result = loadYaml(await client
.run("cat /configs/GameModes/$gm.yml")
.then((value) => String.fromCharCodes(value)));
final gmMaps = result?["Server"]?["Maps"] as YamlList?;
if (gmMaps != null) {
maps[gm] = gmMaps.toList().map((e) => e.toString()).toList();
}
}
return maps;
});
final usersProvider = FutureProvider((ref) async {
final client = await ref.watch(mysqlClientProvider.future);
final result = await client.execute("SELECT id, UserName FROM UserData");
return result.rows
.map((e) => User(e.typedColByName<int>("id")!, e.colByName("UserName")!))
.toList();
});
class User {
final int id;
final String name;
User(this.id, this.name);
}

Datei anzeigen

@ -26,7 +26,7 @@ final clientFunctionProvider =
default:
throw Exception("Invalid method");
}
});
}, dependencies: [userDataProvider]);
final userDataProvider = FutureProvider<UserData>((ref) async {
final prefs = await SharedPreferences.getInstance();
@ -34,11 +34,20 @@ final userDataProvider = FutureProvider<UserData>((ref) async {
final method = prefs.getInt("method")!;
final password = prefs.getString("password");
final privateKey = prefs.getString("privateKeyFile");
final sqlUsername = prefs.getString("sqlUsername");
final sqlPassword = prefs.getString("sqlPassword");
final useDevDB = prefs.getBool("sqlUseDevDB") ?? false;
final uneditablePastEvents = prefs.getBool("uneditablePastEvents") ?? false;
return UserData(
username: username,
method: method,
password: password,
privateKeyFile: privateKey,
sqlPassword: sqlPassword,
sqlUserName: sqlUsername,
useDevDB: useDevDB,
uneditablePastEvents: uneditablePastEvents,
);
});
@ -48,15 +57,26 @@ class UserData {
final String? privateKeyFile;
final int method;
final String? sqlUserName;
final String? sqlPassword;
final bool useDevDB;
final bool uneditablePastEvents;
UserData({
required this.username,
this.password,
this.privateKeyFile,
required this.password,
required this.privateKeyFile,
required this.method,
required this.sqlUserName,
required this.sqlPassword,
required this.useDevDB,
required this.uneditablePastEvents,
});
}
final sshClientProvider = FutureProvider<SSHClient>((ref) async {
final clientFunction = await ref.watch(clientFunctionProvider.future);
return clientFunction!(await SSHSocket.connect("steamwar.de", 22));
});
final client = clientFunction(await SSHSocket.connect("steamwar.de", 22));
await client.authenticated;
return client;
}, dependencies: [clientFunctionProvider]);

Datei anzeigen

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
class ErrorComponent extends StatelessWidget {
final Object error;
final StackTrace? stackTrace;
const ErrorComponent(this.error, this.stackTrace, {Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
child: Center(
child: Column(
children: [
SelectableText(error.toString()),
if (stackTrace != null) Text(stackTrace.toString()),
],
),
),
);
}
}

Datei anzeigen

@ -0,0 +1,85 @@
import 'package:dev_server_starter/src/provider/events.dart';
import 'package:dev_server_starter/src/screens/event.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class EventDialog extends HookConsumerWidget {
const EventDialog({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final events = ref.watch(eventsListProvider);
final eventName = useTextEditingController();
final eventExists = useState(false);
final startTime = useState(DateTime.now());
final endTime = useState(DateTime.now().add(const Duration(hours: 1)));
return AlertDialog(
title: const Text('Create Event'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: InputDecoration(
labelText: "Event Name",
errorText: eventExists.value ? "Event already exists" : null,
),
controller: eventName,
onChanged: (value) {
if (events.hasValue) {
eventExists.value =
events.value!.any((element) => element.name == value);
}
},
),
Row(
children: [
const Text("Start: "),
DateTimeEditor((p0) {
startTime.value = p0;
}, startTime.value, true),
],
),
Row(
children: [
const Text("End: "),
DateTimeEditor((p0) {
endTime.value = p0;
}, endTime.value, true),
],
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text(
"Cancel",
style: TextStyle(color: Colors.red),
),
),
TextButton(
onPressed: () async {
final nav = Navigator.of(context);
final event = await ref.read(eventRepositoryProvider.future).then(
(value) => value.createEvent(
eventName.text, startTime.value, endTime.value));
nav.pop();
nav.push(
MaterialPageRoute(
builder: (context) {
return EditEventScreen(true, event);
},
),
);
},
child: const Text("Create"),
),
],
);
}
}

Datei anzeigen

@ -0,0 +1,108 @@
import 'package:dev_server_starter/src/provider/events.dart';
import 'package:dev_server_starter/src/provider/user.dart';
import 'package:dev_server_starter/src/screens/components/error.dart';
import 'package:dev_server_starter/src/screens/event.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'event_dialog.dart';
class EventListComponent extends HookConsumerWidget {
const EventListComponent({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final events = ref.watch(eventsListProvider);
final userData = ref.watch(userDataProvider);
return events.when(data: (data) {
final upcomingEvents = data.where((element) => element.isUpcoming);
final pastEvents = data.where((element) => !element.isUpcoming);
return ListView(
children: [
FloatingActionButton.extended(
onPressed: () {
showDialog(context: context, builder: (context) => EventDialog());
},
label: const Text("Create Event"),
icon: const Icon(Icons.add),
),
const SizedBox(
height: 16,
),
if (upcomingEvents.isNotEmpty)
Text(
"Upcoming Events",
style: Theme.of(context).textTheme.headline6!,
),
...upcomingEvents.map(
(e) => ListTile(
title: Text(e.name),
subtitle: Text("${e.start} - ${e.end}"),
leading: const Icon(Icons.event),
onTap: () async {
final nav = Navigator.of(context);
final event = await ref
.read(eventRepositoryProvider.future)
.then((value) => value.getEvent(e.id));
nav.push(
MaterialPageRoute(
builder: (context) {
return EditEventScreen(true, event);
},
),
);
},
),
),
if (pastEvents.isNotEmpty)
Text(
"Past Events",
style: Theme.of(context).textTheme.headline6!,
),
...pastEvents.map((e) => ListTile(
title: Text(e.name),
leading: const Icon(Icons.check),
subtitle: Text("${e.start} - ${e.end}"),
onTap: () async {
final nav = Navigator.of(context);
final event = await ref
.read(eventRepositoryProvider.future)
.then((value) => value.getEvent(e.id));
nav.push(
MaterialPageRoute(
builder: (context) {
return EditEventScreen(
false ||
(userData.valueOrNull?.uneditablePastEvents ??
false),
event);
},
),
);
},
)),
],
);
}, error: (err, stack) {
final userdata = ref.read(userDataProvider);
if (userdata.value?.sqlUserName == null ||
userdata.value?.sqlPassword == null) {
return const Center(
child: Text("Please set your SQL credentials in the settings"),
);
}
if (err.toString() ==
"MySQLClientException: Can not execute query: connection closed") {
ref.invalidate(mysqlClientProvider);
return const Center(
child: CircularProgressIndicator(),
);
}
return ErrorComponent(err, stack);
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
});
}
}

Datei anzeigen

@ -0,0 +1,290 @@
import 'dart:convert';
import 'package:dev_server_starter/src/screens/components/error.dart';
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';
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"),
),
],
);
}
}

Datei anzeigen

@ -5,8 +5,11 @@ import 'package:dev_server_starter/src/provider/user.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);
@ -90,7 +93,8 @@ class _ConsoleScreenState extends ConsumerState<ConsoleScreen> {
final clientFut = ref.read(sshClientProvider);
final client = clientFut.value!;
client
.execute("python3 /binarys/dev.py ${widget.parameters.name} -w 3")
.execute(
"python3 /binarys/dev.py ${widget.parameters.name} ${widget.parameters.extraArguments}")
.then((value) {
session = value;
session.stdout.listen((event) {
@ -111,6 +115,7 @@ class _ConsoleScreenState extends ConsumerState<ConsoleScreen> {
}
}
@JsonSerializable()
class ServerStartParameters {
final String name;
final String? plugins;
@ -118,4 +123,26 @@ class ServerStartParameters {
final int? port;
ServerStartParameters(this.name, {this.plugins, this.world, this.port});
String get extraArguments {
final args = <String>[];
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<String, dynamic> json) =>
_$ServerStartParametersFromJson(json);
Map<String, dynamic> toJson() => _$ServerStartParametersToJson(this);
}

Datei anzeigen

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'console.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ServerStartParameters _$ServerStartParametersFromJson(
Map<String, dynamic> json) =>
ServerStartParameters(
json['name'] as String,
plugins: json['plugins'] as String?,
world: json['world'] as String?,
port: json['port'] as int?,
);
Map<String, dynamic> _$ServerStartParametersToJson(
ServerStartParameters instance) =>
<String, dynamic>{
'name': instance.name,
'plugins': instance.plugins,
'world': instance.world,
'port': instance.port,
};

657
lib/src/screens/event.dart Normale Datei
Datei anzeigen

@ -0,0 +1,657 @@
import 'dart:math';
import 'package:dev_server_starter/src/provider/events.dart';
import 'package:dev_server_starter/src/provider/types.dart';
import 'package:dev_server_starter/src/screens/components/error.dart';
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 'search_delegates.dart';
class EditEventScreen extends HookConsumerWidget {
final bool editable;
final Event event;
const EditEventScreen(this.editable, this.event, {Key? key})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
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(event.schemType);
final publicOnlyState = useState(event.publicOnly);
final spectateSystemState = useState(event.useSpectateSystem);
final changed = useState(false);
final teams = useMemoized(() {
return ref
.read(eventRepositoryProvider.future)
.then((value) => value.getTeams(event.id));
});
return Scaffold(
appBar: AppBar(
title: Text("${editable ? "Edit" : "View"} ${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,
publicOnlyState.value,
spectateSystemState.value,
))
.whenComplete(
() => ref.invalidate(eventsListProvider));
Navigator.of(context).pop();
}
: 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) Navigator.of(context).pop();
},
icon: const Icon(Icons.arrow_back),
),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: "Name",
border: OutlineInputBorder(),
),
enabled: editable,
onChanged: (value) {
changed.value = true;
},
),
const SizedBox(height: 8),
Row(
children: [
const Text("Deadline: "),
DateTimeEditor((p0) {
deadlineState.value = p0;
changed.value = true;
}, deadlineState.value, editable),
],
),
const SizedBox(height: 8),
Row(
children: [
const Text("Start: "),
DateTimeEditor((p0) {
startDateState.value = p0;
changed.value = true;
}, startDateState.value, editable),
],
),
const SizedBox(height: 8),
Row(
children: [
const Text("End: "),
DateTimeEditor((p0) {
endDateState.value = p0;
changed.value = true;
}, endDateState.value, editable),
],
),
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),
enabled: editable,
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;
}
},
),
if (editable)
Slider(
value: maxTeamMembersState.value.toDouble(),
onChanged: editable
? (p0) {
maxTeamMembersState.value = p0.toInt();
maxTeamMembersController.text = p0.toInt().toString();
changed.value = true;
}
: null,
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: editable
? () async {
final types =
await ref.read(schematicTypesProvider.future);
final out = await showSearch(
context: context,
delegate: SchematicTypeSearchDelegate(types));
if (out == "reset") {
schematicTypeState.value = null;
changed.value = true;
} else {
schematicTypeState.value =
out ?? schematicTypeState.value;
changed.value = true;
}
}
: null,
child: Text(schematicTypeState.value ?? "Select")),
],
),
const SizedBox(height: 8),
CheckboxListTile(
value: publicOnlyState.value,
onChanged: (value) {
publicOnlyState.value = value ?? publicOnlyState.value;
changed.value = true;
},
title: const Text("Public Only"),
enabled: editable,
),
const SizedBox(height: 8),
CheckboxListTile(
value: spectateSystemState.value,
onChanged: (value) {
spectateSystemState.value = value ?? spectateSystemState.value;
changed.value = true;
},
title: const Text("Use Spectate System"),
enabled: editable,
),
const SizedBox(height: 8),
Text("Teams", style: Theme.of(context).textTheme.headline6),
FutureBuilder<List<ShortTeam>>(
future: teams,
builder: (context, snap) {
if (snap.hasData) {
return Wrap(
children: [
for (final team in snap.data!)
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,
),
)
],
);
} else if (snap.hasError) {
return ErrorComponent(snap.error!, null);
} else {
return const LinearProgressIndicator();
}
},
),
const SizedBox(height: 8),
Text("Fights", style: Theme.of(context).textTheme.headline6),
_EventFightList(event: event),
],
),
),
);
}
}
class _EventFightList extends HookConsumerWidget {
final Event event;
const _EventFightList({Key? key, required this.event}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final fightsRefresher = useState(0);
final fights = useMemoized(() {
return ref
.read(eventRepositoryProvider.future)
.then((value) => value.getFightOfEvent(event.id));
}, [fightsRefresher.value]);
return FutureBuilder<List<EventFight>>(
future: fights,
builder: (context, data) {
if (data.hasError) {
return ErrorComponent(data.error!, null);
} else if (data.hasData) {
return ListView(
shrinkWrap: true,
children: [
const SizedBox(height: 8),
FloatingActionButton.extended(
onPressed: () {
showDialog(
context: context,
builder: (context) {
return _AddFightWidget(
event,
() => fightsRefresher.value++,
);
},
);
},
label: const Text("Add Fight"),
icon: const Icon(Icons.add),
),
const SizedBox(height: 8),
for (final fight in data.data!)
ListTile(
title:
Text("${fight.blueTeam.name} vs ${fight.redTeam.name}"),
subtitle: fight.score != 0
? Text("Winner: ${fight.winner.name}")
: Text(fight.start.toString()),
onTap: () {
showDialog(
context: context,
builder: (context) {
return EditEventFightDialog(
fight: fight, fightsRefresher: fightsRefresher);
});
},
tileColor: fight.score != 0 ? fight.winner.color : null,
)
],
);
} else {
return const LinearProgressIndicator();
}
});
}
}
class EditEventFightDialog extends HookConsumerWidget {
const EditEventFightDialog({
Key? key,
required this.fight,
required this.fightsRefresher,
}) : super(key: key);
final EventFight fight;
final ValueNotifier<int> fightsRefresher;
@override
Widget build(BuildContext context, WidgetRef ref) {
final start = useState(fight.start);
final mode = useState(fight.gameMode);
final map = useState(fight.map);
final referrer = useState(fight.fightLeaderId);
return AlertDialog(
title: Text("${fight.blueTeam.name} vs ${fight.redTeam.name}"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
const Text("Start"),
const SizedBox(height: 8),
DateTimeEditor((p0) {
start.value = p0;
}, start.value, true),
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.value++;
}
},
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.id;
fightsRefresher.value++;
}
},
child:
Text(referrer.value == 0 ? "None" : referrer.value.toString()),
),
],
),
actions: [
TextButton(
onPressed: () async {
Navigator.of(context).pop();
ref.read(eventRepositoryProvider.future).then(
(value) => value.deleteFight(fight.id).then(
(value) {
fightsRefresher.value++;
},
),
);
},
child: const Text("Delete")),
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);
fightsRefresher.value++;
nav.pop();
},
child: const Text("Save")),
],
);
}
}
class _AddFightWidget extends HookConsumerWidget {
final void Function() onAdded;
final Event event;
const _AddFightWidget(
this.event,
this.onAdded, {
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final gamemode = useState<String?>(null);
final map = useState<String?>(null);
final blueTeam = useState<ShortTeam?>(null);
final redTeam = useState<ShortTeam?>(null);
final date = useState<DateTime>(event.start);
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),
const SizedBox(height: 8),
const Text("Red Team"),
_TeamSelector(event, redTeam.value, (p0) => redTeam.value = p0),
const SizedBox(height: 8),
DateTimeEditor((p0) => date.value = p0, date.value, true),
],
),
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!);
onAdded.call();
nav.pop();
}
: null,
child: const Text("Add")),
],
);
}
}
class _TeamSelector extends HookConsumerWidget {
final Event event;
final ShortTeam? selectedTeam;
final void Function(ShortTeam) onSelected;
const _TeamSelector(
this.event,
this.selectedTeam,
this.onSelected, {
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () async {
final teams = await ref
.read(eventRepositoryProvider.future)
.then((value) => value.getTeams(event.id));
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;
const DateTimeEditor(this.onChanged, this.initialDate, this.enabled,
{Key? key})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Row(
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(initialDate.toString()),
),
IconButton(
onPressed: enabled
? () async {
final time = await showTimePicker(
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)),
],
);
}
}

Datei anzeigen

@ -1,5 +1,9 @@
import 'package:dev_server_starter/src/provider/events.dart';
import 'package:dev_server_starter/src/screens/components/events_list.dart';
import 'package:dev_server_starter/src/screens/components/server_list.dart';
import 'package:dev_server_starter/src/screens/console.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -11,19 +15,19 @@ class HomeScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userData = ref.read(userDataProvider).value;
final servers = ref.watch(serversProvider);
final navRailIndex = useState(0);
return Scaffold(
appBar: AppBar(
title: Text('Home: ${userData?.username ?? ""}'),
title: const Text('Home'),
actions: [
IconButton(
onPressed: servers.isLoading
? null
: () {
ref.refresh(serversProvider);
ref.refresh(eventsListProvider);
},
icon: const Icon(Icons.refresh),
),
@ -59,45 +63,42 @@ class HomeScreen extends HookConsumerWidget {
),
],
),
body: RefreshIndicator(
onRefresh: () {
ref.refresh(serversProvider);
return Future.value();
},
child: SingleChildScrollView(
child: servers.when(
data: (data) {
return Column(
children: [
for (final server in data)
ListTile(
leading: const Icon(Icons.dns),
title: Text(server),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ConsoleScreen(ServerStartParameters(server)),
),
);
},
),
],
);
},
error: (err, stack) {
return Center(
child: Text(err.toString()),
);
},
loading: () {
return const Center(
child: CircularProgressIndicator(),
);
body: Row(
children: [
NavigationRail(
elevation: 1,
destinations: const [
NavigationRailDestination(
icon: Icon(Icons.dns),
label: Text('Server'),
),
NavigationRailDestination(
icon: Icon(Icons.calendar_today),
label: Text('Events'),
),
],
labelType: NavigationRailLabelType.selected,
trailing: IconButton(
icon: const Icon(Icons.person),
onPressed: () {
Navigator.pushNamed(context, "/settings");
},
),
selectedIndex: navRailIndex.value,
onDestinationSelected: (index) {
navRailIndex.value = index;
},
),
),
Expanded(
child: IndexedStack(
index: navRailIndex.value,
children: const [
ServerListComponent(),
EventListComponent(),
],
),
),
],
),
);
}

Datei anzeigen

@ -8,6 +8,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path/path.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../main.dart';
class LoginScreenWidget extends HookConsumerWidget {
const LoginScreenWidget({
Key? key,
@ -157,6 +159,3 @@ class LoginScreenWidget extends HookConsumerWidget {
});
}
}
String? get userHome =>
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];

Datei anzeigen

@ -0,0 +1,305 @@
import 'package:dev_server_starter/src/provider/types.dart';
import 'package:flutter/material.dart';
import '../provider/events.dart';
class SchematicTypeSearchDelegate extends SearchDelegate<String?> {
final List<String> types;
SchematicTypeSearchDelegate(this.types);
@override
List<Widget>? 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.toLowerCase().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");
},
);
}
final type = out[index - 1];
return ListTile(
title: Text(type),
onTap: () {
close(context, type);
},
);
},
);
}
}
class GamemodeSearchDelegate extends SearchDelegate<String?> {
final List<String> modes;
GamemodeSearchDelegate(this.modes);
@override
List<Widget>? 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<String?> {
final List<String> maps;
MapSearchDelegate(this.maps);
@override
List<Widget>? 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<User?> {
final List<User> users;
UserSearchDelegate(this.users);
@override
List<Widget>? 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<ShortTeam?> {
final List<ShortTeam> teams;
TeamSearchDelegate(this.teams);
@override
List<Widget>? 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 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: [
for (final team in teams)
if (team.name.toLowerCase().contains(query.toLowerCase()))
ListTile(
title: Text(team.name),
onTap: () {
close(context, team);
},
)
],
);
}
}

213
lib/src/screens/userinfo.dart Normale Datei
Datei anzeigen

@ -0,0 +1,213 @@
import 'package:dev_server_starter/src/provider/user.dart';
import 'package:dev_server_starter/src/screens/components/error.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path/path.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../main.dart';
class SettingsScreen extends HookConsumerWidget {
const SettingsScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final userDate = ref.watch(userDataProvider);
return userDate.when(
data: (data) {
final userName = useTextEditingController(text: data.username);
final password = useTextEditingController(text: data.password ?? "");
final privateKeyFile = useState(data.privateKeyFile);
final sqlUsername =
useTextEditingController(text: data.sqlUserName ?? "");
final sqlPassword =
useTextEditingController(text: data.sqlPassword ?? "");
final useDevDB = useState(data.useDevDB);
final uneditablePastEvents = useState(data.uneditablePastEvents);
final changed = useState(false);
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
actions: [
IconButton(
onPressed: changed.value
? () async {
if (userName.text.isEmpty ||
(privateKeyFile.value == null &&
password.text.isEmpty)) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
"Username and password or private key are required"),
),
);
return;
}
final prefs = await SharedPreferences.getInstance();
await prefs.setString("username", userName.text);
if (password.text.isNotEmpty) {
await prefs.setString("password", password.text);
} else {
await prefs.remove("password");
}
if (privateKeyFile.value != null) {
await prefs.setString(
"privateKeyFile", privateKeyFile.value!);
} else {
await prefs.remove("privateKeyFile");
}
if (sqlUsername.text.isNotEmpty) {
await prefs.setString(
"sqlUsername", sqlUsername.text);
} else {
await prefs.remove("sqlUsername");
}
if (sqlPassword.text.isNotEmpty) {
await prefs.setString(
"sqlPassword", sqlPassword.text);
} else {
await prefs.remove("sqlPassword");
}
await prefs.setBool("sqlUseDevDB", useDevDB.value);
await prefs.setBool(
"uneditablePastEvents", uneditablePastEvents.value);
ref.refresh(userDataProvider);
changed.value = false;
}
: null,
icon: const Icon(Icons.save),
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
TextField(
controller: userName,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Username',
),
onChanged: (value) {
changed.value = true;
},
),
const SizedBox(height: 10),
TextField(
controller: password,
obscureText: true,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password',
),
onChanged: (value) {
changed.value = true;
},
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () async {
final result = await FilePicker.platform.pickFiles(
type: FileType.any,
allowMultiple: false,
dialogTitle: "Select a private key",
initialDirectory:
userHome != null ? join(userHome!, ".ssh") : null,
);
if (result != null) {
final file = result.files.first;
privateKeyFile.value = file.path;
changed.value = true;
}
},
child:
Text(privateKeyFile.value ?? "Select private key file"),
onLongPress: () async {
final remove = await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("Remove private key file?"),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context, false);
},
child: const Text("Cancel"),
),
TextButton(
onPressed: () {
Navigator.pop(context, true);
},
child: const Text("Remove"),
),
],
);
});
if (remove) {
privateKeyFile.value = null;
changed.value = true;
}
},
),
const SizedBox(height: 10),
TextField(
controller: sqlUsername,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'SQL Username',
),
onChanged: (value) {
changed.value = true;
},
),
const SizedBox(height: 10),
TextField(
controller: sqlPassword,
obscureText: true,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'SQL Password',
),
onChanged: (value) {
changed.value = true;
},
),
CheckboxListTile(
title: const Text("Use dev database"),
value: useDevDB.value,
onChanged: (value) {
useDevDB.value = value ?? false;
changed.value = true;
},
),
CheckboxListTile(
title: const Text("Disable Past Events uneditable"),
value: uneditablePastEvents.value,
onChanged: (value) {
uneditablePastEvents.value = value ?? false;
changed.value = true;
},
),
],
),
),
),
);
},
error: (err, stack) => ErrorComponent(err, stack),
loading: () => const Center(
child: CircularProgressIndicator(),
),
);
}
}

Datei anzeigen

@ -7,14 +7,14 @@ packages:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "49.0.0"
version: "50.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.0"
version: "5.2.0"
args:
dependency: transitive
description:
@ -28,7 +28,7 @@ packages:
name: asn1lib
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
version: "1.4.0"
async:
dependency: transitive
description:
@ -43,6 +43,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
buffer:
dependency: transitive
description:
name: buffer
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
build:
dependency: transitive
description:
@ -70,21 +77,21 @@ packages:
name: build_resolvers
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.10"
version: "2.1.0"
build_runner:
dependency: "direct dev"
description:
name: build_runner
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.2"
version: "2.3.3"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
url: "https://pub.dartlang.org"
source: hosted
version: "7.2.6"
version: "7.2.7"
built_collection:
dependency: transitive
description:
@ -98,7 +105,7 @@ packages:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "8.4.1"
version: "8.4.2"
characters:
dependency: transitive
description:
@ -203,7 +210,7 @@ packages:
name: file_picker
url: "https://pub.dartlang.org"
source: hosted
version: "5.2.2"
version: "5.2.4"
fixnum:
dependency: transitive
description:
@ -243,7 +250,7 @@ packages:
name: flutter_riverpod
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.1.1"
flutter_test:
dependency: "direct dev"
description: flutter
@ -260,7 +267,7 @@ packages:
name: freezed
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
version: "2.3.2"
freezed_annotation:
dependency: transitive
description:
@ -274,14 +281,14 @@ packages:
name: frontend_server_client
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
version: "3.2.0"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.1"
graphs:
dependency: transitive
description:
@ -295,7 +302,7 @@ packages:
name: hooks_riverpod
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.1.1"
http_multi_server:
dependency: transitive
description:
@ -337,7 +344,7 @@ packages:
name: json_serializable
url: "https://pub.dartlang.org"
source: hosted
version: "6.5.3"
version: "6.5.4"
lints:
dependency: transitive
description:
@ -379,7 +386,14 @@ packages:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
version: "1.0.3"
mysql_client:
dependency: "direct main"
description:
name: mysql_client
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.27"
package_config:
dependency: transitive
description:
@ -407,7 +421,7 @@ packages:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.20"
version: "2.0.22"
path_provider_ios:
dependency: transitive
description:
@ -498,7 +512,7 @@ packages:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
version: "2.1.3"
pubspec_parse:
dependency: transitive
description:
@ -512,14 +526,14 @@ packages:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
version: "3.2.1"
riverpod:
dependency: transitive
description:
name: riverpod
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.1.1"
shared_preferences:
dependency: "direct main"
description:
@ -589,7 +603,7 @@ packages:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
version: "1.0.3"
sky_engine:
dependency: transitive
description: flutter
@ -643,7 +657,7 @@ packages:
name: stream_transform
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
version: "2.1.0"
string_scanner:
dependency: transitive
description:
@ -672,6 +686,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
tuple:
dependency: transitive
description:
name: tuple
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
typed_data:
dependency: transitive
description:
@ -706,7 +727,7 @@ packages:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.1.3"
xdg_directories:
dependency: transitive
description:
@ -722,7 +743,7 @@ packages:
source: hosted
version: "3.4.0"
yaml:
dependency: transitive
dependency: "direct main"
description:
name: yaml
url: "https://pub.dartlang.org"

Datei anzeigen

@ -1,9 +1,11 @@
name: dev_server_starter
description: A new Flutter project.
authors:
- Chaoscaot
publish_to: 'none'
version: 1.0.0+1
version: 1.0.0
environment:
sdk: '>=2.18.1 <3.0.0'
@ -21,6 +23,8 @@ dependencies:
shared_preferences: ^2.0.15
file_picker: ^5.2.2
xterm: ^3.4.0
mysql_client: ^0.0.27
yaml: ^3.1.1
dev_dependencies: