test are green !?!
This commit is contained in:
parent
cbe307033b
commit
d0186d41b0
25 changed files with 145 additions and 230 deletions
23
Makefile
23
Makefile
|
|
@ -165,9 +165,10 @@ LOGCAT_LINES ?= 200
|
|||
LOGCAT_FOLLOW ?= 0
|
||||
LOGCAT_BOOT_WAIT ?= 30
|
||||
|
||||
ANDROID_PACKAGE_NAME = fr.pschneider.PostIt
|
||||
POSTIT_ANDROID_CSPROJ := src/PostIt/PostIt.Android/PostIt.Android.csproj
|
||||
POSTIT_APK_DIR := src/PostIt/PostIt.Android/bin/$(CONFIG)/net10.0-android/$(POSTIT_RID)
|
||||
POSTIT_APK := $(POSTIT_APK_DIR)/fr.pschneider.PostIt-Signed.apk
|
||||
POSTIT_APK := $(POSTIT_APK_DIR)/$(ANDROID_PACKAGE_NAME)-Signed.apk
|
||||
|
||||
qemu-run:
|
||||
@echo " Starting AVD $(AVD_NAME) on $(ADB_SERIAL)..."
|
||||
|
|
@ -227,12 +228,12 @@ qemu-install: qemu-build
|
|||
adb -s $(ADB_SERIAL) install -r "$(POSTIT_APK)" -r
|
||||
|
||||
qemu-uninstall:
|
||||
adb -s $(ADB_SERIAL) uninstall fr.pschneider.PostIt
|
||||
adb -s $(ADB_SERIAL) uninstall $(ANDROID_PACKAGE_NAME)
|
||||
|
||||
# Dump recent logcat output for the running PostIt.Android process.
|
||||
# By default, prints the last $(LOGCAT_LINES) lines (one-shot, with
|
||||
# `-d`). Set LOGCAT_FOLLOW=1 to follow the stream live instead.
|
||||
# Filtering is by PID (pidof fr.pschneider.PostIt), not by tag,
|
||||
# Filtering is by PID (pidof $(ANDROID_PACKAGE_NAME)), not by tag,
|
||||
# because Mono/Xamarin can emit logs under several tags
|
||||
# (mono, PostIt.Android, Avalonia.Android) and tag-based filtering
|
||||
# would miss the ones not matching. PID-based filtering is exact.
|
||||
|
|
@ -240,17 +241,17 @@ qemu-uninstall:
|
|||
# silently with no output; that is the expected behaviour for
|
||||
# "no logs yet".
|
||||
qemu-logcat:
|
||||
@PID=$$(adb -s $(ADB_SERIAL) shell pidof fr.pschneider.PostIt 2>/dev/null | tr -d '\r\n'); \
|
||||
@PID=$$(adb -s $(ADB_SERIAL) shell pidof $(ANDROID_PACKAGE_NAME) 2>/dev/null | tr -d '\r\n'); \
|
||||
if [ -z "$$PID" ]; then \
|
||||
echo " fr.pschneider.PostIt is not running on $(ADB_SERIAL)."; \
|
||||
echo " Start the app first (am start -n fr.pschneider.PostIt/PostIt.Android.PostItMainActivity)"; \
|
||||
echo " $(ANDROID_PACKAGE_NAME) is not running on $(ADB_SERIAL)."; \
|
||||
echo " Start the app first (am start -n $(ANDROID_PACKAGE_NAME)/PostIt.Android.PostItMainActivity)"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo " Following PID $$PID (LOGCAT_FOLLOW=$(LOGCAT_FOLLOW), LOGCAT_LINES=$(LOGCAT_LINES))"; \
|
||||
if [ "$(LOGCAT_FOLLOW)" = "1" ]; then \
|
||||
adb -s $(ADB_SERIAL) logcat -v time --pid=$$PID fr.pschneider.PostIt:F; \
|
||||
adb -s $(ADB_SERIAL) logcat -v time --pid=$$PID $(ANDROID_PACKAGE_NAME):F; \
|
||||
else \
|
||||
adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID fr.pschneider.PostIt:F; \
|
||||
adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID $(ANDROID_PACKAGE_NAME):F; \
|
||||
fi
|
||||
|
||||
# Clear logcat, launch PostIt.Android, then dump everything that was
|
||||
|
|
@ -264,13 +265,13 @@ LOGCAT_BOOT_WAIT ?= 15
|
|||
qemu-logcat-boot:
|
||||
@echo " Clearing logcat buffer..."
|
||||
adb -s $(ADB_SERIAL) logcat -c
|
||||
@echo " Launching fr.pschneider.PostIt..."
|
||||
@echo " Launching $(ANDROID_PACKAGE_NAME)..."
|
||||
adb -s $(ADB_SERIAL) shell am start \
|
||||
-n fr.pschneider.PostIt/PostIt.Android.PostItMainActivity
|
||||
-n $(ANDROID_PACKAGE_NAME)/PostIt.Android.PostItMainActivity
|
||||
@echo " Waiting $(LOGCAT_BOOT_WAIT)s for the app to start rendering..."
|
||||
@sleep $(LOGCAT_BOOT_WAIT)
|
||||
@echo " Dumping logcat (PostIt PID + system buffer):"
|
||||
@PID=$$(adb -s $(ADB_SERIAL) shell pidof fr.pschneider.PostIt 2>/dev/null | tr -d '\r\n'); \
|
||||
@PID=$$(adb -s $(ADB_SERIAL) shell pidof $(ANDROID_PACKAGE_NAME) 2>/dev/null | tr -d '\r\n'); \
|
||||
if [ -n "$$PID" ]; then \
|
||||
echo " (PID $$PID at dump time)"; \
|
||||
adb -s $(ADB_SERIAL) logcat -d -v time --pid=$$PID; \
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
<Project>
|
||||
<!-- Pull in shared package versions from the repository root. -->
|
||||
<Import
|
||||
Project="$([MSBuild]::GetPathOfFileAbove('Directory.Packages.props', '$(MSBuildThisFileDirectory)../'))" />
|
||||
<!-- PostIt-product-specific versions -->
|
||||
<ItemGroup> <!-- Avalonia packages -->
|
||||
<!-- https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management -->
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Avalonia packages -->
|
||||
<!-- Important: keep version in sync! -->
|
||||
<PackageVersion Include="Avalonia" Version="12.1.1" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.1" />
|
||||
|
|
@ -11,28 +15,23 @@
|
|||
<PackageVersion Include="Avalonia.Desktop" Version="12.1.1" />
|
||||
<PackageVersion Include="Avalonia.Browser" Version="12.1.1" />
|
||||
<PackageVersion Include="Avalonia.Android" Version="12.1.1" />
|
||||
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
|
||||
|
||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
|
||||
<PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.0.1.15" />
|
||||
|
||||
<PackageVersion Include="Xamarin.AndroidX.Core.Core.KtX" Version="1.19.0.1" />
|
||||
<PackageVersion Include="Xamarin.AndroidX.Compose.Runtime.Annotation.Jvm" Version="1.11.4" />
|
||||
<PackageVersion Include="Xamarin.AndroidX.Core" Version="1.19.0.1" />
|
||||
<PackageVersion Include="Xamarin.AndroidX.AppCompat" Version="1.7.1.4" />
|
||||
<PackageVersion Include="Xamarin.AndroidX.Browser" Version="1.10.0.1" />
|
||||
|
||||
<PackageVersion Include="Xamarin.UITest.Core" Version="4.4.2" />
|
||||
<PackageVersion Include="Xamarin.UITest.AndroidX" Version="4.4.2" />
|
||||
<PackageVersion Include="Xamarin.UITest.Queries" Version="4.4.2" />
|
||||
<PackageVersion Include="Xamarin.UITest" Version="4.4.2" />
|
||||
|
||||
<PackageVersion Include="Avalonia.Headless" Version="12.1.1" />
|
||||
<PackageVersion Include="Avalonia.Headless.Xunit" Version="12.1.1" />
|
||||
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||
<PackageVersion Include="Material.Avalonia" Version="3.19.0" />
|
||||
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
|
||||
|
||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
|
||||
<PackageVersion Include="Xamarin.AndroidX.Browser" Version="1.10.0.1" />
|
||||
<PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.0.1.15" />
|
||||
<PackageVersion Include="Xamarin.AndroidX.Lifecycle.Runtime" Version="2.11.0.1" />
|
||||
<PackageVersion Include="Xamarin.AndroidX.Lifecycle.Common" Version="2.11.0.1" />
|
||||
<PackageVersion Include="Xamarin.UITest" Version="4.4.2" />
|
||||
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Maui.Essentials" Version="10.0.100" />
|
||||
|
||||
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
using Android.App;
|
||||
using Android.Content.PM;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Avalonia;
|
||||
using Avalonia.Android;
|
||||
using AndroidX.Emoji2.Text;
|
||||
using AndroidX.Core.Provider;
|
||||
using Android;
|
||||
using Android.Graphics;
|
||||
|
||||
namespace PostIt.Android;
|
||||
|
||||
|
|
@ -15,7 +12,6 @@ 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
|
||||
{
|
||||
|
|
@ -33,7 +29,6 @@ public class MainActivity : AvaloniaMainActivity
|
|||
PlatformBootstrap.EnsureInitialized();
|
||||
Current = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receives the deep-link Intent fired by the system browser after the
|
||||
/// user completes the OIDC login on https://yavsc.pschneider.fr. The
|
||||
|
|
|
|||
|
|
@ -10,19 +10,22 @@
|
|||
<AndroidPackageFormat>apk</AndroidPackageFormat>
|
||||
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Icon.png">
|
||||
<Link>Resources\drawable\Icon.png</Link>
|
||||
</AndroidResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PostIt\PostIt.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Android" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" />
|
||||
<PackageReference Include="Avalonia.Browser" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Browser" />
|
||||
<PackageReference Include="IdentityModel.OidcClient" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Runtime" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Common" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PostIt\PostIt.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<application android:label="PostIt" android:icon="@drawable/Icon">
|
||||
|
|
|
|||
|
|
@ -8,5 +8,14 @@
|
|||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowBackground">@null</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowSplashScreenBackground">@color/splash_background</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/avalonia_anim</item>
|
||||
<item name="android:windowSplashScreenAnimationDuration">1000</item>
|
||||
<item name="postSplashScreenTheme">@style/MyTheme.Main</item>
|
||||
|
||||
</style>
|
||||
<style name="MyTheme.Main"
|
||||
parent ="MyTheme.NoActionBar">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
</style>
|
||||
|
||||
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:windowBackground">@drawable/splash_screen</item>
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowBackground">@drawable/splash_screen</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -40,9 +40,9 @@ public sealed class AndroidSystemBrowser : IBrowser
|
|||
var callbackTask = MainActivity.AndroidOidcCallbackSink.AwaitNextCallbackAsync();
|
||||
|
||||
var tabsIntent = new CustomTabsIntent.Builder()
|
||||
.SetShowTitle(true)
|
||||
.SetShowTitle(true)!
|
||||
.Build();
|
||||
tabsIntent.LaunchUrl(_activity, uri);
|
||||
tabsIntent!.LaunchUrl(_activity, uri);
|
||||
|
||||
string responseUri;
|
||||
try
|
||||
|
|
|
|||
|
|
@ -15,7 +15,4 @@
|
|||
<ItemGroup>
|
||||
<ProjectReference Include="..\PostIt\PostIt.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GitVersion.MsBuild" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,4 @@
|
|||
<ItemGroup>
|
||||
<ProjectReference Include="..\PostIt\PostIt.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GitVersion.MsBuild" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -68,9 +68,6 @@ sealed class Program
|
|||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
#if DEBUG
|
||||
.WithDeveloperTools()
|
||||
#endif
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SignaturePage>();
|
||||
services.AddTransient<CirclesPage>();
|
||||
services.AddTransient<PostAclDialog>();
|
||||
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 <c>KeyPressQwerty</c> has a real
|
||||
/// <see cref="TopLevel"/> to dispatch against.
|
||||
/// </summary>
|
||||
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 };
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:PostIt"
|
||||
x:Class="PostIt.App">
|
||||
x:Class="PostIt.App"
|
||||
RequestedThemeVariant="Default">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
|
||||
<!-- ViewLocator is registered in App.axaml.cs with the real DI container. -->
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator />
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
|
||||
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Styling;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PostIt.Services;
|
||||
using Yavsc.Api.Client;
|
||||
using PostIt.ViewModels;
|
||||
using PostIt.Views;
|
||||
using Yavsc.Api.Client;
|
||||
|
||||
namespace PostIt;
|
||||
|
||||
|
|
@ -28,9 +28,7 @@ public partial class App : Application
|
|||
/// </summary>
|
||||
public IServiceProvider? ServiceProvider { get; private set; }
|
||||
|
||||
public App()
|
||||
{
|
||||
}
|
||||
MainWindow window;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -42,101 +40,31 @@ public partial class App : Application
|
|||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
// Belt-and-braces 2nd-instance guard. The primary check now
|
||||
// lives in PostIt.Desktop.Program.Main and exits before
|
||||
// Avalonia boots — preventing a flash of the MainWindow on
|
||||
// every postit://callback launch. This block is kept for any
|
||||
// entry point that bypasses Program.Main (PostIt.Browser,
|
||||
// PostIt.Android's process lifecycle, ad-hoc tests that build
|
||||
// App directly) and as defence-in-depth in case the Desktop
|
||||
// build is ever reconfigured to skip the early check.
|
||||
if (TryHandOffCustomSchemeUrl()) return;
|
||||
|
||||
this.ServiceProvider = BuildServices(new ServiceCollection());
|
||||
AttachServiceProvider(ServiceProvider);
|
||||
var settings = ServiceProvider.GetRequiredService<Settings>();
|
||||
|
||||
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<SettingsPage>().DataContext = settings;
|
||||
|
||||
// Settings.DarkMode was previously a dead field: it round-
|
||||
// tripped through the settings file and the SettingsPage
|
||||
// CheckBox, but no consumer ever read it. Wire it here to
|
||||
// Application.RequestedThemeVariant so the toggle takes
|
||||
// effect immediately, and seed the initial theme from the
|
||||
// value Load() just populated (so a dark-mode user lands on
|
||||
// a dark window on first launch, not on a default-light
|
||||
// window that flips after the user touches the toggle).
|
||||
ApplyDarkMode(settings);
|
||||
settings.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(Settings.DarkMode))
|
||||
{
|
||||
ApplyDarkMode(settings);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = CreateMainWindow();
|
||||
}
|
||||
else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
|
||||
{
|
||||
singleViewFactoryApplicationLifetime.MainViewFactory = () => CreateMainWindow();
|
||||
singleViewFactoryApplicationLifetime.MainViewFactory =
|
||||
() => CreateMainWindow();
|
||||
}
|
||||
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
|
||||
{
|
||||
singleViewPlatform.MainView = CreateMainWindow();
|
||||
}
|
||||
ApplyDarkMode(settings);
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
MainWindow window;
|
||||
private MainWindow CreateMainWindow()
|
||||
{
|
||||
window = new MainWindow();
|
||||
var api = ServiceProvider!.GetRequiredService<YavscApiClient>();
|
||||
window.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api);
|
||||
var sessionStatus = ServiceProvider!.GetRequiredService<SessionStatusViewModel>();
|
||||
sessionStatus.LogoutCompleted += () =>
|
||||
{
|
||||
window.NavRoot.PopToRootAsync();
|
||||
};
|
||||
|
||||
sessionStatus.LoginSucceeded += () =>
|
||||
{
|
||||
PushMainPageAsync();
|
||||
};
|
||||
|
||||
var homeVm = ServiceProvider!.GetRequiredService<HomePageViewModel>();
|
||||
|
||||
this.PushPageAsync(homeVm).Wait();
|
||||
window.SessionBanner.DataContext = sessionStatus;
|
||||
return window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build the DI container the app uses. Pulled out of
|
||||
/// <see cref="OnFrameworkInitializationCompleted"/> so headless
|
||||
/// tests can construct the same container at <c>TestApp</c> 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.
|
||||
/// </summary>
|
||||
internal static IServiceProvider BuildServices(ServiceCollection services)
|
||||
{
|
||||
var settings = new Settings();
|
||||
|
|
@ -182,14 +110,13 @@ public partial class App : Application
|
|||
// ViewModels
|
||||
services.AddSingleton(settings);
|
||||
services.AddSingleton<YavscApiClient>(api);
|
||||
services.AddSingleton<IYavscApiClient>(api);
|
||||
services.AddSingleton(client);
|
||||
services.AddSingleton(circleClient);
|
||||
services.AddSingleton(blogAclClient);
|
||||
services.AddSingleton(userSearchClient);
|
||||
services.AddSingleton<IContactService>(contactService);
|
||||
services.AddSingleton<IUserDirectory>(userDirectory);
|
||||
services.AddTransient<MainPageViewModel>();
|
||||
services.AddTransient<MainViewModel>();
|
||||
services.AddTransient<HomePageViewModel>();
|
||||
services.AddTransient<SignaturePageViewModel>();
|
||||
services.AddTransient<CirclesPageViewModel>();
|
||||
|
|
@ -204,18 +131,27 @@ public partial class App : Application
|
|||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attach a pre-built DI container to this <see cref="App"/>
|
||||
/// instance. Used by headless tests after
|
||||
/// <see cref="BuildServices"/>; in production this happens
|
||||
/// implicitly via <see cref="OnFrameworkInitializationCompleted"/>.
|
||||
/// Idempotent w.r.t. <see cref="Settings.BindToServiceProvider"/>:
|
||||
/// re-binding from a second App boot is a no-op.
|
||||
/// </summary>
|
||||
internal void AttachServiceProvider(IServiceProvider sp)
|
||||
private MainWindow CreateMainWindow()
|
||||
{
|
||||
ServiceProvider = sp;
|
||||
Settings.BindToServiceProvider(sp);
|
||||
window = new MainWindow();
|
||||
var api = ServiceProvider!.GetRequiredService<YavscApiClient>();
|
||||
window.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api);
|
||||
var sessionStatus = ServiceProvider!.GetRequiredService<SessionStatusViewModel>();
|
||||
sessionStatus.LogoutCompleted += () =>
|
||||
{
|
||||
window.NavRoot.PopToRootAsync();
|
||||
};
|
||||
|
||||
sessionStatus.LoginSucceeded += () =>
|
||||
{
|
||||
PushMainPageAsync();
|
||||
};
|
||||
|
||||
var homeVm = ServiceProvider!.GetRequiredService<HomePageViewModel>();
|
||||
|
||||
this.PushPageAsync(homeVm).Wait();
|
||||
window.SessionBanner.DataContext = sessionStatus;
|
||||
return window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -266,7 +202,7 @@ public partial class App : Application
|
|||
public static Task PushMainPageAsync()
|
||||
{
|
||||
var app = (App)Current!;
|
||||
var mainVm = app.ServiceProvider!.GetRequiredService<MainPageViewModel>();
|
||||
var mainVm = app.ServiceProvider!.GetRequiredService<MainViewModel>();
|
||||
return app.PushPageAsync(mainVm);
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
src/PostIt/PostIt/Assets/avalonia-logo.ico
Normal file
BIN
src/PostIt/PostIt/Assets/avalonia-logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
|
|
@ -1,32 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
||||
<FileVersion>1.1.0.0</FileVersion>
|
||||
<InformationalVersion>1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3</InformationalVersion>
|
||||
<Version>1.1.0-beta.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" />
|
||||
<PackageReference Include="Avalonia.AvaloniaEdit" />
|
||||
<PackageReference Include="AvaloniaUI.DiagnosticsSupport">
|
||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="IdentityModel.OidcClient" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<ProjectReference Include="../../Yavsc.Abstract/Yavsc.Abstract.csproj" />
|
||||
<ProjectReference Include="../../Yavsc.Api.Client/Yavsc.Api.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="postit-settings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
|
|
@ -38,6 +20,21 @@
|
|||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GitVersion.MsBuild" />
|
||||
<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" />
|
||||
<PackageReference Include="AvaloniaUI.DiagnosticsSupport">
|
||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="Avalonia.AvaloniaEdit" />
|
||||
<PackageReference Include="IdentityModel.OidcClient" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../Yavsc.Abstract/Yavsc.Abstract.csproj" />
|
||||
<ProjectReference Include="../../Yavsc.Api.Client/Yavsc.Api.Client.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -10,7 +11,9 @@ namespace PostIt;
|
|||
/// <summary>
|
||||
/// Given a view model, returns the corresponding view if possible.
|
||||
/// </summary>
|
||||
|
||||
[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;
|
||||
|
|
@ -32,11 +35,12 @@ public class ViewLocator : IDataTemplate
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private Control BuildCore(object? data)
|
||||
{
|
||||
return data switch
|
||||
{
|
||||
MainPageViewModel => _services.GetRequiredService<MainPage>(),
|
||||
MainViewModel => _services.GetRequiredService<MainPage>(),
|
||||
Settings => _services.GetRequiredService<SettingsPage>(),
|
||||
HomePageViewModel => _services.GetRequiredService<HomePage>(),
|
||||
SignaturePageViewModel => _services.GetRequiredService<SignaturePage>(),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ using Yavsc.Api.Client;
|
|||
|
||||
namespace PostIt.ViewModels;
|
||||
|
||||
public partial class MainPageViewModel : ViewModelBase
|
||||
public partial class MainViewModel : ViewModelBase
|
||||
{
|
||||
/// <summary>Window/tab title. Cosmetic — bound by
|
||||
/// <c>MainPage.axaml</c> if at all. Not the post title.</summary>
|
||||
|
|
@ -120,7 +120,7 @@ public partial class MainPageViewModel : ViewModelBase
|
|||
}
|
||||
|
||||
|
||||
public MainPageViewModel()
|
||||
public MainViewModel()
|
||||
{
|
||||
SettingsModel = new Settings();
|
||||
Init(SettingsModel);
|
||||
|
|
@ -170,7 +170,7 @@ public partial class MainPageViewModel : ViewModelBase
|
|||
/// <see cref="BlogApiClient"/>. Production code uses the
|
||||
/// (Settings, BlogApiClient) overload below.
|
||||
/// </summary>
|
||||
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)); ;
|
||||
|
|
@ -64,24 +64,6 @@ public partial class Settings : ViewModelBase
|
|||
null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the canonical Settings instance previously bound through
|
||||
/// <see cref="BindToServiceProvider"/>, or <c>null</c> when called
|
||||
/// outside a running Avalonia application (tests, CLI tools).
|
||||
/// </summary>
|
||||
public static Settings? GetCurrent() => Volatile.Read(ref s_current);
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the canonical Settings instance or throw. Use this in
|
||||
/// production code paths that must not silently fall back to a
|
||||
/// freshly-constructed <see cref="Settings"/> (which used to be
|
||||
/// the root cause of the postit://callback crash: two Settings
|
||||
/// instances racing on PropertyChanged from different threads).
|
||||
/// </summary>
|
||||
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();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ namespace PostIt.ViewModels;
|
|||
|
||||
public abstract partial class ViewModelBase : ObservableObject
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Gets if the user can navigate to the next page
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<Design.DataContext>
|
||||
<vm:MainPageViewModel />
|
||||
<vm:MainViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid Margin="12" RowSpacing="12"
|
||||
|
|
|
|||
|
|
@ -1,27 +1,17 @@
|
|||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:PostIt.ViewModels"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="using:PostIt.Views"
|
||||
x:DataType="vm:MainPageViewModel"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="PostIt.Views.MainWindow"
|
||||
>
|
||||
Icon="/Assets/avalonia-logo.ico"
|
||||
Title="PostIt" >
|
||||
|
||||
<!--
|
||||
Root layout: persistent session banner on top, navigation
|
||||
surface below. The banner is the single source of truth for
|
||||
"Connecté / Déconnecté" and the logout button — visible on
|
||||
every page (HomePage, MainPage) so the user never has to dig
|
||||
through a menu to find their session state.
|
||||
|
||||
The navigation stack is built programmatically in
|
||||
App.OnFrameworkInitializationCompleted: HomePage is the
|
||||
root, MainPage is pushed on top when the silent refresh
|
||||
succeeds at boot or after a fresh login from HomePage.
|
||||
-->
|
||||
<DockPanel LastChildFill="True">
|
||||
<views:SessionStatusBanner x:Name="SessionBanner"
|
||||
DockPanel.Dock="Bottom"/>
|
||||
<NavigationPage x:Name="NavRoot"/>
|
||||
</DockPanel>
|
||||
|
||||
</Window>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue