6using CommunityToolkit.Maui.Core;
25 private const string providerFlagKey =
"IsServerThemeDictionary";
26 private const string localFlagKey =
"IsLocalThemeDictionary";
27 private static readonly TimeSpan themeExpiry =
Constants.
Cache.DefaultImageCache;
30 private static readonly
string brandingSchemaKeyV1 =
Constants.
Schemes.NeuroAccessBrandingV1;
33 private enum ProviderThemeStatus { NotStarted, InProgress, Applied, NotSupported, FailedPermanent, FailedTransient }
34 private enum BrandingFetchClassification { Success, NotFound, TransientFailure, PermanentFailure }
35 private ProviderThemeStatus providerThemeState = ProviderThemeStatus.NotStarted;
36 private string? lastDomainAttempted;
37 private Task? backgroundRefreshTask;
38 private const string unsupportedCachePrefix =
"BrandingUnsupported:";
39 private static readonly TimeSpan blockingFetchTimeout = TimeSpan.FromSeconds(6);
40 private static readonly TimeSpan backgroundFetchTimeout = TimeSpan.FromSeconds(2);
41 private static readonly TimeSpan manualFetchTimeout = TimeSpan.FromSeconds(8);
42 private static readonly TimeSpan probeTimeout = TimeSpan.FromSeconds(2);
43 private static readonly HttpClient httpClient =
new() { Timeout = TimeSpan.FromSeconds(5) };
45 private readonly FileCacheManager cacheManager;
46 private readonly Dictionary<string, Uri> imageUrisMap;
47 private ResourceDictionary? localLightDict =
new Light();
48 private ResourceDictionary? localDarkDict =
new Dark();
49 private AppTheme? lastAppliedLocalTheme;
50 private bool disposedValue;
57 this.cacheManager =
new FileCacheManager(
"BrandingThemes", themeExpiry);
58 this.imageUrisMap =
new(StringComparer.OrdinalIgnoreCase);
64 public IReadOnlyDictionary<string, Uri>
ImageUris =>
new Dictionary<string, Uri>(this.imageUrisMap);
69 public Task<AppTheme>
GetTheme() => Task.FromResult(Application.Current?.UserAppTheme ?? AppTheme.Unspecified);
83 MainThread.BeginInvokeOnMainThread(() =>
87 ServiceRef.TagProfile.Theme = Theme;
88 Application.Current!.UserAppTheme = Theme;
95 CommunityToolkit.Maui.Core.Platform.StatusBar.SetStyle(Theme == AppTheme.Dark ? StatusBarStyle.LightContent : StatusBarStyle.DarkContent);
104 {
ServiceRef.
LogService.LogException(
new Exception($
"SetTheme failed for theme {Theme}.", Ex)); }
114 if (Theme == AppTheme.Unspecified)
115 Theme = Application.Current?.RequestedTheme ?? AppTheme.Unspecified;
117 MainThread.BeginInvokeOnMainThread(() =>
121 ICollection<ResourceDictionary> Merged = Application.Current!.Resources.MergedDictionaries;
122 this.RemoveAllThemeDictionaries(Merged, this.localLightDict);
123 this.RemoveAllThemeDictionaries(Merged, this.localDarkDict);
124 this.localLightDict ??=
new Light();
125 this.localDarkDict ??=
new Dark();
126 if (Theme == AppTheme.Dark && !Merged.Contains(
this.localDarkDict))
127 Merged.Add(this.localDarkDict);
128 else if (Theme != AppTheme.Dark && !Merged.Contains(
this.localLightDict))
129 Merged.Add(this.localLightDict);
130 this.lastAppliedLocalTheme = Theme;
133 {
ServiceRef.
LogService.LogException(
new Exception($
"SetLocalTheme failed for theme {Theme}.", Ex)); }
154 if (
string.IsNullOrWhiteSpace(Domain))
161 if (this.lastDomainAttempted is not
null && this.lastDomainAttempted.Equals(Domain, StringComparison.OrdinalIgnoreCase) &&
162 this.providerThemeState is ProviderThemeStatus.Applied or ProviderThemeStatus.NotSupported or ProviderThemeStatus.FailedPermanent)
164 ServiceRef.
LogService.LogDebug($
"ApplyProviderThemeAsync: Already finalized for {Domain} state={this.providerThemeState}.");
166 return this.providerThemeState
switch
170 _ => ThemeApplyOutcome.AppliedFromCache
174 this.providerThemeState = ProviderThemeStatus.InProgress;
175 this.lastDomainAttempted = Domain;
179 if (Policy !=
ThemeFetchPolicy.ManualRefresh && await
this.IsBrandingUnsupportedAsync(Domain))
182 this.providerThemeState = ProviderThemeStatus.NotSupported;
183 await this.SetLocalThemeFromBackgroundThread();
187 (
bool AppliedFromCache,
bool CacheExpired) = await this.TryApplyCachedProviderThemeAsync(Domain, Policy, CancellationToken);
188 if (AppliedFromCache)
190 this.providerThemeState = ProviderThemeStatus.Applied;
193 ThemeApplyOutcome RefreshOutcome = await this.FetchAndApplyProviderThemeAsync(Domain, Policy,
false, CancellationToken,
true);
194 return RefreshOutcome == ThemeApplyOutcome.Applied ? ThemeApplyOutcome.Applied :
ThemeApplyOutcome.AppliedFromCache;
197 this.StartBackgroundRefresh(Domain);
203 this.StartBackgroundRefresh(Domain);
204 this.providerThemeState = ProviderThemeStatus.FailedTransient;
205 await this.SetLocalThemeFromBackgroundThread();
210 ThemeApplyOutcome Outcome = await this.FetchAndApplyProviderThemeAsync(Domain, Policy,
true, CancellationToken, ForceNetwork);
211 this.providerThemeState = Outcome
switch
213 ThemeApplyOutcome.Applied => ProviderThemeStatus.Applied,
214 ThemeApplyOutcome.AppliedFromCache => ProviderThemeStatus.Applied,
215 ThemeApplyOutcome.NotSupported => ProviderThemeStatus.NotSupported,
216 ThemeApplyOutcome.FailedPermanent => ProviderThemeStatus.FailedPermanent,
217 ThemeApplyOutcome.FailedTransient => ProviderThemeStatus.FailedTransient,
218 _ => this.providerThemeState
228 private async Task SetLocalThemeFromBackgroundThread()
236 ServiceRef.
LogService.LogException(
new Exception(
"SetLocalThemeFromBackgroundThread failed.", Ex));
240 private void StartBackgroundRefresh(
string Domain)
244 if (this.backgroundRefreshTask is not
null && !this.backgroundRefreshTask.IsCompleted)
246 this.backgroundRefreshTask = Task.Run(async () =>
252 this.providerThemeState = ProviderThemeStatus.Applied;
256 ServiceRef.
LogService.LogException(
new Exception($
"Background provider theme refresh failed for {Domain}.", Ex));
261 private async Task<(
bool Applied,
bool CacheExpired)> TryApplyCachedProviderThemeAsync(
string Domain,
ThemeFetchPolicy Policy, CancellationToken CancellationToken)
263 (
bool Applied,
bool CacheExpired) = await this.TryApplyCachedBrandingAsync(Domain,
true, Policy, CancellationToken);
265 return (
true, CacheExpired);
266 return await this.TryApplyCachedBrandingAsync(Domain,
false, Policy, CancellationToken);
269 private async Task<(
bool Applied,
bool CacheExpired)> TryApplyCachedBrandingAsync(
string Domain,
bool IsV2,
ThemeFetchPolicy Policy, CancellationToken CancellationToken)
271 (XmlDocument? Document,
bool IsExpired) = await this.TryGetCachedBrandingXmlAsync(Domain, IsV2);
272 if (Document is
null)
273 return (
false,
false);
276 await this.ApplyV2Async(Document, Policy, CancellationToken,
false);
278 await this.ApplyV1Async(Document, Policy, CancellationToken,
false);
280 return (
true, IsExpired);
283 private async Task<ThemeApplyOutcome> FetchAndApplyProviderThemeAsync(
string Domain,
ThemeFetchPolicy Policy,
bool ApplyFallbackOnFailure, CancellationToken CancellationToken,
bool ForceNetwork)
286 return this.HandleFetchFailure(
ThemeApplyOutcome.FailedTransient, ApplyFallbackOnFailure);
288 (
bool Success, XmlDocument? Document, BrandingFetchClassification Classification) V2 = await this.TryGetBrandingXmlAsync(Domain,
true, Policy, ForceNetwork, CancellationToken);
289 if (V2.Success && V2.Document is not
null)
291 await this.ApplyV2Async(V2.Document, Policy, CancellationToken, ForceNetwork);
292 await this.ClearBrandingUnsupportedAsync(Domain);
295 if (V2.Classification == BrandingFetchClassification.TransientFailure)
296 return this.HandleFetchFailure(
ThemeApplyOutcome.FailedTransient, ApplyFallbackOnFailure);
297 if (V2.Classification == BrandingFetchClassification.PermanentFailure)
298 return this.HandleFetchFailure(
ThemeApplyOutcome.FailedPermanent, ApplyFallbackOnFailure);
300 (
bool Success, XmlDocument? Document, BrandingFetchClassification Classification) V1 = await this.TryGetBrandingXmlAsync(Domain,
false, Policy, ForceNetwork, CancellationToken);
301 if (V1.Success && V1.Document is not
null)
303 await this.ApplyV1Async(V1.Document, Policy, CancellationToken, ForceNetwork);
304 await this.ClearBrandingUnsupportedAsync(Domain);
308 return V1.Classification
switch
310 BrandingFetchClassification.NotFound => await this.HandleNotSupportedAsync(Domain, ApplyFallbackOnFailure),
311 BrandingFetchClassification.TransientFailure => this.HandleFetchFailure(
ThemeApplyOutcome.FailedTransient, ApplyFallbackOnFailure),
312 _ => this.HandleFetchFailure(
ThemeApplyOutcome.FailedPermanent, ApplyFallbackOnFailure)
316 private async Task<ThemeApplyOutcome> HandleNotSupportedAsync(
string Domain,
bool ApplyFallbackOnFailure)
318 await this.MarkBrandingUnsupportedAsync(Domain);
319 if (ApplyFallbackOnFailure)
320 await this.SetLocalThemeFromBackgroundThread();
326 if (ApplyFallbackOnFailure)
328 _ = this.SetLocalThemeFromBackgroundThread();
333 private async Task<bool> IsBrandingUnsupportedAsync(
string Domain)
335 string Key = GetUnsupportedCacheKey(Domain);
336 (
byte[]? Data,
string _,
bool IsExpired) = await this.cacheManager.TryGetWithExpiry(Key);
337 return Data is not
null && !IsExpired;
340 private async Task MarkBrandingUnsupportedAsync(
string Domain)
342 string Key = GetUnsupportedCacheKey(Domain);
343 byte[] Data = Encoding.ASCII.GetBytes(
"1");
344 await this.cacheManager.AddOrUpdate(Key, Domain,
false, Data,
"text/plain");
347 private Task<bool> ClearBrandingUnsupportedAsync(
string Domain)
349 string Key = GetUnsupportedCacheKey(Domain);
350 return this.cacheManager.Remove(Key);
353 private static string GetUnsupportedCacheKey(
string Domain)
355 string KeyDomain = Domain.ToLowerInvariant();
356 return $
"{unsupportedCachePrefix}{KeyDomain}";
363 ThemeFetchPolicy.BlockingFirstRun => blockingFetchTimeout,
364 ThemeFetchPolicy.ManualRefresh => manualFetchTimeout,
365 _ => backgroundFetchTimeout
369 private async Task<(XmlDocument? Document,
bool IsExpired)> TryGetCachedBrandingXmlAsync(
string Domain,
bool IsV2)
371 Uri Uri = BuildBrandingItemUrl(Domain, IsV2 ?
"BrandingV2" :
"Branding");
372 string Key = Uri.AbsoluteUri;
373 (
byte[]? Cached,
string _,
bool IsExpired) = await this.cacheManager.TryGetWithExpiry(Key);
375 return (
null,
false);
377 string XmlString = Encoding.UTF8.GetString(Cached);
378 bool Valid = await this.ValidateBrandingXmlAsync(XmlString, IsV2, Domain);
381 await this.cacheManager.Remove(Key);
382 return (
null,
false);
386 XmlDocument Doc =
new();
387 Doc.LoadXml(XmlString);
388 return (Doc, IsExpired);
392 ServiceRef.
LogService.LogException(
new Exception($
"TryGetCachedBrandingXmlAsync parse {(IsV2 ? "V2
" : "V1
")} {Uri}", Ex));
393 await this.cacheManager.Remove(Key);
394 return (
null,
false);
398 private async Task<(
bool Success, XmlDocument? Document, BrandingFetchClassification Classification)> TryGetBrandingXmlAsync(
string Domain,
bool IsV2,
ThemeFetchPolicy Policy,
bool ForceNetwork, CancellationToken CancellationToken)
400 Uri Uri = BuildBrandingItemUrl(Domain, IsV2 ?
"BrandingV2" :
"Branding");
401 string Key = Uri.AbsoluteUri;
402 (
byte[]? Bytes,
bool _,
bool AttemptedNetwork) = await this.FetchOrGetCachedAsync(Uri, Key, Policy, ForceNetwork, CancellationToken);
403 if (Bytes is not
null)
405 string XmlString = Encoding.UTF8.GetString(Bytes);
406 bool Valid = await this.ValidateBrandingXmlAsync(XmlString, IsV2, Domain);
409 await this.cacheManager.Remove(Key);
410 return (
false,
null, BrandingFetchClassification.PermanentFailure);
414 XmlDocument Doc =
new();
415 Doc.LoadXml(XmlString);
416 return (
true, Doc, BrandingFetchClassification.Success);
420 ServiceRef.
LogService.LogException(
new Exception($
"TryGetBrandingXmlAsync parse {(IsV2 ? "V2
" : "V1
")} {Uri}", Ex));
421 await this.cacheManager.Remove(Key);
422 return (
false,
null, BrandingFetchClassification.PermanentFailure);
425 if (!AttemptedNetwork)
426 return (
false,
null, BrandingFetchClassification.TransientFailure);
428 TimeSpan ProbeTimeout = GetFetchTimeout(Policy);
429 if (ProbeTimeout > probeTimeout)
430 ProbeTimeout = probeTimeout;
431 HttpStatusCode Probe = await ProbeUriAsync(Uri, ProbeTimeout, CancellationToken);
434 HttpStatusCode.NotFound => (
false,
null, BrandingFetchClassification.NotFound),
435 HttpStatusCode.ServiceUnavailable or HttpStatusCode.GatewayTimeout or HttpStatusCode.BadGateway => (
false,
null, BrandingFetchClassification.TransientFailure),
436 HttpStatusCode.RequestTimeout => (
false,
null, BrandingFetchClassification.TransientFailure),
437 0 => (
false,
null, BrandingFetchClassification.PermanentFailure),
438 _ => (
false,
null, BrandingFetchClassification.PermanentFailure)
442 private static async Task<HttpStatusCode> ProbeUriAsync(Uri Uri, TimeSpan Timeout, CancellationToken CancellationToken)
446 using CancellationTokenSource LinkedCts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken);
447 LinkedCts.CancelAfter(Timeout);
448 using HttpRequestMessage Req =
new HttpRequestMessage(HttpMethod.Head, Uri);
449 using HttpResponseMessage Resp = await httpClient.SendAsync(Req, LinkedCts.Token);
450 return Resp.StatusCode;
452 catch (HttpRequestException Ex) when (IsTransientNetwork(Ex))
456 catch (OperationCanceledException)
465 private static bool IsTransientNetwork(Exception Ex) => Ex is HttpRequestException or SocketException or TaskCanceledException or TimeoutException;
467 private static Uri BuildBrandingItemUrl(
string Domain,
string ItemId) =>
ServiceRef.
TagProfile.DomainIsLocal() ?
468 new($
"http://{Domain}/PubSub/NeuroAccessBranding/{ItemId}")
470 new($
"https://{Domain}/PubSub/NeuroAccessBranding/{ItemId}");
476 public string GetImageUri(
string Id) => this.imageUrisMap.TryGetValue(Id, out Uri? ImageUri) ? ImageUri.AbsoluteUri :
string.Empty;
478 private async Task ApplyV2Async(XmlDocument Doc,
ThemeFetchPolicy Policy, CancellationToken CancellationToken,
bool ForceNetwork)
480 XmlElement? Root = Doc.DocumentElement;
485 this.imageUrisMap.Clear();
486 foreach (XmlElement Node
in Root.SelectNodes(
"//*[local-name()='ImageRef']")?.OfType<XmlElement>() ?? [])
488 string Id = Node.GetAttribute(
"id");
489 string UriText = Node.GetAttribute(
"uri");
490 if (!
string.IsNullOrEmpty(Id) && !
string.IsNullOrEmpty(UriText) && Uri.TryCreate(UriText, UriKind.Absolute, out Uri? ImgUri))
491 this.imageUrisMap[Id] = ImgUri;
493 Uri? LightUri =
null;
495 foreach (XmlElement Node
in Root.SelectNodes(
"//*[local-name()='ColorsUri']")?.OfType<XmlElement>() ?? [])
497 string ThemeName = Node.GetAttribute(
"theme").ToLowerInvariant();
498 string UriText2 = Node.InnerText.Trim();
499 if (ThemeName ==
"light")
500 LightUri =
new Uri(UriText2);
501 else if (ThemeName ==
"dark")
502 DarkUri =
new Uri(UriText2);
504 await MainThread.InvokeOnMainThreadAsync(async () =>
506 ICollection<ResourceDictionary>? Merged = Application.Current?.Resources.MergedDictionaries;
509 this.RemoveAllThemeDictionaries(Merged, this.localLightDict);
510 this.RemoveAllThemeDictionaries(Merged, this.localDarkDict);
511 this.localLightDict ??=
new Light();
512 this.localDarkDict ??=
new Dark();
513 this.localLightDict.Clear();
514 this.localDarkDict.Clear();
515 if (LightUri is not
null)
517 ResourceDictionary? LightDict = await this.LoadProviderDictionaryAsync(LightUri,
"Light", Policy, CancellationToken, ForceNetwork);
518 if (LightDict is not
null)
foreach (
string K
in LightDict.Keys.OfType<
string>()) this.localLightDict[K] = LightDict[K];
520 if (DarkUri is not
null)
522 ResourceDictionary? DarkDict = await this.LoadProviderDictionaryAsync(DarkUri,
"Dark", Policy, CancellationToken, ForceNetwork);
523 if (DarkDict is not
null)
foreach (
string K
in DarkDict.Keys.OfType<
string>()) this.localDarkDict[K] = DarkDict[K];
525 this.localLightDict[localFlagKey] =
true;
526 this.localDarkDict[localFlagKey] =
true;
527 AppTheme Current = await this.
GetTheme();
528 if (Current == AppTheme.Dark && !Merged.Contains(
this.localDarkDict))
529 Merged.Add(this.localDarkDict);
530 else if (Current != AppTheme.Dark && !Merged.Contains(
this.localLightDict))
531 Merged.Add(this.localLightDict);
532 this.lastAppliedLocalTheme = Current;
536 private async Task ApplyV1Async(XmlDocument Doc,
ThemeFetchPolicy Policy, CancellationToken CancellationToken,
bool ForceNetwork)
538 XmlElement? Root = Doc.DocumentElement;
539 if (Root is
null || Root.NamespaceURI !=
Constants.
Schemes.NeuroAccessBrandingV1)
541 this.imageUrisMap.Clear();
542 foreach (XmlElement Node
in Root.SelectNodes(
"//*[local-name()='ImageRef']")?.OfType<XmlElement>() ?? [])
544 string Id = Node.GetAttribute(
"id");
string UriText = Node.GetAttribute(
"uri");
545 if (!
string.IsNullOrEmpty(Id) && !
string.IsNullOrEmpty(UriText) && Uri.TryCreate(UriText, UriKind.Absolute, out Uri? ImgUri)) this.imageUrisMap[Id] = ImgUri;
547 if (Root.SelectSingleNode(
"//*[local-name()='ColorsUri']") is not XmlElement ColorsNode)
549 Uri ColorsUri =
new(ColorsNode.InnerText.Trim());
550 ResourceDictionary? Orig = await this.LoadProviderDictionaryAsync(ColorsUri,
"V1", Policy, CancellationToken, ForceNetwork);
553 Dictionary<string, object>
Light = [];
554 Dictionary<string, object>
Dark = [];
555 foreach (
string K
in Orig.Keys.OfType<
string>())
557 if (K.EndsWith(
"Light", StringComparison.OrdinalIgnoreCase))
Light[K[..^5]] = Orig[K];
558 else if (K.EndsWith(
"Dark", StringComparison.OrdinalIgnoreCase))
Dark[K[..^4]] = Orig[K];
559 else {
Light[K] = Orig[K];
Dark[K] = Orig[K]; }
561 await MainThread.InvokeOnMainThreadAsync(async () =>
563 ICollection<ResourceDictionary>? Merged = Application.Current?.Resources.MergedDictionaries;
566 this.RemoveAllThemeDictionaries(Merged, this.localLightDict);
567 this.RemoveAllThemeDictionaries(Merged, this.localDarkDict);
568 this.localLightDict ??=
new Light(); this.localDarkDict ??=
new Dark();
569 this.localLightDict.Clear();
570 this.localDarkDict.Clear();
571 foreach (KeyValuePair<string, object> Kv
in Light)
572 this.localLightDict[Kv.Key] = Kv.Value;
573 foreach (KeyValuePair<string, object> Kv
in Dark)
574 this.localDarkDict[Kv.Key] = Kv.Value;
575 this.localLightDict[localFlagKey] =
true;
576 this.localDarkDict[localFlagKey] =
true;
577 AppTheme Current = await this.
GetTheme();
578 if (Current == AppTheme.Dark && !Merged.Contains(
this.localDarkDict))
579 Merged.Add(this.localDarkDict);
580 else if (Current != AppTheme.Dark && !Merged.Contains(
this.localLightDict))
581 Merged.Add(this.localLightDict);
582 this.lastAppliedLocalTheme = Current;
586 private async Task<ResourceDictionary?> LoadProviderDictionaryAsync(Uri Uri,
string Tag,
ThemeFetchPolicy Policy, CancellationToken CancellationToken,
bool ForceNetwork)
588 (
byte[]? Bytes,
bool _,
bool _) = await this.FetchOrGetCachedAsync(Uri, Uri.AbsoluteUri, Policy, ForceNetwork, CancellationToken);
593 ResourceDictionary Dict =
new ResourceDictionary().LoadFromXaml(Encoding.UTF8.GetString(Bytes));
594 Dict.TryAdd(providerFlagKey,
true); Dict.TryAdd(
"Theme", Tag);
599 ServiceRef.
LogService.LogException(
new Exception($
"Failed to load provider ResourceDictionary from {Uri}", Ex));
604 private async Task<(
byte[]? Data,
bool CacheExpired,
bool AttemptedNetwork)> FetchOrGetCachedAsync(Uri Uri,
string Key,
ThemeFetchPolicy Policy,
bool ForceNetwork, CancellationToken CancellationToken)
608 if (CancellationToken.IsCancellationRequested)
609 return (
null,
false,
false);
610 (
byte[]? Cached,
string _,
bool IsExpired) = await this.cacheManager.TryGetWithExpiry(Key);
611 if (Cached is not
null && !ForceNetwork)
612 return (Cached, IsExpired,
false);
615 return (Cached, IsExpired,
false);
617 TimeSpan Timeout = GetFetchTimeout(Policy);
619 if (Fetched is not
null)
621 await this.cacheManager.AddOrUpdate(Key,
ServiceRef.
TagProfile.PubSubJid!,
false, Fetched,
"application/xml");
622 return (Fetched,
false,
true);
624 return (Cached, IsExpired,
true);
628 ServiceRef.
LogService.LogException(
new Exception($
"FetchOrGetCachedAsync failed for {Uri}", Ex));
629 return (
null,
false,
false);
633 private async Task<bool> ValidateBrandingXmlAsync(
string Xml,
bool V2,
string Domain)
644 if (
string.Equals(SchemaKey,
Constants.
Schemes.NeuroAccessBrandingV2Url, StringComparison.Ordinal))
646 ServiceRef.
LogService.LogDebug(
"BrandingXmlValidationPrimarySuccess",
new KeyValuePair<string, object?>(
"Domain", Domain));
651 "BrandingXmlValidationFallbackSuccess",
652 new KeyValuePair<string, object?>(
"Domain", Domain),
653 new KeyValuePair<string, object?>(
"LegacyKey", SchemaKey));
659 "BrandingXmlValidationBothFailed",
660 new KeyValuePair<string, object?>(
"Domain", Domain),
661 new KeyValuePair<string, object?>(
"PrimaryKey",
Constants.
Schemes.NeuroAccessBrandingV2Url),
662 new KeyValuePair<string, object?>(
"LegacyKey",
Constants.
Schemes.NeuroAccessBrandingV2));
670 "BrandingXmlValidationV1Failed",
671 new KeyValuePair<string, object?>(
"Domain", Domain),
672 new KeyValuePair<string, object?>(
"Key", brandingSchemaKeyV1));
678 ServiceRef.
LogService.LogException(
new Exception($
"Branding XML validation failed for schema version {(V2 ? "V2
" : "V1
")}.", Ex),
new KeyValuePair<string, object?>(
"Domain", Domain));
683 private void RemoveAllThemeDictionaries(ICollection<ResourceDictionary> Merged, ResourceDictionary? Instance)
685 if (Instance is not
null)
while (Merged.Contains(Instance)) Merged.Remove(Instance);
686 foreach (ResourceDictionary? D
in Merged.Where(D => D.ContainsKey(localFlagKey) || D.ContainsKey(providerFlagKey)).ToList()) Merged.Remove(D);
695 if (
string.IsNullOrWhiteSpace(ParentId))
698 int Removed = await this.cacheManager.RemoveByParentId(ParentId);
700 if (!
string.IsNullOrWhiteSpace(Domain))
701 await this.cacheManager.Remove(GetUnsupportedCacheKey(Domain));
705 private void Dispose(
bool disposing)
707 if (!this.disposedValue)
712 this.disposedValue =
true;
XML Schemes (namespaces used also as schema registration keys) + packaged file names.
A set of never changing property constants and helpful values.
Base class that references services in the app.
static ILogService LogService
Log service.
static INetworkService NetworkService
Network service.
static ITagProfile TagProfile
TAG Profile service.
static IXmlSchemaValidationService XmlSchemaValidationService
XML schema validation service.
Manages application theming. Applies bundled (local) light/dark themes and, when a provider domain is...
void SetLocalTheme(AppTheme Theme)
Applies only bundled local theme dictionaries for the selected theme (no remote branding fetch).
string GetImageUri(string Id)
Returns the absolute URI string for a branding image reference id, or empty string if not present.
void SetTheme(AppTheme Theme)
Sets the desired application theme and applies its local resource dictionary. If Unspecified,...
IReadOnlyDictionary< string, Uri > ImageUris
Current mapping of branding image identifiers to absolute URIs (empty if no provider theme applied).
Task< AppTheme > GetTheme()
Returns the current user application theme (may be AppTheme.Unspecified if unset).
async Task ApplyProviderThemeAsync()
Attempts to fetch and apply provider branding using the background refresh policy.
async Task< ThemeApplyOutcome > ApplyProviderThemeAsync(ThemeFetchPolicy Policy, CancellationToken CancellationToken=default)
Attempts to fetch and apply provider branding according to the specified policy.
async Task< int > ClearBrandingCacheForCurrentDomain()
Clears locally cached branding descriptors for the current provider domain.
void Dispose()
Disposes the service, releasing synchronization resources.
TaskCompletionSource ThemeLoaded
Completes when a provider theme application attempt (success, unsupported, or failure) finishes.
ThemeService()
Initializes a new ThemeService instance with cache manager and synchronization primitives.
Static class that gives access to app-specific themed colors. All colors are fetched directly from th...
static Color PrimaryBackground
Primary background color.
Service for loading, applying, and retrieving themes and branding in the application....
ThemeApplyOutcome
Represents the outcome of a provider theme application attempt.
ThemeFetchPolicy
Defines how provider branding should be fetched and applied.