add ThrowTimer

This commit is contained in:
beo3000 2025-12-27 14:18:45 +01:00
parent 5db650f2b2
commit c33a6f9d91
6 changed files with 222 additions and 1 deletions

View File

@ -0,0 +1,40 @@
@using Koogle.Web.Store.TimerState
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@inject IState<TimerState> TimerState
@inject IDispatcher Dispatcher
@if (TimerState.Value.IsRunning)
{
<MudButton OnClick="AbortTimer"
Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
Class="throw-timer-button">
@TimerState.Value.RemainingSeconds
</MudButton>
}
<style>
.throw-timer-button {
font-size: 2rem;
font-weight: bold;
min-width: 80px;
min-height: 60px;
}
</style>
@code {
/// <summary>
/// Callback when timer is aborted by user click.
/// </summary>
[Parameter]
public EventCallback OnTimerAborted { get; set; }
private async Task AbortTimer()
{
Dispatcher.Dispatch(new StopTimerAction());
await OnTimerAborted.InvokeAsync();
}
}

View File

@ -2,6 +2,7 @@
@attribute [Authorize(Policy = "ClubViewer")]
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@implements IDisposable
@using Fluxor
@using Koogle.Application.DTOs
@ -9,12 +10,14 @@
@using Koogle.Web.Store.DayState
@using Koogle.Web.Store.GameState
@using Koogle.Web.Store.PersonState
@using Koogle.Web.Store.TimerState
@using Koogle.Web.Components.Game
@using Microsoft.AspNetCore.Authorization
@inject IState<DayState> DayState
@inject IState<GameState> GameState
@inject IState<PersonState> PersonState
@inject IState<TimerState> TimerState
@inject IDispatcher Dispatcher
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@ -426,6 +429,12 @@ else
<!-- Tab 3: Tafel (Game Board) -->
<MudTabPanel Text="Tafel" Icon="@Icons.Material.Filled.TableChart" Disabled="@(!GameState.Value.IsGameActive)">
@if (TimerState.Value.IsRunning)
{
<div class="d-flex justify-center mb-4">
<ThrowTimer OnTimerAborted="HandleTimerAborted" />
</div>
}
<GameBoardPanel />
</MudTabPanel>
</MudTabs>
@ -835,9 +844,43 @@ else
Snackbar.Add("Spieler-Auswahl noch nicht implementiert", Severity.Info);
}
private const int TimerDurationSeconds = 3;
private Task HandleThrowCompleted(ThrowResult result)
{
// Throw was completed - game logic will handle state updates
// Switch to Tafel tab and start timer
_activeTabIndex = 2;
Dispatcher.Dispatch(new StartTimerAction(TimerDurationSeconds));
return Task.CompletedTask;
}
private void HandleTimerAborted()
{
// User clicked timer button - switch back to Eingabe tab
_activeTabIndex = 1;
}
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
if (firstRender)
{
TimerState.StateChanged += OnTimerStateChanged;
}
}
private void OnTimerStateChanged(object? sender, EventArgs e)
{
// When timer stops (completed naturally), switch back to Eingabe tab
if (!TimerState.Value.IsRunning && _activeTabIndex == 2)
{
_activeTabIndex = 1;
InvokeAsync(StateHasChanged);
}
}
public void Dispose()
{
TimerState.StateChanged -= OnTimerStateChanged;
}
}

View File

@ -0,0 +1,21 @@
namespace Koogle.Web.Store.TimerState;
/// <summary>
/// Action to start the throw timer.
/// </summary>
public record StartTimerAction(int Seconds);
/// <summary>
/// Action dispatched when timer completes naturally.
/// </summary>
public record TimerCompletedAction;
/// <summary>
/// Action to stop the timer manually (user click).
/// </summary>
public record StopTimerAction;
/// <summary>
/// Action dispatched every second while timer is running.
/// </summary>
public record TickAction;

View File

@ -0,0 +1,54 @@
using Fluxor;
namespace Koogle.Web.Store.TimerState;
/// <summary>
/// Effects for handling timer async operations.
/// </summary>
public class TimerEffects(ILogger<TimerEffects> logger)
{
private CancellationTokenSource? _timerCancellation;
[EffectMethod]
public async Task HandleStartTimer(StartTimerAction action, IDispatcher dispatcher)
{
logger.LogInformation("Timer started for {Seconds} seconds", action.Seconds);
// Cancel previous timer if running
if (_timerCancellation != null)
{
await _timerCancellation.CancelAsync();
}
_timerCancellation = new CancellationTokenSource();
try
{
for (var i = 0; i < action.Seconds; i++)
{
var token = _timerCancellation?.Token ?? CancellationToken.None;
await Task.Delay(1000, token);
dispatcher.Dispatch(new TickAction());
}
// Timer completed naturally
dispatcher.Dispatch(new TimerCompletedAction());
logger.LogInformation("Timer completed");
}
catch (TaskCanceledException)
{
logger.LogInformation("Timer was cancelled");
}
}
[EffectMethod]
public async Task HandleStopTimer(StopTimerAction action, IDispatcher dispatcher)
{
if (_timerCancellation is { IsCancellationRequested: false })
{
logger.LogInformation("Timer stopped manually");
await _timerCancellation.CancelAsync();
_timerCancellation = null;
}
}
}

View File

@ -0,0 +1,49 @@
using Fluxor;
namespace Koogle.Web.Store.TimerState;
public static class TimerReducers
{
[ReducerMethod]
public static TimerState OnStartTimer(TimerState state, StartTimerAction action)
{
return state with
{
RemainingSeconds = action.Seconds,
IsRunning = true
};
}
[ReducerMethod]
public static TimerState OnTick(TimerState state, TickAction action)
{
if (!state.IsRunning || state.RemainingSeconds <= 0)
return state;
return state with
{
RemainingSeconds = state.RemainingSeconds - 1,
IsRunning = state.RemainingSeconds - 1 > 0
};
}
[ReducerMethod]
public static TimerState OnTimerCompleted(TimerState state, TimerCompletedAction action)
{
return state with
{
IsRunning = false,
RemainingSeconds = 0
};
}
[ReducerMethod]
public static TimerState OnStopTimer(TimerState state, StopTimerAction action)
{
return state with
{
IsRunning = false,
RemainingSeconds = 0
};
}
}

View File

@ -0,0 +1,14 @@
using Fluxor;
namespace Koogle.Web.Store.TimerState;
/// <summary>
/// Fluxor state for throw timer management.
/// </summary>
[FeatureState]
public record TimerState(int RemainingSeconds, bool IsRunning)
{
public TimerState() : this(0, false)
{
}
}