mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-12 10:04:43 +01:00
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:
parent
939db8ebe0
commit
74e90da662
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +63,7 @@
|
||||
</Frame>
|
||||
</Grid>
|
||||
<controls:MonoLabel
|
||||
x:Name="lblPassword"
|
||||
StyleClass="text-lg, text-html"
|
||||
Text="{Binding ColoredPassword, Mode=OneWay}"
|
||||
Margin="0, 20"
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Bit.App.Styles
|
||||
{
|
||||
public partial class Black : ResourceDictionary
|
||||
public partial class Black : ResourceDictionary, IThemeResourceDictionary
|
||||
{
|
||||
public Black()
|
||||
{
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Bit.App.Styles
|
||||
{
|
||||
public partial class Dark : ResourceDictionary
|
||||
public partial class Dark : ResourceDictionary, IThemeResourceDictionary
|
||||
{
|
||||
public Dark()
|
||||
{
|
||||
|
15
src/App/Styles/IThemeDirtablePage.cs
Normal file
15
src/App/Styles/IThemeDirtablePage.cs
Normal 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();
|
||||
}
|
||||
}
|
6
src/App/Styles/IThemeResourceDictionary.cs
Normal file
6
src/App/Styles/IThemeResourceDictionary.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Bit.App.Styles
|
||||
{
|
||||
public interface IThemeResourceDictionary
|
||||
{
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Bit.App.Styles
|
||||
{
|
||||
public partial class Light : ResourceDictionary
|
||||
public partial class Light : ResourceDictionary, IThemeResourceDictionary
|
||||
{
|
||||
public Light()
|
||||
{
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Bit.App.Styles
|
||||
{
|
||||
public partial class Nord : ResourceDictionary
|
||||
public partial class Nord : ResourceDictionary, IThemeResourceDictionary
|
||||
{
|
||||
public Nord()
|
||||
{
|
||||
|
53
src/App/Utilities/PageExtensions.cs
Normal file
53
src/App/Utilities/PageExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -216,6 +216,8 @@ namespace Bit.iOS
|
||||
view.RemoveFromSuperview();
|
||||
UIApplication.SharedApplication.SetStatusBarHidden(false, false);
|
||||
}
|
||||
|
||||
ThemeManager.UpdateThemeOnPagesAsync();
|
||||
}
|
||||
|
||||
public override void WillEnterForeground(UIApplication uiApplication)
|
||||
|
Loading…
Reference in New Issue
Block a user