reintroduce microsoft login. upgrade a bunch of stuff.
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
@page "/authentication/{action}"
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||
@*<RemoteAuthenticatorView Action="@Action" />*@
|
||||
|
||||
<RemoteAuthenticatorView Action="@Action" />
|
||||
@code{
|
||||
[Parameter] public string? Action { get; set; }
|
||||
// https://github.com/dotnet/aspnetcore/blob/main/src/Components/WebAssembly/WebAssembly.Authentication/src/Models/RemoteAuthenticationActions.cs
|
||||
// https://github.com/dotnet/aspnetcore/blob/7c810658463f35c39c54d5fb8a8dbbfd463bf747/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticatorViewCore.cs
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||
using Shogi.UI.Pages.Home.Api;
|
||||
using Shogi.UI.Shared;
|
||||
|
||||
@@ -7,132 +8,144 @@ namespace Shogi.UI.Pages.Home.Account;
|
||||
|
||||
public class AccountManager
|
||||
{
|
||||
private readonly AccountState accountState;
|
||||
private readonly IShogiApi shogiApi;
|
||||
private readonly IConfiguration configuration;
|
||||
private readonly ILocalStorage localStorage;
|
||||
private readonly AuthenticationStateProvider authState;
|
||||
private readonly NavigationManager navigation;
|
||||
private readonly ShogiSocket shogiSocket;
|
||||
private readonly AccountState accountState;
|
||||
private readonly IShogiApi shogiApi;
|
||||
private readonly IConfiguration configuration;
|
||||
private readonly ILocalStorage localStorage;
|
||||
private readonly AuthenticationStateProvider authState;
|
||||
private readonly NavigationManager navigation;
|
||||
private readonly ShogiSocket shogiSocket;
|
||||
|
||||
public AccountManager(
|
||||
AccountState accountState,
|
||||
IShogiApi unauthenticatedClient,
|
||||
IConfiguration configuration,
|
||||
AuthenticationStateProvider authState,
|
||||
ILocalStorage localStorage,
|
||||
NavigationManager navigation,
|
||||
ShogiSocket shogiSocket)
|
||||
{
|
||||
this.accountState = accountState;
|
||||
this.shogiApi = unauthenticatedClient;
|
||||
this.configuration = configuration;
|
||||
this.authState = authState;
|
||||
this.localStorage = localStorage;
|
||||
this.navigation = navigation;
|
||||
this.shogiSocket = shogiSocket;
|
||||
}
|
||||
|
||||
private User? User { get => accountState.User; set => accountState.User = value; }
|
||||
|
||||
public async Task LoginWithGuestAccount()
|
||||
{
|
||||
var response = await shogiApi.GetToken();
|
||||
if (response != null)
|
||||
public AccountManager(
|
||||
AccountState accountState,
|
||||
IShogiApi unauthenticatedClient,
|
||||
IConfiguration configuration,
|
||||
AuthenticationStateProvider authState,
|
||||
ILocalStorage localStorage,
|
||||
NavigationManager navigation,
|
||||
ShogiSocket shogiSocket)
|
||||
{
|
||||
User = new User
|
||||
{
|
||||
DisplayName = response.DisplayName,
|
||||
Id = response.UserId,
|
||||
OneTimeSocketToken = response.OneTimeToken,
|
||||
WhichAccountPlatform = WhichAccountPlatform.Guest
|
||||
};
|
||||
await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString());
|
||||
await localStorage.SetAccountPlatform(WhichAccountPlatform.Guest);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoginWithMicrosoftAccount()
|
||||
{
|
||||
var state = await authState.GetAuthenticationStateAsync();
|
||||
|
||||
if (state.User?.Identity?.Name == null || state.User?.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
navigation.NavigateTo("authentication/login");
|
||||
return;
|
||||
this.accountState = accountState;
|
||||
this.shogiApi = unauthenticatedClient;
|
||||
this.configuration = configuration;
|
||||
this.authState = authState;
|
||||
this.localStorage = localStorage;
|
||||
this.navigation = navigation;
|
||||
this.shogiSocket = shogiSocket;
|
||||
}
|
||||
|
||||
var response = await shogiApi.GetToken();
|
||||
if (response != null)
|
||||
{
|
||||
User = new User
|
||||
{
|
||||
DisplayName = response.DisplayName,
|
||||
Id = response.UserId,
|
||||
OneTimeSocketToken = response.OneTimeToken,
|
||||
WhichAccountPlatform = WhichAccountPlatform.Microsoft,
|
||||
};
|
||||
private User? MyUser { get => accountState.User; set => accountState.User = value; }
|
||||
|
||||
await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString());
|
||||
await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to log in with the account used from the previous browser session.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> TryLoginSilentAsync()
|
||||
{
|
||||
var platform = await localStorage.GetAccountPlatform();
|
||||
if (platform == WhichAccountPlatform.Guest)
|
||||
public async Task LoginWithGuestAccount()
|
||||
{
|
||||
var response = await shogiApi.GetToken();
|
||||
if (response != null)
|
||||
{
|
||||
User = new User
|
||||
var response = await shogiApi.GetToken();
|
||||
if (response != null)
|
||||
{
|
||||
DisplayName = response.DisplayName,
|
||||
Id = response.UserId,
|
||||
OneTimeSocketToken = response.OneTimeToken,
|
||||
WhichAccountPlatform = WhichAccountPlatform.Guest
|
||||
};
|
||||
}
|
||||
MyUser = new User
|
||||
{
|
||||
DisplayName = response.DisplayName,
|
||||
Id = response.UserId,
|
||||
OneTimeSocketToken = response.OneTimeToken,
|
||||
WhichAccountPlatform = WhichAccountPlatform.Guest
|
||||
};
|
||||
await shogiSocket.OpenAsync(MyUser.Value.OneTimeSocketToken.ToString());
|
||||
await localStorage.SetAccountPlatform(WhichAccountPlatform.Guest);
|
||||
}
|
||||
}
|
||||
else if (platform == WhichAccountPlatform.Microsoft)
|
||||
|
||||
public async Task LoginWithMicrosoftAccount()
|
||||
{
|
||||
Console.WriteLine("Login Microsoft");
|
||||
throw new NotImplementedException();
|
||||
//var state = await authState.GetAuthenticationStateAsync();
|
||||
//if (state.User?.Identity?.Name != null)
|
||||
//{
|
||||
// var id = state.User.Identity;
|
||||
// User = new User
|
||||
// {
|
||||
// DisplayName = id.Name,
|
||||
// Id = id.Name
|
||||
// };
|
||||
// var token = await shogiApi.GetToken();
|
||||
// if (token.HasValue)
|
||||
// {
|
||||
// User.OneTimeSocketToken = token.Value;
|
||||
// }
|
||||
//}
|
||||
// TODO: If this fails then platform saved to localStorage should get cleared
|
||||
var state = await authState.GetAuthenticationStateAsync();
|
||||
|
||||
if (state.User?.Identity?.Name == null || state.User?.Identity?.IsAuthenticated == false)
|
||||
{
|
||||
// Set the login platform so that we know to log in with microsoft after being redirected away from the UI.
|
||||
await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft);
|
||||
navigation.NavigateToLogin("authentication/login");
|
||||
return;
|
||||
}
|
||||
|
||||
//var response = await shogiApi.GetToken();
|
||||
//if (response != null)
|
||||
//{
|
||||
// MyUser = new User
|
||||
// {
|
||||
// DisplayName = response.DisplayName,
|
||||
// Id = response.UserId,
|
||||
// OneTimeSocketToken = response.OneTimeToken,
|
||||
// WhichAccountPlatform = WhichAccountPlatform.Microsoft,
|
||||
// };
|
||||
|
||||
// await shogiSocket.OpenAsync(MyUser.Value.OneTimeSocketToken.ToString());
|
||||
// await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft);
|
||||
//}
|
||||
}
|
||||
|
||||
if (User != null)
|
||||
/// <summary>
|
||||
/// Try to log in with the account used from the previous browser session.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> TryLoginSilentAsync()
|
||||
{
|
||||
await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString());
|
||||
return true;
|
||||
var platform = await localStorage.GetAccountPlatform();
|
||||
Console.WriteLine($"Try Login Silent - {platform}");
|
||||
if (platform == WhichAccountPlatform.Guest)
|
||||
{
|
||||
var response = await shogiApi.GetToken();
|
||||
if (response != null)
|
||||
{
|
||||
MyUser = new User
|
||||
{
|
||||
DisplayName = response.DisplayName,
|
||||
Id = response.UserId,
|
||||
OneTimeSocketToken = response.OneTimeToken,
|
||||
WhichAccountPlatform = WhichAccountPlatform.Guest
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (platform == WhichAccountPlatform.Microsoft)
|
||||
{
|
||||
var state = await authState.GetAuthenticationStateAsync();
|
||||
if (state.User?.Identity?.Name != null)
|
||||
{
|
||||
var response = await shogiApi.GetToken();
|
||||
if (response == null)
|
||||
{
|
||||
// Login failed, so reset local storage to avoid putting the user in a broken state.
|
||||
await localStorage.DeleteAccountPlatform();
|
||||
return false;
|
||||
}
|
||||
var id = state.User.Identity;
|
||||
MyUser = new User
|
||||
{
|
||||
DisplayName = id.Name,
|
||||
Id = id.Name,
|
||||
OneTimeSocketToken = response.OneTimeToken
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (MyUser != null)
|
||||
{
|
||||
await shogiSocket.OpenAsync(MyUser.Value.OneTimeSocketToken.ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task LogoutAsync()
|
||||
{
|
||||
await Task.WhenAll(shogiApi.GuestLogout(), localStorage.DeleteAccountPlatform());
|
||||
User = null;
|
||||
}
|
||||
public async Task LogoutAsync()
|
||||
{
|
||||
MyUser = null;
|
||||
var platform = await localStorage.GetAccountPlatform();
|
||||
await localStorage.DeleteAccountPlatform();
|
||||
|
||||
if (platform == WhichAccountPlatform.Guest)
|
||||
{
|
||||
await shogiApi.GuestLogout();
|
||||
} else if (platform == WhichAccountPlatform.Microsoft)
|
||||
{
|
||||
navigation.NavigateToLogout("authentication/logout");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
|
||||
public class AccountState
|
||||
{
|
||||
public event EventHandler<LoginEventArgs>? LoginChangedEvent;
|
||||
public event EventHandler<LoginEventArgs>? LoginChangedEvent;
|
||||
|
||||
private User? user;
|
||||
public User? User
|
||||
{
|
||||
get => user;
|
||||
set
|
||||
{
|
||||
if (user != value)
|
||||
{
|
||||
user = value;
|
||||
EmitLoginChangedEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
private User? user;
|
||||
public User? User
|
||||
{
|
||||
get => user;
|
||||
set
|
||||
{
|
||||
if (user != value)
|
||||
{
|
||||
user = value;
|
||||
EmitLoginChangedEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitLoginChangedEvent()
|
||||
{
|
||||
LoginChangedEvent?.Invoke(this, new LoginEventArgs
|
||||
{
|
||||
User = User
|
||||
});
|
||||
}
|
||||
private void EmitLoginChangedEvent()
|
||||
{
|
||||
LoginChangedEvent?.Invoke(this, new LoginEventArgs
|
||||
{
|
||||
User = User
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
namespace Shogi.UI.Pages.Home.Account
|
||||
{
|
||||
public class User
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
public WhichAccountPlatform WhichAccountPlatform { get; set; }
|
||||
|
||||
public Guid OneTimeSocketToken { get; set; }
|
||||
}
|
||||
public readonly record struct User(
|
||||
string Id,
|
||||
string DisplayName,
|
||||
WhichAccountPlatform WhichAccountPlatform,
|
||||
Guid OneTimeSocketToken)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,74 +7,75 @@ using System.Text.Json;
|
||||
|
||||
namespace Shogi.UI.Pages.Home.Api
|
||||
{
|
||||
public class ShogiApi : IShogiApi
|
||||
{
|
||||
public const string GuestClientName = "Guest";
|
||||
public const string MsalClientName = "Msal";
|
||||
public const string AnonymouseClientName = "Anonymous";
|
||||
|
||||
private readonly JsonSerializerOptions serializerOptions;
|
||||
private readonly IHttpClientFactory clientFactory;
|
||||
private readonly AccountState accountState;
|
||||
|
||||
public ShogiApi(IHttpClientFactory clientFactory, AccountState accountState)
|
||||
public class ShogiApi : IShogiApi
|
||||
{
|
||||
serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
this.clientFactory = clientFactory;
|
||||
this.accountState = accountState;
|
||||
public const string GuestClientName = "Guest";
|
||||
public const string MsalClientName = "Msal";
|
||||
//public const string AnonymousClientName = "Anonymous";
|
||||
|
||||
private readonly JsonSerializerOptions serializerOptions;
|
||||
private readonly IHttpClientFactory clientFactory;
|
||||
private readonly AccountState accountState;
|
||||
|
||||
public ShogiApi(IHttpClientFactory clientFactory, AccountState accountState)
|
||||
{
|
||||
serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
this.clientFactory = clientFactory;
|
||||
this.accountState = accountState;
|
||||
}
|
||||
|
||||
private HttpClient HttpClient => accountState.User?.WhichAccountPlatform switch
|
||||
{
|
||||
WhichAccountPlatform.Guest => clientFactory.CreateClient(GuestClientName),
|
||||
WhichAccountPlatform.Microsoft => clientFactory.CreateClient(MsalClientName),
|
||||
_ => clientFactory.CreateClient(GuestClientName)
|
||||
};
|
||||
|
||||
public async Task GuestLogout()
|
||||
{
|
||||
var response = await HttpClient.PutAsync(new Uri("User/GuestLogout", UriKind.Relative), null);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<Session?> GetSession(string name)
|
||||
{
|
||||
var response = await HttpClient.GetAsync(new Uri($"Sessions/{name}", UriKind.Relative));
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return (await response.Content.ReadFromJsonAsync<ReadSessionResponse>(serializerOptions))?.Session;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<ReadSessionsPlayerCountResponse?> GetSessionsPlayerCount()
|
||||
{
|
||||
var response = await HttpClient.GetAsync(new Uri("Sessions/PlayerCount", UriKind.Relative));
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return await response.Content.ReadFromJsonAsync<ReadSessionsPlayerCountResponse>(serializerOptions);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<CreateTokenResponse?> GetToken()
|
||||
{
|
||||
Console.WriteLine($"GetToken() - {accountState.User?.WhichAccountPlatform}");
|
||||
var response = await HttpClient.GetFromJsonAsync<CreateTokenResponse>(new Uri("User/Token", UriKind.Relative), serializerOptions);
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task Move(string sessionName, MovePieceCommand command)
|
||||
{
|
||||
await this.HttpClient.PatchAsync($"Sessions/{sessionName}/Move", JsonContent.Create(command));
|
||||
}
|
||||
|
||||
public async Task<HttpStatusCode> PostSession(string name, bool isPrivate)
|
||||
{
|
||||
var response = await HttpClient.PostAsJsonAsync(new Uri("Sessions", UriKind.Relative), new CreateSessionCommand
|
||||
{
|
||||
Name = name,
|
||||
});
|
||||
return response.StatusCode;
|
||||
}
|
||||
}
|
||||
|
||||
private HttpClient HttpClient => accountState.User?.WhichAccountPlatform switch
|
||||
{
|
||||
WhichAccountPlatform.Guest => clientFactory.CreateClient(GuestClientName),
|
||||
WhichAccountPlatform.Microsoft => clientFactory.CreateClient(MsalClientName),
|
||||
_ => clientFactory.CreateClient(AnonymouseClientName)
|
||||
};
|
||||
|
||||
public async Task GuestLogout()
|
||||
{
|
||||
var response = await HttpClient.PutAsync(new Uri("User/GuestLogout", UriKind.Relative), null);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<Session?> GetSession(string name)
|
||||
{
|
||||
var response = await HttpClient.GetAsync(new Uri($"Sessions/{name}", UriKind.Relative));
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return (await response.Content.ReadFromJsonAsync<ReadSessionResponse>(serializerOptions))?.Session;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<ReadSessionsPlayerCountResponse?> GetSessionsPlayerCount()
|
||||
{
|
||||
var response = await HttpClient.GetAsync(new Uri("Sessions/PlayerCount", UriKind.Relative));
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return await response.Content.ReadFromJsonAsync<ReadSessionsPlayerCountResponse>(serializerOptions);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<CreateTokenResponse?> GetToken()
|
||||
{
|
||||
var response = await HttpClient.GetFromJsonAsync<CreateTokenResponse>(new Uri("User/Token", UriKind.Relative), serializerOptions);
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task Move(string sessionName, MovePieceCommand command)
|
||||
{
|
||||
await this.HttpClient.PatchAsync($"Sessions/{sessionName}/Move", JsonContent.Create(command));
|
||||
}
|
||||
|
||||
public async Task<HttpStatusCode> PostSession(string name, bool isPrivate)
|
||||
{
|
||||
var response = await HttpClient.PostAsJsonAsync(new Uri("Sessions", UriKind.Relative), new CreateSessionCommand
|
||||
{
|
||||
Name = name,
|
||||
});
|
||||
return response.StatusCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
@using Shogi.Contracts.Api
|
||||
@using Shogi.Contracts.Types;
|
||||
@using System.Text.RegularExpressions;
|
||||
@inject IShogiApi ShogiApi
|
||||
@inject AccountState Account;
|
||||
@inject PromotePrompt PromotePrompt;
|
||||
|
||||
<article class="game-board">
|
||||
<!-- Game board -->
|
||||
<section class="board" data-perspective="@Perspective">
|
||||
@for (var rank = 1; rank < 10; rank++)
|
||||
{
|
||||
foreach (var file in Files)
|
||||
{
|
||||
var position = $"{file}{rank}";
|
||||
var piece = session?.BoardState.Board[position];
|
||||
<div class="tile"
|
||||
data-position="@(position)"
|
||||
data-selected="@(piece != null && selectedPosition == position)"
|
||||
style="grid-area: @(position)"
|
||||
@onclick="() => OnClickTile(piece, position)">
|
||||
<GamePiece Piece="piece" Perspective="Perspective" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div class="ruler vertical" style="grid-area: rank">
|
||||
<span>9</span>
|
||||
<span>8</span>
|
||||
<span>7</span>
|
||||
<span>6</span>
|
||||
<span>5</span>
|
||||
<span>4</span>
|
||||
<span>3</span>
|
||||
<span>2</span>
|
||||
<span>1</span>
|
||||
</div>
|
||||
<div class="ruler" style="grid-area: file">
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
<span>D</span>
|
||||
<span>E</span>
|
||||
<span>F</span>
|
||||
<span>G</span>
|
||||
<span>H</span>
|
||||
<span>I</span>
|
||||
</div>
|
||||
<!-- Promote prompt -->
|
||||
<div class="promote-prompt" data-visible="@PromotePrompt.IsVisible">
|
||||
<p>Do you wish to promote?</p>
|
||||
<div>
|
||||
<button type="button">Yes</button>
|
||||
<button type="button">No</button>
|
||||
<button type="button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Side board -->
|
||||
@if (session != null)
|
||||
{
|
||||
<aside class="side-board">
|
||||
<div class="hand">
|
||||
@foreach (var piece in OpponentHand)
|
||||
{
|
||||
<div class="tile">
|
||||
<GamePiece Piece="piece" Perspective="Perspective" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="spacer" />
|
||||
|
||||
<div class="hand">
|
||||
@foreach (var piece in UserHand)
|
||||
{
|
||||
<div class="title" @onclick="() => OnClickHand(piece)">
|
||||
<GamePiece Piece="piece" Perspective="Perspective" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
</article>
|
||||
|
||||
@code {
|
||||
static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" };
|
||||
|
||||
[Parameter]
|
||||
public string? SessionName { get; set; }
|
||||
|
||||
WhichPlayer Perspective => Account.User?.Id == session?.Player1
|
||||
? WhichPlayer.Player1
|
||||
: WhichPlayer.Player2;
|
||||
Session? session;
|
||||
IReadOnlyCollection<Piece> OpponentHand
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.session == null) return Array.Empty<Piece>();
|
||||
|
||||
return Perspective == WhichPlayer.Player1
|
||||
? this.session.BoardState.Player1Hand
|
||||
: this.session.BoardState.Player2Hand;
|
||||
}
|
||||
}
|
||||
IReadOnlyCollection<Piece> UserHand
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.session == null) return Array.Empty<Piece>();
|
||||
|
||||
return Perspective == WhichPlayer.Player1
|
||||
? this.session.BoardState.Player1Hand
|
||||
: this.session.BoardState.Player2Hand;
|
||||
}
|
||||
}
|
||||
bool IsMyTurn => session?.BoardState.WhoseTurn == Perspective;
|
||||
|
||||
string? selectedPosition;
|
||||
WhichPiece? selectedPiece;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(SessionName))
|
||||
{
|
||||
this.session = await ShogiApi.GetSession(SessionName);
|
||||
}
|
||||
}
|
||||
|
||||
bool ShouldPromptForPromotion(string position)
|
||||
{
|
||||
if (Perspective == WhichPlayer.Player1 && Regex.IsMatch(position, ".[7-9]"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (Perspective == WhichPlayer.Player2 && Regex.IsMatch(position, ".[1-3]"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async void OnClickTile(Piece? piece, string position)
|
||||
{
|
||||
if (SessionName == null || !IsMyTurn) return;
|
||||
|
||||
if (selectedPosition == null || piece?.Owner == Perspective)
|
||||
{
|
||||
// Select a position.
|
||||
selectedPosition = position;
|
||||
return;
|
||||
}
|
||||
if (selectedPosition == position)
|
||||
{
|
||||
// Deselect the selected position.
|
||||
selectedPosition = null;
|
||||
return;
|
||||
}
|
||||
if (piece == null)
|
||||
{
|
||||
if (ShouldPromptForPromotion(position) || ShouldPromptForPromotion(selectedPosition))
|
||||
{
|
||||
PromotePrompt.Show(SessionName, new MovePieceCommand
|
||||
{
|
||||
From = selectedPosition,
|
||||
To = position
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await ShogiApi.Move(SessionName, new MovePieceCommand
|
||||
{
|
||||
From = selectedPosition,
|
||||
IsPromotion = false,
|
||||
To = position
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnClickHand(Piece piece)
|
||||
{
|
||||
selectedPiece = piece.WhichPiece;
|
||||
}
|
||||
}
|
||||
11
Shogi.UI/Pages/Home/GameBoard/EmptyGameBoard.razor
Normal file
11
Shogi.UI/Pages/Home/GameBoard/EmptyGameBoard.razor
Normal file
@@ -0,0 +1,11 @@
|
||||
@using Contracts.Types;
|
||||
|
||||
<GameBoardPresentation Perspective="WhichPlayer.Player1" />
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
Console.WriteLine("Empty Game Board.");
|
||||
}
|
||||
}
|
||||
45
Shogi.UI/Pages/Home/GameBoard/GameBoard.razor
Normal file
45
Shogi.UI/Pages/Home/GameBoard/GameBoard.razor
Normal file
@@ -0,0 +1,45 @@
|
||||
@using Shogi.Contracts.Api
|
||||
@using Shogi.Contracts.Types;
|
||||
@using System.Text.RegularExpressions;
|
||||
@inject IShogiApi ShogiApi
|
||||
@inject AccountState Account;
|
||||
@inject PromotePrompt PromotePrompt;
|
||||
|
||||
@if (session == null)
|
||||
{
|
||||
<EmptyGameBoard />
|
||||
}
|
||||
else if (isSpectating)
|
||||
{
|
||||
<SpectatorGameBoard Session="session" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<SeatedGameBoard Perspective="perspective" Session="session" />
|
||||
}
|
||||
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string? SessionName { get; set; }
|
||||
|
||||
Session? session;
|
||||
private WhichPlayer perspective;
|
||||
private bool isSpectating;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(SessionName))
|
||||
{
|
||||
this.session = await ShogiApi.GetSession(SessionName);
|
||||
if (this.session != null)
|
||||
{
|
||||
var accountId = Account.User?.Id;
|
||||
|
||||
this.perspective = accountId == session.Player2 ? WhichPlayer.Player2 : WhichPlayer.Player1;
|
||||
this.isSpectating = !(accountId == this.session.Player1 || accountId == this.session.Player2);
|
||||
Console.WriteLine($"IsSpectating - {isSpectating}. AccountId - {accountId}. Player1 - {this.session.Player1}. Player2 - {this.session.Player2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
116
Shogi.UI/Pages/Home/GameBoard/GameBoardPresentation.razor
Normal file
116
Shogi.UI/Pages/Home/GameBoard/GameBoardPresentation.razor
Normal file
@@ -0,0 +1,116 @@
|
||||
@using Shogi.Contracts.Types;
|
||||
@inject PromotePrompt PromotePrompt;
|
||||
|
||||
<article class="game-board">
|
||||
<!-- Game board -->
|
||||
<section class="board" data-perspective="@Perspective">
|
||||
@for (var rank = 1; rank < 10; rank++)
|
||||
{
|
||||
foreach (var file in Files)
|
||||
{
|
||||
var position = $"{file}{rank}";
|
||||
var piece = Session?.BoardState.Board[position];
|
||||
var isSelected = piece != null && SelectedPosition == position;
|
||||
<div class="tile" @onclick="OnClickTileInternal(piece, position)"
|
||||
data-position="@(position)"
|
||||
data-selected="@(isSelected)"
|
||||
style="grid-area: @position">
|
||||
<GamePiece Piece="piece" Perspective="Perspective" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div class="ruler vertical" style="grid-area: rank">
|
||||
<span>9</span>
|
||||
<span>8</span>
|
||||
<span>7</span>
|
||||
<span>6</span>
|
||||
<span>5</span>
|
||||
<span>4</span>
|
||||
<span>3</span>
|
||||
<span>2</span>
|
||||
<span>1</span>
|
||||
</div>
|
||||
<div class="ruler" style="grid-area: file">
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
<span>D</span>
|
||||
<span>E</span>
|
||||
<span>F</span>
|
||||
<span>G</span>
|
||||
<span>H</span>
|
||||
<span>I</span>
|
||||
</div>
|
||||
<!-- Promote prompt -->
|
||||
<div class="promote-prompt" data-visible="@PromotePrompt.IsVisible">
|
||||
<p>Do you wish to promote?</p>
|
||||
<div>
|
||||
<button type="button">Yes</button>
|
||||
<button type="button">No</button>
|
||||
<button type="button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Side board -->
|
||||
@if (Session != null)
|
||||
{
|
||||
<aside class="side-board">
|
||||
<div class="hand">
|
||||
@foreach (var piece in OpponentHand)
|
||||
{
|
||||
<div class="tile">
|
||||
<GamePiece Piece="piece" Perspective="Perspective" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="spacer" />
|
||||
|
||||
<div class="hand">
|
||||
@foreach (var piece in UserHand)
|
||||
{
|
||||
<div class="title" @onclick="OnClickHandInternal(piece)">
|
||||
<GamePiece Piece="piece" Perspective="Perspective" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
</article>
|
||||
|
||||
@code {
|
||||
[Parameter] public WhichPlayer Perspective { get; set; }
|
||||
[Parameter] public Session? Session { get; set; }
|
||||
[Parameter] public string? SelectedPosition { get; set; }
|
||||
// TODO: Exchange these OnClick actions for events like "SelectionChangedEvent" and "MoveFromBoardEvent" and "MoveFromHandEvent".
|
||||
[Parameter] public Action<Piece?, string>? OnClickTile { get; set; }
|
||||
[Parameter] public Action<Piece>? OnClickHand { get; set; }
|
||||
|
||||
static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" };
|
||||
|
||||
private IReadOnlyCollection<Piece> OpponentHand
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.Session == null) return Array.Empty<Piece>();
|
||||
|
||||
return Perspective == WhichPlayer.Player1
|
||||
? this.Session.BoardState.Player1Hand
|
||||
: this.Session.BoardState.Player2Hand;
|
||||
}
|
||||
}
|
||||
IReadOnlyCollection<Piece> UserHand
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.Session == null) return Array.Empty<Piece>();
|
||||
|
||||
return Perspective == WhichPlayer.Player1
|
||||
? this.Session.BoardState.Player1Hand
|
||||
: this.Session.BoardState.Player2Hand;
|
||||
}
|
||||
}
|
||||
|
||||
private Action OnClickTileInternal(Piece? piece, string position) => () => OnClickTile?.Invoke(piece, position);
|
||||
private Action OnClickHandInternal(Piece piece) => () => OnClickHand?.Invoke(piece);
|
||||
}
|
||||
71
Shogi.UI/Pages/Home/GameBoard/SeatedGameBoard.razor
Normal file
71
Shogi.UI/Pages/Home/GameBoard/SeatedGameBoard.razor
Normal file
@@ -0,0 +1,71 @@
|
||||
@using Shogi.Contracts.Api;
|
||||
@using Shogi.Contracts.Types;
|
||||
@using System.Text.RegularExpressions;
|
||||
@inject PromotePrompt PromotePrompt;
|
||||
@inject IShogiApi ShogiApi;
|
||||
|
||||
<GameBoardPresentation OnClickHand="OnClickHand" OnClickTile="OnClickTile" Session="Session" Perspective="Perspective" />
|
||||
|
||||
@code {
|
||||
[Parameter] public WhichPlayer Perspective { get; set; }
|
||||
[Parameter] public Session Session { get; set; }
|
||||
private bool IsMyTurn => Session?.BoardState.WhoseTurn == Perspective;
|
||||
private string? selectedBoardPosition;
|
||||
private WhichPiece? selectedPieceFromHand;
|
||||
|
||||
bool ShouldPromptForPromotion(string position)
|
||||
{
|
||||
if (Perspective == WhichPlayer.Player1 && Regex.IsMatch(position, ".[7-9]"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (Perspective == WhichPlayer.Player2 && Regex.IsMatch(position, ".[1-3]"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async void OnClickTile(Piece? piece, string position)
|
||||
{
|
||||
if (!IsMyTurn) return;
|
||||
|
||||
if (selectedBoardPosition == null || piece?.Owner == Perspective)
|
||||
{
|
||||
// Select a position.
|
||||
selectedBoardPosition = position;
|
||||
return;
|
||||
}
|
||||
if (selectedBoardPosition == position)
|
||||
{
|
||||
// Deselect the selected position.
|
||||
selectedBoardPosition = null;
|
||||
return;
|
||||
}
|
||||
if (piece == null)
|
||||
{
|
||||
if (ShouldPromptForPromotion(position) || ShouldPromptForPromotion(selectedBoardPosition))
|
||||
{
|
||||
PromotePrompt.Show(Session.SessionName, new MovePieceCommand
|
||||
{
|
||||
From = selectedBoardPosition,
|
||||
To = position
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await ShogiApi.Move(Session.SessionName, new MovePieceCommand
|
||||
{
|
||||
From = selectedBoardPosition,
|
||||
IsPromotion = false,
|
||||
To = position
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnClickHand(Piece piece)
|
||||
{
|
||||
selectedPieceFromHand = piece.WhichPiece;
|
||||
}
|
||||
}
|
||||
8
Shogi.UI/Pages/Home/GameBoard/SpectatorGameBoard.razor
Normal file
8
Shogi.UI/Pages/Home/GameBoard/SpectatorGameBoard.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
@using Contracts.Types;
|
||||
|
||||
<p>You are spectating</p>
|
||||
<GameBoardPresentation Perspective="WhichPlayer.Player1" Session="Session" />
|
||||
|
||||
@code {
|
||||
[Parameter] public Session Session { get; set; }
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
@using Shogi.Contracts.Types
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using System.Net
|
||||
@using Shogi.Contracts.Types;
|
||||
@using System.ComponentModel.DataAnnotations;
|
||||
@using System.Net;
|
||||
@using System.Text.Json;
|
||||
@inject IShogiApi ShogiApi;
|
||||
@inject ShogiSocket ShogiSocket;
|
||||
@inject AccountState Account;
|
||||
@@ -17,12 +18,27 @@
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="search-pane">
|
||||
<h3>Games you're seated at</h3>
|
||||
<div class="list-group">
|
||||
@if (!sessions.Any())
|
||||
@if (!joinedSessions.Any())
|
||||
{
|
||||
<p>No games exist</p>
|
||||
<p>You have not joined any games.</p>
|
||||
}
|
||||
@foreach (var session in sessions)
|
||||
@foreach (var session in joinedSessions)
|
||||
{
|
||||
<button class="list-group-item list-group-item-action @ActiveCss(session)" @onclick="() => OnClickSession(session)">
|
||||
<span>@session.Name</span>
|
||||
<span>(@session.PlayerCount/2)</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<h3>Other games</h3>
|
||||
<div class="list-group">
|
||||
@if (!otherSessions.Any())
|
||||
{
|
||||
<p>You have not joined any games.</p>
|
||||
}
|
||||
@foreach (var session in otherSessions)
|
||||
{
|
||||
<button class="list-group-item list-group-item-action @ActiveCss(session)" @onclick="() => OnClickSession(session)">
|
||||
<span>@session.Name</span>
|
||||
@@ -69,16 +85,19 @@
|
||||
@code {
|
||||
[Parameter]
|
||||
public Action<SessionMetadata>? ActiveSessionChanged { get; set; }
|
||||
CreateForm createForm = new();
|
||||
SessionMetadata[] sessions = Array.Empty<SessionMetadata>();
|
||||
SessionMetadata? activeSession;
|
||||
HttpStatusCode? createSessionStatusCode;
|
||||
|
||||
private CreateForm createForm = new();
|
||||
private SessionMetadata[] joinedSessions = Array.Empty<SessionMetadata>();
|
||||
private SessionMetadata[] otherSessions = Array.Empty<SessionMetadata>();
|
||||
private SessionMetadata? activeSession;
|
||||
private HttpStatusCode? createSessionStatusCode;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
ShogiSocket.OnCreateGameMessage += async (sender, message) => await FetchSessions();
|
||||
Account.LoginChangedEvent += async (sender, message) =>
|
||||
{
|
||||
Console.WriteLine($"LoginEvent. Message={JsonSerializer.Serialize(message)}.");
|
||||
if (message.User != null)
|
||||
{
|
||||
await FetchSessions();
|
||||
@@ -99,7 +118,8 @@
|
||||
var sessions = await ShogiApi.GetSessionsPlayerCount();
|
||||
if (sessions != null)
|
||||
{
|
||||
this.sessions = sessions.PlayerHasJoinedSessions.Concat(sessions.AllOtherSessions).ToArray();
|
||||
this.joinedSessions = sessions.PlayerHasJoinedSessions.ToArray();
|
||||
this.otherSessions = sessions.AllOtherSessions.ToArray();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,14 @@
|
||||
}
|
||||
<PageHeader />
|
||||
<GameBrowser ActiveSessionChanged="OnChangeSession" />
|
||||
<GameBoard SessionName="@activeSessionName" />
|
||||
@if (Account.User == null || activeSessionName == null)
|
||||
{
|
||||
<EmptyGameBoard />
|
||||
}
|
||||
else
|
||||
{
|
||||
<GameBoard SessionName="@activeSessionName" />
|
||||
}
|
||||
</main>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@if (user != null)
|
||||
{
|
||||
<div class="user">
|
||||
<div>@user.DisplayName</div>
|
||||
<div>@user.Value.DisplayName</div>
|
||||
<button type="button" class="logout" @onclick="AccountManager.LogoutAsync">Logout</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user