Some™ Changes
Dieser Commit ist enthalten in:
Ursprung
be8295747f
Commit
907eda9d63
@ -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'];
|
||||
|
@ -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
303
lib/src/provider/events.dart
Normale Datei
@ -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());
|
||||
}
|
@ -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
61
lib/src/provider/types.dart
Normale Datei
@ -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);
|
||||
}
|
@ -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]);
|
||||
|
23
lib/src/screens/components/error.dart
Normale Datei
23
lib/src/screens/components/error.dart
Normale Datei
@ -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()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
85
lib/src/screens/components/event_dialog.dart
Normale Datei
85
lib/src/screens/components/event_dialog.dart
Normale Datei
@ -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"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
108
lib/src/screens/components/events_list.dart
Normale Datei
108
lib/src/screens/components/events_list.dart
Normale Datei
@ -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(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
290
lib/src/screens/components/server_list.dart
Normale Datei
290
lib/src/screens/components/server_list.dart
Normale Datei
@ -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"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
25
lib/src/screens/console.g.dart
Normale Datei
25
lib/src/screens/console.g.dart
Normale Datei
@ -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
657
lib/src/screens/event.dart
Normale Datei
@ -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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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'];
|
||||
|
305
lib/src/screens/search_delegates.dart
Normale Datei
305
lib/src/screens/search_delegates.dart
Normale Datei
@ -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
213
lib/src/screens/userinfo.dart
Normale Datei
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
67
pubspec.lock
67
pubspec.lock
@ -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"
|
||||
|
@ -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:
|
||||
|
In neuem Issue referenzieren
Einen Benutzer sperren