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
This commit is contained in:
Federico Maccaroni 2022-01-24 17:20:48 -03:00 committed by GitHub
parent 939db8ebe0
commit 74e90da662
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 264 additions and 84 deletions

View file

@ -1,25 +1,24 @@
using Android.App; using System;
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.IO; using System.IO;
using System; using System.Linq;
using Android.Content;
using Bit.Droid.Utilities;
using Bit.Droid.Receivers;
using Bit.App.Models;
using Bit.Core.Enums;
using Android.Nfc;
using System.Threading.Tasks; 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 AndroidX.Core.Content;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Utilities; 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 ZXing.Net.Mobile.Android;
using Android.Util;
namespace Bit.Droid namespace Bit.Droid
{ {
@ -120,6 +119,9 @@ namespace Bit.Droid
base.OnResume(); base.OnResume();
Xamarin.Essentials.Platform.OnResume(); Xamarin.Essentials.Platform.OnResume();
AppearanceAdjustments(); AppearanceAdjustments();
ThemeManager.UpdateThemeOnPagesAsync();
if (_deviceActionService.SupportsNfc()) if (_deviceActionService.SupportsNfc())
{ {
try try

View file

@ -8,6 +8,7 @@ using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using System; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xamarin.Forms; using Xamarin.Forms;
using Xamarin.Forms.Xaml; using Xamarin.Forms.Xaml;
@ -216,7 +217,8 @@ namespace Bit.App
private async void ResumedAsync() private async void ResumedAsync()
{ {
UpdateTheme(); await UpdateThemeAsync();
await _vaultTimeoutService.CheckVaultTimeoutAsync(); await _vaultTimeoutService.CheckVaultTimeoutAsync();
_messagingService.Send("startEventTimer"); _messagingService.Send("startEventTimer");
await ClearCacheIfNeededAsync(); 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() private void SetCulture()
{ {
// Calendars are removed by linker. ref https://bugzilla.xamarin.com/show_bug.cgi?id=59077 // 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); ThemeManager.SetTheme(Device.RuntimePlatform == Device.Android, Current.Resources);
Current.RequestedThemeChanged += (s, a) => Current.RequestedThemeChanged += (s, a) =>
{ {
UpdateTheme(); UpdateThemeAsync();
}; };
Current.MainPage = new HomePage(); Current.MainPage = new HomePage();
var mainPageTask = SetMainPageAsync(); 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) private async Task LockedAsync(bool autoPromptBiometric)
{ {
await _stateService.PurgeAsync(); await _stateService.PurgeAsync();

View file

@ -30,9 +30,17 @@ namespace Bit.App.Pages
public DateTime? LastPageAction { get; set; } public DateTime? LastPageAction { get; set; }
public bool IsThemeDirty { get; set; }
protected override void OnAppearing() protected override void OnAppearing()
{ {
base.OnAppearing(); base.OnAppearing();
if (IsThemeDirty)
{
UpdateOnThemeChanged();
}
SaveActivity(); SaveActivity();
} }
@ -123,5 +131,11 @@ namespace Bit.App.Pages
SetServices(); SetServices();
_storageService.SaveAsync(Constants.LastActiveTimeKey, _deviceActionService.GetActiveTime()); _storageService.SaveAsync(Constants.LastActiveTimeKey, _deviceActionService.GetActiveTime());
} }
public virtual Task UpdateOnThemeChanged()
{
IsThemeDirty = false;
return Task.CompletedTask;
}
} }
} }

View file

@ -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; using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
public partial class GeneratorHistoryPage : BaseContentPage public partial class GeneratorHistoryPage : BaseContentPage, IThemeDirtablePage
{ {
private GeneratorHistoryPageViewModel _vm; private GeneratorHistoryPageViewModel _vm;
@ -28,6 +30,7 @@ namespace Bit.App.Pages
protected override async void OnAppearing() protected override async void OnAppearing()
{ {
base.OnAppearing(); base.OnAppearing();
await LoadOnAppearedAsync(_mainLayout, true, async () => { await LoadOnAppearedAsync(_mainLayout, true, async () => {
await _vm.InitAsync(); await _vm.InitAsync();
}); });
@ -59,5 +62,12 @@ namespace Bit.App.Pages
await _vm.ClearAsync(); await _vm.ClearAsync();
} }
} }
public override async Task UpdateOnThemeChanged()
{
await base.UpdateOnThemeChanged();
await _vm?.UpdateOnThemeChanged();
}
} }
} }

