using System; using System.Linq; using System.Threading.Tasks; using Bit.App.Models; using Bit.App.Styles; using Bit.Core.Abstractions; using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.Forms; namespace Bit.App.Utilities { public static class ThemeManager { public static bool UsingLightTheme = true; public static Func Resources = () => null; public static bool IsThemeDirty = false; public const string Light = "light"; public const string Dark = "dark"; public const string Black = "black"; public const string Nord = "nord"; public static void SetThemeStyle(string name, string autoDarkName, ResourceDictionary resources) { try { Resources = () => resources; var newTheme = NeedsThemeUpdate(name, autoDarkName, resources); if (newTheme is null) { return; } var currentTheme = resources.MergedDictionaries.FirstOrDefault(md => md is IThemeResourceDictionary); if (currentTheme != null) { resources.MergedDictionaries.Remove(currentTheme); resources.MergedDictionaries.Add(newTheme); UsingLightTheme = newTheme is Light; IsThemeDirty = true; return; } // Reset styles resources.Clear(); resources.MergedDictionaries.Clear(); // Variables resources.MergedDictionaries.Add(new Variables()); // Theme resources.MergedDictionaries.Add(newTheme); UsingLightTheme = newTheme is Light; // Base styles resources.MergedDictionaries.Add(new Base()); // Platform styles if (Device.RuntimePlatform == Device.Android) { resources.MergedDictionaries.Add(new Android()); } else if (Device.RuntimePlatform == Device.iOS) { resources.MergedDictionaries.Add(new iOS()); } } catch (InvalidOperationException ioex) when (ioex.Message != null && ioex.Message.StartsWith("Collection was modified")) { // https://github.com/bitwarden/mobile/issues/1689 There are certain scenarios where this might cause "collection was modified; enumeration operation may not execute" // the way I found to prevent this for now was to catch the exception here and move on. // Because on the screens that I found it to happen, the screen is being closed while trying to apply the resources // so we shouldn't be introducing any issues. // TODO: Maybe something like this https://github.com/matteobortolazzo/HtmlLabelPlugin/pull/113 can be implemented to avoid this // on html labels. } catch (Exception ex) { LoggerHelper.LogEvenIfCantBeResolved(ex); } } static ResourceDictionary CheckAndGetThemeForMergedDictionaries(Type themeType, ResourceDictionary resources) { return resources.MergedDictionaries.Any(rd => rd.GetType() == themeType) ? null : Activator.CreateInstance(themeType) as ResourceDictionary; } static ResourceDictionary NeedsThemeUpdate(string themeName, string autoDarkThemeName, ResourceDictionary resources) { switch (themeName) { case Dark: return CheckAndGetThemeForMergedDictionaries(typeof(Dark), resources); case Black: return CheckAndGetThemeForMergedDictionaries(typeof(Black), resources); case Nord: return CheckAndGetThemeForMergedDictionaries(typeof(Nord), resources); case Light: return CheckAndGetThemeForMergedDictionaries(typeof(Light), resources); default: if (OsDarkModeEnabled()) { switch (autoDarkThemeName) { case Black: return CheckAndGetThemeForMergedDictionaries(typeof(Black), resources); case Nord: return CheckAndGetThemeForMergedDictionaries(typeof(Nord), resources); default: return CheckAndGetThemeForMergedDictionaries(typeof(Dark), resources); } } return CheckAndGetThemeForMergedDictionaries(typeof(Light), resources); } } public static void SetTheme(ResourceDictionary resources) { SetThemeStyle(GetTheme(), GetAutoDarkTheme(), resources); } public static string GetTheme() { var stateService = ServiceContainer.Resolve("stateService"); return stateService.GetThemeAsync().GetAwaiter().GetResult(); } public static string GetAutoDarkTheme() { var stateService = ServiceContainer.Resolve("stateService"); return stateService.GetAutoDarkThemeAsync().GetAwaiter().GetResult(); } public static bool OsDarkModeEnabled() { if (Application.Current == null) { // called from iOS extension var app = new App(new AppOptions { IosExtension = true }); return app.RequestedTheme == OSAppTheme.Dark; } return Application.Current.RequestedTheme == OSAppTheme.Dark; } public static void ApplyResourcesToPage(ContentPage page) { foreach (var resourceDict in Resources().MergedDictionaries) { page.Resources.Add(resourceDict); } } public static Color GetResourceColor(string color) { return (Color)Resources()[color]; } public static async Task UpdateThemeOnPagesAsync() { try { if (IsThemeDirty) { IsThemeDirty = false; await Application.Current.MainPage.TraverseNavigationRecursivelyAsync(async p => { if (p is IThemeDirtablePage themeDirtablePage) { themeDirtablePage.IsThemeDirty = true; if (p.IsVisible) { await themeDirtablePage.UpdateOnThemeChanged(); } } }); } } catch (Exception ex) { LoggerHelper.LogEvenIfCantBeResolved(ex); } } } }