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.
This commit is contained in:
Marco 2023-12-23 16:44:23 +01:00
parent 4a9047fd67
commit 7a51e71767
10 changed files with 337 additions and 140 deletions

3
.gitignore vendored
View File

@ -42,3 +42,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# Custom ignores
deploy-web.sh

View File

@ -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<PlayerInfo?> 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 {

View File

@ -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;
}
}

View File

@ -26,6 +26,9 @@ class _HostGameWidgetState extends State<HostGameWidget> {
@override
void initState() {
registerResponse = hostPrivateGame();
registerResponse.then((value) {
value?.store();
});
connectToWebsocket(registerResponse);
super.initState();

View File

@ -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<JoinGameWidget> createState() => _JoinGameWidgetState();
}
class _JoinGameWidgetState extends State<JoinGameWidget> {
final myController = TextEditingController();
late Future<PlayerInfo?> 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<PlayerInfo?> 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<PlayerInfo?> 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;
}
}

View File

@ -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<LobbySelector> createState() => _LobbySelectorState();
}
class _LobbySelectorState extends State<LobbySelector> {
final buttonStyle = const ButtonStyle();
final myController = TextEditingController();
late Future<PlayerInfo?> 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,13 +57,20 @@ class LobbySelector extends StatelessWidget {
);
}
Future<void> _dialogBuilder(BuildContext context) {
Future<void> buildJoinOrHostDialog(BuildContext context) {
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
return Scaffold(
body: AlertDialog(
title: const Text('Host or join?'),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () {
context.pop();
},
),
TextButton(
child: const Text('Host'),
onPressed: () {
@ -50,7 +80,42 @@ class LobbySelector extends StatelessWidget {
TextButton(
child: const Text('Join'),
onPressed: () {
context.push('/join');
buildEnterPassphraseDialog(context);
},
),
],
),
);
},
);
}
Future<void> buildEnterPassphraseDialog(BuildContext context) {
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
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: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () {
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<PlayerInfo?> 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;
}
}

View File

@ -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',

View File

@ -5,6 +5,8 @@
import FlutterMacOS
import Foundation
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}

View File

@ -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"

View File

@ -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: