diff --git a/.vscode/launch.json b/.vscode/launch.json
index efdaa6ea..42be176a 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/net10.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..d6d69196 100644
--- a/Makefile
+++ b/Makefile
@@ -161,13 +161,14 @@ 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
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)..."
@@ -214,24 +215,25 @@ 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
+ 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.
@@ -239,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); \
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); \
fi
# Clear logcat, launch PostIt.Android, then dump everything that was
@@ -263,19 +265,21 @@ 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)"; \
+ 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/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 59b6b600..69e8a896 100644
--- a/src/PostIt/Directory.Packages.props
+++ b/src/PostIt/Directory.Packages.props
@@ -1,21 +1,37 @@
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 1b413a9f..62c29e10 100644
--- a/src/PostIt/PostIt.Android/MainActivity.cs
+++ b/src/PostIt/PostIt.Android/MainActivity.cs
@@ -1,8 +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;
namespace PostIt.Android;
@@ -12,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
@@ -26,13 +25,11 @@ public class MainActivity : AvaloniaMainActivity
protected override void OnCreate(global::Android.OS.Bundle? savedInstanceState)
{
- EmojiCompat.Init(this);
base.OnCreate(savedInstanceState);
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 20a27816..a4af1eb2 100644
--- a/src/PostIt/PostIt.Android/PostIt.Android.csproj
+++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj
@@ -2,35 +2,32 @@
Exenet10.0-android
-
- android-arm64;android-x64
- 23.0.0
+ 23enablefr.pschneider.PostIt11.0apk
- false
- android-arm;android-arm64;android-x86;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
+ false
+ SdkOnly
+ partial
+
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 @@
-
+
-
+
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/PostIt/PostIt.Tests/MainPageButtonsTests.cs b/src/PostIt/PostIt.Tests/MainPageButtonsTests.cs
index a84b85a3..3e7344a2 100644
--- a/src/PostIt/PostIt.Tests/MainPageButtonsTests.cs
+++ b/src/PostIt/PostIt.Tests/MainPageButtonsTests.cs
@@ -72,7 +72,7 @@ public class MainPageButtonsTests
{ }
}
- private static MainPageViewModel MakeViewModel(BlogPostDto? selectedPost = null)
+ private static MainViewModel MakeViewModel(BlogPostDto? selectedPost = null)
{
var api = new ThrowingApi();
var blog = new BlogApiClient(api, "http://localhost/");
@@ -93,7 +93,7 @@ public class MainPageButtonsTests
services.AddTransient();
services.AddTransient();
services.AddTransient();
- var vm = new MainPageViewModel(blog, services: services.BuildServiceProvider());
+ var vm = new MainViewModel(blog, services: services.BuildServiceProvider());
if (selectedPost is not null) vm.SelectedPost = selectedPost;
return vm;
}
@@ -109,7 +109,7 @@ public class MainPageButtonsTests
/// realised and KeyPressQwerty has a real
/// to dispatch against.
///
- private static (MainWindow window, MainPage page) MountMainPage(MainPageViewModel vm)
+ private static (MainWindow window, MainPage page) MountMainPage(MainViewModel vm)
{
var window = new MainWindow();
var page = new MainPage { DataContext = vm };
diff --git a/src/PostIt/PostIt.Tests/MainPageSaveTests.cs b/src/PostIt/PostIt.Tests/MainPageSaveTests.cs
index 1eaf3f17..5baae658 100644
--- a/src/PostIt/PostIt.Tests/MainPageSaveTests.cs
+++ b/src/PostIt/PostIt.Tests/MainPageSaveTests.cs
@@ -40,7 +40,7 @@ public class MainPageSaveTests
var recorder = new CallRecorder();
var api = new RecordingYavscApiClient(recorder);
var blog = new BlogApiClient(api, "http://localhost/");
- var viewModel = new MainPageViewModel(blog);
+ var viewModel = new MainViewModel(blog);
var page = new MainPage { DataContext = viewModel };
// MainPage is a ContentPage (a Page, not a Control), so it
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 d8517ba0..433f36c3 100644
--- a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj
+++ b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj
@@ -16,7 +16,9 @@
+
+
@@ -27,6 +29,5 @@
-
diff --git a/src/PostIt/PostIt.Tests/PostItViewModelTests.cs b/src/PostIt/PostIt.Tests/PostItViewModelTests.cs
index 780f674c..d2c5d78e 100644
--- a/src/PostIt/PostIt.Tests/PostItViewModelTests.cs
+++ b/src/PostIt/PostIt.Tests/PostItViewModelTests.cs
@@ -16,7 +16,7 @@ public class PostItViewModelTests
// throws on any call (we never call the API in this test).
var fakeApi = new ThrowingYavscApiClient();
var blog = new BlogApiClient(fakeApi, "http://localhost/");
- var viewModel = new MainPageViewModel(blog);
+ var viewModel = new MainViewModel(blog);
viewModel.Posts.Add(new BlogPostDto { Id = 1, Title = "First post", Article = "Hello world", AuthorId = "alice" });
viewModel.Posts.Add(new BlogPostDto { Id = 2, Title = "Second post", Article = "Nothing here", AuthorId = "bob" });
diff --git a/src/PostIt/PostIt/App.axaml b/src/PostIt/PostIt/App.axaml
index 92497b74..362f398e 100644
--- a/src/PostIt/PostIt/App.axaml
+++ b/src/PostIt/PostIt/App.axaml
@@ -1,13 +1,16 @@
+ x:Class="PostIt.App"
+ RequestedThemeVariant="Default">
+
-
+
+
+
-
+
-
-
+
diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs
index d2399873..10a0c8a5 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 PostIt.Helpers;
namespace PostIt;
@@ -27,198 +27,70 @@ public partial class App : Application
/// DataValidationErrors.SetErrors.
///
public IServiceProvider? ServiceProvider { get; private set; }
- private MainWindow window;
- public App()
- {
- }
+
+ public MainWindow? Window { get; private set; }
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
+#if DEBUG
+ this.AttachDeveloperTools();
+#endif
}
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);
+ this.ServiceProvider = new ServiceCollection().BuildServices();
var settings = ServiceProvider.GetRequiredService();
- var sessionStatus = ServiceProvider.GetRequiredService();
- var api = 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)
{
- 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();
+ ApplyDarkMode(settings);
}
- else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
+ else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
{
- singleView.MainView = new MainWindow
- {
- DataContext = ServiceProvider.GetRequiredService()
- };
+ singleViewFactoryApplicationLifetime.MainViewFactory =
+ () =>
+ {
+ Window = CreateMainWindow();
+ ApplyDarkMode(settings);
+ return Window;
+ };
}
+ else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
+ {
+ singleViewPlatform.MainView = CreateMainWindow();
+ ApplyDarkMode(settings);
+ }
+ base.OnFrameworkInitializationCompleted();
}
- ///
- /// 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)
+
+
+ private MainWindow CreateMainWindow()
{
- var settings = new Settings();
- settings.Load();
+ Window = new MainWindow();
+ var api = ServiceProvider!.GetRequiredService();
+ Window.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api);
+ var sessionStatus = ServiceProvider!.GetRequiredService();
+ sessionStatus.LogoutCompleted += () =>
+ {
+ Window.NavRoot.PopToRootAsync();
+ };
- var tokenStore = new TokenStore(System.IO.Path.Combine(
- System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
- "PostIt", "tokens.json"));
+ sessionStatus.LoginSucceeded += () =>
+ {
+ PushMainPageAsync().Wait();
+ };
- 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);
+ var homeVm = ServiceProvider!.GetRequiredService();
-
- // 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(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();
- }
-
- ///
- /// 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)
- {
- ServiceProvider = sp;
- Settings.BindToServiceProvider(sp);
+ this.PushPageAsync(homeVm).Wait();
+ Window.SessionBanner.DataContext = sessionStatus;
+ return Window;
}
///
@@ -229,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)
@@ -266,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);
+ var app = (App)Current!;
+ var mainVm = app.ServiceProvider!.GetRequiredService();
+ await app.PushPageAsync(mainVm);
}
private bool TryHandOffCustomSchemeUrl()
@@ -305,53 +177,8 @@ public partial class App : Application
return true;
}
- internal void PushPage(ViewModelBase vm)
- {
- _ = PushPageAsync(vm);
- }
-
- internal 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 Task.CompletedTask;
- }
-
- return window.NavRoot.PushAsync(page);
- }
-
internal async Task GoBackAsync()
{
- await window.NavRoot.PopAsync();
+ await Window!.NavRoot.PopAsync();
}
}
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/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/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/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/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/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/MainPageViewModel.cs b/src/PostIt/PostIt/ViewModels/MainViewModel.cs
similarity index 96%
rename from src/PostIt/PostIt/ViewModels/MainPageViewModel.cs
rename to src/PostIt/PostIt/ViewModels/MainViewModel.cs
index d374c9e4..1aa3eee0 100644
--- a/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/MainViewModel.cs
@@ -8,10 +8,11 @@ using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;
using Yavsc.Blogspot;
using Yavsc.Api.Client;
+using PostIt.Helpers;
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,10 +121,10 @@ public partial class MainPageViewModel : ViewModelBase
}
- public MainPageViewModel()
+ public MainViewModel()
{
- Init(null);
SettingsModel = new Settings();
+ Init(SettingsModel);
BlogClient = null;
}
@@ -135,6 +136,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 +151,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
@@ -169,12 +171,11 @@ 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)); ;
Services = services;
-
Init(settings);
}
@@ -210,7 +211,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 +260,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 +279,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 +298,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 +332,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 +373,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/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 890ca15c..2115a0cd 100644
--- a/src/PostIt/PostIt/ViewModels/Settings.cs
+++ b/src/PostIt/PostIt/ViewModels/Settings.cs
@@ -47,40 +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);
- }
-
- ///
- /// 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();
@@ -99,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.
@@ -350,15 +317,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 +337,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 +371,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/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" >
-
-
+
-
diff --git a/src/Yavsc.Abstract/Yavsc.Abstract.csproj b/src/Yavsc.Abstract/Yavsc.Abstract.csproj
index 7a4f83ae..f76e42fd 100644
--- a/src/Yavsc.Abstract/Yavsc.Abstract.csproj
+++ b/src/Yavsc.Abstract/Yavsc.Abstract.csproj
@@ -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..00e6e2db 100644
--- a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj
+++ b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj
@@ -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..d9a814f6 100644
--- a/src/Yavsc.Api/Yavsc.Api.csproj
+++ b/src/Yavsc.Api/Yavsc.Api.csproj
@@ -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..83c0dc37 100644
--- a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj
+++ b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj
@@ -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..63b3d977 100644
--- a/src/Yavsc.Blogs/Yavsc.Blogs.csproj
+++ b/src/Yavsc.Blogs/Yavsc.Blogs.csproj
@@ -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..d332d250 100644
--- a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj
+++ b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj
@@ -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..0403c7eb 100644
--- a/src/Yavsc.Org/Yavsc.Org.csproj
+++ b/src/Yavsc.Org/Yavsc.Org.csproj
@@ -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..ac9380a4 100644
--- a/src/Yavsc.Server/Yavsc.Server.csproj
+++ b/src/Yavsc.Server/Yavsc.Server.csproj
@@ -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..6ccd44c0 100644
--- a/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj
+++ b/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj
@@ -23,4 +23,4 @@
-
\ No newline at end of file
+