From 7a51e717675d26d0c9b217749b90a9ea0a17f6b6 Mon Sep 17 00:00:00 2001 From: Marco Date: Sat, 23 Dec 2023 16:44:23 +0100 Subject: [PATCH] Make passphrase entry a dialog instead of a page. Additionally, we set some groundwork for storing the game data (lobby id, player id, passphrase) in permanent storage in order to reconnect with it later. --- .gitignore | 3 + lib/api/register.dart | 37 ++++ lib/pages/chess_game.dart | 14 ++ lib/pages/host_game.dart | 3 + lib/pages/join_game.dart | 118 ------------- lib/pages/lobby_selector.dart | 158 ++++++++++++++++-- lib/utils/chess_router.dart | 7 - macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 134 ++++++++++++++- pubspec.yaml | 1 + 10 files changed, 337 insertions(+), 140 deletions(-) delete mode 100644 lib/pages/join_game.dart diff --git a/.gitignore b/.gitignore index 24476c5..576b80c 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Custom ignores +deploy-web.sh diff --git a/lib/api/register.dart b/lib/api/register.dart index 95b6bfa..c3f30fd 100644 --- a/lib/api/register.dart +++ b/lib/api/register.dart @@ -1,3 +1,4 @@ +import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; class PlayerInfo { @@ -25,6 +26,42 @@ class PlayerInfo { 'lobbyID': lobbyID, 'passphrase': passphrase, }; + + void store() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + + await prefs.setBool("contains", true); + await prefs.setString("playerID", playerID.toString()); + await prefs.setString("lobbyID", lobbyID.toString()); + await prefs.setString("passphrase", passphrase.toString()); + } + + void delete() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + + await prefs.setBool("contains", false); + } + + Future get() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + var contains = prefs.getBool("contains"); + var playerID = prefs.getString("playerID"); + var lobbyID = prefs.getString("lobbyID"); + var passphrase = prefs.getString("passphrase"); + + if (contains == null || + !contains || + playerID == null || + lobbyID == null || + passphrase == null) { + return null; + } + + return PlayerInfo( + playerID: UuidValue.fromString(playerID), + lobbyID: UuidValue.fromString(lobbyID), + passphrase: passphrase); + } } class WebsocketMessageIdentifyPlayer { diff --git a/lib/pages/chess_game.dart b/lib/pages/chess_game.dart index 29b352a..55971d6 100644 --- a/lib/pages/chess_game.dart +++ b/lib/pages/chess_game.dart @@ -85,4 +85,18 @@ class ChessGameArguments { required this.playerID, required this.passphrase, }); + + bool isValid() { + try { + lobbyID.validate(); + playerID.validate(); + } catch (e) { + return false; + } + + if (passphrase == null) return false; + if (passphrase!.isEmpty) return false; + + return true; + } } diff --git a/lib/pages/host_game.dart b/lib/pages/host_game.dart index bdc7245..be2f302 100644 --- a/lib/pages/host_game.dart +++ b/lib/pages/host_game.dart @@ -26,6 +26,9 @@ class _HostGameWidgetState extends State { @override void initState() { registerResponse = hostPrivateGame(); + registerResponse.then((value) { + value?.store(); + }); connectToWebsocket(registerResponse); super.initState(); diff --git a/lib/pages/join_game.dart b/lib/pages/join_game.dart deleted file mode 100644 index 4c447f2..0000000 --- a/lib/pages/join_game.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'dart:convert'; -import 'dart:developer'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:http/http.dart' as http; -import 'package:http/http.dart'; -import 'package:mchess/api/register.dart'; -import 'package:mchess/connection_cubit/connection_cubit.dart'; -import 'package:mchess/pages/chess_game.dart'; - -class JoinGameWidget extends StatefulWidget { - const JoinGameWidget({ - super.key, - }); - - @override - State createState() => _JoinGameWidgetState(); -} - -class _JoinGameWidgetState extends State { - final myController = TextEditingController(); - late Future joinGameFuture; - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: TextField( - controller: myController, - decoration: InputDecoration( - hintText: 'Enter passphrase here', - suffixIcon: IconButton( - onPressed: () { - joinGameFuture = joinPrivateGame(); - switchToGame(joinGameFuture); - }, - icon: const Icon(Icons.check), - )), - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - context.push('/'); - }, - child: const Icon(Icons.cancel), - ), - ); - } - - void switchToGame(Future resp) { - resp.then((value) { - if (value == null) return; - - var chessGameArgs = ChessGameArguments( - lobbyID: value.lobbyID!, - playerID: value.playerID!, - passphrase: value.passphrase); - - ConnectionCubit.getInstance().connect( - value.playerID!.uuid, - value.lobbyID!.uuid, - value.passphrase, - ); - - context.push('/game', extra: chessGameArgs); - }); - } - - Future joinPrivateGame() async { - String addr; - Response response; - - // server expects us to send the passphrase - var info = PlayerInfo( - playerID: null, lobbyID: null, passphrase: myController.text); - - if (kDebugMode) { - addr = 'http://localhost:8080/api/joinPrivate'; - } else { - addr = 'https://chess.sw-gross.de:9999/api/joinPrivate'; - } - - try { - response = await http.post(Uri.parse(addr), - body: jsonEncode(info), headers: {"Accept": "application/json"}); - } catch (e) { - log(e.toString()); - - if (!context.mounted) return null; - - const snackBar = SnackBar( - backgroundColor: Colors.amberAccent, - content: Text("mChess server is not responding. Try again or give up"), - ); - Future.delayed(const Duration(seconds: 2), () { - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar(snackBar); - context.push('/'); // We go back to lobby selector - }); - return null; - } - - if (response.statusCode == 200) { - var info = PlayerInfo.fromJson(jsonDecode(response.body)); - log('Player info received from server: '); - log('lobbyID: ${info.lobbyID}'); - log('playerID: ${info.playerID}'); - log('passphrase: ${info.passphrase}'); - return info; - } - return null; - } -} diff --git a/lib/pages/lobby_selector.dart b/lib/pages/lobby_selector.dart index 5db446c..dbedeb6 100644 --- a/lib/pages/lobby_selector.dart +++ b/lib/pages/lobby_selector.dart @@ -1,13 +1,36 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; +import 'package:mchess/api/register.dart'; +import 'package:mchess/connection_cubit/connection_cubit.dart'; +import 'package:mchess/pages/chess_game.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -class LobbySelector extends StatelessWidget { +class LobbySelector extends StatefulWidget { const LobbySelector({super.key}); + @override + State createState() => _LobbySelectorState(); +} + +class _LobbySelectorState extends State { final buttonStyle = const ButtonStyle(); + final myController = TextEditingController(); + late Future joinGameFuture; @override Widget build(BuildContext context) { + SharedPreferences.getInstance().then((prefs) { + final playerID = prefs.getString("playerID"); + final lobbyID = prefs.getString("lobbyID"); + final passphrase = prefs.getString("passphrase"); + log("lobbyID: $lobbyID and playerID: $playerID and passphrase: $passphrase"); + }); + return Scaffold( body: Center( child: Column( @@ -15,7 +38,7 @@ class LobbySelector extends StatelessWidget { children: [ ElevatedButton( onPressed: () { - _dialogBuilder(context); + buildJoinOrHostDialog(context); }, child: const Row( mainAxisSize: MainAxisSize.min, @@ -34,23 +57,65 @@ class LobbySelector extends StatelessWidget { ); } - Future _dialogBuilder(BuildContext context) { + Future buildJoinOrHostDialog(BuildContext context) { + return showDialog( + context: context, + builder: (BuildContext context) { + return Scaffold( + body: AlertDialog( + title: const Text('Host or join?'), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + context.pop(); + }, + ), + TextButton( + child: const Text('Host'), + onPressed: () { + context.push('/host'); + }, + ), + TextButton( + child: const Text('Join'), + onPressed: () { + buildEnterPassphraseDialog(context); + }, + ), + ], + ), + ); + }, + ); + } + + Future buildEnterPassphraseDialog(BuildContext context) { return showDialog( context: context, builder: (BuildContext context) { return AlertDialog( - title: const Text('Host or join?'), + title: const Text('Enter the passphrase here:'), + content: TextField( + controller: myController, + decoration: InputDecoration( + hintText: 'Enter passphrase here', + suffixIcon: IconButton( + onPressed: () { + joinGameFuture = joinPrivateGame(); + joinGameFuture.then((value) { + if (value == null) return; + switchToGame(value); + }); + }, + icon: const Icon(Icons.check), + )), + ), actions: [ TextButton( - child: const Text('Host'), + child: const Text('Cancel'), onPressed: () { - context.push('/host'); - }, - ), - TextButton( - child: const Text('Join'), - onPressed: () { - context.push('/join'); + context.pop(); }, ), ], @@ -58,4 +123,73 @@ class LobbySelector extends StatelessWidget { }, ); } + + void switchToGame(PlayerInfo info) { + var chessGameArgs = ChessGameArguments( + lobbyID: info.lobbyID!, + playerID: info.playerID!, + passphrase: info.passphrase); + + ConnectionCubit.getInstance().connect( + info.playerID!.uuid, + info.lobbyID!.uuid, + info.passphrase, + ); + + if (!chessGameArgs.isValid()) { + context.push('/'); + const snackBar = SnackBar( + backgroundColor: Colors.amberAccent, + content: Text("Game information is corrupted"), + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + context.push('/game', extra: chessGameArgs); + } + + Future joinPrivateGame() async { + String addr; + http.Response response; + + // server expects us to send the passphrase + var info = PlayerInfo( + playerID: null, lobbyID: null, passphrase: myController.text); + + if (kDebugMode) { + addr = 'http://localhost:8080/api/joinPrivate'; + } else { + addr = 'https://chess.sw-gross.de:9999/api/joinPrivate'; + } + + try { + response = await http.post(Uri.parse(addr), + body: jsonEncode(info), headers: {"Accept": "application/json"}); + } catch (e) { + log(e.toString()); + + if (!context.mounted) return null; + + const snackBar = SnackBar( + backgroundColor: Colors.amberAccent, + content: Text("mChess server is not responding. Try again or give up"), + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + return null; + } + + if (response.statusCode == 200) { + var info = PlayerInfo.fromJson(jsonDecode(response.body)); + log('Player info received from server: '); + log('lobbyID: ${info.lobbyID}'); + log('playerID: ${info.playerID}'); + log('passphrase: ${info.passphrase}'); + + return info; + } + + return null; + } } diff --git a/lib/utils/chess_router.dart b/lib/utils/chess_router.dart index 4a02267..f483ebc 100644 --- a/lib/utils/chess_router.dart +++ b/lib/utils/chess_router.dart @@ -1,6 +1,5 @@ import 'package:go_router/go_router.dart'; import 'package:mchess/pages/chess_game.dart'; -import 'package:mchess/pages/join_game.dart'; import 'package:mchess/pages/lobby_selector.dart'; import 'package:mchess/pages/host_game.dart'; import 'package:mchess/pages/prepare_random_game.dart'; @@ -35,12 +34,6 @@ class ChessAppRouter { builder: (context, state) { return const HostGameWidget(); }), - GoRoute( - path: '/join', - name: 'join', - builder: (context, state) { - return const JoinGameWidget(); - }), GoRoute( path: '/game', name: 'game', diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..724bb2a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 9ddd967..0bb45f3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" flutter: dependency: "direct main" description: flutter @@ -208,6 +224,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" petitparser: dependency: transitive description: @@ -216,6 +256,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 + url: "https://pub.dev" + source: hosted + version: "2.1.7" provider: dependency: transitive description: @@ -232,6 +288,62 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + url: "https://pub.dev" + source: hosted + version: "2.3.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" sky_engine: dependency: transitive description: flutter @@ -305,10 +417,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921 + sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f" url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "4.2.2" vector_graphics: dependency: transitive description: @@ -357,6 +469,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + win32: + dependency: transitive + description: + name: win32 + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + url: "https://pub.dev" + source: hosted + version: "1.0.3" xml: dependency: transitive description: @@ -367,4 +495,4 @@ packages: version: "6.5.0" sdks: dart: ">=3.2.0 <4.0.0" - flutter: ">=3.10.0" + flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3536aa3..8ec68e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: go_router: ^12.0.0 http: ^1.0.0 uuid: ^4.0.0 + shared_preferences: ^2.2.2 dev_dependencies: flutter_test: