< Summary

Information
Class: Pomodoro.Web.Services.PipTimerService
Assembly: Pomodoro.Web
File(s): /home/runner/work/Pomodoro/Pomodoro/src/Pomodoro.Web/Services/PipTimerService.cs
Line coverage
100%
Covered lines: 179
Uncovered lines: 0
Coverable lines: 179
Total lines: 294
Line coverage: 100%
Branch coverage
100%
Covered branches: 40
Total branches: 40
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_IsSupported()100%11100%
get_IsOpen()100%11100%
.ctor(...)100%11100%
InitializeAsync()100%22100%
OpenAsync()100%66100%
CloseAsync()100%44100%
UpdateTimerAsync()100%44100%
StartCurrentSessionAsync()100%88100%
GetTimerState()100%22100%
OnPipToggleTimer()100%88100%
OnPipResetTimer()100%11100%
OnPipSwitchSession()100%11100%
OnPipClosedJs()100%22100%
OnTimerTick()100%11100%
OnTimerStateChanged()100%11100%
DisposeAsync()100%44100%

File(s)

/home/runner/work/Pomodoro/Pomodoro/src/Pomodoro.Web/Services/PipTimerService.cs

#LineLine coverage
 1using System.Text.Json;
 2using Microsoft.JSInterop;
 3using Microsoft.Extensions.Logging;
 4using Pomodoro.Web.Models;
 5
 6namespace Pomodoro.Web.Services;
 7
 8/// <summary>
 9/// Service for Picture-in-Picture timer functionality
 10/// Provides a floating, always-on-top timer window
 11/// </summary>
 12public class PipTimerService : IPipTimerService
 13{
 14    private readonly IJSRuntime _jsRuntime;
 15    private readonly ITimerService _timerService;
 16    private readonly ITaskService _taskService;
 17    private readonly AppState _appState;
 18    private readonly ILogger<PipTimerService> _logger;
 19    private DotNetObjectReference<PipTimerService>? _dotNetRef;
 20    private bool _isInitialized;
 21    private bool _isDisposed;
 22
 2223    public bool IsSupported { get; private set; }
 4624    public bool IsOpen { get; private set; }
 25
 26    public event Action? OnPipOpened;
 27    public event Action? OnPipClosed;
 28
 4929    public PipTimerService(
 4930        IJSRuntime jsRuntime,
 4931        ITimerService timerService,
 4932        ITaskService taskService,
 4933        AppState appState,
 4934        ILogger<PipTimerService> logger)
 4935    {
 4936        _jsRuntime = jsRuntime;
 4937        _timerService = timerService;
 4938        _taskService = taskService;
 4939        _appState = appState;
 4940        _logger = logger;
 4941    }
 42
 43    public async Task InitializeAsync()
 1044    {
 1145        if (_isInitialized) return;
 46
 47        try
 948        {
 49            // Check if PiP API is supported
 950            IsSupported = await _jsRuntime.InvokeAsync<bool>(Constants.PipJsFunctions.IsSupported);
 51
 52            // Create .NET reference for JS callbacks
 853            _dotNetRef = DotNetObjectReference.Create(this);
 854            await _jsRuntime.InvokeVoidAsync(Constants.PipJsFunctions.RegisterDotNetRef, _dotNetRef);
 55
 56            // Subscribe to timer events to update PiP window
 857            _timerService.OnTick += OnTimerTick;
 858            _timerService.OnStateChanged += OnTimerStateChanged;
 59
 860            _isInitialized = true;
 61
 862            _logger.LogDebug(Constants.Messages.LogPipInitialized, IsSupported);
 863        }
 164        catch (Exception ex)
 165        {
 166            _logger.LogError(ex, Constants.Messages.LogPipInitializationFailed);
 167        }
 1068    }
 69
 70    public async Task<bool> OpenAsync()
 1571    {
 1872        if (_isDisposed) return false;
 73
 74        try
 1275        {
 1276            var timerState = GetTimerState();
 1277            var success = await _jsRuntime.InvokeAsync<bool>(Constants.PipJsFunctions.Open, timerState);
 78
 1179            if (success)
 1080            {
 1081                IsOpen = true;
 1082                OnPipOpened?.Invoke();
 1083            }
 84
 1185            return success;
 86        }
 187        catch (Exception ex)
 188        {
 189            _logger.LogError(ex, Constants.Messages.LogPipOpenFailed);
 190            return false;
 91        }
 1592    }
 93
 94    public async Task CloseAsync()
 1395    {
 2396        if (_isDisposed) return;
 97
 98        try
 399        {
 3100            await _jsRuntime.InvokeVoidAsync(Constants.PipJsFunctions.Close);
 2101            IsOpen = false;
 2102            OnPipClosed?.Invoke();
 2103        }
 1104        catch (Exception ex)
 1105        {
 1106            _logger.LogError(ex, Constants.Messages.LogPipCloseFailed);
 1107        }
 13108    }
 109
 110    public async Task UpdateTimerAsync()
 16111    {
 28112        if (_isDisposed || !IsOpen) return;
 113
 114        try
 4115        {
 4116            var timerState = GetTimerState();
 4117            await _jsRuntime.InvokeVoidAsync(Constants.PipJsFunctions.Update, timerState);
 3118        }
 1119        catch (Exception ex)
 1120        {
 1121            _logger.LogError(ex, Constants.Messages.LogPipUpdateFailed);
 1122        }
 16123    }
 124
 125    /// <summary>
 126    /// Starts the current session type timer.
 127    /// Only starts pomodoro if a task is selected.
 128    /// </summary>
 129    private async Task StartCurrentSessionAsync()
 4130    {
 4131        var sessionType = _timerService.CurrentSessionType;
 4132        if (sessionType == SessionType.Pomodoro)
 2133        {
 2134            if (_taskService.CurrentTaskId.HasValue)
 1135            {
 1136                await _timerService.StartPomodoroAsync(_taskService.CurrentTaskId.Value);
 1137            }
 2138        }
 2139        else if (sessionType == SessionType.ShortBreak)
 1140        {
 1141            await _timerService.StartShortBreakAsync();
 1142        }
 1143        else if (sessionType == SessionType.LongBreak)
 1144        {
 1145            await _timerService.StartLongBreakAsync();
 1146        }
 4147    }
 148
 149    /// <summary>
 150    /// Gets the current timer state for the PiP window.
 151    /// Single source of truth for UI state - PiP uses pre-computed values.
 152    /// </summary>
 153    private object GetTimerState()
 16154    {
 16155        var isRunning = _timerService.IsRunning;
 16156        var isStarted = _timerService.IsStarted;
 157
 16158        return new
 16159        {
 16160            remainingSeconds = _timerService.RemainingSeconds,
 16161            sessionType = (int)_timerService.CurrentSessionType,
 16162            isRunning = isRunning,
 16163            isStarted = isStarted,
 16164            // Single source of truth: showReset when timer is running OR was started
 16165            showReset = isRunning || isStarted,
 16166            taskName = _taskService.CurrentTask?.Name
 16167        };
 16168    }
 169
 170    /// <summary>
 171    /// Called from JavaScript when timer toggle is clicked in PiP window.
 172    /// Handles play/pause toggle and ensures PiP window state is synchronized.
 173    /// </summary>
 174    [JSInvokable(Constants.JsInvokableMethods.OnPipToggleTimer)]
 175    public async Task OnPipToggleTimer()
 7176    {
 177        try
 7178        {
 179            // Capture the operation type for logging
 7180            var operation = _timerService.IsRunning ? "pause" :
 7181                           _timerService.IsPaused ? "resume" : "start";
 182
 7183            if (_timerService.IsRunning)
 2184            {
 2185                await _timerService.PauseAsync();
 1186            }
 5187            else if (_timerService.IsPaused)
 1188            {
 1189                await _timerService.ResumeAsync();
 1190            }
 191            else
 4192            {
 4193                await StartCurrentSessionAsync();
 4194            }
 195
 196            // Force immediate update with fresh state to ensure UI consistency.
 197            // This is critical because the event-based update via OnTimerStateChanged
 198            // uses SafeTaskRunner.RunAndForget which may complete before state is fully propagated.
 199            // We explicitly await here to ensure the PiP window receives the correct state.
 6200            await UpdateTimerAsync();
 201
 6202            _logger.LogDebug("PiP toggle operation '{Operation}' completed. IsRunning: {IsRunning}, IsStarted: {IsStarte
 6203                operation, _timerService.IsRunning, _timerService.IsStarted);
 6204        }
 1205        catch (Exception ex)
 1206        {
 1207            _logger.LogError(ex, Constants.Messages.LogPipToggleTimerError);
 1208        }
 7209    }
 210
 211    /// <summary>
 212    /// Called from JavaScript when reset is clicked in PiP window
 213    /// </summary>
 214    [JSInvokable(Constants.JsInvokableMethods.OnPipResetTimer)]
 215    public async Task OnPipResetTimer()
 2216    {
 217        try
 2218        {
 2219            await _timerService.ResetAsync();
 1220            await UpdateTimerAsync();
 1221        }
 1222        catch (Exception ex)
 1223        {
 1224            _logger.LogError(ex, Constants.Messages.LogPipResetTimerError);
 1225        }
 2226    }
 227
 228    /// <summary>
 229    /// Called from JavaScript when session switch is clicked in PiP window
 230    /// </summary>
 231    [JSInvokable(Constants.JsInvokableMethods.OnPipSwitchSession)]
 232    public async Task OnPipSwitchSession(int sessionType)
 4233    {
 234        try
 4235        {
 4236            var type = (SessionType)sessionType;
 4237            await _timerService.SwitchSessionTypeAsync(type);
 3238            await UpdateTimerAsync();
 3239        }
 1240        catch (Exception ex)
 1241        {
 1242            _logger.LogError(ex, Constants.Messages.LogPipSwitchSessionError);
 1243        }
 4244    }
 245
 246    /// <summary>
 247    /// Called from JavaScript when PiP window is closed
 248    /// </summary>
 249    [JSInvokable(Constants.JsInvokableMethods.OnPipClosed)]
 250    public void OnPipClosedJs()
 4251    {
 4252        IsOpen = false;
 4253        OnPipClosed?.Invoke();
 4254    }
 255
 256    private void OnTimerTick()
 1257    {
 1258        SafeTaskRunner.RunAndForget(
 1259            UpdateTimerAsync,
 1260            _logger,
 1261            Constants.SafeTaskOperations.PipTimerTick
 1262        );
 1263    }
 264
 265    private void OnTimerStateChanged()
 1266    {
 1267        SafeTaskRunner.RunAndForget(
 1268            UpdateTimerAsync,
 1269            _logger,
 1270            Constants.SafeTaskOperations.PipTimerStateChanged
 1271        );
 1272    }
 273
 274    public async ValueTask DisposeAsync()
 11275    {
 12276        if (_isDisposed) return;
 10277        _isDisposed = true;
 278
 279        try
 10280        {
 10281            _timerService.OnTick -= OnTimerTick;
 10282            _timerService.OnStateChanged -= OnTimerStateChanged;
 283
 10284            await _jsRuntime.InvokeVoidAsync(Constants.PipJsFunctions.UnregisterDotNetRef);
 9285            await CloseAsync();
 9286        }
 1287        catch
 1288        {
 289            // Ignore errors during disposal
 1290        }
 291
 10292        _dotNetRef?.Dispose();
 11293    }
 294}