From 74e90da662e1f7c17f1ce621a5ad1a1beb04e144 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Mon, 24 Jan 2022 17:20:48 -0300 Subject: [PATCH] Improve Theming (#1707) * Improved theming logic and performance, also fixed some issues regarding changing the theme after vault timeout and fixed theme applying on password generator/history * Removed messenger from theme update, and now the navigation stack is traversed and each IThemeDirtablePage gets theme updated * Improved code on update theme on pages --- src/Android/MainActivity.cs | 36 +++--- src/App/App.xaml.cs | 24 ++-- src/App/Pages/BaseContentPage.cs | 14 +++ .../Generator/GeneratorHistoryPage.xaml.cs | 16 ++- .../GeneratorHistoryPageViewModel.cs | 28 ++++- src/App/Pages/Generator/GeneratorPage.xaml | 1 + src/App/Pages/Generator/GeneratorPage.xaml.cs | 31 +++-- src/App/Styles/Black.xaml.cs | 2 +- src/App/Styles/Dark.xaml.cs | 2 +- src/App/Styles/IThemeDirtablePage.cs | 15 +++ src/App/Styles/IThemeResourceDictionary.cs | 6 + src/App/Styles/Light.xaml.cs | 2 +- src/App/Styles/Nord.xaml.cs | 2 +- src/App/Utilities/PageExtensions.cs | 53 ++++++++ src/App/Utilities/ThemeManager.cs | 114 ++++++++++++------ src/iOS/AppDelegate.cs | 2 + 16 files changed, 264 insertions(+), 84 deletions(-) create mode 100644 src/App/Styles/IThemeDirtablePage.cs create mode 100644 src/App/Styles/IThemeResourceDictionary.cs create mode 100644 src/App/Utilities/PageExtensions.cs diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index b73e42dc2..c44c83a3a 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -1,25 +1,24 @@ -using Android.App; -using Android.Content.PM; -using Android.Runtime; -using Android.OS; -using Bit.Core; -using System.Linq; -using Bit.App.Abstractions; -using Bit.Core.Utilities; -using Bit.Core.Abstractions; +using System; using System.IO; -using System; -using Android.Content; -using Bit.Droid.Utilities; -using Bit.Droid.Receivers; -using Bit.App.Models; -using Bit.Core.Enums; -using Android.Nfc; +using System.Linq; using System.Threading.Tasks; +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.Nfc; +using Android.OS; +using Android.Runtime; using AndroidX.Core.Content; +using Bit.App.Abstractions; +using Bit.App.Models; using Bit.App.Utilities; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Utilities; +using Bit.Droid.Receivers; +using Bit.Droid.Utilities; using ZXing.Net.Mobile.Android; -using Android.Util; namespace Bit.Droid { @@ -120,6 +119,9 @@ namespace Bit.Droid base.OnResume(); Xamarin.Essentials.Platform.OnResume(); AppearanceAdjustments(); + + ThemeManager.UpdateThemeOnPagesAsync(); + if (_deviceActionService.SupportsNfc()) { try diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index 8c671bb65..e75f711dc 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -8,6 +8,7 @@ using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Utilities; using System; +using System.Threading; using System.Threading.Tasks; using Xamarin.Forms; using Xamarin.Forms.Xaml; @@ -216,7 +217,8 @@ namespace Bit.App private async void ResumedAsync() { - UpdateTheme(); + await UpdateThemeAsync(); + await _vaultTimeoutService.CheckVaultTimeoutAsync(); _messagingService.Send("startEventTimer"); await ClearCacheIfNeededAsync(); @@ -228,6 +230,15 @@ namespace Bit.App } } + public async Task UpdateThemeAsync() + { + await Device.InvokeOnMainThreadAsync(() => + { + ThemeManager.SetTheme(Device.RuntimePlatform == Device.Android, Current.Resources); + _messagingService.Send("updatedTheme"); + }); + } + private void SetCulture() { // Calendars are removed by linker. ref https://bugzilla.xamarin.com/show_bug.cgi?id=59077 @@ -329,7 +340,7 @@ namespace Bit.App ThemeManager.SetTheme(Device.RuntimePlatform == Device.Android, Current.Resources); Current.RequestedThemeChanged += (s, a) => { - UpdateTheme(); + UpdateThemeAsync(); }; Current.MainPage = new HomePage(); var mainPageTask = SetMainPageAsync(); @@ -353,15 +364,6 @@ namespace Bit.App }); } - private void UpdateTheme() - { - Device.BeginInvokeOnMainThread(() => - { - ThemeManager.SetTheme(Device.RuntimePlatform == Device.Android, Current.Resources); - _messagingService.Send("updatedTheme"); - }); - } - private async Task LockedAsync(bool autoPromptBiometric) { await _stateService.PurgeAsync(); diff --git a/src/App/Pages/BaseContentPage.cs b/src/App/Pages/BaseContentPage.cs index eaf592bf6..681e36820 100644 --- a/src/App/Pages/BaseContentPage.cs +++ b/src/App/Pages/BaseContentPage.cs @@ -30,9 +30,17 @@ namespace Bit.App.Pages public DateTime? LastPageAction { get; set; } + public bool IsThemeDirty { get; set; } + protected override void OnAppearing() { base.OnAppearing(); + + if (IsThemeDirty) + { + UpdateOnThemeChanged(); + } + SaveActivity(); } @@ -123,5 +131,11 @@ namespace Bit.App.Pages SetServices(); _storageService.SaveAsync(Constants.LastActiveTimeKey, _deviceActionService.GetActiveTime()); } + + public virtual Task UpdateOnThemeChanged() + { + IsThemeDirty = false; + return Task.CompletedTask; + } } } diff --git a/src/App/Pages/Generator/GeneratorHistoryPage.xaml.cs b/src/App/Pages/Generator/GeneratorHistoryPage.xaml.cs index d40ba0d50..c4f75644c 100644 --- a/src/App/Pages/Generator/GeneratorHistoryPage.xaml.cs +++ b/src/App/Pages/Generator/GeneratorHistoryPage.xaml.cs @@ -1,10 +1,12 @@ -using Bit.App.Resources; -using System; +using System; +using System.Threading.Tasks; +using Bit.App.Resources; +using Bit.App.Styles; using Xamarin.Forms; namespace Bit.App.Pages { - public partial class GeneratorHistoryPage : BaseContentPage + public partial class GeneratorHistoryPage : BaseContentPage, IThemeDirtablePage { private GeneratorHistoryPageViewModel _vm; @@ -28,6 +30,7 @@ namespace Bit.App.Pages protected override async void OnAppearing() { base.OnAppearing(); + await LoadOnAppearedAsync(_mainLayout, true, async () => { await _vm.InitAsync(); }); @@ -59,5 +62,12 @@ namespace Bit.App.Pages await _vm.ClearAsync(); } } + + public override async Task UpdateOnThemeChanged() + { + await base.UpdateOnThemeChanged(); + + await _vm?.UpdateOnThemeChanged(); + } } } diff --git a/src/App/Pages/Generator/GeneratorHistoryPageViewModel.cs b/src/App/Pages/Generator/GeneratorHistoryPageViewModel.cs index f4b78c5d4..e92aedde4 100644 --- a/src/App/Pages/Generator/GeneratorHistoryPageViewModel.cs +++ b/src/App/Pages/Generator/GeneratorHistoryPageViewModel.cs @@ -1,9 +1,12 @@ -using Bit.App.Resources; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Models.Domain; using Bit.Core.Utilities; -using System.Collections.Generic; -using System.Threading.Tasks; +#if !FDROID +using Microsoft.AppCenter.Crashes; +#endif using Xamarin.Forms; namespace Bit.App.Pages @@ -19,8 +22,7 @@ namespace Bit.App.Pages public GeneratorHistoryPageViewModel() { _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); - _passwordGenerationService = ServiceContainer.Resolve( - "passwordGenerationService"); + _passwordGenerationService = ServiceContainer.Resolve("passwordGenerationService"); _clipboardService = ServiceContainer.Resolve("clipboardService"); PageTitle = AppResources.PasswordHistory; @@ -57,5 +59,21 @@ namespace Bit.App.Pages _platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.Password)); } + + public async Task UpdateOnThemeChanged() + { + try + { + await Device.InvokeOnMainThreadAsync(() => History.ResetWithRange(new List())); + + await InitAsync(); + } + catch (System.Exception ex) + { +#if !FDROID + Crashes.TrackError(ex); +#endif + } + } } } diff --git a/src/App/Pages/Generator/GeneratorPage.xaml b/src/App/Pages/Generator/GeneratorPage.xaml index 51f8b99c8..14c32e2c4 100644 --- a/src/App/Pages/Generator/GeneratorPage.xaml +++ b/src/App/Pages/Generator/GeneratorPage.xaml @@ -63,6 +63,7 @@ ().SetUpdateMode(UpdateMode.WhenFinished); + _typePicker.On().SetUpdateMode(UpdateMode.WhenFinished); } } @@ -61,18 +61,19 @@ namespace Bit.App.Pages protected async override void OnAppearing() { base.OnAppearing(); + + lblPassword.IsVisible = true; + if (!_fromTabPage) { await InitAsync(); } - _broadcasterService.Subscribe(nameof(GeneratorPage), async (message) => + + _broadcasterService.Subscribe(nameof(GeneratorPage), (message) => { if (message.Command == "updatedTheme") { - Device.BeginInvokeOnMainThread(() => - { - _vm.RedrawPassword(); - }); + Device.BeginInvokeOnMainThread(() => _vm.RedrawPassword()); } }); } @@ -80,6 +81,9 @@ namespace Bit.App.Pages protected override void OnDisappearing() { base.OnDisappearing(); + + lblPassword.IsVisible = false; + _broadcasterService.Unsubscribe(nameof(GeneratorPage)); } @@ -141,5 +145,12 @@ namespace Bit.App.Pages await Navigation.PopModalAsync(); } } + + public override async Task UpdateOnThemeChanged() + { + await base.UpdateOnThemeChanged(); + + await Device.InvokeOnMainThreadAsync(() => _vm?.RedrawPassword()); + } } } diff --git a/src/App/Styles/Black.xaml.cs b/src/App/Styles/Black.xaml.cs index 24787b6b7..c13d1698f 100644 --- a/src/App/Styles/Black.xaml.cs +++ b/src/App/Styles/Black.xaml.cs @@ -2,7 +2,7 @@ namespace Bit.App.Styles { - public partial class Black : ResourceDictionary + public partial class Black : ResourceDictionary, IThemeResourceDictionary { public Black() { diff --git a/src/App/Styles/Dark.xaml.cs b/src/App/Styles/Dark.xaml.cs index d84c47e4f..b0bf3d949 100644 --- a/src/App/Styles/Dark.xaml.cs +++ b/src/App/Styles/Dark.xaml.cs @@ -2,7 +2,7 @@ namespace Bit.App.Styles { - public partial class Dark : ResourceDictionary + public partial class Dark : ResourceDictionary, IThemeResourceDictionary { public Dark() { diff --git a/src/App/Styles/IThemeDirtablePage.cs b/src/App/Styles/IThemeDirtablePage.cs new file mode 100644 index 000000000..d393a88dd --- /dev/null +++ b/src/App/Styles/IThemeDirtablePage.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; + +namespace Bit.App.Styles +{ + /// + /// This is an interface to mark the pages that need theme update special treatment + /// given that they aren't updated automatically by the Forms theme system. + /// + public interface IThemeDirtablePage + { + bool IsThemeDirty { get; set; } + + Task UpdateOnThemeChanged(); + } +} diff --git a/src/App/Styles/IThemeResourceDictionary.cs b/src/App/Styles/IThemeResourceDictionary.cs new file mode 100644 index 000000000..b080b0284 --- /dev/null +++ b/src/App/Styles/IThemeResourceDictionary.cs @@ -0,0 +1,6 @@ +namespace Bit.App.Styles +{ + public interface IThemeResourceDictionary + { + } +} diff --git a/src/App/Styles/Light.xaml.cs b/src/App/Styles/Light.xaml.cs index 99a500c97..f7defeec7 100644 --- a/src/App/Styles/Light.xaml.cs +++ b/src/App/Styles/Light.xaml.cs @@ -2,7 +2,7 @@ namespace Bit.App.Styles { - public partial class Light : ResourceDictionary + public partial class Light : ResourceDictionary, IThemeResourceDictionary { public Light() { diff --git a/src/App/Styles/Nord.xaml.cs b/src/App/Styles/Nord.xaml.cs index 64a9be61f..3ec5209f5 100644 --- a/src/App/Styles/Nord.xaml.cs +++ b/src/App/Styles/Nord.xaml.cs @@ -2,7 +2,7 @@ namespace Bit.App.Styles { - public partial class Nord : ResourceDictionary + public partial class Nord : ResourceDictionary, IThemeResourceDictionary { public Nord() { diff --git a/src/App/Utilities/PageExtensions.cs b/src/App/Utilities/PageExtensions.cs new file mode 100644 index 000000000..ec64f20ef --- /dev/null +++ b/src/App/Utilities/PageExtensions.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Utilities +{ + public static class PageExtensions + { + public static async Task TraverseNavigationRecursivelyAsync(this Page page, Func actionOnPage) + { + if (page?.Navigation?.ModalStack != null) + { + foreach (var p in page.Navigation.ModalStack) + { + if (p is NavigationPage modalNavPage) + { + await TraverseNavigationStackRecursivelyAsync(modalNavPage.CurrentPage, actionOnPage); + } + else + { + await TraverseNavigationStackRecursivelyAsync(p, actionOnPage); + } + } + } + + await TraverseNavigationStackRecursivelyAsync(page, actionOnPage); + } + + private static async Task TraverseNavigationStackRecursivelyAsync(this Page page, Func actionOnPage) + { + if (page is MultiPage multiPage && multiPage.Children != null) + { + foreach (var p in multiPage.Children) + { + await TraverseNavigationStackRecursivelyAsync(p, actionOnPage); + } + } + + if (page is NavigationPage && page.Navigation != null) + { + if (page.Navigation.NavigationStack != null) + { + foreach (var p in page.Navigation.NavigationStack) + { + await TraverseNavigationStackRecursivelyAsync(p, actionOnPage); + } + } + } + + await actionOnPage(page); + } + } +} diff --git a/src/App/Utilities/ThemeManager.cs b/src/App/Utilities/ThemeManager.cs index fbce368ab..9fc24305d 100644 --- a/src/App/Utilities/ThemeManager.cs +++ b/src/App/Utilities/ThemeManager.cs @@ -4,6 +4,8 @@ using Bit.App.Services; using Bit.App.Styles; using Bit.Core; using Xamarin.Forms; +using System.Linq; +using System.Threading.Tasks; #if !FDROID using Microsoft.AppCenter.Crashes; #endif @@ -15,12 +17,30 @@ namespace Bit.App.Utilities public static bool UsingLightTheme = true; public static Func Resources = () => null; + public static bool IsThemeDirty = false; + public static void SetThemeStyle(string name, ResourceDictionary resources) { try { Resources = () => resources; + var newTheme = NeedsThemeUpdate(name, 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(); @@ -28,40 +48,9 @@ namespace Bit.App.Utilities // Variables resources.MergedDictionaries.Add(new Variables()); - // Themed variables - if (name == "dark") - { - resources.MergedDictionaries.Add(new Dark()); - UsingLightTheme = false; - } - else if (name == "black") - { - resources.MergedDictionaries.Add(new Black()); - UsingLightTheme = false; - } - else if (name == "nord") - { - resources.MergedDictionaries.Add(new Nord()); - UsingLightTheme = false; - } - else if (name == "light") - { - resources.MergedDictionaries.Add(new Light()); - UsingLightTheme = true; - } - else - { - if (OsDarkModeEnabled()) - { - resources.MergedDictionaries.Add(new Dark()); - UsingLightTheme = false; - } - else - { - resources.MergedDictionaries.Add(new Light()); - UsingLightTheme = true; - } - } + // Theme + resources.MergedDictionaries.Add(newTheme); + UsingLightTheme = newTheme is Light; // Base styles resources.MergedDictionaries.Add(new Base()); @@ -93,6 +82,34 @@ namespace Bit.App.Utilities } } + 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, 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()) + { + return CheckAndGetThemeForMergedDictionaries(typeof(Dark), resources); + } + return CheckAndGetThemeForMergedDictionaries(typeof(Light), resources); + } + } + public static void SetTheme(bool android, ResourceDictionary resources) { SetThemeStyle(GetTheme(android), resources); @@ -128,5 +145,34 @@ namespace Bit.App.Utilities { 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) + { +#if !FDROID + Crashes.TrackError(ex); +#endif + } + } } } diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 5e29d5514..48c1bdd64 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -216,6 +216,8 @@ namespace Bit.iOS view.RemoveFromSuperview(); UIApplication.SharedApplication.SetStatusBarHidden(false, false); } + + ThemeManager.UpdateThemeOnPagesAsync(); } public override void WillEnterForeground(UIApplication uiApplication)