View file

@ -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.Abstractions;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using System.Collections.Generic; #if !FDROID
using System.Threading.Tasks; using Microsoft.AppCenter.Crashes;
#endif
using Xamarin.Forms; using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
@ -19,8 +22,7 @@ namespace Bit.App.Pages
public GeneratorHistoryPageViewModel() public GeneratorHistoryPageViewModel()
{ {
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"); _platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>( _passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
"passwordGenerationService");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService"); _clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
PageTitle = AppResources.PasswordHistory; PageTitle = AppResources.PasswordHistory;
@ -57,5 +59,21 @@ namespace Bit.App.Pages
_platformUtilsService.ShowToast("info", null, _platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password)); string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
} }
public async Task UpdateOnThemeChanged()
{
try
{
await Device.InvokeOnMainThreadAsync(() => History.ResetWithRange(new List<GeneratedPasswordHistory>()));
await InitAsync();
}
catch (System.Exception ex)
{
#if !FDROID
Crashes.TrackError(ex);
#endif
}
}
} }
} }

View file

@ -63,6 +63,7 @@
</Frame> </Frame>
</Grid> </Grid>
<controls:MonoLabel <controls:MonoLabel
x:Name="lblPassword"
StyleClass="text-lg, text-html" StyleClass="text-lg, text-html"
Text="{Binding ColoredPassword, Mode=OneWay}" Text="{Binding ColoredPassword, Mode=OneWay}"
Margin="0, 20" Margin="0, 20"

View file

@ -1,15 +1,15 @@
using Bit.App.Resources; using System;
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Resources;
using Bit.App.Styles;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.Forms; using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific; using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
public partial class GeneratorPage : BaseContentPage public partial class GeneratorPage : BaseContentPage, IThemeDirtablePage
{ {
private readonly IBroadcasterService _broadcasterService; private readonly IBroadcasterService _broadcasterService;
@ -49,7 +49,7 @@ namespace Bit.App.Pages
} }
if (isIos) if (isIos)
{ {
_typePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished); _typePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
} }
} }
@ -61,18 +61,19 @@ namespace Bit.App.Pages
protected async override void OnAppearing() protected async override void OnAppearing()
{ {
base.OnAppearing(); base.OnAppearing();
lblPassword.IsVisible = true;
if (!_fromTabPage) if (!_fromTabPage)
{ {
await InitAsync(); await InitAsync();
} }
_broadcasterService.Subscribe(nameof(GeneratorPage), async (message) =>
_broadcasterService.Subscribe(nameof(GeneratorPage), (message) =>
{ {
if (message.Command == "updatedTheme") if (message.Command == "updatedTheme")
{ {
Device.BeginInvokeOnMainThread(() => Device.BeginInvokeOnMainThread(() => _vm.RedrawPassword());
{
_vm.RedrawPassword();
});
} }
}); });
} }
@ -80,6 +81,9 @@ namespace Bit.App.Pages
protected override void OnDisappearing() protected override void OnDisappearing()
{ {
base.OnDisappearing(); base.OnDisappearing();
lblPassword.IsVisible = false;
_broadcasterService.Unsubscribe(nameof(GeneratorPage)); _broadcasterService.Unsubscribe(nameof(GeneratorPage));
} }
@ -141,5 +145,12 @@ namespace Bit.App.Pages
await Navigation.PopModalAsync(); await Navigation.PopModalAsync();
} }
} }
public override async Task UpdateOnThemeChanged()
{
await base.UpdateOnThemeChanged();
await Device.InvokeOnMainThreadAsync(() => _vm?.RedrawPassword());
}
} }
} }

View file

