@page "/application" @page "/application.html" @using System.Net @using System.Net.Http.Json @using System.Security.Claims @using ChessCubing.App.Models.Users @implements IDisposable @inject BrowserBridge Browser @inject MatchStore Store @inject NavigationManager Navigation @inject AuthenticationStateProvider AuthenticationStateProvider @inject HttpClient Http @inject SocialRealtimeService Realtime ChessCubing Arena | Application
Icone ChessCubing

Application officielle de match

ChessCubing Arena

Nouvelle rencontre

Configuration

Les reglages ci-dessous preparent les pages chrono et cube.

@if (Form.CompetitionMode) { }
Mode officiel
Cadence du match
Temps personnalises
@if (!UsesMoveLimit) { } @if (UsesMoveLimit) { }
@if (IsAuthenticated) {
@if (!string.IsNullOrWhiteSpace(SocialLoadError)) {

@SocialLoadError

} @if (!string.IsNullOrWhiteSpace(InviteActionError)) {

@InviteActionError

} @if (!string.IsNullOrWhiteSpace(Realtime.LastInviteNotice)) { } @if (Realtime.OutgoingPlayInvite is not null) {
En attente @Realtime.OutgoingPlayInvite.RecipientDisplayName

Invitation envoyee pour jouer cote @(BuildColorLabel(Realtime.OutgoingPlayInvite.RecipientColor)).

} @if (IsSocialLoading) { } else if (OnlineFriends.Length > 0) {
@foreach (var friend in OnlineFriends) {
}
} else { }
} @if (Form.CompetitionMode) { }
@CurrentMode.Label @CurrentPreset.Description @TimingText @TimeImpact @QuotaText
Consulter le reglement
@code { private SetupFormModel Form { get; set; } = new(); private bool _ready; private bool IsAuthenticated; private bool IsSocialLoading; private int _knownSocialVersion; private string? ConnectedPlayerName; private string? SocialLoadError; private string? InviteActionError; private SocialOverviewResponse? SocialOverview; private MatchState? CurrentMatch => Store.Current; private string SetupBodyClass => UsesMoveLimit ? string.Empty : "time-setup-mode"; private bool CanUseConnectedPlayerName => !string.IsNullOrWhiteSpace(ConnectedPlayerName); private SocialFriendResponse[] OnlineFriends => SocialOverview?.Friends .Where(friend => ResolveOnlineStatus(friend.Subject, friend.IsOnline)) .OrderBy(friend => friend.DisplayName, StringComparer.OrdinalIgnoreCase) .ToArray() ?? []; private bool UsesMoveLimit => MatchEngine.UsesMoveLimit(Form.Mode); private MatchPresetInfo CurrentPreset => MatchEngine.Presets.TryGetValue(Form.Preset, out var preset) ? preset : MatchEngine.Presets[MatchEngine.PresetFast]; private MatchModeInfo CurrentMode => MatchEngine.Modes.TryGetValue(Form.Mode, out var mode) ? mode : MatchEngine.Modes[MatchEngine.ModeTwice]; private string TimingText => UsesMoveLimit ? $"Temps configures : Block {MatchEngine.FormatClock(Form.BlockMinutes * 60_000L)}, coup {MatchEngine.FormatClock(Form.MoveSeconds * 1_000L)}." : $"Temps configures : Block {MatchEngine.FormatClock(Form.BlockMinutes * 60_000L)}, temps de chaque joueur {MatchEngine.FormatClock(Form.TimeInitialMinutes * 60_000L)}."; private string TimeImpact => Form.Mode == MatchEngine.ModeTime ? $"Chronos cumules de {MatchEngine.FormatClock(Form.TimeInitialMinutes * 60_000L)} par joueur, ajustes apres chaque phase cube avec plafond de 120 s pris en compte. Aucun temps par coup en mode Time." : "Le gagnant du cube commence le Block suivant, avec double coup V2 possible."; private string QuotaText => UsesMoveLimit ? $"Quota actif : {CurrentPreset.Quota} coups par joueur." : $"Quota actif : {CurrentPreset.Quota} coups par joueur et par Block."; protected override async Task OnInitializedAsync() { AuthenticationStateProvider.AuthenticationStateChanged += HandleAuthenticationStateChanged; Realtime.Changed += HandleRealtimeChanged; await Realtime.EnsureStartedAsync(); await LoadApplicationContextAsync(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) { return; } await Store.EnsureLoadedAsync(); _ready = true; StateHasChanged(); } private void HandleAuthenticationStateChanged(Task authenticationStateTask) => _ = InvokeAsync(LoadApplicationContextAsync); private void HandleRealtimeChanged() => _ = InvokeAsync(HandleRealtimeChangedAsync); private async Task HandleRealtimeChangedAsync() { ApplyAcceptedPlaySession(); if (IsAuthenticated && _knownSocialVersion != Realtime.SocialVersion) { _knownSocialVersion = Realtime.SocialVersion; await LoadSocialOverviewAsync(); return; } StateHasChanged(); } private async Task LoadApplicationContextAsync() { string? fallbackName = null; try { var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); var user = authState.User; if (user.Identity?.IsAuthenticated != true) { IsAuthenticated = false; ConnectedPlayerName = null; ResetSocialState(); await InvokeAsync(StateHasChanged); return; } IsAuthenticated = true; fallbackName = BuildConnectedPlayerFallback(user); var response = await Http.GetAsync("api/users/me"); if (!response.IsSuccessStatusCode) { if (response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) { IsAuthenticated = false; ConnectedPlayerName = null; ResetSocialState(); await InvokeAsync(StateHasChanged); return; } ConnectedPlayerName = fallbackName; await LoadSocialOverviewAsync(); await InvokeAsync(StateHasChanged); return; } var profile = await response.Content.ReadFromJsonAsync(); ConnectedPlayerName = !string.IsNullOrWhiteSpace(profile?.DisplayName) ? profile.DisplayName : fallbackName; await LoadSocialOverviewAsync(); } catch { ConnectedPlayerName = fallbackName; } ApplyAcceptedPlaySession(); await InvokeAsync(StateHasChanged); } private async Task LoadSocialOverviewAsync() { if (!IsAuthenticated) { ResetSocialState(); return; } IsSocialLoading = true; SocialLoadError = null; try { var response = await Http.GetAsync("api/social/overview"); if (!response.IsSuccessStatusCode) { SocialLoadError = response.StatusCode switch { HttpStatusCode.Unauthorized => "La session a expire. Reconnecte-toi puis recharge la page.", _ => "Le chargement des amis a echoue.", }; SocialOverview = null; return; } SocialOverview = await response.Content.ReadFromJsonAsync() ?? new SocialOverviewResponse(); _knownSocialVersion = Realtime.SocialVersion; } catch (HttpRequestException) { SocialLoadError = "Le service social est temporairement indisponible."; SocialOverview = null; } catch (TaskCanceledException) { SocialLoadError = "Le chargement des amis a pris trop de temps."; SocialOverview = null; } finally { IsSocialLoading = false; StateHasChanged(); } } private async Task HandleSubmit() { await Store.EnsureLoadedAsync(); var match = MatchEngine.CreateMatch(Form.ToMatchConfig()); Store.SetCurrent(match); try { await Store.SaveAsync(); } catch { // La navigation vers la phase chrono doit rester possible meme si la persistence // du navigateur echoue ponctuellement. } Navigation.NavigateTo("/chrono.html"); } private void LoadDemo() => Form = SetupFormModel.CreateDemo(); private void FillWhiteWithConnectedPlayer() { if (!CanUseConnectedPlayerName) { return; } Form.WhiteName = ConnectedPlayerName!; } private void FillBlackWithConnectedPlayer() { if (!CanUseConnectedPlayerName) { return; } Form.BlackName = ConnectedPlayerName!; } private async Task InviteFriendToPlayAsync(string friendSubject, string recipientColor) { InviteActionError = null; try { await Realtime.SendPlayInviteAsync(friendSubject, recipientColor); } catch (InvalidOperationException exception) { InviteActionError = exception.Message; } catch (Exception exception) { InviteActionError = string.IsNullOrWhiteSpace(exception.Message) ? "L'invitation de partie n'a pas pu etre envoyee." : exception.Message; } } private async Task CancelOutgoingPlayInviteAsync() { InviteActionError = null; try { await Realtime.CancelOutgoingPlayInviteAsync(); } catch (Exception exception) { InviteActionError = string.IsNullOrWhiteSpace(exception.Message) ? "L'invitation de partie n'a pas pu etre annulee." : exception.Message; } } private Task InviteFriendAsWhiteAsync(string friendSubject) => InviteFriendToPlayAsync(friendSubject, "white"); private Task InviteFriendAsBlackAsync(string friendSubject) => InviteFriendToPlayAsync(friendSubject, "black"); private void SetMode(string mode) => Form.Mode = mode; private void SetPreset(string preset) => Form.Preset = preset; private void ResumeMatch() { if (CurrentMatch is null) { return; } Navigation.NavigateTo(MatchEngine.RouteForMatch(CurrentMatch)); } private async Task ClearMatchAsync() { await Store.ClearAsync(); StateHasChanged(); } private Task ForceRefreshAsync() => Browser.ForceRefreshAsync("/application.html").AsTask(); private static string ResumeMatchLabel(MatchState match) => string.IsNullOrWhiteSpace(match.Config.MatchLabel) ? "Rencontre ChessCubing" : match.Config.MatchLabel; private static string CurrentMatchMode(MatchState match) => MatchEngine.Modes.TryGetValue(match.Config.Mode, out var mode) ? mode.Label : "ChessCubing Twice"; private static string ResumePhaseLabel(MatchState match) { if (!string.IsNullOrEmpty(match.Result)) { return MatchEngine.ResultText(match); } return match.Phase == MatchEngine.PhaseCube ? "Page cube prete" : "Page chrono prete"; } private string BuildPrefillTitle(string color) => $"Utiliser mon nom cote {color}"; private void ApplyAcceptedPlaySession() { var session = Realtime.TakeAcceptedPlaySession(); if (session is null) { return; } Form.WhiteName = session.WhiteName; Form.BlackName = session.BlackName; } private bool ResolveOnlineStatus(string subject, bool fallbackStatus) => Realtime.GetKnownOnlineStatus(subject) ?? fallbackStatus; private string BuildPresenceClass(string subject, bool fallbackStatus) => ResolveOnlineStatus(subject, fallbackStatus) ? "presence-badge online" : "presence-badge"; private string BuildPresenceLabel(string subject, bool fallbackStatus) => ResolveOnlineStatus(subject, fallbackStatus) ? "En ligne" : "Hors ligne"; private static string BuildColorLabel(string color) => string.Equals(color, "white", StringComparison.OrdinalIgnoreCase) ? "blanc" : "noir"; private void ResetSocialState() { SocialOverview = null; SocialLoadError = null; InviteActionError = null; _knownSocialVersion = Realtime.SocialVersion; } private static string? BuildConnectedPlayerFallback(ClaimsPrincipal user) => FirstNonEmpty( user.FindFirst("name")?.Value, user.FindFirst(ClaimTypes.Name)?.Value, user.FindFirst("preferred_username")?.Value, user.FindFirst(ClaimTypes.Email)?.Value); private static string? FirstNonEmpty(params string?[] candidates) => candidates.FirstOrDefault(candidate => !string.IsNullOrWhiteSpace(candidate)); public void Dispose() { AuthenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged; Realtime.Changed -= HandleRealtimeChanged; } }