diff --git a/.vscode/launch.json b/.vscode/launch.json
index 42be176a..efdaa6ea 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -4,21 +4,6 @@
// 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",
@@ -48,10 +33,12 @@
"name": "Test PostIt.Android launch (Xamarin.UITest)",
"type": "coreclr",
"request": "launch",
- "program": "${workspaceFolder}/src/PostIt/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests.dll",
+ "program": "${workspaceFolder}/src/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/PostIt.Tests",
+ "cwd": "${workspaceFolder}/src/PostIt.Tests",
"console": "integratedTerminal",
"stopAtEntry": false
}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index c900fa6a..e45a9921 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -1,29 +1,6 @@
{
"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 d6d69196..830c48f2 100644
--- a/Makefile
+++ b/Makefile
@@ -161,14 +161,13 @@ ADB_SERIAL ?= emulator-5554
ANDROID_HOME ?= /opt/android-sdk
POSTIT_RID ?= android-x64
EMU_HEADLESS ?= 0
-LOGCAT_LINES ?= 600
+LOGCAT_LINES ?= 200
LOGCAT_FOLLOW ?= 0
-LOGCAT_BOOT_WAIT ?= 20
+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)/$(ANDROID_PACKAGE_NAME)-Signed.apk
+POSTIT_APK := $(POSTIT_APK_DIR)/fr.pschneider.PostIt-Signed.apk
qemu-run:
@echo " Starting AVD $(AVD_NAME) on $(ADB_SERIAL)..."
@@ -215,25 +214,24 @@ 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)" -r
+ adb -s $(ADB_SERIAL) install -r "$(POSTIT_APK)"
qemu-uninstall:
- adb -s $(ADB_SERIAL) uninstall $(ANDROID_PACKAGE_NAME)
+ adb -s $(ADB_SERIAL) uninstall fr.pschneider.PostIt
# 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 $(ANDROID_PACKAGE_NAME)), not by tag,
+# Filtering is by PID (pidof fr.pschneider.PostIt), 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.
@@ -241,17 +239,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 $(ANDROID_PACKAGE_NAME) 2>/dev/null | tr -d '\r\n'); \
+ @PID=$$(adb -s $(ADB_SERIAL) shell pidof fr.pschneider.PostIt 2>/dev/null | tr -d '\r\n'); \
if [ -z "$$PID" ]; then \
- echo " $(ANDROID_PACKAGE_NAME) is not running on $(ADB_SERIAL)."; \
- echo " Start the app first (am start -n $(ANDROID_PACKAGE_NAME)/PostIt.Android.PostItMainActivity)"; \
+ echo " fr.pschneider.PostIt is not running on $(ADB_SERIAL)."; \
+ echo " Start the app first (am start -n fr.pschneider.PostIt/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 $(ANDROID_PACKAGE_NAME); \
+ adb -s $(ADB_SERIAL) logcat -v time --pid=$$PID fr.pschneider.PostIt:F; \
else \
- adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID $(ANDROID_PACKAGE_NAME); \
+ adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID fr.pschneider.PostIt:F; \
fi
# Clear logcat, launch PostIt.Android, then dump everything that was
@@ -265,21 +263,19 @@ LOGCAT_BOOT_WAIT ?= 15
qemu-logcat-boot:
@echo " Clearing logcat buffer..."
adb -s $(ADB_SERIAL) logcat -c
- @echo " Launching $(ANDROID_PACKAGE_NAME)..."
+ @echo " Launching fr.pschneider.PostIt..."
adb -s $(ADB_SERIAL) shell am start \
- -n $(ANDROID_PACKAGE_NAME)/PostIt.Android.PostItMainActivity
+ -n fr.pschneider.PostIt/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'); \
+ @PID=$$(adb -s $(ADB_SERIAL) shell pidof fr.pschneider.PostIt 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
new file mode 160000
index 00000000..0695a6c1
--- /dev/null
+++ b/external/dotnet-android-build-image
@@ -0,0 +1 @@
+Subproject commit 0695a6c1fea6508f1a88f7ad0ad9cb93733aa52d
diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props
index 69e8a896..59b6b600 100644
--- a/src/PostIt/Directory.Packages.props
+++ b/src/PostIt/Directory.Packages.props
@@ -1,37 +1,21 @@
-
+
+
-
-
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PostIt/PostIt.Android/Application.cs b/src/PostIt/PostIt.Android/Application.cs
index f5a7908d..fb6b08d3 100644
--- a/src/PostIt/PostIt.Android/Application.cs
+++ b/src/PostIt/PostIt.Android/Application.cs
@@ -2,12 +2,6 @@
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 62c29e10..1b413a9f 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;
+using Android.App;
using Android.Content.PM;
-using Avalonia;
+using Android.Content;
using Avalonia.Android;
+using AndroidX.Emoji2.Text;
namespace PostIt.Android;
@@ -12,10 +12,11 @@ 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
@@ -25,11 +26,13 @@ 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 a4af1eb2..20a27816 100644
--- a/src/PostIt/PostIt.Android/PostIt.Android.csproj
+++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj
@@ -2,32 +2,35 @@
Exenet10.0-android
- 23
+
+ android-arm64;android-x64
+ 23.0.0enablefr.pschneider.PostIt11.0apk
- false
- SdkOnly
- partial
+ 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
-
Resources\drawable\Icon.png
-
-
-
-
+
+
+
diff --git a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml
index 91b61d05..2472d06d 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 3e7344a2..a84b85a3 100644
--- a/src/PostIt/PostIt.Tests/MainPageButtonsTests.cs
+++ b/src/PostIt/PostIt.Tests/MainPageButtonsTests.cs
@@ -72,7 +72,7 @@ public class MainPageButtonsTests
{ }
}
- private static MainViewModel MakeViewModel(BlogPostDto? selectedPost = null)
+ private static MainPageViewModel 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 MainViewModel(blog, services: services.BuildServiceProvider());
+ var vm = new MainPageViewModel(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(MainViewModel vm)
+ private static (MainWindow window, MainPage page) MountMainPage(MainPageViewModel 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 5baae658..1eaf3f17 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 MainViewModel(blog);
+ var viewModel = new MainPageViewModel(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 9bc28888..2708ba8c 100644
--- a/src/PostIt/PostIt.Tests/PostAclDialogTests.cs
+++ b/src/PostIt/PostIt.Tests/PostAclDialogTests.cs
@@ -4,7 +4,6 @@ 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 433f36c3..d8517ba0 100644
--- a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj
+++ b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj
@@ -16,9 +16,7 @@
-
-
@@ -29,5 +27,6 @@
+
diff --git a/src/PostIt/PostIt.Tests/PostItViewModelTests.cs b/src/PostIt/PostIt.Tests/PostItViewModelTests.cs
index d2c5d78e..780f674c 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 MainViewModel(blog);
+ var viewModel = new MainPageViewModel(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 362f398e..92497b74 100644
--- a/src/PostIt/PostIt/App.axaml
+++ b/src/PostIt/PostIt/App.axaml
@@ -1,16 +1,13 @@
-
+ x:Class="PostIt.App">
-
-
-
+
-
+
-
+
+
diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs
index 10a0c8a5..d2399873 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,70 +27,198 @@ public partial class App : Application
/// DataValidationErrors.SetErrors.
///
public IServiceProvider? ServiceProvider { get; private set; }
-
- public MainWindow? Window { get; private set; }
+ private MainWindow window;
+ public App()
+ {
+ }
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 = new ServiceCollection().BuildServices();
+ 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));
+
+ // 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();
- ApplyDarkMode(settings);
+ 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);
}
- else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
+ else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
{
- singleViewFactoryApplicationLifetime.MainViewFactory =
- () =>
- {
- Window = CreateMainWindow();
- ApplyDarkMode(settings);
- return Window;
- };
+ singleView.MainView = new MainWindow
+ {
+ DataContext = ServiceProvider.GetRequiredService()
+ };
}
- else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
- {
- singleViewPlatform.MainView = CreateMainWindow();
- ApplyDarkMode(settings);
- }
- base.OnFrameworkInitializationCompleted();
}
-
-
- private MainWindow CreateMainWindow()
+ ///
+ /// 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)
{
- 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 settings = new Settings();
+ settings.Load();
- sessionStatus.LoginSucceeded += () =>
- {
- PushMainPageAsync().Wait();
- };
+ var tokenStore = new TokenStore(System.IO.Path.Combine(
+ System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
+ "PostIt", "tokens.json"));
- var homeVm = ServiceProvider!.GetRequiredService();
+ 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);
- this.PushPageAsync(homeVm).Wait();
- Window.SessionBanner.DataContext = sessionStatus;
- return Window;
+
+ // 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);
}
///
@@ -101,7 +229,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)
@@ -138,11 +266,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 async Task PushMainPageAsync()
+ public static Task PushMainPageAsync()
{
- var app = (App)Current!;
- var mainVm = app.ServiceProvider!.GetRequiredService();
- await app.PushPageAsync(mainVm);
+ var app = (App)Current;
+ var mainVm = app.ServiceProvider.GetRequiredService();
+ return app.PushPageAsync(mainVm);
}
private bool TryHandOffCustomSchemeUrl()
@@ -177,8 +305,53 @@ 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
deleted file mode 100644
index f7da8bb5..00000000
Binary files a/src/PostIt/PostIt/Assets/avalonia-logo.ico and /dev/null differ
diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs
deleted file mode 100644
index 9e0382de..00000000
--- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-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
deleted file mode 100644
index 7fc81b3a..00000000
--- a/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-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 70bd72ca..1b48c21f 100644
--- a/src/PostIt/PostIt/PostIt.csproj
+++ b/src/PostIt/PostIt/PostIt.csproj
@@ -1,14 +1,32 @@
-
+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
@@ -20,21 +38,6 @@
-
-
-
-
- None
- All
-
-
-
-
-
-
-
-
-
-
+
diff --git a/src/PostIt/PostIt/Settings/AuthenticationSettings.cs b/src/PostIt/PostIt/Settings/AuthenticationSettings.cs
index 99358cfb..ad71063b 100644
--- a/src/PostIt/PostIt/Settings/AuthenticationSettings.cs
+++ b/src/PostIt/PostIt/Settings/AuthenticationSettings.cs
@@ -21,9 +21,6 @@ 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 03ab98a1..fd92c802 100644
--- a/src/PostIt/PostIt/ViewLocator.cs
+++ b/src/PostIt/PostIt/ViewLocator.cs
@@ -1,5 +1,4 @@
using System;
-using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Microsoft.Extensions.DependencyInjection;
@@ -11,19 +10,17 @@ 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
{
@@ -35,12 +32,11 @@ public class ViewLocator : IDataTemplate
}
}
-
private Control BuildCore(object? data)
{
return data switch
{
- MainViewModel => _services.GetRequiredService(),
+ MainPageViewModel => _services.GetRequiredService(),
Settings => _services.GetRequiredService(),
HomePageViewModel => _services.GetRequiredService(),
SignaturePageViewModel => _services.GetRequiredService(),
@@ -52,5 +48,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 9cee3ea8..33a5bd30 100644
--- a/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs
@@ -5,7 +5,6 @@ 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/MainPageViewModel.cs
similarity index 96%
rename from src/PostIt/PostIt/ViewModels/MainViewModel.cs
rename to src/PostIt/PostIt/ViewModels/MainPageViewModel.cs
index 1aa3eee0..d374c9e4 100644
--- a/src/PostIt/PostIt/ViewModels/MainViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs
@@ -8,11 +8,10 @@ using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;
using Yavsc.Blogspot;
using Yavsc.Api.Client;
-using PostIt.Helpers;
namespace PostIt.ViewModels;
-public partial class MainViewModel : ViewModelBase
+public partial class MainPageViewModel : ViewModelBase
{
/// Window/tab title. Cosmetic — bound by
/// MainPage.axaml if at all. Not the post title.
@@ -121,10 +120,10 @@ public partial class MainViewModel : ViewModelBase
}
- public MainViewModel()
+ public MainPageViewModel()
{
+ Init(null);
SettingsModel = new Settings();
- Init(SettingsModel);
BlogClient = null;
}
@@ -136,11 +135,6 @@ public partial class MainViewModel : 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
@@ -151,7 +145,11 @@ public partial class MainViewModel : 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
@@ -171,11 +169,12 @@ public partial class MainViewModel : ViewModelBase
/// . Production code uses the
/// (Settings, BlogApiClient) overload below.
///
- public MainViewModel(BlogApiClient blogClient, Settings? settings = null, IServiceProvider? services = null)
+ public MainPageViewModel(BlogApiClient blogClient, Settings? settings = null, IServiceProvider? services = null)
{
SettingsModel = new Settings();
BlogClient = blogClient ?? throw new ArgumentNullException(nameof(blogClient)); ;
Services = services;
+
Init(settings);
}
@@ -211,7 +210,7 @@ public partial class MainViewModel : 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))
{
@@ -260,7 +259,7 @@ public partial class MainViewModel : 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;
@@ -279,7 +278,7 @@ public partial class MainViewModel : ViewModelBase
DateCreated = SelectedPost.DateCreated,
DateModified = DateTime.UtcNow,
};
- await BlogClient!.UpdatePostAsync(SelectedPost.Id, update);
+ await BlogClient.UpdatePostAsync(SelectedPost.Id, update);
StatusMessage = $"Saved post {SelectedPost.Id}.";
}
@@ -298,7 +297,7 @@ public partial class MainViewModel : ViewModelBase
await ExecuteAsync(async () =>
{
- await BlogClient!.DeletePostAsync(SelectedPost.Id);
+ await BlogClient.DeletePostAsync(SelectedPost.Id);
StatusMessage = $"Deleted post {SelectedPost.Id}.";
SelectedPost = null;
await RefreshPostsAsync();
@@ -332,7 +331,7 @@ public partial class MainViewModel : 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
@@ -373,7 +372,7 @@ public partial class MainViewModel : 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 f2496b85..a1ad48ce 100644
--- a/src/PostIt/PostIt/ViewModels/SessionStatusViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/SessionStatusViewModel.cs
@@ -3,7 +3,6 @@ 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;
@@ -140,7 +139,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 2115a0cd..890ca15c 100644
--- a/src/PostIt/PostIt/ViewModels/Settings.cs
+++ b/src/PostIt/PostIt/ViewModels/Settings.cs
@@ -47,7 +47,40 @@ 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();
@@ -66,7 +99,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.
@@ -317,14 +350,15 @@ public partial class Settings : ViewModelBase
var settings = JsonSerializer.Deserialize(json);
if (settings is null)
{
- UseDefaultSettings();
+ Console.Error.WriteLine($"🩎 Settings payload is invalid (source: {source}).");
+ return;
}
// 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.
- else lock (_mutationGate)
+ lock (_mutationGate)
{
this.Authentication = settings.Authentication;
this.DarkMode = settings.DarkMode;
@@ -337,11 +371,6 @@ 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;
}
}
@@ -371,18 +400,6 @@ 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 4ca69eea..5ce17aba 100644
--- a/src/PostIt/PostIt/ViewModels/ViewModelBase.cs
+++ b/src/PostIt/PostIt/ViewModels/ViewModelBase.cs
@@ -4,7 +4,8 @@ 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 7f1f1196..7eac39ff 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:MainViewModel"
+ x:DataType="vm:MainPageViewModel"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
-
+
+ >
-
+
+
+
diff --git a/src/Yavsc.Abstract/Yavsc.Abstract.csproj b/src/Yavsc.Abstract/Yavsc.Abstract.csproj
index f76e42fd..7a4f83ae 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 00e6e2db..ade7ca17 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 d9a814f6..5672ab7a 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 83c0dc37..fc7883ce 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 63b3d977..5c175cb5 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 d332d250..88842cb4 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 0403c7eb..ccdb7f24 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 ac9380a4..4eaf6a83 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 6ccd44c0..52effc72 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