From 9cef8feae23c13a76d91a53d492960db14d3b446 Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Fri, 23 Dec 2022 23:12:00 +0100 Subject: [PATCH] Rebranding --- .metadata | 18 +- lib/main.dart | 7 +- lib/src/app.dart | 6 +- lib/src/provider/events.dart | 277 ++++++++---------- lib/src/provider/http.dart | 28 ++ lib/src/provider/server.dart | 26 +- lib/src/provider/types.dart | 76 +++-- lib/src/provider/user.dart | 67 +---- lib/src/screens/components/event_dialog.dart | 10 +- lib/src/screens/components/events_list.dart | 22 +- lib/src/screens/components/server_list.dart | 2 +- lib/src/screens/console.dart | 17 +- lib/src/screens/event.dart | 232 +++++++-------- lib/src/screens/home.dart | 21 +- lib/src/screens/login.dart | 186 +++--------- lib/src/screens/search_delegates.dart | 18 +- lib/src/screens/userinfo.dart | 141 +-------- linux/my_application.cc | 4 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 - pubspec.lock | 94 +----- pubspec.yaml | 9 +- web/favicon.png | Bin 0 -> 917 bytes web/icons/Icon-192.png | Bin 0 -> 5292 bytes web/icons/Icon-512.png | Bin 0 -> 8252 bytes web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes web/index.html | 58 ++++ web/manifest.json | 35 +++ windows/runner/main.cpp | 2 +- 29 files changed, 521 insertions(+), 837 deletions(-) create mode 100644 lib/src/provider/http.dart create mode 100644 web/favicon.png create mode 100644 web/icons/Icon-192.png create mode 100644 web/icons/Icon-512.png create mode 100644 web/icons/Icon-maskable-192.png create mode 100644 web/icons/Icon-maskable-512.png create mode 100644 web/index.html create mode 100644 web/manifest.json diff --git a/.metadata b/.metadata index 541a1ac..32d8944 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled. version: - revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + revision: 135454af32477f815a7525073027a3ff9eff1bfd channel: stable project_type: app @@ -13,17 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - - platform: linux - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - - platform: macos - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - - platform: windows - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: web + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd # User provided section diff --git a/lib/main.dart b/lib/main.dart index a26592d..85e48a4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,7 @@ -import 'dart:io'; - -import 'package:dev_server_starter/src/app.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:steamwar_multitool/src/app.dart'; void main() { runApp(const ProviderScope(child: DevServerStarterApp())); } - -String? get userHome => - Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; diff --git a/lib/src/app.dart b/lib/src/app.dart index 2e4ea3f..3305fe7 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,8 +1,8 @@ -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'; +import 'screens/login.dart'; +import 'screens/userinfo.dart'; class DevServerStarterApp extends StatelessWidget { const DevServerStarterApp({Key? key}) : super(key: key); @@ -15,7 +15,7 @@ class DevServerStarterApp extends StatelessWidget { "/home": (context) => const HomeScreen(), "/settings": (context) => const SettingsScreen(), }, - title: 'Dev Server Starter', + title: 'Event-Tool', theme: ThemeData( useMaterial3: true, colorSchemeSeed: const Color(0xFFFFFF55), diff --git a/lib/src/provider/events.dart b/lib/src/provider/events.dart index 63c58bb..6484dac 100644 --- a/lib/src/provider/events.dart +++ b/lib/src/provider/events.dart @@ -1,56 +1,14 @@ -import 'dart:io'; import 'dart:ui'; -import 'package:dev_server_starter/src/provider/user.dart'; +import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:mysql_client/mysql_client.dart'; +import 'package:json_annotation/json_annotation.dart'; -final portProvider = Provider((ref) { - return 49507; -}); - -final portForwardProvider = FutureProvider((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>().pipe(socket); - socket.pipe(forward.sink); - }); - ref.onDispose(() async { - try { - await serverSocket.close(); - } catch (e) { - print(e); - } - }); - return serverSocket.port; -}); - -final mysqlClientProvider = FutureProvider((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]); +import 'http.dart'; final eventRepositoryProvider = FutureProvider((ref) async { - return EventRepository(await ref.watch(mysqlClientProvider.future)); -}, dependencies: [mysqlClientProvider]); + return EventRepository(await ref.read(httpClient.future)); +}, dependencies: [httpClient]); final eventsListProvider = FutureProvider>((ref) async { final repo = await ref.watch(eventRepositoryProvider.future); @@ -58,43 +16,18 @@ final eventsListProvider = FutureProvider>((ref) async { }, dependencies: [eventRepositoryProvider]); class EventRepository { - final MySQLConnection _connection; + final Dio _client; - final _statements = >{}; - - EventRepository(this._connection); - - Future getStatement(String sql) async { - return _statements.putIfAbsent( - sql.hashCode, () => _connection.prepare(sql)); - } + EventRepository(this._client); Future> 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("EventID")!, - DateTime.parse(e.colByName("Start")!), - DateTime.parse(e.colByName("End")!))) - .toList(); + final res = await _client.get("/events"); + return (res.data as List).map((e) => ShortEvent.fromJson(e)).toList(); } - Future 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> 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("TeamID")!, - e.colByName("TeamName")!, e.colByName("TeamColor")!)) - .toList(); + Future getEvent(int id) async { + final res = await _client.get("/events/$id"); + return EventExtended.fromJson(res.data); } Future updateEvent( @@ -107,76 +40,60 @@ class EventRepository { 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("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("MaximumTeamMembers")!, - schemType: e.colByName("SchemType"), - publicOnly: e.typedColByName("PublicSchemsOnly")!, - useSpectateSystem: e.typedColByName("SpectateSystem")!, - ); + await _client.put("/events/$id", data: { + "name": name, + "deadline": deadline.millisecondsSinceEpoch, + "start": start.millisecondsSinceEpoch, + "end": end.millisecondsSinceEpoch, + "maxTeamMembers": maxTeamMembers, + "schemType": schemType, + "publicSchemsOnly": publicOnly, + "spectateSystem": useSpectateSystem, + }); } Future 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; + final res = await _client.post("/events", data: { + "name": name, + "start": start.millisecondsSinceEpoch, + "end": end.millisecondsSinceEpoch, + }); + return Event.fromJson(res.data); } Future deleteEvent(int id) async { - await getStatement("DELETE FROM Event WHERE EventID = ?") - .then((value) => value.execute([id])); - } - - Future> 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(); + await _client.delete("/events/$id"); } Future deleteFight(int id) async { - await getStatement("DELETE FROM EventFight WHERE FightID = ?") - .then((value) => value.execute([id])); + await _client.delete("/fights/$id"); } Future 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])); + Team blue, Team red) { + return _client.post("/fights", data: { + "event": eventId, + "spielmodus": mode, + "map": map, + "start": start.millisecondsSinceEpoch, + "blueTeam": blue.id, + "redTeam": red.id, + }); + } + + Future> listFights(int eventId) async { + final res = await _client.get("/events/$eventId/fights"); + return (res.data as List).map((e) => EventFight.fromJson(e)).toList(); } Future updateFight( int id, DateTime start, String mode, String map, int referee) { - return getStatement( - "UPDATE `EventFight` SET `StartTime`=?,`Spielmodus`=?,`Map`=?,`Kampfleiter`=? WHERE `FightID` = ?") - .then((value) => - value.execute([start.toIso8601String(), mode, map, referee, id])); + return _client.put("/fights/$id", data: { + "spielmodus": mode, + "map": map, + "start": start.millisecondsSinceEpoch, + "kampfleiter": referee, + }); } } @@ -185,49 +102,47 @@ class EventFight { final String gameMode; final String map; final DateTime start; - final ShortTeam redTeam; - final ShortTeam blueTeam; + final Team redTeam; + final Team 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 { + Team get winner { switch (score) { case 1: return blueTeam; case 2: return redTeam; case 3: - return ShortTeam(-1, "Tie", "7"); + return Team(-1, "Tie", "TIE", "7"); default: - return ShortTeam(-1, "Unknown", "7"); + return Team(-1, "Unknown", "UNK", "7"); } } - factory EventFight.fromResult(ResultSetRow e) { + factory EventFight.fromJson(Map json) { return EventFight( - e.typedColByName("FightID")!, - e.colByName("Spielmodus")!, - e.colByName("Map")!, - DateTime.parse(e.colByName("StartTime")!), - ShortTeam(e.typedColByName("RedTeamID")!, - e.colByName("RedTeamName")!, e.colByName("RedTeamColor")!), - ShortTeam(e.typedColByName("BlueTeamID")!, - e.colByName("BlueTeamName")!, e.colByName("BlueTeamColor")!), - e.typedColByName("Kampfleiter")!, - e.typedColByName("Ergebnis")!, - ); + json["id"], + json["spielmodus"], + json["map"], + DateTime.fromMillisecondsSinceEpoch(json["start"]), + Team.fromJson(json["redTeam"]), + Team.fromJson(json["blueTeam"]), + json["kampfleiter"], + json["ergebnis"]); } } -class ShortTeam { +class Team { final int id; final String name; + final String kuerzel; final String colorCode; - ShortTeam(this.id, this.name, this.colorCode); + Team(this.id, this.name, this.kuerzel, this.colorCode); Color get color { switch (colorCode) { @@ -265,6 +180,33 @@ class ShortTeam { return const Color(0xFF000000); } } + + factory Team.fromJson(Map json) { + return Team( + json['id'] as int, + json['name'] as String, + json['kuerzel'] as String, + json['color'] as String, + ); + } +} + +class EventExtended { + final Event event; + final List fights; + final List teams; + + EventExtended(this.event, this.fights, this.teams); + + factory EventExtended.fromJson(Map json) { + return EventExtended( + Event.fromJson(json['event']), + (json['fights'] as List) + .map((e) => EventFight.fromJson(e)) + .toList(), + (json['teams'] as List).map((e) => Team.fromJson(e)).toList(), + ); + } } class Event { @@ -275,8 +217,8 @@ class Event { final DateTime end; final int maxTeamMembers; final String? schemType; - final bool publicOnly; - final bool useSpectateSystem; + final bool publicSchemsOnly; + final bool spectateSystem; Event({ required this.id, @@ -286,11 +228,26 @@ class Event { required this.end, required this.maxTeamMembers, required this.schemType, - required this.publicOnly, - required this.useSpectateSystem, + required this.publicSchemsOnly, + required this.spectateSystem, }); + + factory Event.fromJson(Map json) { + return Event( + id: json['id'], + name: json['name'], + deadline: DateTime.fromMillisecondsSinceEpoch(json['deadline']), + start: DateTime.fromMillisecondsSinceEpoch(json['start']), + end: DateTime.fromMillisecondsSinceEpoch(json['end']), + maxTeamMembers: json['maxTeamMembers'], + schemType: json['schemType'], + publicSchemsOnly: json['publicSchemsOnly'], + spectateSystem: json['spectateSystem'], + ); + } } +@JsonSerializable() class ShortEvent { final String name; final int id; @@ -300,4 +257,12 @@ class ShortEvent { ShortEvent(this.name, this.id, this.start, this.end); get isUpcoming => start.isAfter(DateTime.now()); + + factory ShortEvent.fromJson(Map json) { + return ShortEvent( + json["name"], + json["id"], + DateTime.fromMillisecondsSinceEpoch(json["start"]), + DateTime.fromMillisecondsSinceEpoch(json["end"])); + } } diff --git a/lib/src/provider/http.dart b/lib/src/provider/http.dart new file mode 100644 index 0000000..4d8ead0 --- /dev/null +++ b/lib/src/provider/http.dart @@ -0,0 +1,28 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:steamwar_multitool/src/provider/user.dart'; + +final serverUrlProvider = Provider((ref) { + if (kDebugMode) { + return "http://localhost:8000"; + } else { + return "https://steamwar.de"; + } +}); + +final httpClient = FutureProvider( + (ref) async => Dio(BaseOptions( + baseUrl: ref.watch(serverUrlProvider), + headers: { + "X-SW-Auth": + await ref.watch(userDataProvider.future).then((value) => value.key), + }, + contentType: "application/json", + )), +); + +final test = FutureProvider((ref) async { + final client = await ref.watch(httpClient.future); + await client.get("/data"); +}); diff --git a/lib/src/provider/server.dart b/lib/src/provider/server.dart index 4af64cc..c0e20ac 100644 --- a/lib/src/provider/server.dart +++ b/lib/src/provider/server.dart @@ -1,27 +1,19 @@ 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'; +import '../screens/console.dart'; final serversProvider = FutureProvider>((ref) async { - final sftp = await ref.watch(sftpProvider.future); - final ret = (await sftp.readdir("/servers").first) - .map((e) => e.filename) - .where((element) => element != "." && element != "..") - .toList(); - ret.sort(); - return ret; -}); - -final sftpProvider = FutureProvider((ref) async { - final client = await ref.watch(sshClientProvider.future); - final sftp = await client.sftp(); - ref.onDispose(sftp.close); - return sftp; + return []; + //final sftp = await ref.watch(sftpProvider.future); + //final ret = (await sftp.readdir("/servers").first) + // .map((e) => e.filename) + // .where((element) => element != "." && element != "..") + // .toList(); + //ret.sort(); + //return ret; }); final favoriteServersProvider = FutureProvider((ref) async { diff --git a/lib/src/provider/types.dart b/lib/src/provider/types.dart index f7ecd9c..53ffcec 100644 --- a/lib/src/provider/types.dart +++ b/lib/src/provider/types.dart @@ -1,56 +1,37 @@ -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'; + +import 'http.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 client = await ref.watch(httpClient.future); + final res = await client.get("/data/gamemodes"); + return (res.data as List).map((e) => e.toString()).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(); + return await ref.watch(httpClient.future).then((client) async { + final res = await client.get("/data/schematicTypes"); + return (res.data as List) + .map((e) => SchematicType.fromJson(e)) + .toList(); + }); }); final mapsProvider = FutureProvider>>((ref) async { - final client = await ref.watch(sshClientProvider.future); - final gamemodes = await ref.read(fightServersProvider.future); - final maps = >{}; - 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(); - } + final client = await ref.watch(httpClient.future); + final servers = await ref.watch(fightServersProvider.future); + final ret = >{}; + for (final server in servers) { + final res = await client.get("/data/gamemodes/$server/maps"); + ret[server] = (res.data as List).map((e) => e.toString()).toList(); } - return maps; + return ret; }); 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("id")!, e.colByName("UserName")!)) - .toList(); + final client = await ref.watch(httpClient.future); + final res = await client.get("/data/users"); + return (res.data as List).map((e) => User.fromJson(e)).toList(); }); class User { @@ -58,4 +39,19 @@ class User { final String name; User(this.id, this.name); + + factory User.fromJson(Map json) { + return User(json["id"] as int, json["name"] as String); + } +} + +class SchematicType { + final String name; + final String db; + + SchematicType(this.name, this.db); + + factory SchematicType.fromJson(Map json) { + return SchematicType(json["name"], json["db"]); + } } diff --git a/lib/src/provider/user.dart b/lib/src/provider/user.dart index 44bb20a..fc001fa 100644 --- a/lib/src/provider/user.dart +++ b/lib/src/provider/user.dart @@ -1,82 +1,23 @@ -import 'dart:io'; - -import 'package:dartssh2/dartssh2.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; -final clientFunctionProvider = - FutureProvider((ref) async { - final userData = await ref.watch(userDataProvider.future); - switch (userData.method) { - case 0: - return (socket) => SSHClient( - socket, - username: userData.username, - onPasswordRequest: () => userData.password, - ); - case 1: - return (socket) => SSHClient( - socket, - username: userData.username, - identities: [ - ...SSHKeyPair.fromPem( - File(userData.privateKeyFile!).readAsStringSync()), - ], - ); - default: - throw Exception("Invalid method"); - } -}, dependencies: [userDataProvider]); - final userDataProvider = FutureProvider((ref) async { final prefs = await SharedPreferences.getInstance(); - final username = prefs.getString("username")!; - 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 key = prefs.getString("key")!; final uneditablePastEvents = prefs.getBool("uneditablePastEvents") ?? false; return UserData( - username: username, - method: method, - password: password, - privateKeyFile: privateKey, - sqlPassword: sqlPassword, - sqlUserName: sqlUsername, - useDevDB: useDevDB, + key: key, uneditablePastEvents: uneditablePastEvents, ); }); class UserData { - final String username; - final String? password; - final String? privateKeyFile; - final int method; - - final String? sqlUserName; - final String? sqlPassword; - final bool useDevDB; + final String key; final bool uneditablePastEvents; UserData({ - required this.username, - required this.password, - required this.privateKeyFile, - required this.method, - required this.sqlUserName, - required this.sqlPassword, - required this.useDevDB, + required this.key, required this.uneditablePastEvents, }); } - -final sshClientProvider = FutureProvider((ref) async { - final clientFunction = await ref.watch(clientFunctionProvider.future); - final client = clientFunction(await SSHSocket.connect("steamwar.de", 22)); - await client.authenticated; - return client; -}, dependencies: [clientFunctionProvider]); diff --git a/lib/src/screens/components/event_dialog.dart b/lib/src/screens/components/event_dialog.dart index b53252e..514cc09 100644 --- a/lib/src/screens/components/event_dialog.dart +++ b/lib/src/screens/components/event_dialog.dart @@ -1,9 +1,10 @@ -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'; +import '../../provider/events.dart'; +import '../event.dart'; + class EventDialog extends HookConsumerWidget { const EventDialog({Key? key}) : super(key: key); @@ -68,11 +69,14 @@ class EventDialog extends HookConsumerWidget { final event = await ref.read(eventRepositoryProvider.future).then( (value) => value.createEvent( eventName.text, startTime.value, endTime.value)); + final eventFull = await ref + .read(eventRepositoryProvider.future) + .then((value) => value.getEvent(event.id)); nav.pop(); nav.push( MaterialPageRoute( builder: (context) { - return EditEventScreen(true, event); + return EditEventScreen(true, eventFull); }, ), ); diff --git a/lib/src/screens/components/events_list.dart b/lib/src/screens/components/events_list.dart index 45de4c0..67dfc76 100644 --- a/lib/src/screens/components/events_list.dart +++ b/lib/src/screens/components/events_list.dart @@ -1,10 +1,10 @@ -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 '../../provider/events.dart'; +import '../../provider/user.dart'; +import '../event.dart'; +import 'error.dart'; import 'event_dialog.dart'; class EventListComponent extends HookConsumerWidget { @@ -84,20 +84,6 @@ class EventListComponent extends HookConsumerWidget { ], ); }, 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( diff --git a/lib/src/screens/components/server_list.dart b/lib/src/screens/components/server_list.dart index 41f77a6..e5363a7 100644 --- a/lib/src/screens/components/server_list.dart +++ b/lib/src/screens/components/server_list.dart @@ -1,6 +1,5 @@ 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'; @@ -9,6 +8,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../../provider/server.dart'; import '../console.dart'; +import 'error.dart'; class ServerListComponent extends HookConsumerWidget { const ServerListComponent({Key? key}) : super(key: key); diff --git a/lib/src/screens/console.dart b/lib/src/screens/console.dart index 947e83d..5415973 100644 --- a/lib/src/screens/console.dart +++ b/lib/src/screens/console.dart @@ -1,7 +1,6 @@ import 'dart:typed_data'; -import 'package:dartssh2/dartssh2.dart'; -import 'package:dev_server_starter/src/provider/user.dart'; +//import 'package:dartssh2/dartssh2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -19,7 +18,7 @@ class ConsoleScreen extends StatefulHookConsumerWidget { } class _ConsoleScreenState extends ConsumerState { - late final SSHSession session; + //late final SSHSession session; late final Terminal terminal; final _controller = ScrollController(keepScrollOffset: true); @@ -56,8 +55,8 @@ class _ConsoleScreenState extends ConsumerState { inputFieldController.text = ""; terminal.write("> $value"); terminal.nextLine(); - session.write( - Uint8List.fromList(value.codeUnits + '\n'.codeUnits)); + //session.write( + // Uint8List.fromList(value.codeUnits + '\n'.codeUnits)); inputFieldFocusNode.requestFocus(); }, decoration: const InputDecoration( @@ -81,8 +80,8 @@ class _ConsoleScreenState extends ConsumerState { @override void deactivate() { super.deactivate(); - session.kill(SSHSignal.KILL); - session.close(); + //session.kill(SSHSignal.KILL); + //session.close(); } @override @@ -90,7 +89,7 @@ class _ConsoleScreenState extends ConsumerState { super.initState(); terminal = Terminal(); terminal.setLineFeedMode(true); - final clientFut = ref.read(sshClientProvider); + /*final clientFut = ref.read(sshClientProvider); final client = clientFut.value!; client .execute( @@ -111,7 +110,7 @@ class _ConsoleScreenState extends ConsumerState { } } }); - }); + });*/ } } diff --git a/lib/src/screens/event.dart b/lib/src/screens/event.dart index 7d86cd8..526c336 100644 --- a/lib/src/screens/event.dart +++ b/lib/src/screens/event.dart @@ -1,23 +1,30 @@ 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 '../provider/events.dart'; +import '../provider/types.dart'; import 'search_delegates.dart'; +T? catchToNull(T Function() f) { + try { + return f(); + } catch (e) { + return null; + } +} + class EditEventScreen extends HookConsumerWidget { final bool editable; - final Event event; - const EditEventScreen(this.editable, this.event, {Key? key}) + final EventExtended eventData; + const EditEventScreen(this.editable, this.eventData, {Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { + final event = eventData.event; final nameController = useTextEditingController(text: event.name); final deadlineState = useState(event.deadline); final startDateState = useState(event.start); @@ -26,18 +33,15 @@ class EditEventScreen extends HookConsumerWidget { 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 schematicTypeState = useState(catchToNull(() => ref + .watch(schematicTypesProvider) + .value! + .firstWhere((element) => element.db == event.schemType))); + final publicOnlyState = useState(event.publicSchemsOnly); + final spectateSystemState = useState(event.spectateSystem); 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}"), @@ -85,7 +89,7 @@ class EditEventScreen extends HookConsumerWidget { startDateState.value, endDateState.value, maxTeamMembersState.value, - schematicTypeState.value, + schematicTypeState.value?.db, publicOnlyState.value, spectateSystemState.value, )) @@ -223,7 +227,7 @@ class EditEventScreen extends HookConsumerWidget { final out = await showSearch( context: context, delegate: SchematicTypeSearchDelegate(types)); - if (out == "reset") { + if (out == RESET_TYPE) { schematicTypeState.value = null; changed.value = true; } else { @@ -233,7 +237,7 @@ class EditEventScreen extends HookConsumerWidget { } } : null, - child: Text(schematicTypeState.value ?? "Select")), + child: Text(schematicTypeState.value?.name ?? "Select")), ], ), const SizedBox(height: 8), @@ -258,36 +262,27 @@ class EditEventScreen extends HookConsumerWidget { ), const SizedBox(height: 8), Text("Teams", style: Theme.of(context).textTheme.headline6), - FutureBuilder>( - 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(); - } - }, + Wrap( + children: [ + for (final team in eventData.teams) + Padding( + padding: const EdgeInsets.all(8.0), + child: Chip( + label: Text(team.name, + style: TextStyle( + color: team.color.computeLuminance() > 0.5 + ? Colors.black + : Colors.white)), + backgroundColor: team.color, + ), + ) + ], ), const SizedBox(height: 8), Text("Fights", style: Theme.of(context).textTheme.headline6), - _EventFightList(event: event), + _EventFightList( + eventData: eventData, + ), ], ), ), @@ -296,68 +291,58 @@ class EditEventScreen extends HookConsumerWidget { } class _EventFightList extends HookConsumerWidget { - final Event event; + final EventExtended eventData; - const _EventFightList({Key? key, required this.event}) : super(key: key); + const _EventFightList({Key? key, required this.eventData}) : 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]); + final event = eventData.event; + final fights = useState>(eventData.fights); - return FutureBuilder>( - 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, - ) - ], + void update() async { + final repo = await ref.read(eventRepositoryProvider.future); + fights.value = await repo.listFights(event.id); + } + + return ListView( + shrinkWrap: true, + children: [ + const SizedBox(height: 8), + FloatingActionButton.extended( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return _AddFightWidget( + eventData, + () => update(), + ); + }, ); - } else { - return const LinearProgressIndicator(); - } - }); + }, + label: const Text("Add Fight"), + icon: const Icon(Icons.add), + ), + const SizedBox(height: 8), + for (final fight in fights.value) + 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: () => update()); + }); + }, + tileColor: fight.score != 0 ? fight.winner.color : null, + ) + ], + ); } } @@ -369,7 +354,7 @@ class EditEventFightDialog extends HookConsumerWidget { }) : super(key: key); final EventFight fight; - final ValueNotifier fightsRefresher; + final void Function() fightsRefresher; @override Widget build(BuildContext context, WidgetRef ref) { @@ -421,7 +406,7 @@ class EditEventFightDialog extends HookConsumerWidget { delegate: MapSearchDelegate(maps[mode.value] ?? [])); if (out != null) { map.value = out; - fightsRefresher.value++; + fightsRefresher(); } }, child: Text(map.value), @@ -436,7 +421,7 @@ class EditEventFightDialog extends HookConsumerWidget { context: context, delegate: UserSearchDelegate(users)); if (user != null) { referrer.value = user.id; - fightsRefresher.value++; + fightsRefresher(); } }, child: @@ -451,7 +436,7 @@ class EditEventFightDialog extends HookConsumerWidget { ref.read(eventRepositoryProvider.future).then( (value) => value.deleteFight(fight.id).then( (value) { - fightsRefresher.value++; + fightsRefresher(); }, ), ); @@ -468,7 +453,7 @@ class EditEventFightDialog extends HookConsumerWidget { final repo = await ref.read(eventRepositoryProvider.future); await repo.updateFight( fight.id, start.value, mode.value, map.value, referrer.value); - fightsRefresher.value++; + fightsRefresher(); nav.pop(); }, child: const Text("Save")), @@ -479,19 +464,20 @@ class EditEventFightDialog extends HookConsumerWidget { class _AddFightWidget extends HookConsumerWidget { final void Function() onAdded; - final Event event; + final EventExtended eventData; const _AddFightWidget( - this.event, + this.eventData, this.onAdded, { Key? key, }) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { + final event = eventData.event; final gamemode = useState(null); final map = useState(null); - final blueTeam = useState(null); - final redTeam = useState(null); + final blueTeam = useState(null); + final redTeam = useState(null); final date = useState(event.start); final canCreate = useMemoized(() { @@ -538,10 +524,12 @@ class _AddFightWidget extends HookConsumerWidget { ), const SizedBox(height: 8), const Text("Blue Team"), - _TeamSelector(event, blueTeam.value, (p0) => blueTeam.value = p0), + _TeamSelector(event, blueTeam.value, (p0) => blueTeam.value = p0, + eventData.teams), const SizedBox(height: 8), const Text("Red Team"), - _TeamSelector(event, redTeam.value, (p0) => redTeam.value = p0), + _TeamSelector(event, redTeam.value, (p0) => redTeam.value = p0, + eventData.teams), const SizedBox(height: 8), DateTimeEditor((p0) => date.value = p0, date.value, true), ], @@ -576,12 +564,15 @@ class _AddFightWidget extends HookConsumerWidget { class _TeamSelector extends HookConsumerWidget { final Event event; - final ShortTeam? selectedTeam; - final void Function(ShortTeam) onSelected; + final Team? selectedTeam; + final void Function(Team) onSelected; + final List teams; + const _TeamSelector( this.event, this.selectedTeam, - this.onSelected, { + this.onSelected, + this.teams, { Key? key, }) : super(key: key); @@ -589,9 +580,6 @@ class _TeamSelector extends HookConsumerWidget { 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) { @@ -635,6 +623,14 @@ class DateTimeEditor extends HookConsumerWidget { onPressed: enabled ? () async { final time = await showTimePicker( + builder: (context, child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + alwaysUse24HourFormat: true, + ), + child: child!, + ); + }, context: context, initialTime: TimeOfDay( hour: initialDate.hour, minute: initialDate.minute), diff --git a/lib/src/screens/home.dart b/lib/src/screens/home.dart index 565d6a6..e357442 100644 --- a/lib/src/screens/home.dart +++ b/lib/src/screens/home.dart @@ -1,14 +1,12 @@ -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'; +import '../provider/events.dart'; import '../provider/server.dart'; import '../provider/user.dart'; +import 'components/events_list.dart'; class HomeScreen extends HookConsumerWidget { const HomeScreen({Key? key}) : super(key: key); @@ -21,12 +19,12 @@ class HomeScreen extends HookConsumerWidget { return Scaffold( appBar: AppBar( title: const Text('Home'), + automaticallyImplyLeading: false, actions: [ IconButton( onPressed: servers.isLoading ? null : () { - ref.refresh(serversProvider); ref.refresh(eventsListProvider); }, icon: const Icon(Icons.refresh), @@ -48,7 +46,7 @@ class HomeScreen extends HookConsumerWidget { onPressed: () async { final nav = Navigator.of(context); final prefs = await SharedPreferences.getInstance(); - await prefs.remove("username"); + await prefs.remove("key"); ref.refresh(userDataProvider); nav.pop(); nav.pushReplacementNamed("/"); @@ -68,14 +66,14 @@ class HomeScreen extends HookConsumerWidget { NavigationRail( elevation: 1, destinations: const [ - NavigationRailDestination( - icon: Icon(Icons.dns), - label: Text('Server'), - ), NavigationRailDestination( icon: Icon(Icons.calendar_today), label: Text('Events'), ), + NavigationRailDestination( + icon: Icon(Icons.dns), + label: Text('Server'), + ), ], labelType: NavigationRailLabelType.selected, trailing: IconButton( @@ -93,8 +91,9 @@ class HomeScreen extends HookConsumerWidget { child: IndexedStack( index: navRailIndex.value, children: const [ - ServerListComponent(), + //ServerListComponent(), EventListComponent(), + Placeholder(), ], ), ), diff --git a/lib/src/screens/login.dart b/lib/src/screens/login.dart index 5c5c5b9..b9b4ad9 100644 --- a/lib/src/screens/login.dart +++ b/lib/src/screens/login.dart @@ -1,14 +1,9 @@ -import 'dart:io'; - -import 'package:dev_server_starter/src/provider/user.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'; +import '../provider/user.dart'; class LoginScreenWidget extends HookConsumerWidget { const LoginScreenWidget({ @@ -17,145 +12,60 @@ class LoginScreenWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final clientFunction = ref.watch(clientFunctionProvider); + final keyController = useTextEditingController(); + final updater = useState(0); - useEffect(() { - clientFunction.whenData((clientFunction) { - Navigator.of(context).pushReplacementNamed("/home"); - }); - return null; - }, []); + ref.read(userDataProvider).when( + data: (data) { + keyController.text = data.key; + updater.value++; + }, + loading: () {}, + error: (error, stack) {}, + ); - return clientFunction.when(data: (data) { - return Scaffold( - appBar: AppBar( - title: const Text('Login'), - ), - body: Center( + return Scaffold( + appBar: AppBar( + title: const Text('Login'), + automaticallyImplyLeading: false, + ), + body: Center( + child: SizedBox( + width: 200, child: Column( - crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text("Redirecting..."), - ElevatedButton( - onPressed: () { - Navigator.of(context).popAndPushNamed("/home"); - }, - child: const Text("Go to home")) + Text( + 'Login', + style: Theme.of(context).textTheme.headline4, + ), + TextField( + decoration: const InputDecoration( + hintText: 'Key', + ), + controller: keyController, + obscureText: true, + onChanged: (v) => updater.value++, + ), + const SizedBox( + height: 10, + ), + FloatingActionButton.large( + onPressed: keyController.text.isNotEmpty + ? () async { + final nav = Navigator.of(context); + final prefs = await SharedPreferences.getInstance(); + await prefs.setString("key", keyController.text); + ref.invalidate(userDataProvider); + nav.pushReplacementNamed("/home"); + } + : null, + child: const Icon(Icons.forward), + ) ], ), ), - ); - }, error: (error, stack) { - final _userNameField = useTextEditingController(); - final _passwordField = useTextEditingController(); - final _privateKeyField = useState(null); - - final canProceed = useState(false); - - void _checkCanProceed(String _) { - canProceed.value = canProceed.value = _userNameField.text.isNotEmpty && - (_passwordField.text.isNotEmpty || _privateKeyField.value != null); - } - - return Scaffold( - appBar: AppBar( - title: const Text('Login'), - ), - body: Center( - child: SizedBox( - width: 200, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Login', - style: Theme.of(context).textTheme.headline4, - ), - TextField( - decoration: const InputDecoration( - hintText: 'Username', - ), - controller: _userNameField, - onChanged: _checkCanProceed, - ), - const SizedBox( - height: 10, - ), - TextField( - decoration: const InputDecoration( - hintText: 'Password', - ), - obscureText: true, - controller: _passwordField, - onChanged: _checkCanProceed, - ), - const SizedBox( - height: 10, - ), - const Text("Or"), - 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; - _privateKeyField.value = file.path; - _checkCanProceed(""); - } - }, - child: const Text('Select Private Key'), - ), - if (_privateKeyField.value != null) - Text(_privateKeyField.value!), - const SizedBox( - height: 10, - ), - FloatingActionButton.large( - onPressed: canProceed.value - ? () async { - final nav = Navigator.of(context); - final prefs = await SharedPreferences.getInstance(); - await prefs.setString( - "username", _userNameField.text); - if (_privateKeyField.value != null) { - await prefs.setInt("method", 1); - await prefs.setString( - "privateKeyFile", _privateKeyField.value!); - } else { - await prefs.setInt("method", 0); - } - await prefs.setString( - "password", _passwordField.text); - ref.refresh(userDataProvider); - nav.pushReplacementNamed("/home"); - } - : null, - child: const Icon(Icons.forward), - ) - ], - ), - ), - ), - ); - }, loading: () { - return Scaffold( - appBar: AppBar( - title: const Text('Login'), - ), - body: const Center( - child: CircularProgressIndicator(), - ), - ); - }); + ), + ); } } diff --git a/lib/src/screens/search_delegates.dart b/lib/src/screens/search_delegates.dart index 86e1a04..68628d8 100644 --- a/lib/src/screens/search_delegates.dart +++ b/lib/src/screens/search_delegates.dart @@ -1,10 +1,12 @@ -import 'package:dev_server_starter/src/provider/types.dart'; import 'package:flutter/material.dart'; import '../provider/events.dart'; +import '../provider/types.dart'; -class SchematicTypeSearchDelegate extends SearchDelegate { - final List types; +final RESET_TYPE = SchematicType("RESET", "RESET"); + +class SchematicTypeSearchDelegate extends SearchDelegate { + final List types; SchematicTypeSearchDelegate(this.types); @@ -42,7 +44,7 @@ class SchematicTypeSearchDelegate extends SearchDelegate { Widget _buildList(BuildContext context) { final out = types - .where((element) => element.toLowerCase().contains(query.toLowerCase())) + .where((element) => element.name.contains(query.toLowerCase())) .toList(); return ListView.builder( itemCount: out.length + 1, @@ -52,7 +54,7 @@ class SchematicTypeSearchDelegate extends SearchDelegate { title: const Text("Reset"), leading: const Icon(Icons.clear), onTap: () { - close(context, "reset"); + close(context, RESET_TYPE); }, ); } @@ -60,7 +62,7 @@ class SchematicTypeSearchDelegate extends SearchDelegate { final type = out[index - 1]; return ListTile( - title: Text(type), + title: Text(type.name), onTap: () { close(context, type); }, @@ -244,8 +246,8 @@ class UserSearchDelegate extends SearchDelegate { } } -class TeamSearchDelegate extends SearchDelegate { - final List teams; +class TeamSearchDelegate extends SearchDelegate { + final List teams; TeamSearchDelegate(this.teams); diff --git a/lib/src/screens/userinfo.dart b/lib/src/screens/userinfo.dart index dd8a293..80332be 100644 --- a/lib/src/screens/userinfo.dart +++ b/lib/src/screens/userinfo.dart @@ -1,13 +1,10 @@ -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'; +import '../provider/user.dart'; +import 'components/error.dart'; class SettingsScreen extends HookConsumerWidget { const SettingsScreen({Key? key}) : super(key: key); @@ -18,14 +15,7 @@ class SettingsScreen extends HookConsumerWidget { 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 keyController = useTextEditingController(text: data.key); final uneditablePastEvents = useState(data.uneditablePastEvents); final changed = useState(false); @@ -37,43 +27,16 @@ class SettingsScreen extends HookConsumerWidget { IconButton( onPressed: changed.value ? () async { - if (userName.text.isEmpty || - (privateKeyFile.value == null && - password.text.isEmpty)) { + if (keyController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text( - "Username and password or private key are required"), + content: Text("The Key must be Set"), ), ); 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.setString("key", keyController.text); await prefs.setBool( "uneditablePastEvents", uneditablePastEvents.value); ref.refresh(userDataProvider); @@ -91,105 +54,17 @@ class SettingsScreen extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ TextField( - controller: userName, + controller: keyController, decoration: const InputDecoration( border: OutlineInputBorder(), - labelText: 'Username', + labelText: 'Key', ), 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, diff --git a/linux/my_application.cc b/linux/my_application.cc index 6edd267..32bf3b3 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -40,11 +40,11 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "SteamWar Dev Server"); + gtk_header_bar_set_title(header_bar, "SteamWar Multitool"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "SteamWar Dev Server"); + gtk_window_set_title(window, "SteamWar Multitool"); } gtk_window_set_default_size(window, 1280, 720); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 14e6964..287b6a9 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,8 @@ import FlutterMacOS import Foundation -import path_provider_macos import shared_preferences_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 61a7989..1fece27 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -22,13 +22,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.3.1" - asn1lib: - dependency: transitive - description: - name: asn1lib - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.0" async: dependency: transitive description: @@ -43,13 +36,6 @@ 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: @@ -169,13 +155,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.4" - dartssh2: + dio: dependency: "direct main" description: - name: dartssh2 + name: dio url: "https://pub.dartlang.org" source: hosted - version: "2.7.3" + version: "4.0.6" equatable: dependency: transitive description: @@ -204,13 +190,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.4" - file_picker: - dependency: "direct main" - description: - name: file_picker - url: "https://pub.dartlang.org" - source: hosted - version: "5.2.4" fixnum: dependency: transitive description: @@ -237,13 +216,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.7" flutter_riverpod: dependency: transitive description: @@ -387,13 +359,6 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: @@ -402,33 +367,12 @@ packages: source: hosted version: "2.1.0" path: - dependency: "direct main" + dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted version: "1.8.2" - path_provider: - dependency: "direct main" - description: - name: path_provider - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.11" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.22" - path_provider_ios: - dependency: transitive - description: - name: path_provider_ios - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.11" path_provider_linux: dependency: transitive description: @@ -436,13 +380,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.7" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.6" path_provider_platform_interface: dependency: transitive description: @@ -457,13 +394,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" - pinenacl: - dependency: transitive - description: - name: pinenacl - url: "https://pub.dartlang.org" - source: hosted - version: "0.5.1" platform: dependency: transitive description: @@ -485,13 +415,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" - pointycastle: - dependency: transitive - description: - name: pointycastle - url: "https://pub.dartlang.org" - source: hosted - version: "3.6.2" pool: dependency: transitive description: @@ -686,13 +609,6 @@ 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: @@ -743,7 +659,7 @@ packages: source: hosted version: "3.4.0" yaml: - dependency: "direct main" + dependency: transitive description: name: yaml url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index f7137e4..fa1cdb7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,4 +1,4 @@ -name: dev_server_starter +name: steamwar_multitool description: A new Flutter project. authors: - Chaoscaot @@ -14,17 +14,12 @@ dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 - dartssh2: ^2.7.3 flutter_hooks: ^0.18.5+1 hooks_riverpod: ^2.0.2 json_annotation: '>=4.7.0 <4.8.0' - path: ^1.8.2 - path_provider: ^2.0.11 shared_preferences: ^2.0.15 - file_picker: ^5.2.2 xterm: ^3.4.0 - mysql_client: ^0.0.27 - yaml: ^3.1.1 + dio: ^4.0.6 dev_dependencies: diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 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 MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..f03efee --- /dev/null +++ b/web/index.html @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + Event-Tool + + + + + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..9587234 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "steamwar_multitool", + "short_name": "steamwar_multitool", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 39f2b96..b7c3582 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.CreateAndShow(L"SteamWar Dev Server", origin, size)) { + if (!window.CreateAndShow(L"SteamWar Multitool", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true);