@ -2,7 +2,7 @@
namespace Bit.App.Styles namespace Bit.App.Styles
{ {
public partial class Black : ResourceDictionary public partial class Black : ResourceDictionary, IThemeResourceDictionary
{ {
public Black() public Black()
{ {

View file

@ -2,7 +2,7 @@
namespace Bit.App.Styles namespace Bit.App.Styles
{ {
public partial class Dark : ResourceDictionary public partial class Dark : ResourceDictionary, IThemeResourceDictionary
{ {
public Dark() public Dark()
{ {

View file

@ -0,0 +1,15 @@
using System.Threading.Tasks;
namespace Bit.App.Styles
{
/// <summary>
/// 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.
/// </summary>
public interface IThemeDirtablePage
{
bool IsThemeDirty { get; set; }
Task UpdateOnThemeChanged();
}
}

View file

@ -0,0 +1,6 @@
namespace Bit.App.Styles
{
public interface IThemeResourceDictionary
{
}
}

View file

@ -2,7 +2,7 @@
namespace Bit.App.Styles namespace Bit.App.Styles
{ {
public partial class Light : ResourceDictionary public partial class Light : ResourceDictionary, IThemeResourceDictionary
{ {
public Light() public Light()
{ {

View file

@ -2,7 +2,7 @@
namespace Bit.App.Styles namespace Bit.App.Styles
{ {
public partial class Nord : ResourceDictionary public partial class Nord : ResourceDictionary, IThemeResourceDictionary
{ {
public Nord() public Nord()
{ {

View file

@ -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<Page, Task> 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<Page, Task> actionOnPage)
{
if (page is MultiPage<Page> 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);
}
}
}

View file

@ -4,6 +4,8 @@ using Bit.App.Services;
using Bit.App.Styles; using Bit.App.Styles;
using Bit.Core; using Bit.Core;
using Xamarin.Forms; using Xamarin.Forms;
using System.Linq;
using System.Threading.Tasks;
#if !FDROID #if !FDROID
using Microsoft.AppCenter.Crashes; using Microsoft.AppCenter.Crashes;
#endif #endif
@ -15,12 +17,30 @@ namespace Bit.App.Utilities
public static bool UsingLightTheme = true; public static bool UsingLightTheme = true;
public static Func<ResourceDictionary> Resources = () => null; public static Func<ResourceDictionary> Resources = () => null;
public static bool IsThemeDirty = false;
public static void SetThemeStyle(string name, ResourceDictionary resources) public static void SetThemeStyle(string name, ResourceDictionary resources)
{ {
try try
{ {
Resources = () => resources; 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 // Reset styles
resources.Clear(); resources.Clear();
resources.MergedDictionaries.Clear(); resources.MergedDictionaries.Clear();
@ -28,40 +48,9 @@ namespace Bit.App.Utilities
// Variables // Variables
resources.MergedDictionaries.Add(new Variables()); resources.MergedDictionaries.Add(new Variables());
// Themed variables // Theme
if (name == "dark") resources.MergedDictionaries.Add(newTheme);
{ UsingLightTheme = newTheme is Light;
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;
}
}
// Base styles // Base styles
resources.MergedDictionaries.Add(new Base()); 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) public static void SetTheme(bool android, ResourceDictionary resources)
{ {
SetThemeStyle(GetTheme(android), resources); SetThemeStyle(GetTheme(android), resources);
@ -128,5 +145,34 @@ namespace Bit.App.Utilities
{ {
return (Color)Resources()[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)
{
#if !FDROID
Crashes.TrackError(ex);
#endif
}
}
} }
} }

View file

@ -216,6 +216,8 @@ namespace Bit.iOS
view.RemoveFromSuperview(); view.RemoveFromSuperview();
UIApplication.SharedApplication.SetStatusBarHidden(false, false); UIApplication.SharedApplication.SetStatusBarHidden(false, false);
} }
ThemeManager.UpdateThemeOnPagesAsync();
} }
public override void WillEnterForeground(UIApplication uiApplication) public override void WillEnterForeground(UIApplication uiApplication)