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 @@
apkfalse
+
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 00000000..f7da8bb5
Binary files /dev/null and b/src/PostIt/PostIt/Assets/avalonia-logo.ico differ
diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj
index 1b48c21f..70bd72ca 100644
--- a/src/PostIt/PostIt/PostIt.csproj
+++ b/src/PostIt/PostIt/PostIt.csproj
@@ -1,32 +1,14 @@
-
+net10.0enablelatest
- 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" >
-
-
+
-