| | | 1 | | using Microsoft.AspNetCore.Components; |
| | | 2 | | using Microsoft.JSInterop; |
| | | 3 | | using Microsoft.Extensions.Logging; |
| | | 4 | | using Pomodoro.Web.Models; |
| | | 5 | | using Pomodoro.Web.Services; |
| | | 6 | | |
| | | 7 | | namespace Pomodoro.Web.Pages; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// Main partial for Index page |
| | | 11 | | /// Contains dependency injection, state management, and lifecycle methods |
| | | 12 | | /// </summary> |
| | | 13 | | public partial class IndexBase : ComponentBase, IDisposable |
| | | 14 | | { |
| | | 15 | | #region Services (Dependency Injection) |
| | | 16 | | |
| | | 17 | | [Inject] |
| | 1731 | 18 | | protected ITaskService TaskService { get; set; } = default!; |
| | | 19 | | |
| | | 20 | | [Inject] |
| | 944 | 21 | | protected ILogger<IndexBase> Logger { get; set; } = default!; |
| | | 22 | | |
| | | 23 | | [Inject] |
| | 2826 | 24 | | protected ITimerService TimerService { get; set; } = default!; |
| | | 25 | | |
| | | 26 | | [Inject] |
| | 2783 | 27 | | protected IConsentService ConsentService { get; set; } = default!; |
| | | 28 | | |
| | | 29 | | [Inject] |
| | 1627 | 30 | | protected INotificationService NotificationService { get; set; } = default!; |
| | | 31 | | |
| | | 32 | | [Inject] |
| | 1355 | 33 | | protected IActivityService ActivityService { get; set; } = default!; |
| | | 34 | | |
| | | 35 | | [Inject] |
| | 2184 | 36 | | protected IPipTimerService PipTimerService { get; set; } = default!; |
| | | 37 | | |
| | | 38 | | [Inject] |
| | 546 | 39 | | protected AppState AppState { get; set; } = default!; |
| | | 40 | | |
| | | 41 | | [Inject] |
| | 812 | 42 | | protected IJSRuntime JSRuntime { get; set; } = default!; |
| | | 43 | | |
| | | 44 | | [Inject] |
| | 4571 | 45 | | protected IKeyboardShortcutService KeyboardShortcutService { get; set; } = default!; |
| | | 46 | | |
| | | 47 | | [Inject] |
| | 819 | 48 | | protected ITodayStatsService TodayStatsService { get; set; } = default!; |
| | | 49 | | |
| | | 50 | | [Inject] |
| | 879 | 51 | | protected IndexPagePresenterService IndexPagePresenterService { get; set; } = default!; |
| | | 52 | | |
| | | 53 | | #endregion |
| | | 54 | | |
| | | 55 | | #region State |
| | | 56 | | |
| | 1255 | 57 | | protected List<TaskItem> Tasks { get; set; } = new(); |
| | 1308 | 58 | | protected Guid? CurrentTaskId { get; set; } |
| | 930 | 59 | | protected TimeSpan RemainingTime { get; set; } = TimeSpan.FromMinutes(Constants.Timer.DefaultPomodoroMinutes); |
| | 4203 | 60 | | public SessionType CurrentSessionType { get; set; } = SessionType.Pomodoro; |
| | 983 | 61 | | protected bool IsTimerRunning { get; set; } |
| | 658 | 62 | | protected bool IsTimerPaused { get; set; } |
| | 658 | 63 | | protected bool IsTimerStarted { get; set; } |
| | 329 | 64 | | protected bool IsConsentModalVisible { get; set; } |
| | 329 | 65 | | protected int ConsentCountdown { get; set; } |
| | 599 | 66 | | protected List<ConsentOption> ConsentOptions { get; set; } = new(); |
| | 339 | 67 | | protected bool ShowKeyboardHelp { get; set; } |
| | 484 | 68 | | public string? ErrorMessage { get; set; } |
| | 341 | 69 | | public bool IsPipOpen { get; set; } |
| | | 70 | | |
| | | 71 | | private (int TotalFocusMinutes, int PomodoroCount, int TasksWorkedOn)? _cachedTodayStats; |
| | | 72 | | |
| | 3 | 73 | | private void InvalidateTodayStatsCache() => _cachedTodayStats = null; |
| | | 74 | | |
| | 350 | 75 | | protected int TodayTotalFocusMinutes => GetTodayStats().TotalFocusMinutes; |
| | 325 | 76 | | protected int TodayPomodoroCount => GetTodayStats().PomodoroCount; |
| | 325 | 77 | | protected int TodayTasksWorkedOn => GetTodayStats().TasksWorkedOn; |
| | | 78 | | |
| | | 79 | | private (int TotalFocusMinutes, int PomodoroCount, int TasksWorkedOn) GetTodayStats() |
| | 1000 | 80 | | { |
| | 1000 | 81 | | return _cachedTodayStats ??= TodayStatsService.GetTodayStats(); |
| | 975 | 82 | | } |
| | | 83 | | |
| | | 84 | | #endregion |
| | | 85 | | |
| | | 86 | | #region Lifecycle Methods |
| | | 87 | | |
| | | 88 | | protected override async Task OnInitializedAsync() |
| | 272 | 89 | | { |
| | | 90 | | try |
| | 272 | 91 | | { |
| | | 92 | | // Initialize notification service |
| | 272 | 93 | | await NotificationService.InitializeAsync(); |
| | | 94 | | |
| | | 95 | | // Initialize PiP timer service |
| | 268 | 96 | | await PipTimerService.InitializeAsync(); |
| | | 97 | | |
| | | 98 | | // Subscribe to service events |
| | 267 | 99 | | TaskService.OnChange += OnTaskServiceChanged; |
| | 267 | 100 | | TimerService.OnTick += OnTimerTick; |
| | 267 | 101 | | TimerService.OnTimerComplete += OnTimerComplete; |
| | 267 | 102 | | TimerService.OnStateChanged += OnTimerStateChanged; |
| | 267 | 103 | | ConsentService.OnConsentRequired += OnConsentRequired; |
| | 267 | 104 | | ConsentService.OnCountdownTick += OnConsentCountdownTick; |
| | 267 | 105 | | ConsentService.OnConsentHandled += OnConsentHandled; |
| | | 106 | | |
| | | 107 | | // Subscribe to notification action events |
| | 267 | 108 | | NotificationService.OnNotificationAction += OnNotificationAction; |
| | | 109 | | |
| | | 110 | | // Subscribe to activity changes to refresh today's summary |
| | 267 | 111 | | ActivityService.OnActivityChanged += OnActivityChanged; |
| | | 112 | | |
| | | 113 | | // Subscribe to PiP events |
| | 267 | 114 | | PipTimerService.OnPipOpened += OnPipOpened; |
| | 267 | 115 | | PipTimerService.OnPipClosed += OnPipClosed; |
| | | 116 | | |
| | | 117 | | // Register keyboard shortcuts with proper error handling |
| | 267 | 118 | | KeyboardShortcutService.RegisterShortcut("space", () => |
| | 3 | 119 | | { |
| | 3 | 120 | | SafeTaskRunner.RunAndForget( |
| | 3 | 121 | | async () => |
| | 3 | 122 | | { |
| | 3 | 123 | | if (TimerService.IsRunning) |
| | 1 | 124 | | { |
| | 1 | 125 | | await TimerService.PauseAsync(); |
| | 1 | 126 | | } |
| | 2 | 127 | | else if (TimerService.IsPaused) |
| | 1 | 128 | | { |
| | 1 | 129 | | await TimerService.ResumeAsync(); |
| | 1 | 130 | | } |
| | 3 | 131 | | else |
| | 1 | 132 | | { |
| | 1 | 133 | | await TimerService.StartPomodoroAsync(); |
| | 1 | 134 | | } |
| | 3 | 135 | | }, |
| | 3 | 136 | | Logger, |
| | 3 | 137 | | Constants.SafeTaskOperations.KeyboardShortcutPlayPause |
| | 3 | 138 | | ); |
| | 270 | 139 | | }, Constants.KeyboardShortcuts.PlayPauseDescription); |
| | | 140 | | |
| | 267 | 141 | | KeyboardShortcutService.RegisterShortcut("r", () => |
| | 1 | 142 | | { |
| | 1 | 143 | | SafeTaskRunner.RunAndForget( |
| | 1 | 144 | | () => TimerService.ResetAsync(), |
| | 1 | 145 | | Logger, |
| | 1 | 146 | | Constants.SafeTaskOperations.KeyboardShortcutReset |
| | 1 | 147 | | ); |
| | 268 | 148 | | }, Constants.KeyboardShortcuts.ResetDescription); |
| | | 149 | | |
| | | 150 | | // Session switching shortcuts |
| | 267 | 151 | | KeyboardShortcutService.RegisterShortcut("p", () => |
| | 1 | 152 | | { |
| | 1 | 153 | | SafeTaskRunner.RunAndForget( |
| | 1 | 154 | | () => TimerService.StartPomodoroAsync(), |
| | 1 | 155 | | Logger, |
| | 1 | 156 | | Constants.SafeTaskOperations.KeyboardShortcutPomodoro |
| | 1 | 157 | | ); |
| | 268 | 158 | | }, Constants.KeyboardShortcuts.PomodoroDescription); |
| | | 159 | | |
| | 267 | 160 | | KeyboardShortcutService.RegisterShortcut("s", () => |
| | 1 | 161 | | { |
| | 1 | 162 | | SafeTaskRunner.RunAndForget( |
| | 1 | 163 | | () => TimerService.StartShortBreakAsync(), |
| | 1 | 164 | | Logger, |
| | 1 | 165 | | Constants.SafeTaskOperations.KeyboardShortcutShortBreak |
| | 1 | 166 | | ); |
| | 268 | 167 | | }, Constants.KeyboardShortcuts.ShortBreakDescription); |
| | | 168 | | |
| | 267 | 169 | | KeyboardShortcutService.RegisterShortcut("l", () => |
| | 1 | 170 | | { |
| | 1 | 171 | | SafeTaskRunner.RunAndForget( |
| | 1 | 172 | | () => TimerService.StartLongBreakAsync(), |
| | 1 | 173 | | Logger, |
| | 1 | 174 | | Constants.SafeTaskOperations.KeyboardShortcutLongBreak |
| | 1 | 175 | | ); |
| | 268 | 176 | | }, Constants.KeyboardShortcuts.LongBreakDescription); |
| | | 177 | | |
| | | 178 | | // Help shortcut |
| | 267 | 179 | | KeyboardShortcutService.RegisterShortcut("?", () => |
| | 2 | 180 | | { |
| | 2 | 181 | | ShowKeyboardHelp = true; |
| | 2 | 182 | | StateHasChanged(); |
| | 269 | 183 | | }, Constants.KeyboardShortcuts.HelpDescription); |
| | | 184 | | |
| | | 185 | | // Escape shortcut - close keyboard help modal |
| | 267 | 186 | | KeyboardShortcutService.RegisterShortcut("escape", () => |
| | 2 | 187 | | { |
| | 2 | 188 | | if (ShowKeyboardHelp) |
| | 1 | 189 | | { |
| | 1 | 190 | | ShowKeyboardHelp = false; |
| | 1 | 191 | | StateHasChanged(); |
| | 1 | 192 | | } |
| | 269 | 193 | | }, "Close keyboard shortcuts"); |
| | | 194 | | |
| | | 195 | | // Load initial state |
| | 267 | 196 | | UpdateState(); |
| | | 197 | | |
| | | 198 | | // Check for pending notification action from URL |
| | | 199 | | // Delay slightly to ensure all services are ready |
| | | 200 | | // Using SafeTaskRunner for proper exception handling |
| | 267 | 201 | | SafeTaskRunner.RunAndForget( |
| | 267 | 202 | | async () => |
| | 267 | 203 | | { |
| | 267 | 204 | | await Task.Delay(Constants.UI.NotificationCheckDelayMs); |
| | 263 | 205 | | await CheckPendingNotificationActionAsync(); |
| | 261 | 206 | | }, |
| | 267 | 207 | | Logger, |
| | 267 | 208 | | Constants.SafeTaskOperations.CheckPendingNotificationAction |
| | 267 | 209 | | ); |
| | 267 | 210 | | } |
| | 5 | 211 | | catch (Exception ex) |
| | 5 | 212 | | { |
| | 5 | 213 | | ErrorMessage = $"{Constants.Messages.ErrorInitializing}: {ex.Message}"; |
| | 5 | 214 | | } |
| | 272 | 215 | | } |
| | | 216 | | |
| | | 217 | | /// <summary> |
| | | 218 | | /// Check for pending notification action from URL parameter |
| | | 219 | | /// This handles the case when the app is opened from a notification click |
| | | 220 | | /// </summary> |
| | | 221 | | private async Task CheckPendingNotificationActionAsync() |
| | 265 | 222 | | { |
| | | 223 | | try |
| | 265 | 224 | | { |
| | | 225 | | // Check URL parameter (set by service worker when opening new window) |
| | 265 | 226 | | var urlAction = await JSRuntime.InvokeAsync<string>(Constants.JsFunctions.GetUrlParameter, Constants.UrlPara |
| | 147 | 227 | | if (!string.IsNullOrEmpty(urlAction)) |
| | 3 | 228 | | { |
| | 3 | 229 | | var decodedAction = Uri.UnescapeDataString(urlAction); |
| | | 230 | | // Clean up URL |
| | 3 | 231 | | await JSRuntime.InvokeVoidAsync(Constants.JsFunctions.RemoveUrlParameter, Constants.UrlParameters.Notifi |
| | | 232 | | // Process the action |
| | 6 | 233 | | await InvokeAsync(() => OnNotificationAction(decodedAction)); |
| | 3 | 234 | | } |
| | 147 | 235 | | } |
| | 118 | 236 | | catch (Exception ex) |
| | 118 | 237 | | { |
| | 118 | 238 | | Logger.LogError(ex, Constants.Messages.ErrorCheckingPendingNotificationAction); |
| | 116 | 239 | | } |
| | 263 | 240 | | } |
| | | 241 | | |
| | | 242 | | #endregion |
| | | 243 | | |
| | | 244 | | #region Helper Methods |
| | | 245 | | |
| | | 246 | | private void UpdateState() |
| | 334 | 247 | | { |
| | 334 | 248 | | var state = IndexPagePresenterService.UpdateState(TaskService, TimerService); |
| | | 249 | | |
| | 333 | 250 | | Tasks = state.Tasks; |
| | 333 | 251 | | CurrentTaskId = state.CurrentTaskId; |
| | 333 | 252 | | RemainingTime = state.RemainingTime; |
| | 333 | 253 | | CurrentSessionType = state.CurrentSessionType; |
| | 333 | 254 | | IsTimerRunning = state.IsTimerRunning; |
| | 333 | 255 | | IsTimerPaused = state.IsTimerPaused; |
| | 333 | 256 | | IsTimerStarted = state.IsTimerStarted; |
| | 333 | 257 | | } |
| | | 258 | | |
| | | 259 | | #endregion |
| | | 260 | | |
| | | 261 | | #region Cleanup |
| | | 262 | | |
| | | 263 | | private bool _isDisposed; |
| | | 264 | | |
| | | 265 | | public void Dispose() |
| | 276 | 266 | | { |
| | 280 | 267 | | if (_isDisposed) return; |
| | 272 | 268 | | _isDisposed = true; |
| | | 269 | | |
| | | 270 | | try |
| | 272 | 271 | | { |
| | 272 | 272 | | UnsubscribeFromAllServices(); |
| | 272 | 273 | | UnregisterKeyboardShortcuts(); |
| | 269 | 274 | | } |
| | 3 | 275 | | catch (Exception ex) |
| | 3 | 276 | | { |
| | 3 | 277 | | Logger?.LogError(ex, Constants.Messages.ErrorInDispose); |
| | 3 | 278 | | } |
| | 276 | 279 | | } |
| | | 280 | | |
| | | 281 | | private void UnsubscribeFromAllServices() |
| | 272 | 282 | | { |
| | 272 | 283 | | if (TaskService != null) |
| | 272 | 284 | | TaskService.OnChange -= OnTaskServiceChanged; |
| | 272 | 285 | | if (TimerService != null) |
| | 272 | 286 | | { |
| | 272 | 287 | | TimerService.OnTick -= OnTimerTick; |
| | 272 | 288 | | TimerService.OnTimerComplete -= OnTimerComplete; |
| | 272 | 289 | | TimerService.OnStateChanged -= OnTimerStateChanged; |
| | 272 | 290 | | } |
| | 272 | 291 | | if (ConsentService != null) |
| | 272 | 292 | | { |
| | 272 | 293 | | ConsentService.OnConsentRequired -= OnConsentRequired; |
| | 272 | 294 | | ConsentService.OnCountdownTick -= OnConsentCountdownTick; |
| | 272 | 295 | | ConsentService.OnConsentHandled -= OnConsentHandled; |
| | 272 | 296 | | } |
| | 272 | 297 | | if (NotificationService != null) |
| | 272 | 298 | | NotificationService.OnNotificationAction -= OnNotificationAction; |
| | 272 | 299 | | if (ActivityService != null) |
| | 272 | 300 | | ActivityService.OnActivityChanged -= OnActivityChanged; |
| | 272 | 301 | | if (PipTimerService != null) |
| | 272 | 302 | | { |
| | 272 | 303 | | PipTimerService.OnPipOpened -= OnPipOpened; |
| | 272 | 304 | | PipTimerService.OnPipClosed -= OnPipClosed; |
| | 272 | 305 | | } |
| | 272 | 306 | | } |
| | | 307 | | |
| | | 308 | | private void UnregisterKeyboardShortcuts() |
| | 272 | 309 | | { |
| | 272 | 310 | | if (KeyboardShortcutService != null) |
| | 272 | 311 | | { |
| | 272 | 312 | | KeyboardShortcutService.UnregisterShortcut("space"); |
| | 269 | 313 | | KeyboardShortcutService.UnregisterShortcut("r"); |
| | 269 | 314 | | KeyboardShortcutService.UnregisterShortcut("p"); |
| | 269 | 315 | | KeyboardShortcutService.UnregisterShortcut("s"); |
| | 269 | 316 | | KeyboardShortcutService.UnregisterShortcut("l"); |
| | 269 | 317 | | KeyboardShortcutService.UnregisterShortcut("?"); |
| | 269 | 318 | | KeyboardShortcutService.UnregisterShortcut("escape"); |
| | 269 | 319 | | } |
| | 269 | 320 | | } |
| | | 321 | | |
| | | 322 | | #endregion |
| | | 323 | | } |