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 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

View File

@ -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();

View File

@ -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;
}
}
}

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;
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();
}
}
}

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.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<IPlatformUtilsService>("platformUtilsService");
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>(
"passwordGenerationService");
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("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<GeneratedPasswordHistory>()));
await InitAsync();
}
catch (System.Exception ex)
{
#if !FDROID
Crashes.TrackError(ex);
#endif
}
}
}
}

View File

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

View File

@ -1,15 +1,15 @@
using Bit.App.Resources;
using System;
using System;
using System.Threading.Tasks;
using Bit.App.Resources;
using Bit.App.Styles;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
namespace Bit.App.Pages
{
public partial class GeneratorPage : BaseContentPage
public partial class GeneratorPage : BaseContentPage, IThemeDirtablePage
{
private readonly IBroadcasterService _broadcasterService;
@ -49,7 +49,7 @@ namespace Bit.App.Pages
}
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()
{
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());
}
}
}

View File

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

View File

@ -2,7 +2,7 @@
namespace Bit.App.Styles
{
public partial class Dark : ResourceDictionary
public partial class Dark : ResourceDictionary, IThemeResourceDictionary
{
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
{
public partial class Light : ResourceDictionary
public partial class Light : ResourceDictionary, IThemeResourceDictionary
{
public Light()
{

View File

@ -2,7 +2,7 @@
namespace Bit.App.Styles
{
public partial class Nord : ResourceDictionary
public partial class Nord : ResourceDictionary, IThemeResourceDictionary
{
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.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<ResourceDictionary> 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
}
}
}
}

View File

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