Architektur-Redesign: SignalR-Multiplayer & MVVM
Umfassende Umstrukturierung der SlipItIn-App für eine moderne, zustandsbasierte und SignalR-gestützte Multiplayer-Architektur. Einführung neuer Services für Auth, API, SignalR, lokalen Speicher und Game-State-Management via Dependency Injection. Komplette Neugestaltung der Views (Login, Registrierung, Lobby, Spielbrett) mit MVVM und CommunityToolkit.Mvvm. Navigation jetzt über MAUI Shell mit expliziten Routen. Game-Logik auf Events/Messaging umgestellt, UI und Logik entkoppelt, Echtzeit-Updates integriert. SignalR für alle Spielaktionen inkl. Wiederverbindung und Offline-Queueing. Lokaler Speicher für Auth, Userdaten, GameState und Aktionen. MainPage entfernt, neue Views implementiert. Projektdatei um NuGet-Pakete und Shared-Projekt erweitert. Backend-Änderungen für direkte Spieler-Zuordnung und korrekte EF Core-Relationen. App ist nun robust, testbar und unterstützt Multiplayer- sowie Offline-Szenarien.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
var builder = DistributedApplication.CreateBuilder(args);
|
||||
|
||||
var db = builder.AddPostgres("pgsql")
|
||||
.AddDatabase("postgresdb");
|
||||
.AddDatabase("postgresdb")
|
||||
;
|
||||
|
||||
var server = builder.AddProject<Projects.SlipItIn_Server>("server")
|
||||
.WithReference(db)
|
||||
|
||||
@@ -29,11 +29,15 @@ public class GameService : IGameService
|
||||
Status = GameStatus.Lobby
|
||||
};
|
||||
|
||||
context.Games.Add(game);
|
||||
|
||||
// Host wird automatisch als erster Spieler hinzugefügt
|
||||
var hostPlayer = new Player { UserId = host.Id, GameId = game.Id, ConnectionId = connectionId };
|
||||
context.Players.Add(hostPlayer);
|
||||
var hostPlayer = new Player { UserId = host.Id, ConnectionId = connectionId };
|
||||
|
||||
// Spieler direkt zur Liste des Spiels hinzufügen
|
||||
game.Players.Add(hostPlayer);
|
||||
|
||||
// Es reicht, nur das Game hinzuzufügen — EF Core fügt den Player automatisch mit hinzu
|
||||
context.Games.Add(game);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SlipItIn.Infrastructure;
|
||||
using SlipItIn.Services.Interfaces;
|
||||
|
||||
namespace SlipItIn;
|
||||
|
||||
@@ -7,10 +8,34 @@ public partial class App : Application
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
protected override Window CreateWindow(IActivationState? activationState)
|
||||
{
|
||||
return new Window(new AppShell());
|
||||
var window = new Window(new AppShell());
|
||||
window.Resumed += async (_, _) =>
|
||||
{
|
||||
var gameStateService = ServiceHelper.GetRequiredService<IGameStateService>();
|
||||
await gameStateService.ResyncAsync();
|
||||
};
|
||||
return window;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task InitializeAsync()
|
||||
{
|
||||
var authSession = ServiceHelper.GetRequiredService<IAuthSessionService>();
|
||||
var signalR = ServiceHelper.GetRequiredService<ISignalRService>();
|
||||
var gameStateService = ServiceHelper.GetRequiredService<IGameStateService>();
|
||||
|
||||
await authSession.InitializeAsync();
|
||||
await gameStateService.InitializeAsync();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(authSession.AccessToken))
|
||||
{
|
||||
var connected = await signalR.ConnectAsync(authSession.AccessToken);
|
||||
if (connected)
|
||||
await gameStateService.ResyncAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,22 @@
|
||||
x:Class="SlipItIn.AppShell"
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:local="clr-namespace:SlipItIn"
|
||||
Title="Slip It In">
|
||||
xmlns:views="clr-namespace:SlipItIn.Views">
|
||||
|
||||
<ShellContent
|
||||
Title="Home"
|
||||
ContentTemplate="{DataTemplate local:MainPage}"
|
||||
Route="MainPage" />
|
||||
<ShellContent
|
||||
Route="LoginPage"
|
||||
ContentTemplate="{DataTemplate views:LoginPage}" />
|
||||
|
||||
<ShellContent
|
||||
Route="RegisterPage"
|
||||
ContentTemplate="{DataTemplate views:RegisterPage}" />
|
||||
|
||||
<ShellContent
|
||||
Route="LobbyPage"
|
||||
ContentTemplate="{DataTemplate views:LobbyPage}" />
|
||||
|
||||
<ShellContent
|
||||
Route="GameBoardPage"
|
||||
ContentTemplate="{DataTemplate views:GameBoardPage}" />
|
||||
|
||||
</Shell>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace SlipItIn;
|
||||
using SlipItIn.Views;
|
||||
|
||||
namespace SlipItIn;
|
||||
|
||||
public partial class AppShell : Shell
|
||||
{
|
||||
|
||||
16
SlipItIn/Infrastructure/ServiceHelper.cs
Normal file
16
SlipItIn/Infrastructure/ServiceHelper.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace SlipItIn.Infrastructure;
|
||||
|
||||
public static class ServiceHelper
|
||||
{
|
||||
public static IServiceProvider? Services { get; set; }
|
||||
|
||||
public static T GetRequiredService<T>() where T : notnull
|
||||
{
|
||||
if (Services is null)
|
||||
throw new InvalidOperationException("Service provider is not initialized.");
|
||||
|
||||
return Services.GetRequiredService<T>();
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="SlipItIn.MainPage">
|
||||
|
||||
<ScrollView>
|
||||
<VerticalStackLayout
|
||||
Padding="30,0"
|
||||
Spacing="25">
|
||||
<Image
|
||||
Source="dotnet_bot.png"
|
||||
HeightRequest="185"
|
||||
Aspect="AspectFit"
|
||||
SemanticProperties.Description="dot net bot in a submarine number ten" />
|
||||
|
||||
<Label
|
||||
Text="Hello, World!"
|
||||
Style="{StaticResource Headline}"
|
||||
SemanticProperties.HeadingLevel="Level1" />
|
||||
|
||||
<Label
|
||||
Text="Welcome to .NET Multi-platform App UI"
|
||||
Style="{StaticResource SubHeadline}"
|
||||
SemanticProperties.HeadingLevel="Level2"
|
||||
SemanticProperties.Description="Welcome to dot net Multi platform App U I" />
|
||||
|
||||
<Button
|
||||
x:Name="CounterBtn"
|
||||
Text="Click me"
|
||||
SemanticProperties.Hint="Counts the number of times you click"
|
||||
Clicked="OnCounterClicked"
|
||||
HorizontalOptions="Fill" />
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</ContentPage>
|
||||
@@ -1,23 +0,0 @@
|
||||
namespace SlipItIn;
|
||||
|
||||
public partial class MainPage : ContentPage
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
public MainPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnCounterClicked(object? sender, EventArgs e)
|
||||
{
|
||||
count++;
|
||||
|
||||
if (count == 1)
|
||||
CounterBtn.Text = $"Clicked {count} time";
|
||||
else
|
||||
CounterBtn.Text = $"Clicked {count} times";
|
||||
|
||||
SemanticScreenReader.Announce(CounterBtn.Text);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SlipItIn.Infrastructure;
|
||||
using SlipItIn.Services;
|
||||
using SlipItIn.Services.Interfaces;
|
||||
using SlipItIn.ViewModels;
|
||||
|
||||
namespace SlipItIn;
|
||||
|
||||
@@ -15,10 +19,24 @@ public static class MauiProgram
|
||||
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<IAppConfigurationService, AppConfigurationService>();
|
||||
builder.Services.AddSingleton<ILocalStorageService, LocalStorageService>();
|
||||
builder.Services.AddSingleton<IAuthSessionService, AuthSessionService>();
|
||||
builder.Services.AddSingleton<IApiService, ApiService>();
|
||||
builder.Services.AddSingleton<ISignalRService, SignalRService>();
|
||||
builder.Services.AddSingleton<IGameStateService, GameStateService>();
|
||||
|
||||
builder.Services.AddTransient<LoginViewModel>();
|
||||
builder.Services.AddTransient<RegisterViewModel>();
|
||||
builder.Services.AddSingleton<LobbyViewModel>();
|
||||
builder.Services.AddSingleton<GameBoardViewModel>();
|
||||
|
||||
#if DEBUG
|
||||
builder.Logging.AddDebug();
|
||||
#endif
|
||||
|
||||
return builder.Build();
|
||||
var app = builder.Build();
|
||||
ServiceHelper.Services = app.Services;
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
11
SlipItIn/Messages/ChallengeReceivedMessage.cs
Normal file
11
SlipItIn/Messages/ChallengeReceivedMessage.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.Messages;
|
||||
|
||||
public class ChallengeReceivedMessage : ValueChangedMessage<SlipChallengeDto>
|
||||
{
|
||||
public ChallengeReceivedMessage(SlipChallengeDto value) : base(value)
|
||||
{
|
||||
}
|
||||
}
|
||||
10
SlipItIn/Messages/ConnectionStateChangedMessage.cs
Normal file
10
SlipItIn/Messages/ConnectionStateChangedMessage.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace SlipItIn.Messages;
|
||||
|
||||
public class ConnectionStateChangedMessage : ValueChangedMessage<bool>
|
||||
{
|
||||
public ConnectionStateChangedMessage(bool value) : base(value)
|
||||
{
|
||||
}
|
||||
}
|
||||
10
SlipItIn/Messages/ErrorOccurredMessage.cs
Normal file
10
SlipItIn/Messages/ErrorOccurredMessage.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace SlipItIn.Messages;
|
||||
|
||||
public class ErrorOccurredMessage : ValueChangedMessage<string>
|
||||
{
|
||||
public ErrorOccurredMessage(string value) : base(value)
|
||||
{
|
||||
}
|
||||
}
|
||||
10
SlipItIn/Messages/GameStartedMessage.cs
Normal file
10
SlipItIn/Messages/GameStartedMessage.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace SlipItIn.Messages;
|
||||
|
||||
public class GameStartedMessage : ValueChangedMessage<int>
|
||||
{
|
||||
public GameStartedMessage(int value) : base(value)
|
||||
{
|
||||
}
|
||||
}
|
||||
11
SlipItIn/Messages/GameStateChangedMessage.cs
Normal file
11
SlipItIn/Messages/GameStateChangedMessage.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.Messages;
|
||||
|
||||
public class GameStateChangedMessage : ValueChangedMessage<GameStateDto>
|
||||
{
|
||||
public GameStateChangedMessage(GameStateDto value) : base(value)
|
||||
{
|
||||
}
|
||||
}
|
||||
10
SlipItIn/Messages/LobbyCreatedMessage.cs
Normal file
10
SlipItIn/Messages/LobbyCreatedMessage.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace SlipItIn.Messages;
|
||||
|
||||
public class LobbyCreatedMessage : ValueChangedMessage<(int GameId, string LobbyCode)>
|
||||
{
|
||||
public LobbyCreatedMessage((int GameId, string LobbyCode) value) : base(value)
|
||||
{
|
||||
}
|
||||
}
|
||||
11
SlipItIn/Messages/PlayerHandChangedMessage.cs
Normal file
11
SlipItIn/Messages/PlayerHandChangedMessage.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.Messages;
|
||||
|
||||
public class PlayerHandChangedMessage : ValueChangedMessage<PlayerHandDto>
|
||||
{
|
||||
public PlayerHandChangedMessage(PlayerHandDto value) : base(value)
|
||||
{
|
||||
}
|
||||
}
|
||||
8
SlipItIn/Models/QueuedGameAction.cs
Normal file
8
SlipItIn/Models/QueuedGameAction.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SlipItIn.Models;
|
||||
|
||||
public class QueuedGameAction
|
||||
{
|
||||
public string Method { get; set; } = string.Empty;
|
||||
public string ArgumentsJson { get; set; } = "[]";
|
||||
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
84
SlipItIn/Services/ApiService.cs
Normal file
84
SlipItIn/Services/ApiService.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using SlipItIn.Services.Interfaces;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.Services;
|
||||
|
||||
public class ApiService : IApiService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IAppConfigurationService _configuration;
|
||||
private readonly IAuthSessionService _authSession;
|
||||
|
||||
public ApiService(IAppConfigurationService configuration, IAuthSessionService authSession)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_authSession = authSession;
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(_configuration.ApiBaseUrl)
|
||||
};
|
||||
}
|
||||
|
||||
public Task<AuthResponseDto> RegisterAsync(RegisterRequestDto request, CancellationToken cancellationToken = default)
|
||||
=> PostAuthAsync("api/auth/register", request, cancellationToken);
|
||||
|
||||
public Task<AuthResponseDto> LoginAsync(LoginRequestDto request, CancellationToken cancellationToken = default)
|
||||
=> PostAuthAsync("api/auth/login", request, cancellationToken);
|
||||
|
||||
public Task LogoutAsync() => _authSession.ClearSessionAsync();
|
||||
|
||||
private async Task<AuthResponseDto> PostAuthAsync<TRequest>(string endpoint, TRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
AttachAuthHeader();
|
||||
|
||||
var response = await _httpClient.PostAsJsonAsync(endpoint, request, cancellationToken);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var payload = await response.Content.ReadFromJsonAsync<AuthResponseDto>(cancellationToken: cancellationToken);
|
||||
if (payload is null)
|
||||
throw new InvalidOperationException("Leere Auth-Antwort vom Server.");
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
var message = await TryReadErrorMessageAsync(response, cancellationToken);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
private void AttachAuthHeader()
|
||||
{
|
||||
var token = _authSession.AccessToken;
|
||||
_httpClient.DefaultRequestHeaders.Authorization = string.IsNullOrWhiteSpace(token)
|
||||
? null
|
||||
: new AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
|
||||
private static async Task<string> TryReadErrorMessageAsync(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||
{
|
||||
var raw = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
return $"Serverfehler ({(int)response.StatusCode}).";
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(raw);
|
||||
if (doc.RootElement.ValueKind == JsonValueKind.Object &&
|
||||
doc.RootElement.TryGetProperty("message", out var msgElement))
|
||||
{
|
||||
var msg = msgElement.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(msg))
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
12
SlipItIn/Services/AppConfigurationService.cs
Normal file
12
SlipItIn/Services/AppConfigurationService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using SlipItIn.Services.Interfaces;
|
||||
|
||||
namespace SlipItIn.Services;
|
||||
|
||||
public class AppConfigurationService : IAppConfigurationService
|
||||
{
|
||||
private const string DefaultApiBaseUrl = "https://localhost:7274";
|
||||
|
||||
public string ApiBaseUrl => DefaultApiBaseUrl;
|
||||
|
||||
public string HubUrl => $"{DefaultApiBaseUrl.TrimEnd('/')}/hubs/game";
|
||||
}
|
||||
42
SlipItIn/Services/AuthSessionService.cs
Normal file
42
SlipItIn/Services/AuthSessionService.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using SlipItIn.Services.Interfaces;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.Services;
|
||||
|
||||
public class AuthSessionService : IAuthSessionService
|
||||
{
|
||||
private readonly ILocalStorageService _localStorageService;
|
||||
|
||||
public AuthSessionService(ILocalStorageService localStorageService)
|
||||
{
|
||||
_localStorageService = localStorageService;
|
||||
}
|
||||
|
||||
public string? AccessToken { get; private set; }
|
||||
public AuthResponseDto? CurrentUser { get; private set; }
|
||||
public bool IsAuthenticated => !string.IsNullOrWhiteSpace(AccessToken) && CurrentUser is not null;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
AccessToken = await _localStorageService.GetAuthTokenAsync();
|
||||
CurrentUser = await _localStorageService.GetAuthUserAsync();
|
||||
}
|
||||
|
||||
public async Task SetSessionAsync(AuthResponseDto authResponse)
|
||||
{
|
||||
AccessToken = authResponse.Token;
|
||||
CurrentUser = authResponse;
|
||||
|
||||
await _localStorageService.SaveAuthTokenAsync(authResponse.Token);
|
||||
await _localStorageService.SaveAuthUserAsync(authResponse);
|
||||
}
|
||||
|
||||
public async Task ClearSessionAsync()
|
||||
{
|
||||
AccessToken = null;
|
||||
CurrentUser = null;
|
||||
|
||||
await _localStorageService.ClearAuthTokenAsync();
|
||||
await _localStorageService.ClearAuthUserAsync();
|
||||
}
|
||||
}
|
||||
119
SlipItIn/Services/GameStateService.cs
Normal file
119
SlipItIn/Services/GameStateService.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System.Text.Json;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using SlipItIn.Messages;
|
||||
using SlipItIn.Models;
|
||||
using SlipItIn.Services.Interfaces;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.Services;
|
||||
|
||||
public class GameStateService : IGameStateService
|
||||
{
|
||||
private readonly ISignalRService _signalR;
|
||||
private readonly ILocalStorageService _localStorage;
|
||||
private readonly IMessenger _messenger;
|
||||
|
||||
private readonly List<QueuedGameAction> _queuedActions = [];
|
||||
|
||||
public GameStateDto? CurrentGameState { get; private set; }
|
||||
public PlayerHandDto? CurrentPlayerHand { get; private set; }
|
||||
public SlipChallengeDto? PendingChallenge { get; private set; }
|
||||
|
||||
public GameStateService(ISignalRService signalR, ILocalStorageService localStorage)
|
||||
{
|
||||
_signalR = signalR;
|
||||
_localStorage = localStorage;
|
||||
_messenger = WeakReferenceMessenger.Default;
|
||||
|
||||
_signalR.LobbyCreated += (_, value) => _messenger.Send(new LobbyCreatedMessage(value));
|
||||
|
||||
_signalR.GameStateUpdated += async (_, state) =>
|
||||
{
|
||||
CurrentGameState = state;
|
||||
await _localStorage.SaveGameStateAsync(state);
|
||||
_messenger.Send(new GameStateChangedMessage(state));
|
||||
};
|
||||
|
||||
_signalR.GameStarted += (_, gameId) => _messenger.Send(new GameStartedMessage(gameId));
|
||||
|
||||
_signalR.PlayerHandUpdated += async (_, hand) =>
|
||||
{
|
||||
CurrentPlayerHand = hand;
|
||||
await _localStorage.SavePlayerHandAsync(hand);
|
||||
_messenger.Send(new PlayerHandChangedMessage(hand));
|
||||
};
|
||||
|
||||
_signalR.ChallengeReceived += (_, challenge) =>
|
||||
{
|
||||
PendingChallenge = challenge;
|
||||
_messenger.Send(new ChallengeReceivedMessage(challenge));
|
||||
};
|
||||
|
||||
_signalR.ErrorReceived += (_, error) => _messenger.Send(new ErrorOccurredMessage(error));
|
||||
_signalR.ConnectionStateChanged += (_, connected) => _messenger.Send(new ConnectionStateChangedMessage(connected));
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
CurrentGameState = await _localStorage.GetGameStateAsync();
|
||||
CurrentPlayerHand = await _localStorage.GetPlayerHandAsync();
|
||||
|
||||
var queued = await _localStorage.GetQueuedActionsAsync();
|
||||
_queuedActions.Clear();
|
||||
_queuedActions.AddRange(queued.OrderBy(a => a.CreatedAtUtc));
|
||||
|
||||
if (CurrentGameState is not null)
|
||||
_messenger.Send(new GameStateChangedMessage(CurrentGameState));
|
||||
|
||||
if (CurrentPlayerHand is not null)
|
||||
_messenger.Send(new PlayerHandChangedMessage(CurrentPlayerHand));
|
||||
}
|
||||
|
||||
public async Task ResyncAsync()
|
||||
{
|
||||
if (CurrentGameState?.GameId > 0 && _signalR.IsConnected)
|
||||
await _signalR.RequestGameStateAsync(CurrentGameState.GameId);
|
||||
|
||||
await FlushQueuedActionsAsync();
|
||||
}
|
||||
|
||||
public async Task EnqueueActionAsync(string method, params object[] arguments)
|
||||
{
|
||||
var action = new QueuedGameAction
|
||||
{
|
||||
Method = method,
|
||||
ArgumentsJson = JsonSerializer.Serialize(arguments),
|
||||
CreatedAtUtc = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_queuedActions.Add(action);
|
||||
await _localStorage.SaveQueuedActionsAsync(_queuedActions);
|
||||
}
|
||||
|
||||
public async Task FlushQueuedActionsAsync()
|
||||
{
|
||||
if (!_signalR.IsConnected || _queuedActions.Count == 0)
|
||||
return;
|
||||
|
||||
var snapshot = _queuedActions.ToList();
|
||||
foreach (var action in snapshot)
|
||||
{
|
||||
await _signalR.SendQueuedActionAsync(action);
|
||||
_queuedActions.Remove(action);
|
||||
}
|
||||
|
||||
await _localStorage.SaveQueuedActionsAsync(_queuedActions);
|
||||
}
|
||||
|
||||
public async Task ClearLocalGameDataAsync()
|
||||
{
|
||||
CurrentGameState = null;
|
||||
CurrentPlayerHand = null;
|
||||
PendingChallenge = null;
|
||||
_queuedActions.Clear();
|
||||
|
||||
await _localStorage.ClearGameStateAsync();
|
||||
await _localStorage.ClearPlayerHandAsync();
|
||||
await _localStorage.ClearQueuedActionsAsync();
|
||||
}
|
||||
}
|
||||
10
SlipItIn/Services/Interfaces/IApiService.cs
Normal file
10
SlipItIn/Services/Interfaces/IApiService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.Services.Interfaces;
|
||||
|
||||
public interface IApiService
|
||||
{
|
||||
Task<AuthResponseDto> RegisterAsync(RegisterRequestDto request, CancellationToken cancellationToken = default);
|
||||
Task<AuthResponseDto> LoginAsync(LoginRequestDto request, CancellationToken cancellationToken = default);
|
||||
Task LogoutAsync();
|
||||
}
|
||||
7
SlipItIn/Services/Interfaces/IAppConfigurationService.cs
Normal file
7
SlipItIn/Services/Interfaces/IAppConfigurationService.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace SlipItIn.Services.Interfaces;
|
||||
|
||||
public interface IAppConfigurationService
|
||||
{
|
||||
string ApiBaseUrl { get; }
|
||||
string HubUrl { get; }
|
||||
}
|
||||
14
SlipItIn/Services/Interfaces/IAuthSessionService.cs
Normal file
14
SlipItIn/Services/Interfaces/IAuthSessionService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.Services.Interfaces;
|
||||
|
||||
public interface IAuthSessionService
|
||||
{
|
||||
string? AccessToken { get; }
|
||||
AuthResponseDto? CurrentUser { get; }
|
||||
bool IsAuthenticated { get; }
|
||||
|
||||
Task InitializeAsync();
|
||||
Task SetSessionAsync(AuthResponseDto authResponse);
|
||||
Task ClearSessionAsync();
|
||||
}
|
||||
17
SlipItIn/Services/Interfaces/IGameStateService.cs
Normal file
17
SlipItIn/Services/Interfaces/IGameStateService.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using SlipItIn.Models;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.Services.Interfaces;
|
||||
|
||||
public interface IGameStateService
|
||||
{
|
||||
GameStateDto? CurrentGameState { get; }
|
||||
PlayerHandDto? CurrentPlayerHand { get; }
|
||||
SlipChallengeDto? PendingChallenge { get; }
|
||||
|
||||
Task InitializeAsync();
|
||||
Task ResyncAsync();
|
||||
Task EnqueueActionAsync(string method, params object[] arguments);
|
||||
Task FlushQueuedActionsAsync();
|
||||
Task ClearLocalGameDataAsync();
|
||||
}
|
||||
27
SlipItIn/Services/Interfaces/ILocalStorageService.cs
Normal file
27
SlipItIn/Services/Interfaces/ILocalStorageService.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using SlipItIn.Models;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.Services.Interfaces;
|
||||
|
||||
public interface ILocalStorageService
|
||||
{
|
||||
Task SaveAuthTokenAsync(string token);
|
||||
Task<string?> GetAuthTokenAsync();
|
||||
Task ClearAuthTokenAsync();
|
||||
|
||||
Task SaveAuthUserAsync(AuthResponseDto authResponse);
|
||||
Task<AuthResponseDto?> GetAuthUserAsync();
|
||||
Task ClearAuthUserAsync();
|
||||
|
||||
Task SaveGameStateAsync(GameStateDto state);
|
||||
Task<GameStateDto?> GetGameStateAsync();
|
||||
Task ClearGameStateAsync();
|
||||
|
||||
Task SavePlayerHandAsync(PlayerHandDto hand);
|
||||
Task<PlayerHandDto?> GetPlayerHandAsync();
|
||||
Task ClearPlayerHandAsync();
|
||||
|
||||
Task SaveQueuedActionsAsync(IReadOnlyCollection<QueuedGameAction> actions);
|
||||
Task<IReadOnlyCollection<QueuedGameAction>> GetQueuedActionsAsync();
|
||||
Task ClearQueuedActionsAsync();
|
||||
}
|
||||
31
SlipItIn/Services/Interfaces/ISignalRService.cs
Normal file
31
SlipItIn/Services/Interfaces/ISignalRService.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using SlipItIn.Models;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.Services.Interfaces;
|
||||
|
||||
public interface ISignalRService
|
||||
{
|
||||
bool IsConnected { get; }
|
||||
|
||||
event EventHandler<(int GameId, string LobbyCode)>? LobbyCreated;
|
||||
event EventHandler<GameStateDto>? GameStateUpdated;
|
||||
event EventHandler<int>? GameStarted;
|
||||
event EventHandler<PlayerHandDto>? PlayerHandUpdated;
|
||||
event EventHandler<SlipChallengeDto>? ChallengeReceived;
|
||||
event EventHandler<string>? ErrorReceived;
|
||||
event EventHandler<bool>? ConnectionStateChanged;
|
||||
|
||||
Task<bool> ConnectAsync(string token, CancellationToken cancellationToken = default);
|
||||
Task DisconnectAsync();
|
||||
|
||||
Task CreateLobbyAsync();
|
||||
Task JoinLobbyAsync(string lobbyCode);
|
||||
Task PlayerReadyAsync(int gameId, int playerId);
|
||||
Task StartGameAsync(int gameId);
|
||||
Task SubmitSlipAsync(int gameId, int playerId, int cardId);
|
||||
Task ChallengeSlipAsync(int gameId, int challengingPlayerId, int targetCardId);
|
||||
Task ResolveChallengeAsync(int challengeId, bool approved);
|
||||
Task RequestGameStateAsync(int gameId);
|
||||
|
||||
Task SendQueuedActionAsync(QueuedGameAction action);
|
||||
}
|
||||
110
SlipItIn/Services/LocalStorageService.cs
Normal file
110
SlipItIn/Services/LocalStorageService.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using System.Text.Json;
|
||||
using SlipItIn.Models;
|
||||
using SlipItIn.Services.Interfaces;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.Services;
|
||||
|
||||
public class LocalStorageService : ILocalStorageService
|
||||
{
|
||||
private const string AuthTokenKey = "auth_token";
|
||||
private const string AuthUserKey = "auth_user";
|
||||
private const string GameStateKey = "game_state";
|
||||
private const string PlayerHandKey = "player_hand";
|
||||
private const string QueuedActionsKey = "queued_actions";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public Task SaveAuthTokenAsync(string token) => SecureStorage.SetAsync(AuthTokenKey, token);
|
||||
|
||||
public Task<string?> GetAuthTokenAsync() => SecureStorage.GetAsync(AuthTokenKey);
|
||||
|
||||
public Task ClearAuthTokenAsync()
|
||||
{
|
||||
SecureStorage.Remove(AuthTokenKey);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveAuthUserAsync(AuthResponseDto authResponse)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(authResponse, JsonOptions);
|
||||
Preferences.Default.Set(AuthUserKey, json);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<AuthResponseDto?> GetAuthUserAsync() => ReadFromPreferencesAsync<AuthResponseDto>(AuthUserKey);
|
||||
|
||||
public Task ClearAuthUserAsync()
|
||||
{
|
||||
Preferences.Default.Remove(AuthUserKey);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveGameStateAsync(GameStateDto state)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(state, JsonOptions);
|
||||
Preferences.Default.Set(GameStateKey, json);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<GameStateDto?> GetGameStateAsync() => ReadFromPreferencesAsync<GameStateDto>(GameStateKey);
|
||||
|
||||
public Task ClearGameStateAsync()
|
||||
{
|
||||
Preferences.Default.Remove(GameStateKey);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SavePlayerHandAsync(PlayerHandDto hand)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(hand, JsonOptions);
|
||||
Preferences.Default.Set(PlayerHandKey, json);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<PlayerHandDto?> GetPlayerHandAsync() => ReadFromPreferencesAsync<PlayerHandDto>(PlayerHandKey);
|
||||
|
||||
public Task ClearPlayerHandAsync()
|
||||
{
|
||||
Preferences.Default.Remove(PlayerHandKey);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveQueuedActionsAsync(IReadOnlyCollection<QueuedGameAction> actions)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(actions, JsonOptions);
|
||||
Preferences.Default.Set(QueuedActionsKey, json);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<QueuedGameAction>> GetQueuedActionsAsync()
|
||||
{
|
||||
var actions = await ReadFromPreferencesAsync<List<QueuedGameAction>>(QueuedActionsKey);
|
||||
return actions ?? [];
|
||||
}
|
||||
|
||||
public Task ClearQueuedActionsAsync()
|
||||
{
|
||||
Preferences.Default.Remove(QueuedActionsKey);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static Task<T?> ReadFromPreferencesAsync<T>(string key)
|
||||
{
|
||||
if (!Preferences.Default.ContainsKey(key))
|
||||
return Task.FromResult<T?>(default);
|
||||
|
||||
var json = Preferences.Default.Get(key, string.Empty);
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return Task.FromResult<T?>(default);
|
||||
|
||||
try
|
||||
{
|
||||
return Task.FromResult(JsonSerializer.Deserialize<T>(json, JsonOptions));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Task.FromResult<T?>(default);
|
||||
}
|
||||
}
|
||||
}
|
||||
178
SlipItIn/Services/SignalRService.cs
Normal file
178
SlipItIn/Services/SignalRService.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using SlipItIn.Models;
|
||||
using SlipItIn.Services.Interfaces;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.Services;
|
||||
|
||||
public class SignalRService : ISignalRService
|
||||
{
|
||||
private readonly IAppConfigurationService _configuration;
|
||||
private readonly IAuthSessionService _authSession;
|
||||
private HubConnection? _hubConnection;
|
||||
|
||||
public SignalRService(IAppConfigurationService configuration, IAuthSessionService authSession)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_authSession = authSession;
|
||||
}
|
||||
|
||||
public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected;
|
||||
|
||||
public event EventHandler<(int GameId, string LobbyCode)>? LobbyCreated;
|
||||
public event EventHandler<GameStateDto>? GameStateUpdated;
|
||||
public event EventHandler<int>? GameStarted;
|
||||
public event EventHandler<PlayerHandDto>? PlayerHandUpdated;
|
||||
public event EventHandler<SlipChallengeDto>? ChallengeReceived;
|
||||
public event EventHandler<string>? ErrorReceived;
|
||||
public event EventHandler<bool>? ConnectionStateChanged;
|
||||
|
||||
public async Task<bool> ConnectAsync(string token, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (IsConnected)
|
||||
return true;
|
||||
|
||||
EnsureConnection(token);
|
||||
|
||||
if (_hubConnection is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
await _hubConnection.StartAsync(cancellationToken);
|
||||
ConnectionStateChanged?.Invoke(this, true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorReceived?.Invoke(this, $"SignalR-Verbindung fehlgeschlagen: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
if (_hubConnection is null)
|
||||
return;
|
||||
|
||||
await _hubConnection.StopAsync();
|
||||
ConnectionStateChanged?.Invoke(this, false);
|
||||
}
|
||||
|
||||
public Task CreateLobbyAsync() => InvokeAsync("CreateLobby");
|
||||
|
||||
public Task JoinLobbyAsync(string lobbyCode) => InvokeAsync("JoinLobby", lobbyCode);
|
||||
|
||||
public Task PlayerReadyAsync(int gameId, int playerId) => InvokeAsync("PlayerReady", gameId, playerId);
|
||||
|
||||
public Task StartGameAsync(int gameId) => InvokeAsync("StartGame", gameId);
|
||||
|
||||
public Task SubmitSlipAsync(int gameId, int playerId, int cardId)
|
||||
=> InvokeAsync("SubmitSlip", gameId, playerId, cardId);
|
||||
|
||||
public Task ChallengeSlipAsync(int gameId, int challengingPlayerId, int targetCardId)
|
||||
=> InvokeAsync("ChallengeSlip", gameId, challengingPlayerId, targetCardId);
|
||||
|
||||
public Task ResolveChallengeAsync(int challengeId, bool approved)
|
||||
=> InvokeAsync("ResolveChallenge", challengeId, approved);
|
||||
|
||||
public Task RequestGameStateAsync(int gameId) => InvokeAsync("RequestGameState", gameId);
|
||||
|
||||
public async Task SendQueuedActionAsync(QueuedGameAction action)
|
||||
{
|
||||
if (_hubConnection is null)
|
||||
return;
|
||||
|
||||
object?[] args;
|
||||
try
|
||||
{
|
||||
var doc = JsonDocument.Parse(action.ArgumentsJson);
|
||||
args = doc.RootElement.ValueKind == JsonValueKind.Array
|
||||
? doc.RootElement.EnumerateArray().Select(ToObject).ToArray()
|
||||
: [];
|
||||
}
|
||||
catch
|
||||
{
|
||||
args = [];
|
||||
}
|
||||
|
||||
await InvokeAsync(action.Method, args);
|
||||
}
|
||||
|
||||
private void EnsureConnection(string token)
|
||||
{
|
||||
if (_hubConnection is not null)
|
||||
return;
|
||||
|
||||
_hubConnection = new HubConnectionBuilder()
|
||||
.WithUrl(_configuration.HubUrl, options =>
|
||||
{
|
||||
options.AccessTokenProvider = () => Task.FromResult<string?>(token);
|
||||
})
|
||||
.WithAutomaticReconnect([TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)])
|
||||
.Build();
|
||||
|
||||
_hubConnection.Closed += _ =>
|
||||
{
|
||||
ConnectionStateChanged?.Invoke(this, false);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_hubConnection.Reconnected += _ =>
|
||||
{
|
||||
ConnectionStateChanged?.Invoke(this, true);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_hubConnection.On<JsonElement>("LobbyCreated", payload =>
|
||||
{
|
||||
var gameId = payload.TryGetProperty("gameId", out var gameIdElement) ? gameIdElement.GetInt32() : 0;
|
||||
var lobbyCode = payload.TryGetProperty("lobbyCode", out var codeElement) ? codeElement.GetString() ?? string.Empty : string.Empty;
|
||||
LobbyCreated?.Invoke(this, (gameId, lobbyCode));
|
||||
});
|
||||
|
||||
_hubConnection.On<GameStateDto>("PlayerJoined", dto => GameStateUpdated?.Invoke(this, dto));
|
||||
_hubConnection.On<GameStateDto>("GameStateUpdated", dto => GameStateUpdated?.Invoke(this, dto));
|
||||
|
||||
_hubConnection.On<JsonElement>("GameStarted", payload =>
|
||||
{
|
||||
var gameId = payload.TryGetProperty("gameId", out var gameIdElement) ? gameIdElement.GetInt32() : 0;
|
||||
GameStarted?.Invoke(this, gameId);
|
||||
});
|
||||
|
||||
_hubConnection.On<PlayerHandDto>("PlayerHandUpdated", dto => PlayerHandUpdated?.Invoke(this, dto));
|
||||
_hubConnection.On<SlipChallengeDto>("ChallengeReceived", dto => ChallengeReceived?.Invoke(this, dto));
|
||||
|
||||
_hubConnection.On<JsonElement>("Error", payload =>
|
||||
{
|
||||
var message = payload.TryGetProperty("message", out var msgElement)
|
||||
? msgElement.GetString() ?? "Unbekannter Fehler"
|
||||
: "Unbekannter Fehler";
|
||||
ErrorReceived?.Invoke(this, message);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task InvokeAsync(string method, params object?[] args)
|
||||
{
|
||||
if (_hubConnection is null || _hubConnection.State != HubConnectionState.Connected)
|
||||
throw new InvalidOperationException("Keine aktive SignalR-Verbindung.");
|
||||
|
||||
await _hubConnection.InvokeCoreAsync(method, args);
|
||||
}
|
||||
|
||||
private static object? ToObject(JsonElement element)
|
||||
{
|
||||
return element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => element.GetString(),
|
||||
JsonValueKind.Number when element.TryGetInt32(out var i) => i,
|
||||
JsonValueKind.Number when element.TryGetInt64(out var l) => l,
|
||||
JsonValueKind.Number when element.TryGetDouble(out var d) => d,
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
JsonValueKind.Null => null,
|
||||
_ => element.GetRawText()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
<TargetFrameworks>net10.0-android</TargetFrameworks>
|
||||
<TargetFrameworks Condition="!$([MSBuild]::IsOSPlatform('linux'))">$(TargetFrameworks);net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
|
||||
<LangVersion>preview</LangVersion>
|
||||
|
||||
<!-- Note for MacCatalyst:
|
||||
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
|
||||
@@ -13,7 +14,7 @@
|
||||
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
|
||||
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>SlipItIN</RootNamespace>
|
||||
<RootNamespace>SlipItIn</RootNamespace>
|
||||
<UseMaui>true</UseMaui>
|
||||
<SingleProject>true</SingleProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
@@ -100,6 +101,24 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.90" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="11.0.0-preview.6.26359.118" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SlipItIn.Shared\SlipItIn.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<MauiXaml Update="Views\ChallengeNotificationOverlay.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</MauiXaml>
|
||||
<MauiXaml Update="Views\LoginPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</MauiXaml>
|
||||
<MauiXaml Update="Views\RegisterPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</MauiXaml>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
33
SlipItIn/ViewModels/BaseViewModel.cs
Normal file
33
SlipItIn/ViewModels/BaseViewModel.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SlipItIn.ViewModels;
|
||||
|
||||
public abstract partial class BaseViewModel : ObservableRecipient
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial bool IsBusy { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string StatusMessage { get; set; }
|
||||
|
||||
protected async Task RunSafeAsync(Func<Task> action, string? busyMessage = null)
|
||||
{
|
||||
if (IsBusy)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
IsBusy = true;
|
||||
StatusMessage = busyMessage ?? string.Empty;
|
||||
await action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
177
SlipItIn/ViewModels/GameBoardViewModel.cs
Normal file
177
SlipItIn/ViewModels/GameBoardViewModel.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using SlipItIn.Messages;
|
||||
using SlipItIn.Services.Interfaces;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.ViewModels;
|
||||
|
||||
public partial class GameBoardViewModel : BaseViewModel,
|
||||
IRecipient<GameStateChangedMessage>,
|
||||
IRecipient<PlayerHandChangedMessage>,
|
||||
IRecipient<ChallengeReceivedMessage>,
|
||||
IRecipient<ErrorOccurredMessage>
|
||||
{
|
||||
private readonly ISignalRService _signalR;
|
||||
private readonly IGameStateService _gameStateService;
|
||||
private readonly IAuthSessionService _authSession;
|
||||
|
||||
[ObservableProperty]
|
||||
private int gameId;
|
||||
|
||||
[ObservableProperty]
|
||||
private int currentRound;
|
||||
|
||||
[ObservableProperty]
|
||||
private int roundTimeRemaining;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isChallengeVisible;
|
||||
|
||||
[ObservableProperty]
|
||||
private SlipChallengeDto? pendingChallenge;
|
||||
|
||||
public ObservableCollection<PlayerCardDto> MyCards { get; } = [];
|
||||
public ObservableCollection<PlayerInfoDto> OtherPlayers { get; } = [];
|
||||
|
||||
public GameBoardViewModel(ISignalRService signalR, IGameStateService gameStateService, IAuthSessionService authSession)
|
||||
{
|
||||
_signalR = signalR;
|
||||
_gameStateService = gameStateService;
|
||||
_authSession = authSession;
|
||||
|
||||
WeakReferenceMessenger.Default.RegisterAll(this);
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private Task RefreshStateAsync()
|
||||
{
|
||||
if (GameId <= 0)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return _signalR.RequestGameStateAsync(GameId);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SubmitSlipAsync(PlayerCardDto? card)
|
||||
{
|
||||
if (card is null)
|
||||
return;
|
||||
|
||||
await RunSafeAsync(async () =>
|
||||
{
|
||||
var playerId = GetCurrentPlayerId();
|
||||
if (playerId <= 0 || GameId <= 0)
|
||||
throw new InvalidOperationException("Spielzustand nicht verfügbar.");
|
||||
|
||||
if (_signalR.IsConnected)
|
||||
{
|
||||
await _signalR.SubmitSlipAsync(GameId, playerId, card.CardId);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _gameStateService.EnqueueActionAsync("SubmitSlip", GameId, playerId, card.CardId);
|
||||
StatusMessage = "Offline: Aktion wurde zwischengespeichert.";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ChallengeCardAsync(PlayerCardDto? card)
|
||||
{
|
||||
if (card is null)
|
||||
return;
|
||||
|
||||
await RunSafeAsync(async () =>
|
||||
{
|
||||
var playerId = GetCurrentPlayerId();
|
||||
if (playerId <= 0 || GameId <= 0)
|
||||
throw new InvalidOperationException("Spielzustand nicht verfügbar.");
|
||||
|
||||
if (_signalR.IsConnected)
|
||||
{
|
||||
await _signalR.ChallengeSlipAsync(GameId, playerId, card.CardId);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _gameStateService.EnqueueActionAsync("ChallengeSlip", GameId, playerId, card.CardId);
|
||||
StatusMessage = "Offline: Aktion wurde zwischengespeichert.";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ApproveChallengeAsync()
|
||||
{
|
||||
if (PendingChallenge is null)
|
||||
return;
|
||||
|
||||
await _signalR.ResolveChallengeAsync(PendingChallenge.ChallengeId, true);
|
||||
PendingChallenge = null;
|
||||
IsChallengeVisible = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RejectChallengeAsync()
|
||||
{
|
||||
if (PendingChallenge is null)
|
||||
return;
|
||||
|
||||
await _signalR.ResolveChallengeAsync(PendingChallenge.ChallengeId, false);
|
||||
PendingChallenge = null;
|
||||
IsChallengeVisible = false;
|
||||
}
|
||||
|
||||
public void Receive(GameStateChangedMessage message)
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
GameId = message.Value.GameId;
|
||||
CurrentRound = message.Value.CurrentRound;
|
||||
RoundTimeRemaining = message.Value.RoundTimeRemaining;
|
||||
|
||||
var me = _authSession.CurrentUser?.Username;
|
||||
var others = message.Value.Players
|
||||
.Where(p => !string.Equals(p.Username, me, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
OtherPlayers.Clear();
|
||||
foreach (var player in others)
|
||||
OtherPlayers.Add(player);
|
||||
});
|
||||
}
|
||||
|
||||
public void Receive(PlayerHandChangedMessage message)
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
MyCards.Clear();
|
||||
foreach (var card in message.Value.Cards)
|
||||
MyCards.Add(card);
|
||||
});
|
||||
}
|
||||
|
||||
public void Receive(ChallengeReceivedMessage message)
|
||||
{
|
||||
PendingChallenge = message.Value;
|
||||
IsChallengeVisible = true;
|
||||
}
|
||||
|
||||
public void Receive(ErrorOccurredMessage message)
|
||||
{
|
||||
StatusMessage = message.Value;
|
||||
}
|
||||
|
||||
private int GetCurrentPlayerId()
|
||||
{
|
||||
var username = _authSession.CurrentUser?.Username;
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
return 0;
|
||||
|
||||
return _gameStateService.CurrentGameState?.Players
|
||||
.FirstOrDefault(p => string.Equals(p.Username, username, StringComparison.OrdinalIgnoreCase))?.PlayerId ?? 0;
|
||||
}
|
||||
}
|
||||
152
SlipItIn/ViewModels/LobbyViewModel.cs
Normal file
152
SlipItIn/ViewModels/LobbyViewModel.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using SlipItIn.Messages;
|
||||
using SlipItIn.Services.Interfaces;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
|
||||
namespace SlipItIn.ViewModels;
|
||||
|
||||
public partial class LobbyViewModel : BaseViewModel,
|
||||
IRecipient<GameStateChangedMessage>,
|
||||
IRecipient<LobbyCreatedMessage>,
|
||||
IRecipient<GameStartedMessage>,
|
||||
IRecipient<ErrorOccurredMessage>,
|
||||
IRecipient<ConnectionStateChangedMessage>
|
||||
{
|
||||
private readonly ISignalRService _signalR;
|
||||
private readonly IGameStateService _gameStateService;
|
||||
private readonly IAuthSessionService _authSession;
|
||||
private readonly IApiService _apiService;
|
||||
|
||||
[ObservableProperty]
|
||||
private string lobbyCode = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private int gameId;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isHost;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isConnected;
|
||||
|
||||
public ObservableCollection<PlayerInfoDto> Players { get; } = [];
|
||||
|
||||
public LobbyViewModel(ISignalRService signalR, IGameStateService gameStateService, IAuthSessionService authSession, IApiService apiService)
|
||||
{
|
||||
_signalR = signalR;
|
||||
_gameStateService = gameStateService;
|
||||
_authSession = authSession;
|
||||
_apiService = apiService;
|
||||
|
||||
IsConnected = _signalR.IsConnected;
|
||||
//WeakReferenceMessenger.Default.RegisterAll(this);
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CreateLobbyAsync()
|
||||
{
|
||||
await RunSafeAsync(async () =>
|
||||
{
|
||||
IsHost = true;
|
||||
await _signalR.CreateLobbyAsync();
|
||||
}, "Lobby wird erstellt...");
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task JoinLobbyAsync()
|
||||
{
|
||||
await RunSafeAsync(async () =>
|
||||
{
|
||||
IsHost = false;
|
||||
await _signalR.JoinLobbyAsync(LobbyCode);
|
||||
}, "Lobby wird beigetreten...");
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SetReadyAsync()
|
||||
{
|
||||
await RunSafeAsync(async () =>
|
||||
{
|
||||
var currentPlayerId = GetCurrentPlayerId();
|
||||
if (currentPlayerId <= 0 || GameId <= 0)
|
||||
throw new InvalidOperationException("Spielstatus ist noch nicht verfügbar.");
|
||||
|
||||
await _signalR.PlayerReadyAsync(GameId, currentPlayerId);
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task StartGameAsync()
|
||||
{
|
||||
await RunSafeAsync(async () =>
|
||||
{
|
||||
if (GameId <= 0)
|
||||
throw new InvalidOperationException("Keine aktive Lobby gefunden.");
|
||||
|
||||
await _signalR.StartGameAsync(GameId);
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LogoutAsync()
|
||||
{
|
||||
await RunSafeAsync(async () =>
|
||||
{
|
||||
await _signalR.DisconnectAsync();
|
||||
await _apiService.LogoutAsync();
|
||||
await _gameStateService.ClearLocalGameDataAsync();
|
||||
await Shell.Current.GoToAsync("//LoginPage");
|
||||
});
|
||||
}
|
||||
|
||||
public void Receive(GameStateChangedMessage message)
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
GameId = message.Value.GameId;
|
||||
LobbyCode = message.Value.LobbyCode;
|
||||
|
||||
Players.Clear();
|
||||
foreach (var player in message.Value.Players)
|
||||
Players.Add(player);
|
||||
});
|
||||
}
|
||||
|
||||
public void Receive(LobbyCreatedMessage message)
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
GameId = message.Value.GameId;
|
||||
LobbyCode = message.Value.LobbyCode;
|
||||
});
|
||||
}
|
||||
|
||||
public async void Receive(GameStartedMessage message)
|
||||
{
|
||||
await MainThread.InvokeOnMainThreadAsync(() => Shell.Current.GoToAsync($"//{nameof(Views.GameBoardPage)}"));
|
||||
}
|
||||
|
||||
public void Receive(ErrorOccurredMessage message)
|
||||
{
|
||||
StatusMessage = message.Value;
|
||||
}
|
||||
|
||||
public void Receive(ConnectionStateChangedMessage message)
|
||||
{
|
||||
IsConnected = message.Value;
|
||||
}
|
||||
|
||||
private int GetCurrentPlayerId()
|
||||
{
|
||||
var username = _authSession.CurrentUser?.Username;
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
return 0;
|
||||
|
||||
return _gameStateService.CurrentGameState?.Players
|
||||
.FirstOrDefault(p => string.Equals(p.Username, username, StringComparison.OrdinalIgnoreCase))?.PlayerId ?? 0;
|
||||
}
|
||||
}
|
||||
61
SlipItIn/ViewModels/LoginViewModel.cs
Normal file
61
SlipItIn/ViewModels/LoginViewModel.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using SlipItIn.Services.Interfaces;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
using SlipItIn.Views;
|
||||
|
||||
namespace SlipItIn.ViewModels;
|
||||
|
||||
public partial class LoginViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IApiService _apiService;
|
||||
private readonly IAuthSessionService _authSession;
|
||||
private readonly ISignalRService _signalR;
|
||||
private readonly IGameStateService _gameStateService;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Email { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Password { get; set; }
|
||||
|
||||
public LoginViewModel(IApiService apiService, IAuthSessionService authSession, ISignalRService signalR, IGameStateService gameStateService)
|
||||
{
|
||||
_apiService = apiService;
|
||||
_authSession = authSession;
|
||||
_signalR = signalR;
|
||||
_gameStateService = gameStateService;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private Task GoToRegisterAsync() => Shell.Current.GoToAsync($"//{nameof(RegisterPage)}");
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LoginAsync()
|
||||
{
|
||||
await RunSafeAsync(async () =>
|
||||
{
|
||||
var response = await _apiService.LoginAsync(new LoginRequestDto
|
||||
{
|
||||
Email = Email,
|
||||
Password = Password
|
||||
});
|
||||
|
||||
await _authSession.SetSessionAsync(response);
|
||||
await _signalR.ConnectAsync(response.Token);
|
||||
await _gameStateService.InitializeAsync();
|
||||
await Shell.Current.GoToAsync($"//{nameof(LobbyPage)}");
|
||||
}, "Anmeldung läuft...");
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
bool isAuthenticated = _authSession.IsAuthenticated;
|
||||
|
||||
if ( isAuthenticated)
|
||||
{
|
||||
await Shell.Current.GoToAsync($"//{nameof(LobbyPage)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
51
SlipItIn/ViewModels/RegisterViewModel.cs
Normal file
51
SlipItIn/ViewModels/RegisterViewModel.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using SlipItIn.Services.Interfaces;
|
||||
using SlipItIn.Shared.DTOs;
|
||||
using SlipItIn.Views;
|
||||
|
||||
namespace SlipItIn.ViewModels;
|
||||
|
||||
public partial class RegisterViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IApiService _apiService;
|
||||
private readonly IAuthSessionService _authSession;
|
||||
private readonly ISignalRService _signalR;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Username { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Email { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Password { get; set; } = string.Empty;
|
||||
|
||||
public RegisterViewModel(IApiService apiService, IAuthSessionService authSession, ISignalRService signalR)
|
||||
{
|
||||
_apiService = apiService;
|
||||
_authSession = authSession;
|
||||
_signalR = signalR;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private Task BackToLoginAsync() => Shell.Current.GoToAsync($"//{nameof(LoginPage)}");
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RegisterAsync()
|
||||
{
|
||||
await RunSafeAsync(async () =>
|
||||
{
|
||||
var response = await _apiService.RegisterAsync(new RegisterRequestDto
|
||||
{
|
||||
Username = Username,
|
||||
Email = Email,
|
||||
Password = Password
|
||||
});
|
||||
|
||||
await _authSession.SetSessionAsync(response);
|
||||
await _signalR.ConnectAsync(response.Token);
|
||||
await Shell.Current.GoToAsync($"//{nameof(LobbyPage)}");
|
||||
}, "Registrierung läuft...");
|
||||
}
|
||||
}
|
||||
17
SlipItIn/Views/ChallengeNotificationOverlay.xaml
Normal file
17
SlipItIn/Views/ChallengeNotificationOverlay.xaml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="SlipItIn.Views.ChallengeNotificationOverlay">
|
||||
<Grid BackgroundColor="#88000000" IsVisible="{Binding IsChallengeVisible}" Padding="24">
|
||||
<Frame VerticalOptions="Center" HorizontalOptions="Center">
|
||||
<VerticalStackLayout Spacing="10">
|
||||
<Label Text="Du wurdest beschuldigt" FontAttributes="Bold" />
|
||||
<Label Text="{Binding PendingChallenge.ChallengeId, StringFormat='ChallengeId: {0}'}" />
|
||||
<HorizontalStackLayout>
|
||||
<Button Text="Bestätigen" Command="{Binding ApproveChallengeCommand}" />
|
||||
<Button Text="Ablehnen" Command="{Binding RejectChallengeCommand}" />
|
||||
</HorizontalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
</Grid>
|
||||
</ContentView>
|
||||
9
SlipItIn/Views/ChallengeNotificationOverlay.xaml.cs
Normal file
9
SlipItIn/Views/ChallengeNotificationOverlay.xaml.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SlipItIn.Views;
|
||||
|
||||
public partial class ChallengeNotificationOverlay : ContentView
|
||||
{
|
||||
public ChallengeNotificationOverlay()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
49
SlipItIn/Views/GameBoardPage.xaml
Normal file
49
SlipItIn/Views/GameBoardPage.xaml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:views="clr-namespace:SlipItIn.Views"
|
||||
x:Class="SlipItIn.Views.GameBoardPage"
|
||||
Title="Game Board">
|
||||
<Grid>
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Padding="24" Spacing="12">
|
||||
<Label Text="Spielbrett" FontSize="24" FontAttributes="Bold" />
|
||||
<Label Text="{Binding CurrentRound, StringFormat='Runde: {0}'}" />
|
||||
<Label Text="{Binding RoundTimeRemaining, StringFormat='Verbleibende Zeit: {0}s'}" />
|
||||
|
||||
<Button Text="Status aktualisieren" Command="{Binding RefreshStateCommand}" />
|
||||
|
||||
<Label Text="Meine Phrasen" FontAttributes="Bold" />
|
||||
<CollectionView ItemsSource="{Binding MyCards}">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Frame Margin="0,4" Padding="10">
|
||||
<VerticalStackLayout>
|
||||
<Label Text="{Binding Text}" />
|
||||
<HorizontalStackLayout>
|
||||
<Button Text="Slip" Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.SubmitSlipCommand}" CommandParameter="{Binding .}" />
|
||||
<Button Text="Beschuldigen" Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ChallengeCardCommand}" CommandParameter="{Binding .}" />
|
||||
</HorizontalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</CollectionView>
|
||||
|
||||
<Label Text="Andere Spieler" FontAttributes="Bold" />
|
||||
<CollectionView ItemsSource="{Binding OtherPlayers}">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Label Text="{Binding Username}" />
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</CollectionView>
|
||||
|
||||
<ActivityIndicator IsVisible="{Binding IsBusy}" IsRunning="{Binding IsBusy}" />
|
||||
<Label Text="{Binding StatusMessage}" TextColor="OrangeRed" />
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
|
||||
<views:ChallengeNotificationOverlay />
|
||||
</Grid>
|
||||
</ContentPage>
|
||||
13
SlipItIn/Views/GameBoardPage.xaml.cs
Normal file
13
SlipItIn/Views/GameBoardPage.xaml.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using SlipItIn.Infrastructure;
|
||||
using SlipItIn.ViewModels;
|
||||
|
||||
namespace SlipItIn.Views;
|
||||
|
||||
public partial class GameBoardPage : ContentPage
|
||||
{
|
||||
public GameBoardPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
BindingContext = ServiceHelper.GetRequiredService<GameBoardViewModel>();
|
||||
}
|
||||
}
|
||||
42
SlipItIn/Views/LobbyPage.xaml
Normal file
42
SlipItIn/Views/LobbyPage.xaml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="SlipItIn.Views.LobbyPage"
|
||||
Title="Lobby">
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Padding="24" Spacing="12">
|
||||
<Label Text="Lobby" FontSize="24" FontAttributes="Bold" />
|
||||
<Label Text="{Binding IsConnected, StringFormat='Verbindung: {0}'}" />
|
||||
|
||||
<Button Text="Lobby erstellen" Command="{Binding CreateLobbyCommand}" />
|
||||
|
||||
<HorizontalStackLayout Spacing="8">
|
||||
<Entry Placeholder="Lobby Code" Text="{Binding LobbyCode}" HorizontalOptions="FillAndExpand" />
|
||||
<Button Text="Beitreten" Command="{Binding JoinLobbyCommand}" />
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<Label Text="{Binding GameId, StringFormat='GameId: {0}'}" />
|
||||
<Label Text="{Binding LobbyCode, StringFormat='Code: {0}'}" />
|
||||
|
||||
<Button Text="Bereit" Command="{Binding SetReadyCommand}" />
|
||||
<Button Text="Spiel starten (Host)" Command="{Binding StartGameCommand}" IsVisible="{Binding IsHost}" />
|
||||
|
||||
<Label Text="Spieler" FontAttributes="Bold" />
|
||||
<CollectionView ItemsSource="{Binding Players}">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid ColumnDefinitions="*,Auto,Auto" Padding="4">
|
||||
<Label Grid.Column="0" Text="{Binding Username}" />
|
||||
<Label Grid.Column="1" Text="Score:" />
|
||||
<Label Grid.Column="2" Text="{Binding Score}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</CollectionView>
|
||||
|
||||
<Button Text="Logout" Command="{Binding LogoutCommand}" />
|
||||
<ActivityIndicator IsVisible="{Binding IsBusy}" IsRunning="{Binding IsBusy}" />
|
||||
<Label Text="{Binding StatusMessage}" TextColor="OrangeRed" />
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
</ContentPage>
|
||||
13
SlipItIn/Views/LobbyPage.xaml.cs
Normal file
13
SlipItIn/Views/LobbyPage.xaml.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using SlipItIn.Infrastructure;
|
||||
using SlipItIn.ViewModels;
|
||||
|
||||
namespace SlipItIn.Views;
|
||||
|
||||
public partial class LobbyPage : ContentPage
|
||||
{
|
||||
public LobbyPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
BindingContext = ServiceHelper.GetRequiredService<LobbyViewModel>();
|
||||
}
|
||||
}
|
||||
23
SlipItIn/Views/LoginPage.xaml
Normal file
23
SlipItIn/Views/LoginPage.xaml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:viewmodels="clr-namespace:SlipItIn.ViewModels"
|
||||
x:DataType="viewmodels:LoginViewModel"
|
||||
x:Class="SlipItIn.Views.LoginPage"
|
||||
Title="Login">
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Padding="24" Spacing="12">
|
||||
<Label Text="Slip It In" FontSize="28" FontAttributes="Bold" />
|
||||
<Label Text="Anmelden" FontSize="20" />
|
||||
|
||||
<Entry Placeholder="Email" Keyboard="Email" Text="{Binding Email}" />
|
||||
<Entry Placeholder="Passwort" IsPassword="True" Text="{Binding Password}" />
|
||||
|
||||
<Button Text="Login" Command="{Binding LoginCommand}" />
|
||||
<Button Text="Registrieren" Command="{Binding GoToRegisterCommand}" />
|
||||
|
||||
<ActivityIndicator IsRunning="{Binding IsBusy}" IsVisible="{Binding IsBusy}" />
|
||||
<Label Text="{Binding StatusMessage}" TextColor="OrangeRed" />
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
</ContentPage>
|
||||
25
SlipItIn/Views/LoginPage.xaml.cs
Normal file
25
SlipItIn/Views/LoginPage.xaml.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using SlipItIn.Infrastructure;
|
||||
using SlipItIn.ViewModels;
|
||||
|
||||
namespace SlipItIn.Views;
|
||||
|
||||
public partial class LoginPage : ContentPage
|
||||
{
|
||||
private readonly LoginViewModel _viewModel;
|
||||
|
||||
public LoginPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
BindingContext = _viewModel = ServiceHelper.GetRequiredService<LoginViewModel>();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigatedToEventArgs args)
|
||||
{
|
||||
base.OnNavigatedTo(args);
|
||||
|
||||
if (_viewModel.InitializeCommand.CanExecute(null))
|
||||
{
|
||||
_viewModel.InitializeCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
SlipItIn/Views/RegisterPage.xaml
Normal file
23
SlipItIn/Views/RegisterPage.xaml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:viewmodels="clr-namespace:SlipItIn.ViewModels"
|
||||
x:DataType="viewmodels:RegisterViewModel"
|
||||
x:Class="SlipItIn.Views.RegisterPage"
|
||||
Title="Registrieren">
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Padding="24" Spacing="12">
|
||||
<Label Text="Neuen Account erstellen" FontSize="22" FontAttributes="Bold" />
|
||||
|
||||
<Entry Placeholder="Username" Text="{Binding Username}" />
|
||||
<Entry Placeholder="Email" Keyboard="Email" Text="{Binding Email}" />
|
||||
<Entry Placeholder="Passwort" IsPassword="True" Text="{Binding Password}" />
|
||||
|
||||
<Button Text="Registrieren" Command="{Binding RegisterCommand}" />
|
||||
<Button Text="Zurück" Command="{Binding BackToLoginCommand}" />
|
||||
|
||||
<ActivityIndicator IsRunning="{Binding IsBusy}" IsVisible="{Binding IsBusy}" />
|
||||
<Label Text="{Binding StatusMessage}" TextColor="OrangeRed" />
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
</ContentPage>
|
||||
13
SlipItIn/Views/RegisterPage.xaml.cs
Normal file
13
SlipItIn/Views/RegisterPage.xaml.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using SlipItIn.Infrastructure;
|
||||
using SlipItIn.ViewModels;
|
||||
|
||||
namespace SlipItIn.Views;
|
||||
|
||||
public partial class RegisterPage : ContentPage
|
||||
{
|
||||
public RegisterPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
BindingContext = ServiceHelper.GetRequiredService<RegisterViewModel>();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user