Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions JournalApp/Data/PreferenceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
{
_application = Application.Current;
_application.RequestedThemeChanged += Application_RequestedThemeChanged;

}

ApplyPlatformTheme();
Expand Down Expand Up @@ -211,16 +212,46 @@
var surface = IsDarkMode ? GetTheme().PaletteDark.Background : GetTheme().PaletteLight.Background;
var background = Color.FromRgb(surface.R, surface.G, surface.B);

// On Android 15 and up the system bars are transparent, so what shows behind them is this page background rather than any status bar color we set.
// The page shows through until the web view paints its first frame, so it carries the surface tone as well.
if (_application.Windows.Count > 0 && _application.Windows[0].Page is ContentPage page)
page.BackgroundColor = background;

if (OperatingSystem.IsAndroid())
#if ANDROID
// Go through the MAUI window so this reaches the activity currently hosting it, which Platform.CurrentActivity can lag behind during a relaunch.
var window = (_application.Windows.Count > 0 ? _application.Windows[0].Handler?.PlatformView as Android.App.Activity : null)?.Window;
if (window is null)
return;

var platformColor = Android.Graphics.Color.Rgb(surface.R, surface.G, surface.B);

// The web view draws under the transparent bars on Android 15 and up, so icon contrast is the only knob left and it has to follow the in-app theme.
// MAUI guesses it once at startup from the theme's colorPrimary, which is a dark purple in both modes, so it always lands on light icons.
var controller = AndroidX.Core.View.WindowCompat.GetInsetsController(window, window.DecorView);
controller.AppearanceLightStatusBars = !IsDarkMode;
controller.AppearanceLightNavigationBars = !IsDarkMode;

// Three-button navigation gets a translucent scrim by default, which would grey the surface showing through the bar.
if (OperatingSystem.IsAndroidVersionAtLeast(29))
{
window.StatusBarContrastEnforced = false;

Check warning on line 236 in JournalApp/Data/PreferenceService.cs

View workflow job for this annotation

GitHub Actions / build

This call site is reachable on: 'Android' 27.0 and later. 'Window.StatusBarContrastEnforced' is obsoleted on: 'android' 35.0 and later. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1422)

Check warning on line 236 in JournalApp/Data/PreferenceService.cs

View workflow job for this annotation

GitHub Actions / build

This call site is reachable on: 'Android' 27.0 and later. 'Window.StatusBarContrastEnforced' is obsoleted on: 'android' 35.0 and later. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1422)
window.NavigationBarContrastEnforced = false;
}

// The window itself is what shows through the transparent bars whenever the page is letterboxed, like on a web view too old to report the insets.
window.SetBackgroundDrawable(new Android.Graphics.Drawables.ColorDrawable(platformColor));

// Below Android 15 the bars are opaque, so tint them to the surface tone instead.
// Setting an opaque status bar color through the toolkit also makes the decor fit the system windows again, which letterboxes the page inside the bars.
// That is intentional there: those versions ship web views too old to pad for the bars themselves, and the themed opaque bars read the same as drawing under them.
if (!OperatingSystem.IsAndroidVersionAtLeast(35))
{
// Still needed below Android 15, where the status bar has its own color instead of showing the page through.
StatusBar.SetColor(background);
StatusBar.SetStyle(IsDarkMode ? StatusBarStyle.LightContent : StatusBarStyle.DarkContent);
#pragma warning disable CA1422 // Deprecated in API 35, which the surrounding check excludes.
window.SetNavigationBarColor(platformColor);
#pragma warning restore CA1422
}
#endif
}

public string GetMoodColor(string emoji)
Expand Down
4 changes: 2 additions & 2 deletions JournalApp/JournalApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
<PackageReference Include="Blazor-ApexCharts" Version="6.1.0" />
<PackageReference Include="CommunityToolkit.Maui" Version="14.2.0" />
<PackageReference Include="MaterialColorUtilities" Version="0.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.80" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.100" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
Expand All @@ -59,7 +59,7 @@
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.9" />
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.80" />
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.100" />
<PackageReference Include="MudBlazor" Version="9.7.0" />
<PackageReference Include="Nerdbank.GitVersioning" Version="3.10.91" PrivateAssets="All" />
</ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions JournalApp/MainPage.xaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:JournalApp"
x:Class="JournalApp.MainPage"
SafeAreaEdges="Container"
SafeAreaEdges="None"
BackgroundColor="{AppThemeBinding Light=#FFF8F9, Dark=#181215}">
<!-- The background is a first-frame default only; PreferenceService.ApplyPlatformTheme repaints it from the generated palette so device colors reach the letterbox too. -->
<!-- The background is a first-frame default only; PreferenceService.ApplyPlatformTheme repaints it from the generated palette so device colors show until the web view paints. -->

<BlazorWebView HostPage="wwwroot/index.html" BlazorWebViewInitialized="OnBlazorWebViewInitialized">
<BlazorWebView.RootComponents>
Expand Down
16 changes: 15 additions & 1 deletion JournalApp/MainPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Components.WebView;
using Microsoft.AspNetCore.Components.WebView;

namespace JournalApp;

Expand All @@ -7,7 +7,21 @@ public partial class MainPage : ContentPage
public MainPage()
{
InitializeComponent();

#if ANDROID
// Only WebView 144 and up forward the system bar insets to CSS, so an older one keeps the page letterboxed inside the bars instead of drawing under them blind.
if (!WebViewReportsSafeArea())
SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
#endif
}

#if ANDROID
private static bool WebViewReportsSafeArea()
{
var version = Android.Webkit.WebView.CurrentWebViewPackage?.VersionName;
return version is not null && int.TryParse(version.Split('.')[0], out var major) && major >= 144;
}
#endif

