diff --git a/lib/api/game_info.dart b/lib/api/game_info.dart index c34adc7..5b10048 100644 --- a/lib/api/game_info.dart +++ b/lib/api/game_info.dart @@ -3,43 +3,33 @@ import 'package:uuid/uuid.dart'; class GameInfo { final UuidValue? playerID; - final UuidValue? lobbyID; final String? passphrase; const GameInfo({ required this.playerID, - required this.lobbyID, required this.passphrase, }); factory GameInfo.empty() { - return const GameInfo(playerID: null, lobbyID: null, passphrase: null); + return const GameInfo(playerID: null, passphrase: null); } factory GameInfo.fromJson(Map json) { final playerid = UuidValue.fromString(json['playerID']); - final lobbyid = UuidValue.fromString(json['lobbyID']); final passphrase = json['passphrase']; - return GameInfo( - playerID: playerid, lobbyID: lobbyid, passphrase: passphrase); + return GameInfo(playerID: playerid, passphrase: passphrase); } Map toJson() { String? pid; - String? lid; if (playerID != null) { pid = playerID.toString(); } - if (lobbyID != null) { - lid = lobbyID.toString(); - } - return { 'playerID': pid, - 'lobbyID': lid, 'passphrase': passphrase, }; } @@ -47,51 +37,29 @@ class GameInfo { 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()); + await prefs.setString(passphrase!, playerID.toString()); } - void delete() async { + static Future get(String phrase) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); + var playerID = prefs.getString(phrase); - await prefs.setBool("contains", false); - } - - static 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; - } + if (playerID == null) return null; return GameInfo( - playerID: UuidValue.fromString(playerID), - lobbyID: UuidValue.fromString(lobbyID), - passphrase: passphrase); + playerID: UuidValue.fromString(playerID), passphrase: phrase); } } class WebsocketMessageIdentifyPlayer { final String playerID; - final String lobbyID; final String? passphrase; const WebsocketMessageIdentifyPlayer({ required this.playerID, - required this.lobbyID, required this.passphrase, }); Map toJson() => - {'lobbyID': lobbyID, 'playerID': playerID, 'passphrase': passphrase}; + {'playerID': playerID, 'passphrase': passphrase}; } diff --git a/lib/chess/chess_app.dart b/lib/chess/chess_app.dart index c25efeb..da3a3fa 100644 --- a/lib/chess/chess_app.dart +++ b/lib/chess/chess_app.dart @@ -31,7 +31,7 @@ class ChessApp extends StatelessWidget { useMaterial3: true, ), routerConfig: ChessAppRouter.getInstance().router, - title: 'mChess 1.0.6', + title: 'mChess 1.0.7', ), ); } diff --git a/lib/connection/ws_connection.dart b/lib/connection/ws_connection.dart index 33eaac8..ceaed3e 100644 --- a/lib/connection/ws_connection.dart +++ b/lib/connection/ws_connection.dart @@ -40,29 +40,35 @@ class ServerConnection { channel!.sink.add(message); } - void connect(String playerID, lobbyID, String? passphrase) { - disconnectExistingConnection(); + Future? connect(String playerID, String? passphrase) { + if (channel != null) return null; + channel = WebSocketChannel.connect(Uri.parse(config.getWebsocketURL())); - send( - jsonEncode( - WebsocketMessageIdentifyPlayer( - playerID: (playerID), - lobbyID: (lobbyID), - passphrase: (passphrase), + channel!.ready.then((val) { + send( + jsonEncode( + WebsocketMessageIdentifyPlayer( + playerID: (playerID), + passphrase: (passphrase), + ), ), - ), - ); + ); - log(channel!.closeCode.toString()); - broadcast = channel!.stream.asBroadcastStream(); - broadcast.listen(handleIncomingData); + log(channel!.closeCode.toString()); + broadcast = channel!.stream.asBroadcastStream(); + broadcast.listen(handleIncomingData); + }); + + return channel!.ready; } - void disconnectExistingConnection() { + Future disconnectExistingConnection() async { if (channel == null) return; - channel!.sink.close(); + await channel!.sink.close(); + + channel = null; broadcast = const Stream.empty(); } diff --git a/lib/connection_cubit/connection_cubit.dart b/lib/connection_cubit/connection_cubit.dart index 9c9c3e2..10babd6 100644 --- a/lib/connection_cubit/connection_cubit.dart +++ b/lib/connection_cubit/connection_cubit.dart @@ -15,21 +15,64 @@ class ConnectionCubit extends Cubit { return _instance; } - void connect(String playerID, lobbyID, String? passphrase) { - ServerConnection.getInstance().connect(playerID, lobbyID, passphrase); + void connect(String playerID, String? passphrase) { + var connectedFuture = + ServerConnection.getInstance().connect(playerID, passphrase); + + connectedFuture?.then((val) { + emit(ConnectionCubitState( + iAmConnected: true, + connectedToPhrase: passphrase, + opponentConnected: false)); + }); + } + + void connectIfNotConnected(String playerID, String? passphrase) { + if (state.iAmConnected && state.connectedToPhrase == passphrase) { + return; + } + if (state.iAmConnected && state.connectedToPhrase != passphrase) { + disonnect().then((val) { + connect(playerID, passphrase); + }); + } + + connect(playerID, passphrase); + } + + Future disonnect() async { + var disconnectFuture = + ServerConnection.getInstance().disconnectExistingConnection(); + + disconnectFuture.then( + (val) { + emit(ConnectionCubitState.init()); + }, + ); + + return disconnectFuture; } void opponentConnected() { - emit(ConnectionCubitState(true)); + emit(ConnectionCubitState( + iAmConnected: state.iAmConnected, + connectedToPhrase: state.connectedToPhrase, + opponentConnected: true)); } } class ConnectionCubitState { + final bool iAmConnected; + final String? connectedToPhrase; final bool opponentConnected; - ConnectionCubitState(this.opponentConnected); + ConnectionCubitState( + {required this.iAmConnected, + required this.connectedToPhrase, + required this.opponentConnected}); factory ConnectionCubitState.init() { - return ConnectionCubitState(false); + return ConnectionCubitState( + iAmConnected: false, connectedToPhrase: null, opponentConnected: false); } } diff --git a/lib/pages/create_game_widget.dart b/lib/pages/create_game_widget.dart index 5db7430..199e149 100644 --- a/lib/pages/create_game_widget.dart +++ b/lib/pages/create_game_widget.dart @@ -24,34 +24,21 @@ class CreateGameWidget extends StatefulWidget { class _CreateGameWidgetState extends State { late Future registerResponse; + late Future disconnectFuture; late ChessGameArguments chessGameArgs; @override void initState() { - registerResponse = hostPrivateGame(); - - registerResponse.then((args) { - if (args == null) return; - - chessGameArgs = ChessGameArguments( - lobbyID: args.lobbyID!, - playerID: args.playerID!, - passphrase: args.passphrase); - }); - - connectToWebsocket(registerResponse); super.initState(); - } - - void connectToWebsocket(Future resp) { - resp.then((value) { - if (value == null) return; - - ConnectionCubit.getInstance().connect( - value.playerID!.uuid, - value.lobbyID!.uuid, - value.passphrase, - ); + disconnectFuture = ConnectionCubit().disonnect(); + disconnectFuture.then((val) { + registerResponse = createPrivateGame(); + registerResponse.then((val) { + ConnectionCubit().connectIfNotConnected( + val!.playerID.toString(), + val.passphrase, + ); + }); }); } @@ -72,69 +59,80 @@ class _CreateGameWidgetState extends State { return Scaffold( floatingActionButton: fltnBtn, body: Center( - child: FutureBuilder( - future: registerResponse, - builder: (context, snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return const SizedBox( - height: 100, - width: 100, - child: CircularProgressIndicator(), - ); - } else { - String passphrase = snapshot.data?.passphrase ?? "no passphrase"; - return BlocListener( - listener: (context, state) { - // We wait for our opponent to connect - if (state.opponentConnected) { - //TODO: is goNamed the correct way to navigate? - context.goNamed('game', - pathParameters: {'phrase': passphrase.toURL()}, - extra: chessGameArgs); - } - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Give this phrase to your friend and sit tight:', - style: TextStyle( - color: Theme.of(context).colorScheme.primary), - ), - const SizedBox(height: 25), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SelectableText( - passphrase, - style: const TextStyle(fontWeight: FontWeight.bold), + child: FutureBuilder( + future: disconnectFuture, + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return Container(); + } else { + return FutureBuilder( + future: registerResponse, + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const SizedBox( + height: 100, + width: 100, + child: CircularProgressIndicator(), + ); + } else { + var passphrase = + snapshot.data?.passphrase ?? "no passphrase"; + return BlocListener( + listener: (context, state) { + // We wait for our opponent to connect + if (state.opponentConnected) { + //TODO: is goNamed the correct way to navigate? + context.goNamed('game', pathParameters: { + 'phrase': passphrase.toURL(), + }); + } + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Give this phrase to your friend and sit tight:', + style: TextStyle( + color: Theme.of(context).colorScheme.primary), + ), + const SizedBox(height: 25), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SelectableText( + passphrase, + style: const TextStyle( + fontWeight: FontWeight.bold), + ), + IconButton( + icon: const Icon(Icons.copy), + onPressed: () async { + await Clipboard.setData( + ClipboardData(text: passphrase)); + }, + ) + ], + ), + const SizedBox(height: 25), + const CircularProgressIndicator() + ], ), - IconButton( - icon: const Icon(Icons.copy), - onPressed: () async { - await Clipboard.setData( - ClipboardData(text: passphrase)); - }, - ) - ], - ), - const SizedBox(height: 25), - const CircularProgressIndicator() - ], - ), - ); - } - }, - ), + ); + } + }, + ); + } + }), ), ); } - Future hostPrivateGame() async { + Future createPrivateGame() async { Response response; try { - response = await http.get(Uri.parse(config.getHostURL()), + response = await http.get(Uri.parse(config.getCreateGameURL()), headers: {"Accept": "application/json"}); } catch (e) { log('Exception: ${e.toString()}'); diff --git a/lib/pages/join_game_handle_widget.dart b/lib/pages/join_game_handle_widget.dart index 4d865c5..7cd0724 100644 --- a/lib/pages/join_game_handle_widget.dart +++ b/lib/pages/join_game_handle_widget.dart @@ -3,7 +3,7 @@ import 'dart:developer'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:http/http.dart' as http; import 'package:mchess/api/game_info.dart'; import 'package:mchess/connection_cubit/connection_cubit.dart'; @@ -23,65 +23,62 @@ class _JoinGameHandleWidgetState extends State { @override void initState() { - joinGameFuture = joinPrivateGame(widget.passphrase); - joinGameFuture.then( - (value) { - if (value != null) { - switchToGame(value); - } - }, - ); super.initState(); + joinGameFuture = joinPrivateGame(widget.passphrase); + joinGameFuture.then((val) { + ConnectionCubit.getInstance().connectIfNotConnected( + val!.playerID!.uuid, + val.passphrase, + ); + }); } @override Widget build(BuildContext context) { - return const ChessGame(); - } - - void switchToGame(GameInfo info) { - var chessGameArgs = ChessGameArguments( - lobbyID: info.lobbyID!, - playerID: info.playerID!, - passphrase: info.passphrase); - - ConnectionCubit.getInstance().connect( - info.playerID!.uuid, - info.lobbyID!.uuid, - info.passphrase, + var loadingIndicator = const SizedBox( + height: 100, + width: 100, + child: CircularProgressIndicator(), ); - if (!chessGameArgs.isValid()) { - context.goNamed('lobbySelector'); - const snackBar = SnackBar( - backgroundColor: Colors.amberAccent, - content: Text("Game information is corrupted"), - ); - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar(snackBar); - - return; - } + return Scaffold( + body: Center( + child: FutureBuilder( + future: joinGameFuture, + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return loadingIndicator; + } else { + return BlocBuilder( + builder: (context, state) { + if (state.iAmConnected) { + return const ChessGame(); + } else { + return loadingIndicator; + } + }); + } + }), + ), + ); } Future joinPrivateGame(String phrase) async { http.Response response; - var existingInfo = await GameInfo.get(); - log('lobbyID: ${existingInfo?.lobbyID} and playerID: ${existingInfo?.playerID} and passphrase: "${existingInfo?.passphrase}"'); + + var existingInfo = await GameInfo.get(phrase); + log('playerID: ${existingInfo?.playerID} and passphrase: "${existingInfo?.passphrase}"'); GameInfo info; if (existingInfo?.passphrase == phrase) { // We have player info for this exact passphrase - info = GameInfo( - playerID: existingInfo?.playerID, - lobbyID: existingInfo?.lobbyID, - passphrase: phrase); + info = GameInfo(playerID: existingInfo?.playerID, passphrase: phrase); } else { - info = GameInfo(playerID: null, lobbyID: null, passphrase: phrase); + info = GameInfo(playerID: null, passphrase: phrase); } try { - response = await http.post(Uri.parse(config.getJoinURL()), + response = await http.post(Uri.parse(config.getJoinGameURL()), body: jsonEncode(info), headers: {"Accept": "application/json"}); } catch (e) { log(e.toString()); @@ -114,7 +111,6 @@ class _JoinGameHandleWidgetState extends State { var info = GameInfo.fromJson(jsonDecode(response.body)); info.store(); log('Player info received from server: '); - log('lobbyID: ${info.lobbyID}'); log('playerID: ${info.playerID}'); log('passphrase: ${info.passphrase}'); diff --git a/lib/pages/lobby_selector.dart b/lib/pages/lobby_selector.dart index 353d73b..c6e10b2 100644 --- a/lib/pages/lobby_selector.dart +++ b/lib/pages/lobby_selector.dart @@ -20,7 +20,9 @@ class _LobbySelectorState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( - onPressed: () => context.goNamed('createGame'), + onPressed: () { + context.goNamed('createGame'); + }, child: const Row( mainAxisSize: MainAxisSize.min, children: [ @@ -34,7 +36,9 @@ class _LobbySelectorState extends State { ), const SizedBox(height: 20), ElevatedButton( - onPressed: () => buildEnterPassphraseDialog(context), + onPressed: () { + buildEnterPassphraseDialog(context); + }, child: const Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/utils/chess_router.dart b/lib/utils/chess_router.dart index aab6974..6507b9a 100644 --- a/lib/utils/chess_router.dart +++ b/lib/utils/chess_router.dart @@ -2,7 +2,6 @@ import 'dart:developer'; import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; -import 'package:mchess/connection/ws_connection.dart'; import 'package:mchess/pages/join_game_handle_widget.dart'; import 'package:mchess/pages/lobby_selector.dart'; import 'package:mchess/pages/create_game_widget.dart'; @@ -40,8 +39,6 @@ class ChessAppRouter { path: 'game/:phrase', name: 'game', builder: (context, state) { - ServerConnection.getInstance().disconnectExistingConnection(); - var urlPhrase = state.pathParameters['phrase']; if (urlPhrase == null) { log('in /game route builder: url phrase null'); @@ -49,8 +46,7 @@ class ChessAppRouter { } return JoinGameHandleWidget( - passphrase: urlPhrase.toPhraseWithSpaces(), - ); + passphrase: urlPhrase.toPhraseWithSpaces()); }, ) ], diff --git a/lib/utils/config.dart b/lib/utils/config.dart index 863dedb..89d1b6a 100644 --- a/lib/utils/config.dart +++ b/lib/utils/config.dart @@ -3,7 +3,7 @@ const debugURL = 'localhost:8080'; const useDbgUrl = false; -String getHostURL() { +String getCreateGameURL() { var prot = 'https'; var domain = prodURL; if (useDbgUrl) { @@ -13,7 +13,7 @@ String getHostURL() { return '$prot://$domain/api/hostPrivate'; } -String getJoinURL() { +String getJoinGameURL() { var prot = 'https'; var domain = prodURL; if (useDbgUrl) {