| | | 1 | | namespace Allyaria.Theming.Blazor; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// Provides a Blazor component that connects the <see cref="IThemingService" /> to the browser, handling system theme |
| | | 5 | | /// detection, persisted theme preference, and document direction/language. |
| | | 6 | | /// </summary> |
| | | 7 | | /// <remarks> |
| | | 8 | | /// <para> |
| | | 9 | | /// This component is intended to be placed near the root of the app and wraps the rest of the UI via |
| | | 10 | | /// <see cref="ChildContent" />. It: |
| | | 11 | | /// </para> |
| | | 12 | | /// <list type="bullet"> |
| | | 13 | | /// <item> |
| | | 14 | | /// <description> |
| | | 15 | | /// Synchronizes the effective theme with OS/browser preferences when <see cref="ThemeType.System" /> is |
| | | 16 | | /// selected. |
| | | 17 | | /// </description> |
| | | 18 | | /// </item> |
| | | 19 | | /// <item> |
| | | 20 | | /// <description>Persists the stored theme type in browser storage.</description> |
| | | 21 | | /// </item> |
| | | 22 | | /// <item> |
| | | 23 | | /// <description> |
| | | 24 | | /// Updates <c>dir</c> and <c>lang</c> attributes on <c>document.documentElement</c>, and toggles a <c>rtl</ |
| | | 25 | | /// class on <c>body</c>. |
| | | 26 | | /// </description> |
| | | 27 | | /// </item> |
| | | 28 | | /// </list> |
| | | 29 | | /// </remarks> |
| | | 30 | | public sealed partial class AryThemeProvider : ComponentBase, IAsyncDisposable |
| | | 31 | | { |
| | | 32 | | /// <summary>Local storage key used to persist the user's theme preference.</summary> |
| | | 33 | | private const string StorageKey = "allyaria.themeType"; |
| | | 34 | | |
| | | 35 | | /// <summary> |
| | | 36 | | /// Cancellation token source used to cancel ongoing asynchronous operations when the component is disposed. |
| | | 37 | | /// </summary> |
| | | 38 | | private CancellationTokenSource? _cts; |
| | | 39 | | |
| | | 40 | | /// <summary>DotNet object reference used by JavaScript interop to call back into this component.</summary> |
| | | 41 | | private DotNetObjectReference<AryThemeProvider>? _dotNetRef; |
| | | 42 | | |
| | | 43 | | /// <summary>Tracks the current effective theme type applied by this provider.</summary> |
| | | 44 | | /// <remarks> |
| | | 45 | | /// This value mirrors <see cref="IThemingService.EffectiveType" />, but is cached locally to avoid redundant update |
| | | 46 | | /// </remarks> |
| | | 47 | | private ThemeType _effectiveType; |
| | | 48 | | |
| | | 49 | | /// <summary>Host element reference for this component, used as the root for JS interop initialization.</summary> |
| | | 50 | | private ElementReference _host; |
| | | 51 | | |
| | | 52 | | /// <summary>Indicates whether system theme detection is currently initialized and listening for changes.</summary> |
| | | 53 | | private bool _isStarted; |
| | | 54 | | |
| | | 55 | | /// <summary> |
| | | 56 | | /// Tracks the previously applied culture in order to detect when the effective UI culture changes. Used to avoid re |
| | | 57 | | /// direction and language updates to the document root. |
| | | 58 | | /// </summary> |
| | | 59 | | private CultureInfo? _lastCulture; |
| | | 60 | | |
| | | 61 | | /// <summary>JavaScript module reference for the co-located <c>AryThemeProvider.razor.js</c> file.</summary> |
| | | 62 | | private IJSObjectReference? _module; |
| | | 63 | | |
| | | 64 | | /// <summary>Cached copy of the stored theme type used for persistence and detection lifecycle management.</summary> |
| | | 65 | | private ThemeType _storedType = ThemeType.System; |
| | | 66 | | |
| | | 67 | | /// <summary>Gets or sets the child content that will be rendered within this theme provider.</summary> |
| | | 68 | | [Parameter] |
| | | 69 | | [EditorRequired] |
| | 6 | 70 | | public RenderFragment? ChildContent { get; set; } |
| | | 71 | | |
| | | 72 | | /// <summary>Gets or sets the culture used to determine document direction and language attributes.</summary> |
| | | 73 | | /// <remarks>When not supplied, <see cref="CultureInfo.CurrentUICulture" /> is used as a fallback.</remarks> |
| | | 74 | | [CascadingParameter] |
| | 21 | 75 | | public CultureInfo? Culture { get; set; } |
| | | 76 | | |
| | | 77 | | /// <summary>Gets the document-level CSS for the current effective theme.</summary> |
| | | 78 | | /// <remarks> |
| | | 79 | | /// This value is intended for consumption by the Razor markup to inject global CSS (for example, via a co-located & |
| | | 80 | | /// style> block managed by this component). |
| | | 81 | | /// </remarks> |
| | 3 | 82 | | private string GlobalCss => ThemingService.GetDocumentCss(); |
| | | 83 | | |
| | | 84 | | /// <summary>Gets the JavaScript runtime used for interop with the browser environment.</summary> |
| | | 85 | | [Inject] |
| | 74 | 86 | | public required IJSRuntime JsRuntime { get; init; } |
| | | 87 | | |
| | | 88 | | /// <summary>Gets the theming service that maintains the current stored and effective theme types.</summary> |
| | | 89 | | [Inject] |
| | 89 | 90 | | public required IThemingService ThemingService { get; init; } |
| | | 91 | | |
| | | 92 | | /// <summary>Detects the current system theme using the co-located JavaScript module.</summary> |
| | | 93 | | /// <param name="cancellationToken">A token used to cancel the detection operation.</param> |
| | | 94 | | /// <returns> |
| | | 95 | | /// A <see cref="ThemeType" /> corresponding to the detected system theme, or <c>null</c> when detection fails or th |
| | | 96 | | /// result is <see cref="ThemeType.System" />. |
| | | 97 | | /// </returns> |
| | | 98 | | public async Task<ThemeType?> DetectThemeAsync(CancellationToken cancellationToken = default) |
| | | 99 | | { |
| | 9 | 100 | | if (_module is null || IsCancellationRequested()) |
| | | 101 | | { |
| | 3 | 102 | | return null; |
| | | 103 | | } |
| | | 104 | | |
| | | 105 | | try |
| | | 106 | | { |
| | 6 | 107 | | var raw = await _module |
| | 6 | 108 | | .InvokeAsync<string>(identifier: "detect", cancellationToken: cancellationToken) |
| | 6 | 109 | | .ConfigureAwait(continueOnCapturedContext: false); |
| | | 110 | | |
| | 5 | 111 | | var parsed = ParseThemeType(value: raw); |
| | | 112 | | |
| | 5 | 113 | | return parsed is ThemeType.System |
| | 5 | 114 | | ? null |
| | 5 | 115 | | : parsed; |
| | | 116 | | } |
| | 1 | 117 | | catch |
| | | 118 | | { |
| | | 119 | | // On any JS interop failure, fall back to "no detection". |
| | 1 | 120 | | return null; |
| | | 121 | | } |
| | 9 | 122 | | } |
| | | 123 | | |
| | | 124 | | /// <summary> |
| | | 125 | | /// Asynchronously disposes the component, cancelling outstanding work and releasing JS interop resources. |
| | | 126 | | /// </summary> |
| | | 127 | | /// <returns>A task representing the asynchronous dispose operation.</returns> |
| | | 128 | | public async ValueTask DisposeAsync() |
| | | 129 | | { |
| | | 130 | | try |
| | | 131 | | { |
| | 4 | 132 | | ThemingService.ThemeChanged -= OnThemeChangedAsync; |
| | | 133 | | |
| | 4 | 134 | | if (_cts is not null) |
| | | 135 | | { |
| | 3 | 136 | | await _cts.CancelAsync().ConfigureAwait(continueOnCapturedContext: false); |
| | 3 | 137 | | _cts.Dispose(); |
| | 3 | 138 | | _cts = null; |
| | | 139 | | } |
| | | 140 | | |
| | 4 | 141 | | if (_module is not null) |
| | | 142 | | { |
| | | 143 | | try |
| | | 144 | | { |
| | 3 | 145 | | await _module.InvokeVoidAsync(identifier: "dispose", _host) |
| | 3 | 146 | | .ConfigureAwait(continueOnCapturedContext: false); |
| | 3 | 147 | | } |
| | 0 | 148 | | catch |
| | | 149 | | { |
| | | 150 | | // Ignore teardown errors; they can race with navigation/unload. |
| | 0 | 151 | | } |
| | | 152 | | |
| | 3 | 153 | | await _module.DisposeAsync().ConfigureAwait(continueOnCapturedContext: false); |
| | 3 | 154 | | _module = null; |
| | | 155 | | } |
| | | 156 | | |
| | 4 | 157 | | _dotNetRef?.Dispose(); |
| | 4 | 158 | | _dotNetRef = null; |
| | 4 | 159 | | } |
| | 0 | 160 | | catch |
| | | 161 | | { |
| | | 162 | | // Swallow dispose-time failures to avoid surfacing teardown exceptions to the host. |
| | 0 | 163 | | } |
| | 4 | 164 | | } |
| | | 165 | | |
| | | 166 | | /// <summary>Attempts to load a previously stored theme type from the browser using the JS module.</summary> |
| | | 167 | | /// <param name="cancellationToken">A token used to cancel the retrieval operation.</param> |
| | | 168 | | /// <returns> |
| | | 169 | | /// The stored <see cref="ThemeType" /> if available and valid; otherwise <see cref="ThemeType.System" />. |
| | | 170 | | /// </returns> |
| | | 171 | | private async Task<ThemeType> GetStoredTypeAsync(CancellationToken cancellationToken = default) |
| | | 172 | | { |
| | 7 | 173 | | if (_module is null || IsCancellationRequested()) |
| | | 174 | | { |
| | 1 | 175 | | return ThemeType.System; |
| | | 176 | | } |
| | | 177 | | |
| | | 178 | | try |
| | | 179 | | { |
| | 6 | 180 | | var raw = await _module |
| | 6 | 181 | | .InvokeAsync<string?>( |
| | 6 | 182 | | identifier: "getStoredTheme", |
| | 6 | 183 | | cancellationToken: cancellationToken, |
| | 6 | 184 | | StorageKey |
| | 6 | 185 | | ) |
| | 6 | 186 | | .ConfigureAwait(continueOnCapturedContext: false); |
| | | 187 | | |
| | 4 | 188 | | return Enum.TryParse(value: raw, ignoreCase: true, result: out ThemeType parsed) |
| | 4 | 189 | | ? parsed |
| | 4 | 190 | | : ThemeType.System; |
| | | 191 | | } |
| | 2 | 192 | | catch |
| | | 193 | | { |
| | | 194 | | // On any JS failure, treat as "no stored value". |
| | 2 | 195 | | return ThemeType.System; |
| | | 196 | | } |
| | 7 | 197 | | } |
| | | 198 | | |
| | | 199 | | /// <summary>Determines whether cancellation has been requested for this component's operations.</summary> |
| | | 200 | | /// <returns> |
| | | 201 | | /// <c>true</c> if there is no active <see cref="CancellationTokenSource" /> or the current token has been cancelled |
| | | 202 | | /// otherwise, <c>false</c>. |
| | | 203 | | /// </returns> |
| | | 204 | | private bool IsCancellationRequested() |
| | | 205 | | { |
| | 27 | 206 | | var cts = _cts; |
| | | 207 | | |
| | 27 | 208 | | return cts is null || cts.IsCancellationRequested; |
| | | 209 | | } |
| | | 210 | | |
| | | 211 | | /// <summary> |
| | | 212 | | /// Performs post-render initialization, wiring up JS interop, synchronizing stored theme, and setting document dire |
| | | 213 | | /// </summary> |
| | | 214 | | /// <param name="firstRender"> |
| | | 215 | | /// <c>true</c> when this is the first time the component has been rendered; otherwise, |
| | | 216 | | /// <c>false</c>. |
| | | 217 | | /// </param> |
| | | 218 | | /// <returns>A task that completes when initialization steps have finished.</returns> |
| | | 219 | | protected override async Task OnAfterRenderAsync(bool firstRender) |
| | | 220 | | { |
| | 5 | 221 | | if (!firstRender) |
| | | 222 | | { |
| | 1 | 223 | | return; |
| | | 224 | | } |
| | | 225 | | |
| | 4 | 226 | | ThemingService.ThemeChanged += OnThemeChangedAsync; |
| | | 227 | | |
| | 4 | 228 | | _cts = new CancellationTokenSource(); |
| | | 229 | | |
| | 4 | 230 | | _module ??= await JsRuntime |
| | 4 | 231 | | .InvokeAsync<IJSObjectReference>( |
| | 4 | 232 | | identifier: "import", |
| | 4 | 233 | | cancellationToken: _cts.Token, |
| | 4 | 234 | | "./AryThemeProvider.razor.js" |
| | 4 | 235 | | ) |
| | 4 | 236 | | .ConfigureAwait(continueOnCapturedContext: false); |
| | | 237 | | |
| | 4 | 238 | | var storedType = await GetStoredTypeAsync(cancellationToken: _cts.Token) |
| | 4 | 239 | | .ConfigureAwait(continueOnCapturedContext: false); |
| | | 240 | | |
| | | 241 | | // Always adopt the stored theme when it differs from the current service value, |
| | | 242 | | // including ThemeType.System, so that ThemingService.StoredType is driven by browser storage. |
| | 4 | 243 | | if (storedType != ThemingService.StoredType) |
| | | 244 | | { |
| | 1 | 245 | | ThemingService.SetStoredType(themeType: storedType); |
| | | 246 | | } |
| | | 247 | | |
| | 4 | 248 | | if (ThemingService.StoredType == ThemeType.System) |
| | | 249 | | { |
| | 3 | 250 | | await StartDetectAsync(cancellationToken: _cts.Token) |
| | 3 | 251 | | .ConfigureAwait(continueOnCapturedContext: false); |
| | | 252 | | } |
| | | 253 | | |
| | 4 | 254 | | var detected = ThemingService.StoredType == ThemeType.System |
| | 4 | 255 | | ? await DetectThemeAsync(cancellationToken: _cts.Token) |
| | 4 | 256 | | .ConfigureAwait(continueOnCapturedContext: false) |
| | 4 | 257 | | : null; |
| | | 258 | | |
| | 4 | 259 | | var effectiveType = detected ?? (ThemingService.StoredType == ThemeType.System |
| | 4 | 260 | | ? ThemeType.Light |
| | 4 | 261 | | : ThemingService.StoredType); |
| | | 262 | | |
| | 4 | 263 | | UpdateEffectiveType(effectiveType: effectiveType); |
| | | 264 | | |
| | 4 | 265 | | await UpdateStoredTypeAsync( |
| | 4 | 266 | | storedType: ThemingService.StoredType, |
| | 4 | 267 | | cancellationToken: _cts.Token |
| | 4 | 268 | | ) |
| | 4 | 269 | | .ConfigureAwait(continueOnCapturedContext: false); |
| | | 270 | | |
| | 4 | 271 | | await SetDirectionAsync().ConfigureAwait(continueOnCapturedContext: false); |
| | 5 | 272 | | } |
| | | 273 | | |
| | | 274 | | /// <summary> |
| | | 275 | | /// Invoked by the framework when parameter values or cascading values change. Detects whether the effective UI cult |
| | | 276 | | /// changed (either through <see cref="Culture" /> or <see cref="CultureInfo.CurrentUICulture" />) and, when it has, |
| | | 277 | | /// updates document direction and language attributes by calling <see cref="SetDirectionAsync" />. |
| | | 278 | | /// </summary> |
| | | 279 | | /// <remarks> |
| | | 280 | | /// This method ensures RTL/LTR changes are applied reactively when the application culture updates via |
| | | 281 | | /// <c>CascadingLocalization</c>. |
| | | 282 | | /// </remarks> |
| | | 283 | | /// <returns>A <see cref="Task" /> representing the asynchronous operation.</returns> |
| | | 284 | | protected override async Task OnParametersSetAsync() |
| | | 285 | | { |
| | 6 | 286 | | await base.OnParametersSetAsync(); |
| | | 287 | | |
| | 6 | 288 | | var currentCulture = Culture ?? CultureInfo.CurrentUICulture; |
| | | 289 | | |
| | 6 | 290 | | if (_lastCulture?.Name != currentCulture.Name) |
| | | 291 | | { |
| | 5 | 292 | | _lastCulture = currentCulture; |
| | | 293 | | |
| | 5 | 294 | | await SetDirectionAsync(); |
| | | 295 | | } |
| | 6 | 296 | | } |
| | | 297 | | |
| | | 298 | | /// <summary> |
| | | 299 | | /// Handles <see cref="IThemingService.ThemeChanged" /> events, synchronizing local state and requesting re-render. |
| | | 300 | | /// </summary> |
| | | 301 | | /// <param name="sender">The source of the event (typically the <see cref="IThemingService" /> instance).</param> |
| | | 302 | | /// <param name="e">Event data (unused).</param> |
| | | 303 | | /// <remarks> |
| | | 304 | | /// This method is intentionally <c>async void</c> to conform to the event handler pattern. It avoids throwing excep |
| | | 305 | | /// to the Blazor renderer, instead catching and swallowing failures from asynchronous work. |
| | | 306 | | /// </remarks> |
| | | 307 | | private async void OnThemeChangedAsync(object? sender, EventArgs e) |
| | | 308 | | { |
| | 3 | 309 | | var cts = _cts; |
| | | 310 | | |
| | 3 | 311 | | if (cts is null || cts.IsCancellationRequested) |
| | | 312 | | { |
| | 1 | 313 | | return; |
| | | 314 | | } |
| | | 315 | | |
| | | 316 | | try |
| | | 317 | | { |
| | 2 | 318 | | if (_storedType != ThemingService.StoredType) |
| | | 319 | | { |
| | 1 | 320 | | await UpdateStoredTypeAsync( |
| | 1 | 321 | | storedType: ThemingService.StoredType, |
| | 1 | 322 | | cancellationToken: cts.Token |
| | 1 | 323 | | ) |
| | 1 | 324 | | .ConfigureAwait(continueOnCapturedContext: false); |
| | | 325 | | } |
| | | 326 | | |
| | 2 | 327 | | var effectiveType = ThemingService.EffectiveType; |
| | | 328 | | |
| | 2 | 329 | | if (_effectiveType == effectiveType) |
| | | 330 | | { |
| | 1 | 331 | | return; |
| | | 332 | | } |
| | | 333 | | |
| | 1 | 334 | | _effectiveType = effectiveType; |
| | 1 | 335 | | await InvokeAsync(workItem: StateHasChanged); |
| | 0 | 336 | | } |
| | 1 | 337 | | catch |
| | | 338 | | { |
| | | 339 | | // Intentionally ignore errors from theme change propagation to avoid breaking the render loop. |
| | 1 | 340 | | } |
| | 3 | 341 | | } |
| | | 342 | | |
| | | 343 | | /// <summary>Parses a raw theme type string into a <see cref="ThemeType" /> value, handling known aliases.</summary> |
| | | 344 | | /// <param name="value">The raw string value returned from JavaScript.</param> |
| | | 345 | | /// <returns> |
| | | 346 | | /// The corresponding <see cref="ThemeType" /> when recognized; otherwise <see cref="ThemeType.System" />. |
| | | 347 | | /// </returns> |
| | | 348 | | private static ThemeType ParseThemeType(string? value) |
| | 16 | 349 | | => value?.ToLowerInvariant() switch |
| | 16 | 350 | | { |
| | 5 | 351 | | "highcontrast" or "hc" or "forced" or "highcontrastlight" or "hcl" => ThemeType.HighContrastLight, |
| | 2 | 352 | | "highcontrastdark" or "hcd" => ThemeType.HighContrastDark, |
| | 2 | 353 | | "dark" => ThemeType.Dark, |
| | 1 | 354 | | "light" => ThemeType.Light, |
| | 6 | 355 | | _ => ThemeType.System |
| | 16 | 356 | | }; |
| | | 357 | | |
| | | 358 | | /// <summary>Sets the document direction and language attributes based on the current or cascaded culture.</summary> |
| | | 359 | | /// <returns>A task that completes when the JS interop calls have finished or failed.</returns> |
| | | 360 | | /// <remarks> |
| | | 361 | | /// This method does not currently support cancellation because it is short-lived and best-effort. It silently ignor |
| | | 362 | | /// failures (for example, during prerendering when JS is not available). |
| | | 363 | | /// </remarks> |
| | | 364 | | public async Task SetDirectionAsync() |
| | | 365 | | { |
| | 11 | 366 | | var culture = Culture ?? CultureInfo.CurrentUICulture; |
| | | 367 | | |
| | 11 | 368 | | var dir = culture.TextInfo.IsRightToLeft |
| | 11 | 369 | | ? "rtl" |
| | 11 | 370 | | : "ltr"; |
| | | 371 | | |
| | 11 | 372 | | var lang = culture.IetfLanguageTag; |
| | | 373 | | |
| | | 374 | | try |
| | | 375 | | { |
| | 11 | 376 | | await JsRuntime.InvokeVoidAsync(identifier: "document.documentElement.setAttribute", "dir", dir); |
| | 11 | 377 | | await JsRuntime.InvokeVoidAsync(identifier: "document.documentElement.setAttribute", "lang", lang); |
| | | 378 | | |
| | 11 | 379 | | await JsRuntime.InvokeVoidAsync( |
| | 11 | 380 | | identifier: "(d,cls,add)=>add?d.body.classList.add(cls):d.body.classList.remove(cls)", |
| | 11 | 381 | | "document", |
| | 11 | 382 | | "rtl", |
| | 11 | 383 | | dir == "rtl" |
| | 11 | 384 | | ); |
| | 11 | 385 | | } |
| | 0 | 386 | | catch |
| | | 387 | | { |
| | | 388 | | // Code Coverage: Unreachable code. Ignore if JS is not ready (e.g., during prerendering). |
| | 0 | 389 | | } |
| | 11 | 390 | | } |
| | | 391 | | |
| | | 392 | | /// <summary>Receives theme updates from JavaScript when system theme detection fires.</summary> |
| | | 393 | | /// <param name="raw">The raw theme string supplied by the JS module.</param> |
| | | 394 | | [JSInvokable] |
| | | 395 | | public void SetFromJs(string raw) |
| | | 396 | | { |
| | 11 | 397 | | var effectiveType = ParseThemeType(value: raw); |
| | 11 | 398 | | UpdateEffectiveType(effectiveType: effectiveType); |
| | 11 | 399 | | } |
| | | 400 | | |
| | | 401 | | /// <summary>Persists the current stored theme type to browser storage via the JS module.</summary> |
| | | 402 | | /// <param name="cancellationToken">A token used to cancel the storage operation.</param> |
| | | 403 | | /// <returns>A task that completes when the value has been sent to JS or an error has been ignored.</returns> |
| | | 404 | | private async Task SetStoredTypeAsync(CancellationToken cancellationToken = default) |
| | | 405 | | { |
| | 6 | 406 | | if (_module is null || IsCancellationRequested()) |
| | | 407 | | { |
| | 0 | 408 | | return; |
| | | 409 | | } |
| | | 410 | | |
| | | 411 | | try |
| | | 412 | | { |
| | 6 | 413 | | _ = await _module |
| | 6 | 414 | | .InvokeAsync<bool>( |
| | 6 | 415 | | identifier: "setStoredTheme", |
| | 6 | 416 | | cancellationToken: cancellationToken, |
| | 6 | 417 | | StorageKey, |
| | 6 | 418 | | _storedType.ToString() |
| | 6 | 419 | | ) |
| | 6 | 420 | | .ConfigureAwait(continueOnCapturedContext: false); |
| | 4 | 421 | | } |
| | 2 | 422 | | catch |
| | | 423 | | { |
| | | 424 | | // Code Coverage: Unreachable code. Intentionally ignored: storage may be blocked or unavailable. |
| | 2 | 425 | | } |
| | 6 | 426 | | } |
| | | 427 | | |
| | | 428 | | /// <summary>Starts system theme detection via the JS module if it is not already active.</summary> |
| | | 429 | | /// <param name="cancellationToken">A token used to cancel the initialization operation.</param> |
| | | 430 | | /// <returns>A task that completes when initialization has finished or failed.</returns> |
| | | 431 | | private async Task StartDetectAsync(CancellationToken cancellationToken = default) |
| | | 432 | | { |
| | 5 | 433 | | if (_isStarted || _module is null || IsCancellationRequested()) |
| | | 434 | | { |
| | | 435 | | // Code coverage: Unreachable code. |
| | 0 | 436 | | return; |
| | | 437 | | } |
| | | 438 | | |
| | | 439 | | try |
| | | 440 | | { |
| | 5 | 441 | | _dotNetRef ??= DotNetObjectReference.Create(value: this); |
| | | 442 | | |
| | 5 | 443 | | await _module |
| | 5 | 444 | | .InvokeVoidAsync( |
| | 5 | 445 | | identifier: "init", |
| | 5 | 446 | | cancellationToken: cancellationToken, |
| | 5 | 447 | | _host, |
| | 5 | 448 | | _dotNetRef |
| | 5 | 449 | | ) |
| | 5 | 450 | | .ConfigureAwait(continueOnCapturedContext: false); |
| | | 451 | | |
| | 4 | 452 | | _isStarted = true; |
| | 4 | 453 | | } |
| | 1 | 454 | | catch |
| | | 455 | | { |
| | 1 | 456 | | _dotNetRef?.Dispose(); |
| | 1 | 457 | | _dotNetRef = null; |
| | 1 | 458 | | _isStarted = false; |
| | 1 | 459 | | } |
| | 5 | 460 | | } |
| | | 461 | | |
| | | 462 | | /// <summary>Stops system theme detection and detaches JS listeners.</summary> |
| | | 463 | | /// <returns>A task that completes when listeners have been detached or an error has been ignored.</returns> |
| | | 464 | | private async Task StopDetectAsync() |
| | | 465 | | { |
| | 4 | 466 | | if (!_isStarted || IsCancellationRequested()) |
| | | 467 | | { |
| | 3 | 468 | | return; |
| | | 469 | | } |
| | | 470 | | |
| | | 471 | | try |
| | | 472 | | { |
| | 1 | 473 | | if (_module is not null) |
| | | 474 | | { |
| | 1 | 475 | | await _module.InvokeVoidAsync(identifier: "dispose", _host) |
| | 1 | 476 | | .ConfigureAwait(continueOnCapturedContext: false); |
| | | 477 | | } |
| | 1 | 478 | | } |
| | 0 | 479 | | catch |
| | | 480 | | { |
| | | 481 | | // Code coverage: Unreachable code. Ignore failures during teardown. |
| | 0 | 482 | | } |
| | | 483 | | finally |
| | | 484 | | { |
| | 1 | 485 | | _dotNetRef?.Dispose(); |
| | 1 | 486 | | _dotNetRef = null; |
| | 1 | 487 | | _isStarted = false; |
| | | 488 | | } |
| | 4 | 489 | | } |
| | | 490 | | |
| | | 491 | | /// <summary>Updates the effective theme type both locally and in the theming service when it has changed.</summary> |
| | | 492 | | /// <param name="effectiveType">The new effective <see cref="ThemeType" /> to apply.</param> |
| | | 493 | | private void UpdateEffectiveType(ThemeType effectiveType) |
| | | 494 | | { |
| | 15 | 495 | | if (_effectiveType == effectiveType) |
| | | 496 | | { |
| | 2 | 497 | | return; |
| | | 498 | | } |
| | | 499 | | |
| | 13 | 500 | | _effectiveType = effectiveType; |
| | 13 | 501 | | ThemingService.SetEffectiveType(themeType: effectiveType); |
| | 13 | 502 | | } |
| | | 503 | | |
| | | 504 | | /// <summary>Updates the stored theme type, managing detection lifecycle and persistence.</summary> |
| | | 505 | | /// <param name="storedType">The new stored <see cref="ThemeType" /> value.</param> |
| | | 506 | | /// <param name="cancellationToken">A token used to cancel JS-backed operations.</param> |
| | | 507 | | /// <returns>A task that completes when detection and storage updates have finished.</returns> |
| | | 508 | | private async Task UpdateStoredTypeAsync(ThemeType storedType, CancellationToken cancellationToken = default) |
| | | 509 | | { |
| | 7 | 510 | | if (_storedType == storedType) |
| | | 511 | | { |
| | 3 | 512 | | return; |
| | | 513 | | } |
| | | 514 | | |
| | 4 | 515 | | _storedType = storedType; |
| | | 516 | | |
| | 4 | 517 | | if (_storedType is ThemeType.System) |
| | | 518 | | { |
| | 1 | 519 | | await StartDetectAsync(cancellationToken: cancellationToken) |
| | 1 | 520 | | .ConfigureAwait(continueOnCapturedContext: false); |
| | | 521 | | } |
| | | 522 | | else |
| | | 523 | | { |
| | 3 | 524 | | await StopDetectAsync().ConfigureAwait(continueOnCapturedContext: false); |
| | | 525 | | } |
| | | 526 | | |
| | 4 | 527 | | await SetStoredTypeAsync(cancellationToken: cancellationToken) |
| | 4 | 528 | | .ConfigureAwait(continueOnCapturedContext: false); |
| | 7 | 529 | | } |
| | | 530 | | } |