From 6b2867c84a2363554800c4337dde6b52badd412f Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 22 Aug 2026 15:32:43 +0100 Subject: [PATCH 01/10] build(android): drop unused Xamarin.AndroidX.SplashScreen resources - PostIt.Android.csproj: drop Xamarin.AndroidX.Core.SplashScreen and Xamarin.AndroidX.Browser. Neither is pulled transitively by Avalonia.Android 12.1.1 (its nuspec declares only AppCompat + Window), and they are not referenced anywhere in PostIt code. - Resources/values-v31/styles.xml: drop the windowSplashScreen* items and the 'postSplashScreenTheme' reference, which require the dropped SplashScreen package. We fall back to the system splash screen on Android 12+ devices; a custom one can come back when we add it on purpose. --- src/PostIt/PostIt.Android/PostIt.Android.csproj | 2 -- .../PostIt.Android/Resources/values-v31/styles.xml | 9 --------- 2 files changed, 11 deletions(-) diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index 20a27816..7669ef62 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -24,8 +24,6 @@ - - diff --git a/src/PostIt/PostIt.Android/Resources/values-v31/styles.xml b/src/PostIt/PostIt.Android/Resources/values-v31/styles.xml index d5ecec43..42a008c4 100644 --- a/src/PostIt/PostIt.Android/Resources/values-v31/styles.xml +++ b/src/PostIt/PostIt.Android/Resources/values-v31/styles.xml @@ -8,14 +8,5 @@ false @null true - @color/splash_background - @drawable/avalonia_anim - 1000 - @style/MyTheme.Main - - - From a4bc7b5e6a7892e603878877a8c963fef662c83e Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 22 Aug 2026 15:56:52 +0100 Subject: [PATCH 02/10] build(android): downgrade Avalonia 12 -> 11.3.20 + Avalonia.Maui 11.3.0 WIP: addressing the layer-3 WindowingPlatformStub.NotSupportedException crash on PostIt.Android by downgrading to the Avalonia 11 / Avalonia.Maui 11 stack, where the Android runtime platform layer (IWindowingPlatform) gets properly bound via the MAUI embedding extension instead of being left as a stub. Touched files: - src/PostIt/Directory.Packages.props: - Avalonia 12.1.1 -> 11.3.20 (Avalonia, Avalonia.Android, Avalonia.AvaloniaEdit, Avalonia.Browser, Avalonia.Desktop, Avalonia.Fonts.Inter, Avalonia.Themes.Fluent). - New: Avalonia.Maui 11.3.0. - Microsoft.Maui.Essentials 10.0.100 -> 8.0.100 (aligns with the Microsoft.Maui.Controls 8.0.x family that Avalonia.Maui 11.3.0 pulls transitively). - New: Microsoft.Maui.Controls 8.0.100. - src/PostIt/PostIt.Android/PostIt.Android.csproj: - TargetFramework net10.0-android -> net9.0-android36.0 (Avalonia Android 11.3.20 targets net8.0-android34.0, but the compat-ascending fallback lets us consume from net9.0-android36.0 against the API-36 platform that's already installed locally). - Add Avalonia.Maui PackageReference. - Restore Microsoft.Maui.Essentials PackageReference (left in earlier removal during the MAUI-cause-of-crash hypothesis check). - src/PostIt/PostIt/PostIt.csproj: TargetFramework net10.0 -> net9.0 (must match what PostIt.Android consumes transitively, otherwise the build chain breaks). - src/Yavsc.Abstract/Yavsc.Abstract.csproj: net10.0 -> net9.0 (same reason; six other consumers stay on net10.0 and pick up the new assembly via standard TFM compat). - src/Yavsc.Api.Client/Yavsc.Api.Client.csproj: net10.0 -> net9.0 (same reason). - src/PostIt/PostIt.Android/Application.cs: rewritten as a bare Android.App.Application. Avalonia 11 no longer ships AvaloniaAndroidApplication (that was an Avalonia 12 introduction); the Avalonia init now lives in MainActivity via AvaloniaMainActivity. The class is kept only so the [Application] manifest entry remains. - src/PostIt/PostIt.Android/MainActivity.cs: now inherits AvaloniaMainActivity and overrides CustomizeAppBuilder to chain .UseMaui(this) before .WithInterFont(). AvaloniaMainActivity handles the AvaloniaView initialisation in OnCreate. - src/PostIt/PostIt/Maui/MauiEmbeddingApp.cs (new): empty Microsoft.Maui.Controls.Application used as the embedding host for Avalonia.Maui. The MAUI window it produces is consumed by the Avalonia.Maui embedding pipeline and never surfaces to the user; Avalonia owns the actual visual tree. Build status: 11 errors remaining, all CS0234 / CS0246 against 'Avalonia.Controls.ContentPage'. Avalonia 11 did not ship ContentPage (it's an Avalonia 12 type, presumably for MAUI parity). All seven PostIt pages inherit ContentPage today and need to be reworked to UserControl before the build can complete. Follow-up commit will do that. Reference: AGENTS.md, 'PostIt.Android boot crash sur AVD x86_64 : investigation par couches', couche 3. --- src/PostIt/Directory.Packages.props | 18 +++++----- src/PostIt/PostIt.Android/Application.cs | 32 ++++++++--------- src/PostIt/PostIt.Android/MainActivity.cs | 10 +++++- .../PostIt.Android/PostIt.Android.csproj | 3 +- src/PostIt/PostIt/Maui/MauiEmbeddingApp.cs | 35 +++++++++++++++++++ src/PostIt/PostIt/PostIt.csproj | 2 +- src/Yavsc.Abstract/Yavsc.Abstract.csproj | 2 +- src/Yavsc.Api.Client/Yavsc.Api.Client.csproj | 2 +- 8 files changed, 74 insertions(+), 30 deletions(-) create mode 100644 src/PostIt/PostIt/Maui/MauiEmbeddingApp.cs diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 59b6b600..023d0654 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -3,18 +3,20 @@ - - - - - - - + + + + + + + + - + + diff --git a/src/PostIt/PostIt.Android/Application.cs b/src/PostIt/PostIt.Android/Application.cs index fb6b08d3..62ace02b 100644 --- a/src/PostIt/PostIt.Android/Application.cs +++ b/src/PostIt/PostIt.Android/Application.cs @@ -1,21 +1,19 @@ -using Android.App; -using Android.Runtime; -using Avalonia; -using Avalonia.Android; +using Android.App; -namespace PostIt.Android +namespace PostIt.Android; + +/// +/// Bare shell for Android. Avalonia +/// 11 initialises its platform services from +/// (which extends AvaloniaMainActivity<App>); no per-Application +/// Avalonia setup is needed here. Kept only so that the [Application] entry +/// stays present in the merged manifest, which the Android runtime expects +/// when the manifest declares a custom android:name in the application tag. +/// +[Application] +public class Application : Android.App.Application { - [Application] - public class Application : AvaloniaAndroidApplication + public Application(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { - protected Application(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) - { - } - - protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) - { - return base.CustomizeAppBuilder(builder) - .WithInterFont(); - } } -} +} \ No newline at end of file diff --git a/src/PostIt/PostIt.Android/MainActivity.cs b/src/PostIt/PostIt.Android/MainActivity.cs index 1b413a9f..057703a4 100644 --- a/src/PostIt/PostIt.Android/MainActivity.cs +++ b/src/PostIt/PostIt.Android/MainActivity.cs @@ -2,6 +2,7 @@ using Android.App; using Android.Content.PM; using Android.Content; using Avalonia.Android; +using Avalonia; using AndroidX.Emoji2.Text; namespace PostIt.Android; @@ -14,8 +15,15 @@ namespace PostIt.Android; MainLauncher = true, LaunchMode = LaunchMode.SingleTask, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] -public class MainActivity : AvaloniaMainActivity +public class MainActivity : AvaloniaMainActivity { + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + { + return base.CustomizeAppBuilder(builder) + .UseMaui(this) + .WithInterFont(); + } + /// /// Strongly-typed handle to the current MainActivity instance, set in /// and consumed by platform services such as diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index 7669ef62..1f865b07 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -1,7 +1,7 @@ Exe - net10.0-android + net9.0-android36.0 android-arm64;android-x64 23.0.0 @@ -24,6 +24,7 @@ + diff --git a/src/PostIt/PostIt/Maui/MauiEmbeddingApp.cs b/src/PostIt/PostIt/Maui/MauiEmbeddingApp.cs new file mode 100644 index 00000000..4f58b4f2 --- /dev/null +++ b/src/PostIt/PostIt/Maui/MauiEmbeddingApp.cs @@ -0,0 +1,35 @@ +using Microsoft.Maui.Controls; + +namespace PostIt.Maui; + +/// +/// Empty MAUI application registered as the embedding host for +/// Avalonia.Maui. Avalonia.Maui expects a +/// -derived type so that +/// its .UseMaui<TMauiApp>(activity) extension can build the +/// embedding pipeline (services, handlers, IPlatformApplication). +/// +/// We never actually display any MAUI controls — the embedding sits in front +/// of Avalonia so that Avalonia's Android platform layer (which is otherwise +/// bound to a stub on Avalonia 12 / present-but-broken on Avalonia 11 without +/// MAUI) gets the real Android Context and runs the surface view +/// lifecycle correctly. This class exists to satisfy the generic constraint +/// of UseMaui<TMauiApp> and gives MAUI something to attach its +/// handler tree to. +/// +/// Override CreateWindow with a no-op or leave the default; the +/// virtual MAUI window is consumed by Avalonia.Maui's embedding and +/// never surfaces to the user (Avalonia owns the visual tree). +/// +public class MauiEmbeddingApp : Application +{ + public MauiEmbeddingApp() + { + } + + protected override Window CreateWindow(IActivationState? activationState) + { + // No-op window: Avalonia.Maui wraps it and never shows it. + return new Window(); + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj index 1b48c21f..90fdd837 100644 --- a/src/PostIt/PostIt/PostIt.csproj +++ b/src/PostIt/PostIt/PostIt.csproj @@ -1,6 +1,6 @@ - net10.0 + net9.0 enable latest true diff --git a/src/Yavsc.Abstract/Yavsc.Abstract.csproj b/src/Yavsc.Abstract/Yavsc.Abstract.csproj index 7a4f83ae..2305fc8c 100644 --- a/src/Yavsc.Abstract/Yavsc.Abstract.csproj +++ b/src/Yavsc.Abstract/Yavsc.Abstract.csproj @@ -1,6 +1,6 @@ - net10.0 + net9.0 enable A shared model for a little client/server app, dealing about establishing some contract, between some human client and provider. diff --git a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj index ade7ca17..c078789c 100644 --- a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj +++ b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj @@ -1,6 +1,6 @@ - net10.0 + net9.0 enable Yavsc.Api.Client Yavsc.Api.Client From 77a769327630be68f5138ac0a4ce30f6dd11a260 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 22 Aug 2026 16:09:35 +0100 Subject: [PATCH 03/10] Revert "build(android): downgrade Avalonia 12 -> 11.3.20 + Avalonia.Maui 11.3.0" This reverts commit a4bc7b5e6a7892e603878877a8c963fef662c83e. --- src/PostIt/Directory.Packages.props | 18 +++++----- src/PostIt/PostIt.Android/Application.cs | 32 +++++++++-------- src/PostIt/PostIt.Android/MainActivity.cs | 10 +----- .../PostIt.Android/PostIt.Android.csproj | 3 +- src/PostIt/PostIt/Maui/MauiEmbeddingApp.cs | 35 ------------------- src/PostIt/PostIt/PostIt.csproj | 2 +- src/Yavsc.Abstract/Yavsc.Abstract.csproj | 2 +- src/Yavsc.Api.Client/Yavsc.Api.Client.csproj | 2 +- 8 files changed, 30 insertions(+), 74 deletions(-) delete mode 100644 src/PostIt/PostIt/Maui/MauiEmbeddingApp.cs diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 023d0654..59b6b600 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -3,20 +3,18 @@ - - - - - - - - + + + + + + + - - + diff --git a/src/PostIt/PostIt.Android/Application.cs b/src/PostIt/PostIt.Android/Application.cs index 62ace02b..fb6b08d3 100644 --- a/src/PostIt/PostIt.Android/Application.cs +++ b/src/PostIt/PostIt.Android/Application.cs @@ -1,19 +1,21 @@ -using Android.App; +using Android.App; +using Android.Runtime; +using Avalonia; +using Avalonia.Android; -namespace PostIt.Android; - -/// -/// Bare shell for Android. Avalonia -/// 11 initialises its platform services from -/// (which extends AvaloniaMainActivity<App>); no per-Application -/// Avalonia setup is needed here. Kept only so that the [Application] entry -/// stays present in the merged manifest, which the Android runtime expects -/// when the manifest declares a custom android:name in the application tag. -/// -[Application] -public class Application : Android.App.Application +namespace PostIt.Android { - public Application(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + [Application] + public class Application : AvaloniaAndroidApplication { + protected Application(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + { + return base.CustomizeAppBuilder(builder) + .WithInterFont(); + } } -} \ No newline at end of file +} diff --git a/src/PostIt/PostIt.Android/MainActivity.cs b/src/PostIt/PostIt.Android/MainActivity.cs index 057703a4..1b413a9f 100644 --- a/src/PostIt/PostIt.Android/MainActivity.cs +++ b/src/PostIt/PostIt.Android/MainActivity.cs @@ -2,7 +2,6 @@ using Android.App; using Android.Content.PM; using Android.Content; using Avalonia.Android; -using Avalonia; using AndroidX.Emoji2.Text; namespace PostIt.Android; @@ -15,15 +14,8 @@ namespace PostIt.Android; MainLauncher = true, LaunchMode = LaunchMode.SingleTask, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] -public class MainActivity : AvaloniaMainActivity +public class MainActivity : AvaloniaMainActivity { - protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) - { - return base.CustomizeAppBuilder(builder) - .UseMaui(this) - .WithInterFont(); - } - /// /// Strongly-typed handle to the current MainActivity instance, set in /// and consumed by platform services such as diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index 1f865b07..7669ef62 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -1,7 +1,7 @@ Exe - net9.0-android36.0 + net10.0-android android-arm64;android-x64 23.0.0 @@ -24,7 +24,6 @@ - diff --git a/src/PostIt/PostIt/Maui/MauiEmbeddingApp.cs b/src/PostIt/PostIt/Maui/MauiEmbeddingApp.cs deleted file mode 100644 index 4f58b4f2..00000000 --- a/src/PostIt/PostIt/Maui/MauiEmbeddingApp.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.Maui.Controls; - -namespace PostIt.Maui; - -/// -/// Empty MAUI application registered as the embedding host for -/// Avalonia.Maui. Avalonia.Maui expects a -/// -derived type so that -/// its .UseMaui<TMauiApp>(activity) extension can build the -/// embedding pipeline (services, handlers, IPlatformApplication). -/// -/// We never actually display any MAUI controls — the embedding sits in front -/// of Avalonia so that Avalonia's Android platform layer (which is otherwise -/// bound to a stub on Avalonia 12 / present-but-broken on Avalonia 11 without -/// MAUI) gets the real Android Context and runs the surface view -/// lifecycle correctly. This class exists to satisfy the generic constraint -/// of UseMaui<TMauiApp> and gives MAUI something to attach its -/// handler tree to. -/// -/// Override CreateWindow with a no-op or leave the default; the -/// virtual MAUI window is consumed by Avalonia.Maui's embedding and -/// never surfaces to the user (Avalonia owns the visual tree). -/// -public class MauiEmbeddingApp : Application -{ - public MauiEmbeddingApp() - { - } - - protected override Window CreateWindow(IActivationState? activationState) - { - // No-op window: Avalonia.Maui wraps it and never shows it. - return new Window(); - } -} \ No newline at end of file diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj index 90fdd837..1b48c21f 100644 --- a/src/PostIt/PostIt/PostIt.csproj +++ b/src/PostIt/PostIt/PostIt.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 enable latest true diff --git a/src/Yavsc.Abstract/Yavsc.Abstract.csproj b/src/Yavsc.Abstract/Yavsc.Abstract.csproj index 2305fc8c..7a4f83ae 100644 --- a/src/Yavsc.Abstract/Yavsc.Abstract.csproj +++ b/src/Yavsc.Abstract/Yavsc.Abstract.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 enable A shared model for a little client/server app, dealing about establishing some contract, between some human client and provider. diff --git a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj index c078789c..ade7ca17 100644 --- a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj +++ b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 enable Yavsc.Api.Client Yavsc.Api.Client From 0c693237a3924374b6094bf7240594bca8ac4196 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 22 Aug 2026 16:58:08 +0100 Subject: [PATCH 04/10] sdk version bump --- src/PostIt/PostIt.Android/PostIt.Android.csproj | 4 ++-- src/PostIt/PostIt.Android/Resources/values/styles.xml | 1 - src/PostIt/PostIt.Browser/PostIt.Browser.csproj | 4 ++-- src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj | 4 ++-- src/PostIt/PostIt.Tests/PostIt.Tests.csproj | 2 +- src/PostIt/PostIt/PostIt.csproj | 2 +- src/Yavsc.Abstract/Yavsc.Abstract.csproj | 4 ++-- src/Yavsc.Api.Client/Yavsc.Api.Client.csproj | 4 ++-- src/Yavsc.Api/Yavsc.Api.csproj | 4 ++-- src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj | 4 ++-- src/Yavsc.Blogs/Yavsc.Blogs.csproj | 4 ++-- src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj | 4 ++-- src/Yavsc.Org/Yavsc.Org.csproj | 4 ++-- src/Yavsc.Server/Yavsc.Server.csproj | 4 ++-- src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj | 4 ++-- src/cli/cli.csproj | 2 +- 16 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index 7669ef62..4368da92 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -1,7 +1,7 @@ Exe - net10.0-android + net11.0-android android-arm64;android-x64 23.0.0 @@ -11,7 +11,7 @@ 1.0 apk false - android-arm;android-arm64;android-x86;android-x64 + android-arm;android-arm64;android-x64 1.1.0.0 1.1.0.0 1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3 diff --git a/src/PostIt/PostIt.Android/Resources/values/styles.xml b/src/PostIt/PostIt.Android/Resources/values/styles.xml index 6e534de2..44830966 100644 --- a/src/PostIt/PostIt.Android/Resources/values/styles.xml +++ b/src/PostIt/PostIt.Android/Resources/values/styles.xml @@ -6,7 +6,6 @@ diff --git a/src/PostIt/PostIt.Browser/PostIt.Browser.csproj b/src/PostIt/PostIt.Browser/PostIt.Browser.csproj index 8643fcc6..edbc9227 100644 --- a/src/PostIt/PostIt.Browser/PostIt.Browser.csproj +++ b/src/PostIt/PostIt.Browser/PostIt.Browser.csproj @@ -1,6 +1,6 @@ - net10.0-browser + net11.0-browser Exe true enable @@ -18,4 +18,4 @@ - \ No newline at end of file + diff --git a/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj b/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj index 5043da6e..3baf69f5 100644 --- a/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj +++ b/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj @@ -3,7 +3,7 @@ WinExe - net10.0 + net11.0 enable 1.1.0.0 1.1.0.0 @@ -27,4 +27,4 @@ - \ No newline at end of file + diff --git a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj index d8517ba0..c69d7fa1 100644 --- a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj +++ b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 enable enable false diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj index 1b48c21f..0277d2f6 100644 --- a/src/PostIt/PostIt/PostIt.csproj +++ b/src/PostIt/PostIt/PostIt.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 enable latest true diff --git a/src/Yavsc.Abstract/Yavsc.Abstract.csproj b/src/Yavsc.Abstract/Yavsc.Abstract.csproj index 7a4f83ae..23297848 100644 --- a/src/Yavsc.Abstract/Yavsc.Abstract.csproj +++ b/src/Yavsc.Abstract/Yavsc.Abstract.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 enable A shared model for a little client/server app, dealing about establishing some contract, between some human client and provider. @@ -17,4 +17,4 @@ - \ No newline at end of file + diff --git a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj index ade7ca17..be27960c 100644 --- a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj +++ b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 enable Yavsc.Api.Client Yavsc.Api.Client @@ -26,4 +26,4 @@ - \ No newline at end of file + diff --git a/src/Yavsc.Api/Yavsc.Api.csproj b/src/Yavsc.Api/Yavsc.Api.csproj index 5672ab7a..36a96b8b 100644 --- a/src/Yavsc.Api/Yavsc.Api.csproj +++ b/src/Yavsc.Api/Yavsc.Api.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 enable 1c73094f-959f-4211-b1a1-6a69b236c283 Yavsc.Api @@ -17,4 +17,4 @@ - \ No newline at end of file + diff --git a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj index fc7883ce..e688f730 100644 --- a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj +++ b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 enable enable false @@ -35,4 +35,4 @@ - \ No newline at end of file + diff --git a/src/Yavsc.Blogs/Yavsc.Blogs.csproj b/src/Yavsc.Blogs/Yavsc.Blogs.csproj index 5c175cb5..60f74e06 100644 --- a/src/Yavsc.Blogs/Yavsc.Blogs.csproj +++ b/src/Yavsc.Blogs/Yavsc.Blogs.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 enable 1c73094f-959f-4211-b1a1-6a69b236c283 Yavsc.Blogs @@ -18,4 +18,4 @@ - \ No newline at end of file + diff --git a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj index 88842cb4..41c172a6 100644 --- a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj +++ b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 enable enable false @@ -89,4 +89,4 @@ - \ No newline at end of file + diff --git a/src/Yavsc.Org/Yavsc.Org.csproj b/src/Yavsc.Org/Yavsc.Org.csproj index ccdb7f24..fc633f8a 100644 --- a/src/Yavsc.Org/Yavsc.Org.csproj +++ b/src/Yavsc.Org/Yavsc.Org.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 enable WTFPL 76e56fc2-1619-40d8-8393-365258b7a21d @@ -54,4 +54,4 @@ - \ No newline at end of file + diff --git a/src/Yavsc.Server/Yavsc.Server.csproj b/src/Yavsc.Server/Yavsc.Server.csproj index 4eaf6a83..d47246b6 100644 --- a/src/Yavsc.Server/Yavsc.Server.csproj +++ b/src/Yavsc.Server/Yavsc.Server.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 enable 53bd70e8-ff81-497a-847f-a15fd8ea7a09 Yavsc.Server @@ -43,4 +43,4 @@ - \ No newline at end of file + diff --git a/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj b/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj index 52effc72..bd4a6555 100644 --- a/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj +++ b/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 enable enable false @@ -23,4 +23,4 @@ - \ No newline at end of file + diff --git a/src/cli/cli.csproj b/src/cli/cli.csproj index 010c4eaf..0466998c 100644 --- a/src/cli/cli.csproj +++ b/src/cli/cli.csproj @@ -1,7 +1,7 @@ Exe - net10.0 + net11.0 enable Yavsc.cli true From 151df1c531081d61926cc8a151f66c1e708fad10 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 22 Aug 2026 18:04:27 +0100 Subject: [PATCH 05/10] build(android): align with .NET 11 SDK + drop unused AndroidX refs Follow-up to the 0c693237 'sdk version bump' (net10.0 -> net11.0 across the whole solution) and the work to get PostIt.Android building under the .NET 11 preview SDK installed locally (~/.dotnet): - Drop Xamarin.AndroidX.Browser and Xamarin.AndroidX.Core.SplashScreen from PostIt/Directory.Packages.props. They are not pulled by Avalonia.Android 12.1.1 (its nuspec declares AppCompat + Window only), are not referenced by any PostIt code, and we already removed the SplashScreen XML attributes from styles.xml in commit 6b2867c8. - PostIt.Android.csproj: bump SupportedOSPlatformVersion 23.0.0 -> 24.0.0. Required by the Microsoft.Android.Sdk.Linux 37.0.0-preview.7 target (Android API 24 is the new minimum under .NET 11); below that the build emits NETSDK warnings about an EoL target SDK level. Build status (PostIt.Android, Debug, android-x64, EmbedAssemblies): 0 errors, 15 warnings, ~4m24s cold. The locally-installed .NET 11 SDK 11.0.100-preview.7.26381.103 needs /opt/android-sdk/platforms /android-37.0 to exist; Paul has set up a symlink to the android-37.1 (Android 17 preview) install until Microsoft catches up the SDK manifest. PostIt.Tests: 49/62 passing after the migration; 13 new failures, most of them HttpListener teardown noise around the OIDC stub authority suite. Out of scope of this commit. --- src/PostIt/Directory.Packages.props | 2 -- src/PostIt/PostIt.Android/PostIt.Android.csproj | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 59b6b600..8f8a0187 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -15,7 +15,5 @@ - - diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index 4368da92..d0bda3f4 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -4,7 +4,7 @@ net11.0-android android-arm64;android-x64 - 23.0.0 + 24.0.0 enable fr.pschneider.PostIt 1 From 22e1705e44f5f38afdba831c2db103283f47da48 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 22 Aug 2026 18:31:30 +0100 Subject: [PATCH 06/10] Fixes the test suite --- src/PostIt/Directory.Packages.props | 3 +++ src/PostIt/PostIt.Tests/Directory.Packages.props | 10 ---------- src/PostIt/PostIt.Tests/PostIt.Tests.csproj | 3 ++- 3 files changed, 5 insertions(+), 11 deletions(-) delete mode 100644 src/PostIt/PostIt.Tests/Directory.Packages.props diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 8f8a0187..38ffe289 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -3,7 +3,10 @@ + + + diff --git a/src/PostIt/PostIt.Tests/Directory.Packages.props b/src/PostIt/PostIt.Tests/Directory.Packages.props deleted file mode 100644 index 4731d4f0..00000000 --- a/src/PostIt/PostIt.Tests/Directory.Packages.props +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj index c69d7fa1..c11c17b0 100644 --- a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj +++ b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj @@ -27,6 +27,7 @@ - + + From 426bc68d611b21a15fc6e3e3ae257878590583bf Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 23 Aug 2026 18:34:34 +0100 Subject: [PATCH 07/10] more reliable --- .vscode/launch.json | 21 ++++- .vscode/tasks.json | 23 +++++ Makefile | 9 +- external/dotnet-android-build-image | 1 - src/PostIt/Directory.Packages.props | 36 +++++--- src/PostIt/PostIt.Android/MainActivity.cs | 4 +- .../PostIt.Android/PostIt.Android.csproj | 20 ++--- .../Resources/values/styles.xml | 1 + .../Services/ContactService.Mobile.cs | 84 ------------------ .../PostIt.Browser/PostIt.Browser.csproj | 2 +- .../PostIt.Desktop/PostIt.Desktop.csproj | 2 +- src/PostIt/PostIt/App.axaml.cs | 87 +++++++++---------- src/PostIt/PostIt/PostIt.csproj | 2 +- .../PostIt/Settings/AuthenticationSettings.cs | 3 + .../PostIt/ViewModels/MainPageViewModel.cs | 26 +++--- src/PostIt/PostIt/ViewModels/Settings.cs | 22 ++++- src/Yavsc.Abstract/Yavsc.Abstract.csproj | 2 +- src/Yavsc.Api.Client/Yavsc.Api.Client.csproj | 2 +- src/Yavsc.Api/Yavsc.Api.csproj | 2 +- .../Yavsc.Blogs.Tests.csproj | 2 +- src/Yavsc.Blogs/Yavsc.Blogs.csproj | 2 +- src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj | 2 +- src/Yavsc.Org/Yavsc.Org.csproj | 2 +- src/Yavsc.Server/Yavsc.Server.csproj | 2 +- .../Yavsc.Tests.Shared.csproj | 2 +- src/cli/cli.csproj | 2 +- 26 files changed, 172 insertions(+), 191 deletions(-) delete mode 160000 external/dotnet-android-build-image delete mode 100644 src/PostIt/PostIt.Android/Services/ContactService.Mobile.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index efdaa6ea..2079c716 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,21 @@ // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Debug - Android", + "type": "mono", + "preLaunchTask": "run-debug-android", + "request": "attach", + "address": "localhost", + "port": 10000 + }, + { + "name": "Attach - Android", + "type": "mono", + "request": "attach", + "address": "localhost", + "port": 10000 + }, { "name": "API", "type": "dotnet", @@ -33,12 +48,10 @@ "name": "Test PostIt.Android launch (Xamarin.UITest)", "type": "coreclr", "request": "launch", - "program": "${workspaceFolder}/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests.dll", + "program": "${workspaceFolder}/src/PostIt/PostIt.Tests/bin/Debug/net11.0/PostIt.Tests.dll", "args": [ - "--filter-method", - "PostIt.Tests.AndroidAppLaunchTests.PostIt_starts_and_draws_a_first_frame_on_the_emulator" ], - "cwd": "${workspaceFolder}/src/PostIt.Tests", + "cwd": "${workspaceFolder}/src/PostIt/PostIt.Tests", "console": "integratedTerminal", "stopAtEntry": false } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e45a9921..c900fa6a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,6 +1,29 @@ { "version": "2.0.0", "tasks": [ + { + "label": "run-debug-android", + "command": "dotnet", + "type": "shell", + "options": { + "cwd": "${workspaceFolder}/src/PostIt/PostIt.Android", + "env": { + "DOTNET_HOST_PATH": "/usr/share/dotnet", + "ANDROID_HOME": "/opt/android-sdk", + "JAVA_HOME": "/usr/lib/jvm/java-1.21.0-openjdk-amd64" + } + }, + "args": [ + "build" + "-t:run", + "-p:TargetFramework=net10.0-android", + "-p:Configuration=Debug", + "-p:AndroidAttachDebugger=true", + "-p:AndroidSdbHostPort=10000", + "-p:AndroidSdbTargetPort=10000" + ], + "problemMatcher": "$msCompile" + }, { "label": "build", "command": "dotnet", diff --git a/Makefile b/Makefile index 830c48f2..b086713c 100644 --- a/Makefile +++ b/Makefile @@ -163,7 +163,7 @@ POSTIT_RID ?= android-x64 EMU_HEADLESS ?= 0 LOGCAT_LINES ?= 200 LOGCAT_FOLLOW ?= 0 -LOGCAT_BOOT_WAIT ?= 30 +LOGCAT_BOOT_WAIT ?= 15 POSTIT_ANDROID_CSPROJ := src/PostIt/PostIt.Android/PostIt.Android.csproj POSTIT_APK_DIR := src/PostIt/PostIt.Android/bin/$(CONFIG)/net10.0-android/$(POSTIT_RID) @@ -214,16 +214,17 @@ qemu-build: -p:RuntimeIdentifier=$(POSTIT_RID) \ -p:EmbedAssembliesIntoApk=true \ --nologo - -qemu-install: qemu-build @if [ ! -f "$(POSTIT_APK)" ]; then \ echo " APK not found at $(POSTIT_APK)." >&2; \ echo " Files in $(POSTIT_APK_DIR):" >&2; \ ls -la "$(POSTIT_APK_DIR)" 2>/dev/null || echo " (directory does not exist)" >&2; \ exit 1; \ fi + + +qemu-install: qemu-build @echo " Installing $(POSTIT_APK) on $(ADB_SERIAL)..." - adb -s $(ADB_SERIAL) install -r "$(POSTIT_APK)" + adb -s $(ADB_SERIAL) install -r "$(POSTIT_APK)" -r qemu-uninstall: adb -s $(ADB_SERIAL) uninstall fr.pschneider.PostIt diff --git a/external/dotnet-android-build-image b/external/dotnet-android-build-image deleted file mode 160000 index 0695a6c1..00000000 --- a/external/dotnet-android-build-image +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0695a6c1fea6508f1a88f7ad0ad9cb93733aa52d diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 38ffe289..2c57fb5f 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -1,20 +1,36 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - diff --git a/src/PostIt/PostIt.Android/MainActivity.cs b/src/PostIt/PostIt.Android/MainActivity.cs index 1b413a9f..e85b2dee 100644 --- a/src/PostIt/PostIt.Android/MainActivity.cs +++ b/src/PostIt/PostIt.Android/MainActivity.cs @@ -3,6 +3,9 @@ using Android.Content.PM; using Android.Content; using Avalonia.Android; using AndroidX.Emoji2.Text; +using AndroidX.Core.Provider; +using Android; +using Android.Graphics; namespace PostIt.Android; @@ -26,7 +29,6 @@ public class MainActivity : AvaloniaMainActivity protected override void OnCreate(global::Android.OS.Bundle? savedInstanceState) { - EmojiCompat.Init(this); base.OnCreate(savedInstanceState); PlatformBootstrap.EnsureInitialized(); Current = this; diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index d0bda3f4..c7202c78 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -1,34 +1,28 @@ Exe - net11.0-android - - android-arm64;android-x64 - 24.0.0 + net10.0-android + 23 enable fr.pschneider.PostIt 1 1.0 apk false - android-arm;android-arm64;android-x64 - 1.1.0.0 - 1.1.0.0 - 1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3 - 1.1.0-beta.1 Resources\drawable\Icon.png - - - - + + + + + diff --git a/src/PostIt/PostIt.Android/Resources/values/styles.xml b/src/PostIt/PostIt.Android/Resources/values/styles.xml index 44830966..377db44f 100644 --- a/src/PostIt/PostIt.Android/Resources/values/styles.xml +++ b/src/PostIt/PostIt.Android/Resources/values/styles.xml @@ -5,6 +5,7 @@ diff --git a/src/PostIt/PostIt.Android/Services/ContactService.Mobile.cs b/src/PostIt/PostIt.Android/Services/ContactService.Mobile.cs deleted file mode 100644 index c869256d..00000000 --- a/src/PostIt/PostIt.Android/Services/ContactService.Mobile.cs +++ /dev/null @@ -1,84 +0,0 @@ -#if ANDROID || IOS -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Maui.ApplicationModel.Communication; -using Microsoft.Maui.ApplicationModel; -using Microsoft.Maui.Devices; -using PostIt.Services; -using System.Linq; - -namespace PostIt.Android.Services; - -/// -/// Mobile implementation backed by MAUI Essentials -/// Contacts.Default. -/// -/// Compiled only for ANDROID and IOS. On desktop targets, -/// see ContactService.Desktop.cs (the stub that wins at -/// compile time). -/// -/// Note: at runtime, this class throws -/// NotImplementedInReferenceAssemblyException unless -/// the host application project also references the -/// platform-specific Microsoft.Maui.Essentials implementation -/// (typically PostIt.Android). On iOS the same is -/// required via PostIt.iOS. On desktop the stub is used -/// and this file is excluded. -/// -public sealed class ContactService : IContactService -{ - public async Task> GetDeviceContactsAsync(CancellationToken ct = default) - { - if (DeviceInfo.Current.Platform == DevicePlatform.Unknown) - return Array.Empty(); - - try - { - var status = await Permissions.RequestAsync(); - if (status != PermissionStatus.Granted) - return Array.Empty(); - - var contacts = await Contacts.Default.GetAllAsync(); - if (contacts is null) return Array.Empty(); - - // Carry the per-contact email list as-is. A real - // device contact can carry several addresses (home / - // work / other); the UI use case ("invite / add to a - // circle") can then decide which address to use, or - // let the user pick. The platform-neutral ContactDto - // shape is intentionally richer than the Yavsc - // directory's single-Email shape — the two flows - // answer different questions. - var result = new List(contacts.Count()); - foreach (var c in contacts) - { - var emails = ExtractEmails(c.Emails); - result.Add(new ContactDto( - c.Id, - c.DisplayName ?? string.Empty, - emails)); - } - return result; - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"ContactService: {ex.Message}"); - return Array.Empty(); - } - } - - private static IReadOnlyList ExtractEmails(IEnumerable? emails) - { - if (emails is null) return Array.Empty(); - var list = new List(); - foreach (var e in emails) - { - if (!string.IsNullOrEmpty(e.EmailAddress)) - list.Add(e.EmailAddress); - } - return list; - } -} -#endif diff --git a/src/PostIt/PostIt.Browser/PostIt.Browser.csproj b/src/PostIt/PostIt.Browser/PostIt.Browser.csproj index edbc9227..917ba0ab 100644 --- a/src/PostIt/PostIt.Browser/PostIt.Browser.csproj +++ b/src/PostIt/PostIt.Browser/PostIt.Browser.csproj @@ -1,6 +1,6 @@ - net11.0-browser + net10.0-browser Exe true enable diff --git a/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj b/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj index 3baf69f5..2982ae29 100644 --- a/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj +++ b/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj @@ -3,7 +3,7 @@ WinExe - net11.0 + net10.0 enable 1.1.0.0 1.1.0.0 diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index d2399873..4161635d 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -27,7 +27,7 @@ public partial class App : Application /// DataValidationErrors.SetErrors. /// public IServiceProvider? ServiceProvider { get; private set; } - private MainWindow window; + public App() { } @@ -35,6 +35,9 @@ public partial class App : Application public override void Initialize() { AvaloniaXamlLoader.Load(this); +#if DEBUG + this.AttachDeveloperTools(); +#endif } public override void OnFrameworkInitializationCompleted() @@ -52,8 +55,6 @@ public partial class App : Application this.ServiceProvider = BuildServices(new ServiceCollection()); AttachServiceProvider(ServiceProvider); var settings = ServiceProvider.GetRequiredService(); - var sessionStatus = ServiceProvider.GetRequiredService(); - var api = ServiceProvider.GetRequiredService(); DataTemplates.Clear(); DataTemplates.Add(new ViewLocator(ServiceProvider)); @@ -85,47 +86,44 @@ public partial class App : Application } }; + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - var homeVm = ServiceProvider.GetRequiredService(); - - window = new MainWindow(); - window.SessionBanner.DataContext = sessionStatus; - - // Build the navigation stack from scratch: HomePage is the - // root in both cases. App.BootAsync will push MainPage on - // top if the silent refresh succeeds. - desktop.MainWindow = window; - _ = PushPageAsync(homeVm); - - // When the user logs out, route back to HomePage. We - // ReplaceAsync the current top so we don't grow the stack - // on every logout — otherwise repeated login/logout would - // eventually balloon the back history. - sessionStatus.LogoutCompleted += () => - { - var w = (MainWindow)((IClassicDesktopStyleApplicationLifetime)ApplicationLifetime!).MainWindow!; - var nav = w.NavRoot; - _ = nav.PopToRootAsync(); - }; - - // When the user signs in interactively (Login button on - // the session banner), push MainPage on top of HomePage. - sessionStatus.LoginSucceeded += () => - { - var w = (MainWindow)((IClassicDesktopStyleApplicationLifetime)ApplicationLifetime!).MainWindow!; - _ = PushMainPageAsync(); - }; - - window.Opened += async (_, _) => await BootAsync(this.ServiceProvider, api); + desktop.MainWindow = CreateMainWindow(); } - else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView) + else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) { - singleView.MainView = new MainWindow - { - DataContext = ServiceProvider.GetRequiredService() - }; + singleViewFactoryApplicationLifetime.MainViewFactory = () => CreateMainWindow(); } + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) + { + singleViewPlatform.MainView = CreateMainWindow(); + } + base.OnFrameworkInitializationCompleted(); + } + + MainWindow window; + private MainWindow CreateMainWindow() + { + window = new MainWindow(); + var api = ServiceProvider!.GetRequiredService(); + window.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api); + var sessionStatus = ServiceProvider!.GetRequiredService(); + sessionStatus.LogoutCompleted += () => + { + window.NavRoot.PopToRootAsync(); + }; + + sessionStatus.LoginSucceeded += () => + { + PushMainPageAsync(); + }; + + var homeVm = ServiceProvider!.GetRequiredService(); + + this.PushPageAsync(homeVm).Wait(); + window.SessionBanner.DataContext = sessionStatus; + return window; } /// @@ -156,7 +154,6 @@ public partial class App : Application var contactService = new ContactService(); var userDirectory = new UserDirectory(userSearchClient); - // Vues services.AddTransient(); // SettingsPage is a singleton: there must be one and only one @@ -268,8 +265,8 @@ public partial class App : Application /// public static Task PushMainPageAsync() { - var app = (App)Current; - var mainVm = app.ServiceProvider.GetRequiredService(); + var app = (App)Current!; + var mainVm = app.ServiceProvider!.GetRequiredService(); return app.PushPageAsync(mainVm); } @@ -310,7 +307,7 @@ public partial class App : Application _ = PushPageAsync(vm); } - internal Task PushPageAsync(ViewModelBase vm) + internal async Task PushPageAsync(ViewModelBase vm) { if (window is null) { @@ -344,10 +341,10 @@ public partial class App : Application var stack = window.NavRoot.NavigationStack; if (stack.Count > 0 && ReferenceEquals(stack[stack.Count - 1], page)) { - return Task.CompletedTask; + return; } - return window.NavRoot.PushAsync(page); + await window.NavRoot.PushAsync(page); } internal async Task GoBackAsync() diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj index 0277d2f6..1b48c21f 100644 --- a/src/PostIt/PostIt/PostIt.csproj +++ b/src/PostIt/PostIt/PostIt.csproj @@ -1,6 +1,6 @@ - net11.0 + net10.0 enable latest true diff --git a/src/PostIt/PostIt/Settings/AuthenticationSettings.cs b/src/PostIt/PostIt/Settings/AuthenticationSettings.cs index ad71063b..99358cfb 100644 --- a/src/PostIt/PostIt/Settings/AuthenticationSettings.cs +++ b/src/PostIt/PostIt/Settings/AuthenticationSettings.cs @@ -21,6 +21,9 @@ public partial class AuthenticationSettings : ObservableObject public static string DefaultAuthority { get; internal set; } = "https://yavsc.pschneider.fr"; public static string DefaultClientId { get; internal set; } = "postit"; + + public static string[] DefaultScopes { get; set; } = { "blogs"} ; + [ObservableProperty] public partial string Authority { get; set; } diff --git a/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs b/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs index d374c9e4..ae5b0f63 100644 --- a/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs @@ -122,8 +122,8 @@ public partial class MainPageViewModel : ViewModelBase public MainPageViewModel() { - Init(null); SettingsModel = new Settings(); + Init(SettingsModel); BlogClient = null; } @@ -135,6 +135,11 @@ public partial class MainPageViewModel : ViewModelBase SelectedPost = null; IsBusy = false; StatusMessage = "Ready"; + Settings = settings ?? new Settings(); + WindowTitle = "PostIt"; + DraftTitle = string.Empty; + DraftArticle = string.Empty; + DraftIsPublished = false; // Production path: DI injects the canonical Settings singleton // and we use it as-is. Test path: tests call this constructor // without a Settings argument; we fall back to a fresh @@ -145,11 +150,7 @@ public partial class MainPageViewModel : ViewModelBase // sink; that crash is fixed in Settings.OnPropertyChanged // (thread-safe dispatcher marshalling) so the duplicate // instance is now merely wasteful, not dangerous. - Settings = settings ?? new Settings(); - WindowTitle = "PostIt"; - DraftTitle = string.Empty; - DraftArticle = string.Empty; - DraftIsPublished = false; + } /// Save is enabled as soon as the user has typed @@ -174,7 +175,6 @@ public partial class MainPageViewModel : ViewModelBase SettingsModel = new Settings(); BlogClient = blogClient ?? throw new ArgumentNullException(nameof(blogClient)); ; Services = services; - Init(settings); } @@ -210,7 +210,7 @@ public partial class MainPageViewModel : ViewModelBase { await ExecuteAsync(async () => { - var posts = await BlogClient.GetPostsAsync(); + var posts = await BlogClient!.GetPostsAsync(); Posts.Clear(); foreach (var post in posts.OrderByDescending(p => p.DateModified)) { @@ -259,7 +259,7 @@ public partial class MainPageViewModel : ViewModelBase DateCreated = DateTime.UtcNow, DateModified = DateTime.UtcNow, }; - var created = await BlogClient.CreatePostAsync(draft); + var created = await BlogClient!.CreatePostAsync(draft); if (created is not null) { SelectedPost = created; @@ -278,7 +278,7 @@ public partial class MainPageViewModel : ViewModelBase DateCreated = SelectedPost.DateCreated, DateModified = DateTime.UtcNow, }; - await BlogClient.UpdatePostAsync(SelectedPost.Id, update); + await BlogClient!.UpdatePostAsync(SelectedPost.Id, update); StatusMessage = $"Saved post {SelectedPost.Id}."; } @@ -297,7 +297,7 @@ public partial class MainPageViewModel : ViewModelBase await ExecuteAsync(async () => { - await BlogClient.DeletePostAsync(SelectedPost.Id); + await BlogClient!.DeletePostAsync(SelectedPost.Id); StatusMessage = $"Deleted post {SelectedPost.Id}."; SelectedPost = null; await RefreshPostsAsync(); @@ -331,7 +331,7 @@ public partial class MainPageViewModel : ViewModelBase await ExecuteAsync(async () => { var desired = !DraftIsPublished; - await BlogClient.SetPublishAsync(SelectedPost.Id, desired); + await BlogClient!.SetPublishAsync(SelectedPost.Id, desired); DraftIsPublished = desired; // Mirror into the selected post so a subsequent // RefreshPostsAsync() doesn't blow away the @@ -372,7 +372,7 @@ public partial class MainPageViewModel : ViewModelBase private async Task RefreshPostsAsync() { - var posts = await BlogClient.GetPostsAsync(); + var posts = await BlogClient!.GetPostsAsync(); Posts.Clear(); foreach (var post in posts.OrderByDescending(p => p.DateModified)) { diff --git a/src/PostIt/PostIt/ViewModels/Settings.cs b/src/PostIt/PostIt/ViewModels/Settings.cs index 890ca15c..9b54a842 100644 --- a/src/PostIt/PostIt/ViewModels/Settings.cs +++ b/src/PostIt/PostIt/ViewModels/Settings.cs @@ -350,15 +350,14 @@ public partial class Settings : ViewModelBase var settings = JsonSerializer.Deserialize(json); if (settings is null) { - Console.Error.WriteLine($"🩎 Settings payload is invalid (source: {source})."); - return; + UseDefaultSettings(); } // Apply under the gate so concurrent Load() callers cannot // see half the new values / half the old ones. The actual // PropertyChanged fan-out is handled by [ObservableProperty]'s // setters which we route through SetProperty → OnPropertyChanged // → our overridden dispatcher-safe marshaller below. - lock (_mutationGate) + else lock (_mutationGate) { this.Authentication = settings.Authentication; this.DarkMode = settings.DarkMode; @@ -371,6 +370,11 @@ public partial class Settings : ViewModelBase AuthenticationSettings.DefaultClientId : settings.Authentication.ClientId; this.Authentication.RedirectUri = string.IsNullOrWhiteSpace(settings.Authentication.RedirectUri) ? AuthenticationSettings.DefaultDesktopRedirectUri : settings.Authentication.RedirectUri; + if (settings.Authentication.Scopes is null || settings.Authentication.Scopes.Length == 0) + { + settings.Authentication.Scopes = AuthenticationSettings.DefaultScopes; + } + else this.Authentication.Scopes = settings.Authentication.Scopes; } } @@ -400,6 +404,18 @@ public partial class Settings : ViewModelBase } } + private void UseDefaultSettings() + { + this.Authentication = new AuthenticationSettings + { + Authority = AuthenticationSettings.DefaultAuthority, + ClientId = AuthenticationSettings.DefaultClientId, + RedirectUri = AuthenticationSettings.DefaultDesktopRedirectUri, + Scopes = AuthenticationSettings.DefaultScopes + }; + this.DarkMode = false; + } + /// /// Persist the current in-memory state to /// ~/.config/PostIt/postit-settings.json (Linux) / diff --git a/src/Yavsc.Abstract/Yavsc.Abstract.csproj b/src/Yavsc.Abstract/Yavsc.Abstract.csproj index 23297848..f76e42fd 100644 --- a/src/Yavsc.Abstract/Yavsc.Abstract.csproj +++ b/src/Yavsc.Abstract/Yavsc.Abstract.csproj @@ -1,6 +1,6 @@ - net11.0 + net10.0 enable A shared model for a little client/server app, dealing about establishing some contract, between some human client and provider. diff --git a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj index be27960c..00e6e2db 100644 --- a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj +++ b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj @@ -1,6 +1,6 @@ - net11.0 + net10.0 enable Yavsc.Api.Client Yavsc.Api.Client diff --git a/src/Yavsc.Api/Yavsc.Api.csproj b/src/Yavsc.Api/Yavsc.Api.csproj index 36a96b8b..d9a814f6 100644 --- a/src/Yavsc.Api/Yavsc.Api.csproj +++ b/src/Yavsc.Api/Yavsc.Api.csproj @@ -1,6 +1,6 @@ - net11.0 + net10.0 enable 1c73094f-959f-4211-b1a1-6a69b236c283 Yavsc.Api diff --git a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj index e688f730..83c0dc37 100644 --- a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj +++ b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj @@ -1,6 +1,6 @@ - net11.0 + net10.0 enable enable false diff --git a/src/Yavsc.Blogs/Yavsc.Blogs.csproj b/src/Yavsc.Blogs/Yavsc.Blogs.csproj index 60f74e06..63b3d977 100644 --- a/src/Yavsc.Blogs/Yavsc.Blogs.csproj +++ b/src/Yavsc.Blogs/Yavsc.Blogs.csproj @@ -1,6 +1,6 @@ - net11.0 + net10.0 enable 1c73094f-959f-4211-b1a1-6a69b236c283 Yavsc.Blogs diff --git a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj index 41c172a6..d332d250 100644 --- a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj +++ b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj @@ -1,6 +1,6 @@ - net11.0 + net10.0 enable enable false diff --git a/src/Yavsc.Org/Yavsc.Org.csproj b/src/Yavsc.Org/Yavsc.Org.csproj index fc633f8a..0403c7eb 100644 --- a/src/Yavsc.Org/Yavsc.Org.csproj +++ b/src/Yavsc.Org/Yavsc.Org.csproj @@ -1,6 +1,6 @@ - net11.0 + net10.0 enable WTFPL 76e56fc2-1619-40d8-8393-365258b7a21d diff --git a/src/Yavsc.Server/Yavsc.Server.csproj b/src/Yavsc.Server/Yavsc.Server.csproj index d47246b6..ac9380a4 100644 --- a/src/Yavsc.Server/Yavsc.Server.csproj +++ b/src/Yavsc.Server/Yavsc.Server.csproj @@ -1,6 +1,6 @@ - net11.0 + net10.0 enable 53bd70e8-ff81-497a-847f-a15fd8ea7a09 Yavsc.Server diff --git a/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj b/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj index bd4a6555..6ccd44c0 100644 --- a/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj +++ b/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj @@ -1,6 +1,6 @@ - net11.0 + net10.0 enable enable false diff --git a/src/cli/cli.csproj b/src/cli/cli.csproj index 0466998c..010c4eaf 100644 --- a/src/cli/cli.csproj +++ b/src/cli/cli.csproj @@ -1,7 +1,7 @@ Exe - net11.0 + net10.0 enable Yavsc.cli true From cbe307033bb18f728a3643633d1ea6c6a12494de Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 23 Aug 2026 18:39:37 +0100 Subject: [PATCH 08/10] LOGCAT_BOOT_WAIT ?= 30 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b086713c..b7d49840 100644 --- a/Makefile +++ b/Makefile @@ -163,7 +163,7 @@ POSTIT_RID ?= android-x64 EMU_HEADLESS ?= 0 LOGCAT_LINES ?= 200 LOGCAT_FOLLOW ?= 0 -LOGCAT_BOOT_WAIT ?= 15 +LOGCAT_BOOT_WAIT ?= 30 POSTIT_ANDROID_CSPROJ := src/PostIt/PostIt.Android/PostIt.Android.csproj POSTIT_APK_DIR := src/PostIt/PostIt.Android/bin/$(CONFIG)/net10.0-android/$(POSTIT_RID) From d0186d41b0e6c7cdd64444753fe4f55c1a82410e Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 23 Aug 2026 21:28:26 +0100 Subject: [PATCH 09/10] test are green !?! --- Makefile | 23 ++-- src/PostIt/Directory.Packages.props | 37 +++--- src/PostIt/PostIt.Android/Application.cs | 6 + src/PostIt/PostIt.Android/MainActivity.cs | 15 +-- .../PostIt.Android/PostIt.Android.csproj | 13 +- .../Properties/AndroidManifest.xml | 6 +- .../Resources/values-v31/styles.xml | 9 ++ .../Resources/values/styles.xml | 2 +- .../Services/AndroidSystemBrowser.cs | 6 +- .../PostIt.Browser/PostIt.Browser.csproj | 3 - .../PostIt.Desktop/PostIt.Desktop.csproj | 3 - src/PostIt/PostIt.Desktop/Program.cs | 3 - .../PostIt.Tests/MainPageButtonsTests.cs | 6 +- src/PostIt/PostIt.Tests/MainPageSaveTests.cs | 2 +- .../PostIt.Tests/PostItViewModelTests.cs | 2 +- src/PostIt/PostIt/App.axaml | 13 +- src/PostIt/PostIt/App.axaml.cs | 122 +++++------------- src/PostIt/PostIt/Assets/avalonia-logo.ico | Bin 0 -> 175875 bytes src/PostIt/PostIt/PostIt.csproj | 41 +++--- src/PostIt/PostIt/ViewLocator.cs | 14 +- ...{MainPageViewModel.cs => MainViewModel.cs} | 6 +- src/PostIt/PostIt/ViewModels/Settings.cs | 18 --- src/PostIt/PostIt/ViewModels/ViewModelBase.cs | 3 +- src/PostIt/PostIt/Views/MainPage.axaml | 4 +- src/PostIt/PostIt/Views/MainWindow.axaml | 22 +--- 25 files changed, 147 insertions(+), 232 deletions(-) create mode 100644 src/PostIt/PostIt/Assets/avalonia-logo.ico rename src/PostIt/PostIt/ViewModels/{MainPageViewModel.cs => MainViewModel.cs} (98%) diff --git a/Makefile b/Makefile index b7d49840..a48b9943 100644 --- a/Makefile +++ b/Makefile @@ -165,9 +165,10 @@ LOGCAT_LINES ?= 200 LOGCAT_FOLLOW ?= 0 LOGCAT_BOOT_WAIT ?= 30 +ANDROID_PACKAGE_NAME = fr.pschneider.PostIt POSTIT_ANDROID_CSPROJ := src/PostIt/PostIt.Android/PostIt.Android.csproj POSTIT_APK_DIR := src/PostIt/PostIt.Android/bin/$(CONFIG)/net10.0-android/$(POSTIT_RID) -POSTIT_APK := $(POSTIT_APK_DIR)/fr.pschneider.PostIt-Signed.apk +POSTIT_APK := $(POSTIT_APK_DIR)/$(ANDROID_PACKAGE_NAME)-Signed.apk qemu-run: @echo " Starting AVD $(AVD_NAME) on $(ADB_SERIAL)..." @@ -227,12 +228,12 @@ qemu-install: qemu-build adb -s $(ADB_SERIAL) install -r "$(POSTIT_APK)" -r qemu-uninstall: - adb -s $(ADB_SERIAL) uninstall fr.pschneider.PostIt + adb -s $(ADB_SERIAL) uninstall $(ANDROID_PACKAGE_NAME) # Dump recent logcat output for the running PostIt.Android process. # By default, prints the last $(LOGCAT_LINES) lines (one-shot, with # `-d`). Set LOGCAT_FOLLOW=1 to follow the stream live instead. -# Filtering is by PID (pidof fr.pschneider.PostIt), not by tag, +# Filtering is by PID (pidof $(ANDROID_PACKAGE_NAME)), not by tag, # because Mono/Xamarin can emit logs under several tags # (mono, PostIt.Android, Avalonia.Android) and tag-based filtering # would miss the ones not matching. PID-based filtering is exact. @@ -240,17 +241,17 @@ qemu-uninstall: # silently with no output; that is the expected behaviour for # "no logs yet". qemu-logcat: - @PID=$$(adb -s $(ADB_SERIAL) shell pidof fr.pschneider.PostIt 2>/dev/null | tr -d '\r\n'); \ + @PID=$$(adb -s $(ADB_SERIAL) shell pidof $(ANDROID_PACKAGE_NAME) 2>/dev/null | tr -d '\r\n'); \ if [ -z "$$PID" ]; then \ - echo " fr.pschneider.PostIt is not running on $(ADB_SERIAL)."; \ - echo " Start the app first (am start -n fr.pschneider.PostIt/PostIt.Android.PostItMainActivity)"; \ + echo " $(ANDROID_PACKAGE_NAME) is not running on $(ADB_SERIAL)."; \ + echo " Start the app first (am start -n $(ANDROID_PACKAGE_NAME)/PostIt.Android.PostItMainActivity)"; \ exit 1; \ fi; \ echo " Following PID $$PID (LOGCAT_FOLLOW=$(LOGCAT_FOLLOW), LOGCAT_LINES=$(LOGCAT_LINES))"; \ if [ "$(LOGCAT_FOLLOW)" = "1" ]; then \ - adb -s $(ADB_SERIAL) logcat -v time --pid=$$PID fr.pschneider.PostIt:F; \ + adb -s $(ADB_SERIAL) logcat -v time --pid=$$PID $(ANDROID_PACKAGE_NAME):F; \ else \ - adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID fr.pschneider.PostIt:F; \ + adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID $(ANDROID_PACKAGE_NAME):F; \ fi # Clear logcat, launch PostIt.Android, then dump everything that was @@ -264,13 +265,13 @@ LOGCAT_BOOT_WAIT ?= 15 qemu-logcat-boot: @echo " Clearing logcat buffer..." adb -s $(ADB_SERIAL) logcat -c - @echo " Launching fr.pschneider.PostIt..." + @echo " Launching $(ANDROID_PACKAGE_NAME)..." adb -s $(ADB_SERIAL) shell am start \ - -n fr.pschneider.PostIt/PostIt.Android.PostItMainActivity + -n $(ANDROID_PACKAGE_NAME)/PostIt.Android.PostItMainActivity @echo " Waiting $(LOGCAT_BOOT_WAIT)s for the app to start rendering..." @sleep $(LOGCAT_BOOT_WAIT) @echo " Dumping logcat (PostIt PID + system buffer):" - @PID=$$(adb -s $(ADB_SERIAL) shell pidof fr.pschneider.PostIt 2>/dev/null | tr -d '\r\n'); \ + @PID=$$(adb -s $(ADB_SERIAL) shell pidof $(ANDROID_PACKAGE_NAME) 2>/dev/null | tr -d '\r\n'); \ if [ -n "$$PID" ]; then \ echo " (PID $$PID at dump time)"; \ adb -s $(ADB_SERIAL) logcat -d -v time --pid=$$PID; \ diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 2c57fb5f..69e8a896 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -1,9 +1,13 @@ - - + + + true + + + @@ -11,28 +15,23 @@ + + + + + + + + + + + - - - - - - - - - - - - - - - - - + diff --git a/src/PostIt/PostIt.Android/Application.cs b/src/PostIt/PostIt.Android/Application.cs index fb6b08d3..f5a7908d 100644 --- a/src/PostIt/PostIt.Android/Application.cs +++ b/src/PostIt/PostIt.Android/Application.cs @@ -2,6 +2,12 @@ using Android.Runtime; using Avalonia; using Avalonia.Android; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Avalonia.Controls; +using Avalonia.Styling; +using Yavsc.Api.Client; namespace PostIt.Android { diff --git a/src/PostIt/PostIt.Android/MainActivity.cs b/src/PostIt/PostIt.Android/MainActivity.cs index e85b2dee..62c29e10 100644 --- a/src/PostIt/PostIt.Android/MainActivity.cs +++ b/src/PostIt/PostIt.Android/MainActivity.cs @@ -1,11 +1,8 @@ -using Android.App; -using Android.Content.PM; +using Android.App; using Android.Content; +using Android.Content.PM; +using Avalonia; using Avalonia.Android; -using AndroidX.Emoji2.Text; -using AndroidX.Core.Provider; -using Android; -using Android.Graphics; namespace PostIt.Android; @@ -15,11 +12,10 @@ namespace PostIt.Android; Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, - LaunchMode = LaunchMode.SingleTask, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] public class MainActivity : AvaloniaMainActivity { - /// + /// /// Strongly-typed handle to the current MainActivity instance, set in /// and consumed by platform services such as /// which need to launch @@ -33,8 +29,7 @@ public class MainActivity : AvaloniaMainActivity PlatformBootstrap.EnsureInitialized(); Current = this; } - - /// + /// /// Receives the deep-link Intent fired by the system browser after the /// user completes the OIDC login on https://yavsc.pschneider.fr. The /// Intent URI has the shape android://postit-signin?code=...&state=.... diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index c7202c78..3e4a3c70 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -10,19 +10,22 @@ apk false + Resources\drawable\Icon.png - - - + - - + + + + + + diff --git a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml index 2472d06d..91b61d05 100644 --- a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml +++ b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml @@ -1,7 +1,7 @@ - + - + - + + + - + - - + diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 4161635d..22724f17 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -1,16 +1,16 @@ using System; using System.Linq; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Styling; +using Microsoft.Extensions.DependencyInjection; using PostIt.Services; -using Yavsc.Api.Client; using PostIt.ViewModels; using PostIt.Views; +using Yavsc.Api.Client; namespace PostIt; @@ -28,9 +28,7 @@ public partial class App : Application /// public IServiceProvider? ServiceProvider { get; private set; } - public App() - { - } + MainWindow window; public override void Initialize() { @@ -42,101 +40,31 @@ public partial class App : Application public override void OnFrameworkInitializationCompleted() { - // Belt-and-braces 2nd-instance guard. The primary check now - // lives in PostIt.Desktop.Program.Main and exits before - // Avalonia boots — preventing a flash of the MainWindow on - // every postit://callback launch. This block is kept for any - // entry point that bypasses Program.Main (PostIt.Browser, - // PostIt.Android's process lifecycle, ad-hoc tests that build - // App directly) and as defence-in-depth in case the Desktop - // build is ever reconfigured to skip the early check. if (TryHandOffCustomSchemeUrl()) return; this.ServiceProvider = BuildServices(new ServiceCollection()); - AttachServiceProvider(ServiceProvider); var settings = ServiceProvider.GetRequiredService(); DataTemplates.Clear(); DataTemplates.Add(new ViewLocator(ServiceProvider)); - // Wire the Settings singleton onto the SettingsPage singleton - // once, at composition time. The page is registered as a - // singleton (see above) precisely so this binding is stable - // for the lifetime of the app: every push to / pop from the - // navigation stack finds the same ContentPage with the same - // DataContext, and the TwoWay bindings inside the page keep - // mutating the same in-memory Settings instance that the rest - // of the app reads (OidcClientOptions construction, etc.). - ServiceProvider.GetRequiredService().DataContext = settings; - - // Settings.DarkMode was previously a dead field: it round- - // tripped through the settings file and the SettingsPage - // CheckBox, but no consumer ever read it. Wire it here to - // Application.RequestedThemeVariant so the toggle takes - // effect immediately, and seed the initial theme from the - // value Load() just populated (so a dark-mode user lands on - // a dark window on first launch, not on a default-light - // window that flips after the user touches the toggle). - ApplyDarkMode(settings); - settings.PropertyChanged += (_, e) => - { - if (e.PropertyName == nameof(Settings.DarkMode)) - { - ApplyDarkMode(settings); - } - }; - - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { desktop.MainWindow = CreateMainWindow(); } else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) { - singleViewFactoryApplicationLifetime.MainViewFactory = () => CreateMainWindow(); + singleViewFactoryApplicationLifetime.MainViewFactory = + () => CreateMainWindow(); } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) { singleViewPlatform.MainView = CreateMainWindow(); } + ApplyDarkMode(settings); base.OnFrameworkInitializationCompleted(); } - MainWindow window; - private MainWindow CreateMainWindow() - { - window = new MainWindow(); - var api = ServiceProvider!.GetRequiredService(); - window.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api); - var sessionStatus = ServiceProvider!.GetRequiredService(); - sessionStatus.LogoutCompleted += () => - { - window.NavRoot.PopToRootAsync(); - }; - - sessionStatus.LoginSucceeded += () => - { - PushMainPageAsync(); - }; - - var homeVm = ServiceProvider!.GetRequiredService(); - - this.PushPageAsync(homeVm).Wait(); - window.SessionBanner.DataContext = sessionStatus; - return window; - } - - /// - /// Build the DI container the app uses. Pulled out of - /// so headless - /// tests can construct the same container at TestApp boot - /// without going through the full Avalonia desktop lifetime - /// (which never runs in a unit test). The container returned is - /// the exact one production uses — no test-only fakes, no - /// trimmed service list — so a test that exercises a VM, page, - /// or service resolves through the same wiring the real app - /// does, and a green test is a green contract for prod. - /// internal static IServiceProvider BuildServices(ServiceCollection services) { var settings = new Settings(); @@ -182,14 +110,13 @@ public partial class App : Application // ViewModels services.AddSingleton(settings); services.AddSingleton(api); - services.AddSingleton(api); services.AddSingleton(client); services.AddSingleton(circleClient); services.AddSingleton(blogAclClient); services.AddSingleton(userSearchClient); services.AddSingleton(contactService); services.AddSingleton(userDirectory); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -204,21 +131,30 @@ public partial class App : Application return services.BuildServiceProvider(); } - /// - /// Attach a pre-built DI container to this - /// instance. Used by headless tests after - /// ; in production this happens - /// implicitly via . - /// Idempotent w.r.t. : - /// re-binding from a second App boot is a no-op. - /// - internal void AttachServiceProvider(IServiceProvider sp) + private MainWindow CreateMainWindow() { - ServiceProvider = sp; - Settings.BindToServiceProvider(sp); + window = new MainWindow(); + var api = ServiceProvider!.GetRequiredService(); + window.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api); + var sessionStatus = ServiceProvider!.GetRequiredService(); + sessionStatus.LogoutCompleted += () => + { + window.NavRoot.PopToRootAsync(); + }; + + sessionStatus.LoginSucceeded += () => + { + PushMainPageAsync(); + }; + + var homeVm = ServiceProvider!.GetRequiredService(); + + this.PushPageAsync(homeVm).Wait(); + window.SessionBanner.DataContext = sessionStatus; + return window; } - /// +/// /// Test-only hook: bind a concrete so /// command-driven navigation paths () can /// push onto a real in headless @@ -266,7 +202,7 @@ public partial class App : Application public static Task PushMainPageAsync() { var app = (App)Current!; - var mainVm = app.ServiceProvider!.GetRequiredService(); + var mainVm = app.ServiceProvider!.GetRequiredService(); return app.PushPageAsync(mainVm); } diff --git a/src/PostIt/PostIt/Assets/avalonia-logo.ico b/src/PostIt/PostIt/Assets/avalonia-logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..f7da8bb5863b7cecec2adcdebd948fe2f9418d0c GIT binary patch literal 175875 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 + net10.0 enable latest - true - 1.1.0.0 - 1.1.0.0 - 1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3 - 1.1.0-beta.1 + - - - - - - - None - All - - - - - - - + PreserveNewest @@ -38,6 +20,21 @@ - + + + + + None + All + + + + + + + + + + diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs index fd92c802..03ab98a1 100644 --- a/src/PostIt/PostIt/ViewLocator.cs +++ b/src/PostIt/PostIt/ViewLocator.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; using Avalonia.Controls.Templates; using Microsoft.Extensions.DependencyInjection; @@ -10,17 +11,19 @@ namespace PostIt; /// /// Given a view model, returns the corresponding view if possible. /// - +[RequiresUnreferencedCode( + "Default implementation of ViewLocator involves reflection which may be trimmed away.", + Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")] public class ViewLocator : IDataTemplate { - private readonly IServiceProvider _services; + private readonly IServiceProvider _services; public ViewLocator(IServiceProvider services) { _services = services; } - public Control Build(object? data) + public Control Build(object? data) { try { @@ -32,11 +35,12 @@ public class ViewLocator : IDataTemplate } } + private Control BuildCore(object? data) { return data switch { - MainPageViewModel => _services.GetRequiredService(), + MainViewModel => _services.GetRequiredService(), Settings => _services.GetRequiredService(), HomePageViewModel => _services.GetRequiredService(), SignaturePageViewModel => _services.GetRequiredService(), @@ -48,5 +52,5 @@ public class ViewLocator : IDataTemplate }; } - public bool Match(object? data) => data is ViewModelBase; + public bool Match(object? data) => data is ViewModelBase; } diff --git a/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs b/src/PostIt/PostIt/ViewModels/MainViewModel.cs similarity index 98% rename from src/PostIt/PostIt/ViewModels/MainPageViewModel.cs rename to src/PostIt/PostIt/ViewModels/MainViewModel.cs index ae5b0f63..e56c2a73 100644 --- a/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/MainViewModel.cs @@ -11,7 +11,7 @@ using Yavsc.Api.Client; namespace PostIt.ViewModels; -public partial class MainPageViewModel : ViewModelBase +public partial class MainViewModel : ViewModelBase { /// Window/tab title. Cosmetic — bound by /// MainPage.axaml if at all. Not the post title. @@ -120,7 +120,7 @@ public partial class MainPageViewModel : ViewModelBase } - public MainPageViewModel() + public MainViewModel() { SettingsModel = new Settings(); Init(SettingsModel); @@ -170,7 +170,7 @@ public partial class MainPageViewModel : ViewModelBase /// . Production code uses the /// (Settings, BlogApiClient) overload below. /// - public MainPageViewModel(BlogApiClient blogClient, Settings? settings = null, IServiceProvider? services = null) + public MainViewModel(BlogApiClient blogClient, Settings? settings = null, IServiceProvider? services = null) { SettingsModel = new Settings(); BlogClient = blogClient ?? throw new ArgumentNullException(nameof(blogClient)); ; diff --git a/src/PostIt/PostIt/ViewModels/Settings.cs b/src/PostIt/PostIt/ViewModels/Settings.cs index 9b54a842..40e8548b 100644 --- a/src/PostIt/PostIt/ViewModels/Settings.cs +++ b/src/PostIt/PostIt/ViewModels/Settings.cs @@ -64,24 +64,6 @@ public partial class Settings : ViewModelBase null); } - /// - /// Returns the canonical Settings instance previously bound through - /// , or null when called - /// outside a running Avalonia application (tests, CLI tools). - /// - public static Settings? GetCurrent() => Volatile.Read(ref s_current); - - /// - /// Resolve the canonical Settings instance or throw. Use this in - /// production code paths that must not silently fall back to a - /// freshly-constructed (which used to be - /// the root cause of the postit://callback crash: two Settings - /// instances racing on PropertyChanged from different threads). - /// - public static Settings RequireCurrent() => - GetCurrent() ?? throw new InvalidOperationException( - "Settings.Current is not bound. Call App.OnFrameworkInitializationCompleted first."); - [ObservableProperty] public partial AuthenticationSettings Authentication { get; set; } = new(); diff --git a/src/PostIt/PostIt/ViewModels/ViewModelBase.cs b/src/PostIt/PostIt/ViewModels/ViewModelBase.cs index 5ce17aba..4ca69eea 100644 --- a/src/PostIt/PostIt/ViewModels/ViewModelBase.cs +++ b/src/PostIt/PostIt/ViewModels/ViewModelBase.cs @@ -4,8 +4,7 @@ namespace PostIt.ViewModels; public abstract partial class ViewModelBase : ObservableObject { - - /// + /// /// Gets if the user can navigate to the next page /// public abstract bool CanNavigateNext { get; protected set; } diff --git a/src/PostIt/PostIt/Views/MainPage.axaml b/src/PostIt/PostIt/Views/MainPage.axaml index 7eac39ff..7f1f1196 100644 --- a/src/PostIt/PostIt/Views/MainPage.axaml +++ b/src/PostIt/PostIt/Views/MainPage.axaml @@ -8,11 +8,11 @@ xmlns:AvaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit" mc:Ignorable="d" x:Class="PostIt.Views.MainPage" - x:DataType="vm:MainPageViewModel" + x:DataType="vm:MainViewModel" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + + Icon="/Assets/avalonia-logo.ico" + Title="PostIt" > - - + - From e007c7d6eb4510871e9c6d95ce10f9e0d761cd9e Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 23 Aug 2026 23:07:40 +0100 Subject: [PATCH 10/10] refacto static extension for Service provider --- .vscode/launch.json | 2 +- Makefile | 14 +- .../PostIt.Android/PostIt.Android.csproj | 4 +- .../AddCircleMemberDialogTests.cs | 1 + src/PostIt/PostIt.Tests/PostAclDialogTests.cs | 1 + src/PostIt/PostIt.Tests/PostIt.Tests.csproj | 6 +- src/PostIt/PostIt/App.axaml.cs | 152 +++--------------- .../Helpers/ServiceCollectionHelpers.cs | 77 +++++++++ .../PostIt/Helpers/ViewModelBaseHelpers.cs | 50 ++++++ .../PostIt/ViewModels/CirclesPageViewModel.cs | 1 + src/PostIt/PostIt/ViewModels/MainViewModel.cs | 1 + .../ViewModels/SessionStatusViewModel.cs | 3 +- src/PostIt/PostIt/ViewModels/Settings.cs | 19 +-- 13 files changed, 173 insertions(+), 158 deletions(-) create mode 100644 src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs create mode 100644 src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index 2079c716..42be176a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -48,7 +48,7 @@ "name": "Test PostIt.Android launch (Xamarin.UITest)", "type": "coreclr", "request": "launch", - "program": "${workspaceFolder}/src/PostIt/PostIt.Tests/bin/Debug/net11.0/PostIt.Tests.dll", + "program": "${workspaceFolder}/src/PostIt/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests.dll", "args": [ ], "cwd": "${workspaceFolder}/src/PostIt/PostIt.Tests", diff --git a/Makefile b/Makefile index a48b9943..d6d69196 100644 --- a/Makefile +++ b/Makefile @@ -161,9 +161,9 @@ ADB_SERIAL ?= emulator-5554 ANDROID_HOME ?= /opt/android-sdk POSTIT_RID ?= android-x64 EMU_HEADLESS ?= 0 -LOGCAT_LINES ?= 200 +LOGCAT_LINES ?= 600 LOGCAT_FOLLOW ?= 0 -LOGCAT_BOOT_WAIT ?= 30 +LOGCAT_BOOT_WAIT ?= 20 ANDROID_PACKAGE_NAME = fr.pschneider.PostIt POSTIT_ANDROID_CSPROJ := src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -249,9 +249,9 @@ qemu-logcat: fi; \ echo " Following PID $$PID (LOGCAT_FOLLOW=$(LOGCAT_FOLLOW), LOGCAT_LINES=$(LOGCAT_LINES))"; \ if [ "$(LOGCAT_FOLLOW)" = "1" ]; then \ - adb -s $(ADB_SERIAL) logcat -v time --pid=$$PID $(ANDROID_PACKAGE_NAME):F; \ + adb -s $(ADB_SERIAL) logcat -v time --pid=$$PID $(ANDROID_PACKAGE_NAME); \ else \ - adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID $(ANDROID_PACKAGE_NAME):F; \ + adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID $(ANDROID_PACKAGE_NAME); \ fi # Clear logcat, launch PostIt.Android, then dump everything that was @@ -270,14 +270,16 @@ qemu-logcat-boot: -n $(ANDROID_PACKAGE_NAME)/PostIt.Android.PostItMainActivity @echo " Waiting $(LOGCAT_BOOT_WAIT)s for the app to start rendering..." @sleep $(LOGCAT_BOOT_WAIT) + @echo " Dumping logcat (PostIt PID + system buffer):" @PID=$$(adb -s $(ADB_SERIAL) shell pidof $(ANDROID_PACKAGE_NAME) 2>/dev/null | tr -d '\r\n'); \ if [ -n "$$PID" ]; then \ - echo " (PID $$PID at dump time)"; \ + echo " ✅ (PID $$PID at dump time)"; \ adb -s $(ADB_SERIAL) logcat -d -v time --pid=$$PID; \ else \ - echo " (PostIt process not running at dump time — dumping last $(LOGCAT_LINES) lines unfiltered)"; \ + echo " 👿 (PostIt process not running at dump time — dumping last $(LOGCAT_LINES) lines unfiltered)"; \ adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES); \ + exit 1; \ fi qemu: qemu-run qemu-wait-boot qemu-install diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index 3e4a3c70..a4af1eb2 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -8,7 +8,9 @@ 1 1.0 apk - false + false + SdkOnly + partial diff --git a/src/PostIt/PostIt.Tests/AddCircleMemberDialogTests.cs b/src/PostIt/PostIt.Tests/AddCircleMemberDialogTests.cs index 98859649..e2426457 100644 --- a/src/PostIt/PostIt.Tests/AddCircleMemberDialogTests.cs +++ b/src/PostIt/PostIt.Tests/AddCircleMemberDialogTests.cs @@ -2,6 +2,7 @@ using Avalonia; using Avalonia.Headless.XUnit; using Microsoft.Extensions.DependencyInjection; +using PostIt.Helpers; using PostIt.Services; using PostIt.ViewModels; using PostIt.Views; diff --git a/src/PostIt/PostIt.Tests/PostAclDialogTests.cs b/src/PostIt/PostIt.Tests/PostAclDialogTests.cs index 2708ba8c..9bc28888 100644 --- a/src/PostIt/PostIt.Tests/PostAclDialogTests.cs +++ b/src/PostIt/PostIt.Tests/PostAclDialogTests.cs @@ -4,6 +4,7 @@ using System.Text.Json; using Avalonia; using Avalonia.Headless.XUnit; using Microsoft.Extensions.DependencyInjection; +using PostIt.Helpers; using PostIt.Services; using PostIt.ViewModels; using PostIt.Views; diff --git a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj index c11c17b0..433f36c3 100644 --- a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj +++ b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj @@ -1,6 +1,6 @@ - net11.0 + net10.0 enable enable false @@ -16,7 +16,9 @@ + + @@ -27,7 +29,5 @@ - - diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 22724f17..10a0c8a5 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.DependencyInjection; using PostIt.Services; using PostIt.ViewModels; using PostIt.Views; -using Yavsc.Api.Client; +using PostIt.Helpers; namespace PostIt; @@ -28,7 +28,7 @@ public partial class App : Application /// public IServiceProvider? ServiceProvider { get; private set; } - MainWindow window; + public MainWindow? Window { get; private set; } public override void Initialize() { @@ -42,119 +42,58 @@ public partial class App : Application { if (TryHandOffCustomSchemeUrl()) return; - this.ServiceProvider = BuildServices(new ServiceCollection()); + this.ServiceProvider = new ServiceCollection().BuildServices(); var settings = ServiceProvider.GetRequiredService(); - DataTemplates.Clear(); - DataTemplates.Add(new ViewLocator(ServiceProvider)); - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { desktop.MainWindow = CreateMainWindow(); + ApplyDarkMode(settings); } else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) { singleViewFactoryApplicationLifetime.MainViewFactory = - () => CreateMainWindow(); + () => + { + Window = CreateMainWindow(); + ApplyDarkMode(settings); + return Window; + }; } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) { singleViewPlatform.MainView = CreateMainWindow(); + ApplyDarkMode(settings); } - ApplyDarkMode(settings); base.OnFrameworkInitializationCompleted(); } - internal static IServiceProvider BuildServices(ServiceCollection services) - { - var settings = new Settings(); - settings.Load(); - var tokenStore = new TokenStore(System.IO.Path.Combine( - System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), - "PostIt", "tokens.json")); - - var api = new YavscApiClient(settings, tokenStore); - var client = new BlogApiClient(api, settings.BlogsApiUrl); - var circleClient = new CircleApiClient(api, settings.BlogsApiUrl); - var blogAclClient = new BlogAclApiClient(api, settings.BlogsApiUrl); - var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl); - var contactService = new ContactService(); - var userDirectory = new UserDirectory(userSearchClient); - - // Vues - services.AddTransient(); - // SettingsPage is a singleton: there must be one and only one - // instance of the settings UI for the lifetime of the app. - // This guarantees that (a) the bindings always reflect the - // current in-memory Settings state, (b) the page already has - // its DataContext wired up at composition-root time (see - // below), and (c) PushPageAsync's anti-empilement guard sees - // the same instance across pushes, so a second Settings tap - // is a no-op rather than re-pushing the page. Transient would - // let the user accumulate stale SettingsPage instances on - // the navigation stack, each bound to a fresh - // SettingsViewModel and missing any in-flight edits. - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - // Dialogs (modal-light pages): the ViewLocator resolves - // them when a caller pushes a PostAclDialogViewModel or - // AddCircleMemberDialogViewModel via App.PushPageAsync. - // App.PushPageAsync overwrites the page's DataContext with - // the caller-built VM, so the parameterless ctor is enough - // here — the parametrised ctors stay for direct test wiring. - services.AddTransient(); - services.AddTransient(); - // ViewModels - services.AddSingleton(settings); - services.AddSingleton(api); - services.AddSingleton(client); - services.AddSingleton(circleClient); - services.AddSingleton(blogAclClient); - services.AddSingleton(userSearchClient); - services.AddSingleton(contactService); - services.AddSingleton(userDirectory); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - // Persistent session banner: one instance for the lifetime of - // the app so the same VM survives page navigation. - var sessionStatus = new SessionStatusViewModel { Api = api }; - sessionStatus.Refresh(); - services.AddSingleton(sessionStatus); - services.AddTransient(); - - return services.BuildServiceProvider(); - } private MainWindow CreateMainWindow() { - window = new MainWindow(); + Window = new MainWindow(); var api = ServiceProvider!.GetRequiredService(); - window.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api); - var sessionStatus = ServiceProvider!.GetRequiredService(); + Window.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api); + var sessionStatus = ServiceProvider!.GetRequiredService(); sessionStatus.LogoutCompleted += () => { - window.NavRoot.PopToRootAsync(); + Window.NavRoot.PopToRootAsync(); }; sessionStatus.LoginSucceeded += () => { - PushMainPageAsync(); + PushMainPageAsync().Wait(); }; var homeVm = ServiceProvider!.GetRequiredService(); this.PushPageAsync(homeVm).Wait(); - window.SessionBanner.DataContext = sessionStatus; - return window; + Window.SessionBanner.DataContext = sessionStatus; + return Window; } -/// + /// /// Test-only hook: bind a concrete so /// command-driven navigation paths () can /// push onto a real in headless @@ -162,7 +101,7 @@ public partial class App : Application /// internal void AttachMainWindow(MainWindow mainWindow) { - window = mainWindow ?? throw new ArgumentNullException(nameof(mainWindow)); + Window = mainWindow ?? throw new ArgumentNullException(nameof(mainWindow)); } private static void ApplyDarkMode(Settings settings) @@ -199,11 +138,11 @@ public partial class App : Application /// (interactive login from the banner). Pulled out as a helper so /// the two callers can't drift apart. /// - public static Task PushMainPageAsync() + public static async Task PushMainPageAsync() { var app = (App)Current!; var mainVm = app.ServiceProvider!.GetRequiredService(); - return app.PushPageAsync(mainVm); + await app.PushPageAsync(mainVm); } private bool TryHandOffCustomSchemeUrl() @@ -238,53 +177,8 @@ public partial class App : Application return true; } - internal void PushPage(ViewModelBase vm) - { - _ = PushPageAsync(vm); - } - - internal async Task PushPageAsync(ViewModelBase vm) - { - if (window is null) - { - throw new InvalidOperationException("MainWindow is not initialized yet."); - } - - var template = DataTemplates.FirstOrDefault(t => t.Match(vm)); - if (template is null) - { - throw new InvalidOperationException($"No IDataTemplate found for {vm.GetType().Name}."); - } - - var view = template.Build(vm); - if (view is null) - { - throw new InvalidOperationException( - $"Template for {vm.GetType().Name} returned ."); - } - - var page = view as Page; - if (page is null) - { - // NavigationPage expects Page instances. Wrap any fallback control - // (e.g. ViewLocator error TextBlock) into a ContentPage so it can render. - page = new ContentPage { Content = view }; - } - - page.DataContext = vm; - - // Avoid stacking the same singleton page twice (e.g. SettingsPage). - var stack = window.NavRoot.NavigationStack; - if (stack.Count > 0 && ReferenceEquals(stack[stack.Count - 1], page)) - { - return; - } - - await window.NavRoot.PushAsync(page); - } - internal async Task GoBackAsync() { - await window.NavRoot.PopAsync(); + await Window!.NavRoot.PopAsync(); } } diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs new file mode 100644 index 00000000..9e0382de --- /dev/null +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -0,0 +1,77 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using PostIt.Services; +using PostIt.ViewModels; +using PostIt.Views; +using Yavsc.Api.Client; + +namespace PostIt.Helpers; + +public static class ServiceCollectionHelpers +{ + public static IServiceProvider BuildServices(this ServiceCollection services) + { + var settings = new Settings(); + settings.Load(); + + var tokenStore = new TokenStore(System.IO.Path.Combine( + System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), + "PostIt", "tokens.json")); + + var api = new YavscApiClient(settings, tokenStore); + var client = new BlogApiClient(api, settings.BlogsApiUrl); + var circleClient = new CircleApiClient(api, settings.BlogsApiUrl); + var blogAclClient = new BlogAclApiClient(api, settings.BlogsApiUrl); + var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl); + var contactService = new ContactService(); + var userDirectory = new UserDirectory(userSearchClient); + + // Vues + services.AddTransient(); + // SettingsPage is a singleton: there must be one and only one + // instance of the settings UI for the lifetime of the app. + // This guarantees that (a) the bindings always reflect the + // current in-memory Settings state, (b) the page already has + // its DataContext wired up at composition-root time (see + // below), and (c) PushPageAsync's anti-empilement guard sees + // the same instance across pushes, so a second Settings tap + // is a no-op rather than re-pushing the page. Transient would + // let the user accumulate stale SettingsPage instances on + // the navigation stack, each bound to a fresh + // SettingsViewModel and missing any in-flight edits. + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + // Dialogs (modal-light pages): the ViewLocator resolves + // them when a caller pushes a PostAclDialogViewModel or + // AddCircleMemberDialogViewModel via App.PushPageAsync. + // App.PushPageAsync overwrites the page's DataContext with + // the caller-built VM, so the parameterless ctor is enough + // here — the parametrised ctors stay for direct test wiring. + services.AddTransient(); + services.AddTransient(); + // ViewModels + services.AddSingleton(settings); + services.AddSingleton(api); + services.AddSingleton(client); + services.AddSingleton(circleClient); + services.AddSingleton(blogAclClient); + services.AddSingleton(userSearchClient); + services.AddSingleton(contactService); + services.AddSingleton(userDirectory); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Persistent session banner: one instance for the lifetime of + // the app so the same VM survives page navigation. + var sessionStatus = new SessionStatusViewModel { Api = api }; + sessionStatus.Refresh(); + services.AddSingleton(sessionStatus); + services.AddTransient(); + + return services.BuildServiceProvider(); + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs b/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs new file mode 100644 index 00000000..7fc81b3a --- /dev/null +++ b/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using PostIt.ViewModels; + +namespace PostIt.Helpers; + +public static class ViewModelBaseHelpers +{ + public static async Task PushPageAsync(this App app, ViewModelBase vm) + { + if (app.Window is null) + { + throw new InvalidOperationException("MainWindow is not initialized yet."); + } + + var template = app.DataTemplates.FirstOrDefault(t => t.Match(vm)); + if (template is null) + { + throw new InvalidOperationException($"No IDataTemplate found for {vm.GetType().Name}."); + } + + var view = template.Build(vm); + if (view is null) + { + throw new InvalidOperationException( + $"Template for {vm.GetType().Name} returned ."); + } + + var page = view as Page; + if (page is null) + { + // NavigationPage expects Page instances. Wrap any fallback control + // (e.g. ViewLocator error TextBlock) into a ContentPage so it can render. + page = new ContentPage { Content = view }; + } + + page.DataContext = vm; + + // Avoid stacking the same singleton page twice (e.g. SettingsPage). + var stack = app.Window.NavRoot.NavigationStack; + if (stack.Count > 0 && ReferenceEquals(stack[stack.Count - 1], page)) + { + return; + } + + await app.Window.NavRoot.PushAsync(page); + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs index 33a5bd30..9cee3ea8 100644 --- a/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs @@ -5,6 +5,7 @@ using Avalonia; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; +using PostIt.Helpers; using PostIt.Services; using Yavsc.Api.Client; using Yavsc.Api.Client.Dtos; diff --git a/src/PostIt/PostIt/ViewModels/MainViewModel.cs b/src/PostIt/PostIt/ViewModels/MainViewModel.cs index e56c2a73..1aa3eee0 100644 --- a/src/PostIt/PostIt/ViewModels/MainViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/MainViewModel.cs @@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; using Yavsc.Blogspot; using Yavsc.Api.Client; +using PostIt.Helpers; namespace PostIt.ViewModels; diff --git a/src/PostIt/PostIt/ViewModels/SessionStatusViewModel.cs b/src/PostIt/PostIt/ViewModels/SessionStatusViewModel.cs index a1ad48ce..f2496b85 100644 --- a/src/PostIt/PostIt/ViewModels/SessionStatusViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/SessionStatusViewModel.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; +using PostIt.Helpers; using PostIt.Services; namespace PostIt.ViewModels; @@ -139,7 +140,7 @@ public partial class SessionStatusViewModel : ViewModelBase internal async Task OpenSettings() { var app = (App)App.Current!; - await app.PushPageAsync(app.ServiceProvider.GetRequiredService()).ConfigureAwait(true); + await app.PushPageAsync(app.ServiceProvider!.GetRequiredService()).ConfigureAwait(true); } } diff --git a/src/PostIt/PostIt/ViewModels/Settings.cs b/src/PostIt/PostIt/ViewModels/Settings.cs index 40e8548b..2115a0cd 100644 --- a/src/PostIt/PostIt/ViewModels/Settings.cs +++ b/src/PostIt/PostIt/ViewModels/Settings.cs @@ -47,22 +47,7 @@ public partial class Settings : ViewModelBase /// private static Settings? s_current; - /// - /// Wire the canonical Settings instance to a DI container. Called - /// exactly once from App.axaml.cs after the singleton has - /// been registered. Subsequent calls are no-ops: the DI container - /// owns the instance lifetime and we don't want a stray - /// BindToServiceProvider in a test fixture to silently - /// rebind the production instance. - /// - public static void BindToServiceProvider(IServiceProvider services) - { - if (services is null) throw new ArgumentNullException(nameof(services)); - Interlocked.CompareExchange(ref s_current, - services.GetService() ?? throw new InvalidOperationException( - "Settings is not registered in the DI container."), - null); - } + [ObservableProperty] public partial AuthenticationSettings Authentication { get; set; } = new(); @@ -81,7 +66,7 @@ public partial class Settings : ViewModelBase /// setters above all funnel through here, and we flip /// in lock-step. Sub-property mutations /// (e.g. Authentication.Authority) are caught by the - /// subscription wired up in + /// subscription wired up in /// below. disables the flag during bulk /// hydration so the disk load itself does not count as a user /// edit.