// The native WebView paints white before Blazor's first frame, which flashes hard against a dark splash, so let the themed page background show through instead.
private void OnBlazorWebViewInitialized(object sender, BlazorWebViewInitializedEventArgs e)
Expand Down
7 changes: 7 additions & 0 deletions JournalApp/MauiProgram.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CommunityToolkit.Maui;
using Microsoft.Maui.LifecycleEvents;
using JournalApp.Data;

namespace JournalApp;
Expand All @@ -24,6 +25,12 @@ public static MauiApp CreateMauiApp()
// Enable Blazor WebView.
builder.Services.AddMauiBlazorWebView();

#if ANDROID
// Android relaunches the activity for some configuration changes, like switching the navigation mode, and MAUI resets the system bar appearance each time it connects to one.
builder.ConfigureLifecycleEvents(events => events.AddAndroid(android => android.OnResume(_ =>
IPlatformApplication.Current?.Services.GetService<PreferenceService>()?.ApplyPlatformTheme())));
#endif

#if DEBUG
// Add debugging tools and log output for development.
builder.Services.AddBlazorWebViewDeveloperTools();
Expand Down
45 changes: 21 additions & 24 deletions JournalApp/wwwroot/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,13 @@ textarea,
font-weight: 400;
}

/* The web view draws under the transparent status bar, so the header extends up behind it and carries the surface tone there. */
.page-header {
z-index: var(--mud-zindex-appbar);
display: flex;
flex-direction: column;
padding: 0 !important;
padding-top: env(safe-area-inset-top, 0px) !important;
margin: 0 !important;
top: 0 !important;
position: sticky !important;
Expand All @@ -135,7 +137,7 @@ textarea,
}

/* On-scroll separation for the sticky header: a soft lift fades in once content passes underneath. */
/* MD3 proper would retone the container instead, but that desyncs the natively colored status bar, so elevation stands in until edge-to-edge lands. */
/* MD3 proper would retone the container instead; now that the header owns the status bar region that retone can follow it, so it is a candidate to swap in. */
@supports (animation-timeline: scroll()) {
.page-header {
animation: header-on-scroll linear both;
Expand All @@ -157,7 +159,7 @@ textarea,
max-width: 960px;
margin: 0 auto !important;
padding: 8px !important;
padding-bottom: 15vh !important;
padding-bottom: calc(15vh + env(safe-area-inset-bottom, 0px)) !important;
animation: fadeInUp var(--ja-motion-spatial);
}

Expand Down Expand Up @@ -463,8 +465,14 @@ label.mud-switch.mud-disabled .mud-switch-span {
}

/* The keyboard overlays the WebView and shrinks the visual viewport; pinning the container to it glides dialogs out of the keyboard's way. */
/* The container spans under the system bars as well, so it pads them out; the bottom inset is dropped while the keyboard covers the navigation bar. */
.mud-dialog-container {
box-sizing: border-box;
height: var(--visible-height, 100%);
padding-top: env(safe-area-inset-top, 0px);
padding-left: env(safe-area-inset-left, 0px);
padding-right: env(safe-area-inset-right, 0px);
padding-bottom: max(0px, calc(env(safe-area-inset-bottom, 0px) - var(--keyboard-inset, 0px)));
transition: height 250ms cubic-bezier(0.2, 0, 0, 1);
}

Expand All @@ -473,9 +481,16 @@ label.mud-switch.mud-disabled .mud-switch-span {
max-height: calc(100% - 32px);
}

/* Give the document scroll room so content can move up out from under the keyboard. */
/* Give the document scroll room so content can move up out from under the keyboard, and keep it clear of a side cutout or bar in landscape. */
body {
padding-bottom: var(--keyboard-inset, 0px);
padding-left: env(safe-area-inset-left, 0px);
padding-right: env(safe-area-inset-right, 0px);
}

/* Bottom-anchored chrome floats above the navigation bar instead of under it. */
.mud-snackbar-location-bottom-center {
bottom: calc(24px + env(safe-area-inset-bottom, 0px));
}

/* M3 dialogs sit on surfaceContainerHigh with the 24px container inset, at elevation Level3 rather than MudBlazor's M2 elevation 24. */
Expand Down Expand Up @@ -695,30 +710,12 @@ a.list-group-title {
display: none;
left: 0;
padding: 1.25rem;
padding-bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
position: fixed;
width: 100%;
z-index: 1000;
}

.status-bar-safe-area {
display: none;
}

@supports (-webkit-touch-callout: none) {
.status-bar-safe-area {
display: flex;
position: sticky;
top: 0;
height: env(safe-area-inset-top);
width: 100%;
z-index: 1;
}

.flex-column, .navbar-brand {
padding-left: env(safe-area-inset-left);
}
}

/* Android honors Remove animations under accessibility settings, so drop every entrance, transition and transform the app adds. */
/* The scroll-driven header lift stays: it is tied to scroll position rather than to time, and it is the only cue that content is passing underneath. */
@media (prefers-reduced-motion: reduce) {
Expand All @@ -743,13 +740,13 @@ a.list-group-title {
}

@media (prefers-color-scheme: dark) {
.status-bar-safe-area, #splash-screen {
#splash-screen {
background-color: #181215;
}
}

@media (prefers-color-scheme: light) {
.status-bar-safe-area, #splash-screen {
#splash-screen {
background-color: #FFF8F9;
}
}
2 changes: 0 additions & 2 deletions JournalApp/wwwroot/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@

<body>

<div class="status-bar-safe-area"></div>

<div id="app">
<div id="splash-screen">
<p id="loading-text">Getting things ready...</p>
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.300",
"version": "10.0.400",
"rollForward": "latestPatch",
"allowPrerelease": false
}
Expand Down
Loading