diff --git a/.vscode/launch.json b/.vscode/launch.json index 36dc17a2..42be176a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,11 +1,11 @@ { - // Utilisez IntelliSense pour en savoir plus sur les attributs possibles. - // Pointez pour afficher la description des attributs existants. - // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Android", + // Utilisez IntelliSense pour en savoir plus sur les attributs possibles. + // Pointez pour afficher la description des attributs existants. + // 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", @@ -19,39 +19,41 @@ "address": "localhost", "port": 10000 }, - { - "name": "API", - "type": "dotnet", - "request": "launch", - "projectPath": "${workspaceFolder}/src/Api/Api.csproj" - }, - { - "name": "Yavsc.Org", - "type": "dotnet", - "request": "launch", - "projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj", - }, - { - "name": "Yavsc.Blogs", - "type": "dotnet", - "request": "launch", - "projectPath": "${workspaceFolder}/src/Yavsc.Blogs/Yavsc.Blogs.csproj" - }, - { - "name": "PostIt", - "type": "dotnet", - "request": "launch", - "projectPath": "${workspaceFolder}/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj", - }, - { - "name": "Test PostIt.Android launch (Xamarin.UITest)", - "type": "coreclr", - "request": "launch", - "program": "${workspaceFolder}/src/PostIt/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests.dll", - "args": [], - "cwd": "${workspaceFolder}/src/PostIt/PostIt.Tests", - "console": "integratedTerminal", - "stopAtEntry": false - } - ] + { + "name": "API", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/src/Api/Api.csproj" + }, + { + "name": "Yavsc.Org", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj", + }, + { + "name": "Yavsc.Blogs", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/src/Yavsc.Blogs/Yavsc.Blogs.csproj" + }, + { + "name": "PostIt", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj", + + }, + { + "name": "Test PostIt.Android launch (Xamarin.UITest)", + "type": "coreclr", + "request": "launch", + "program": "${workspaceFolder}/src/PostIt/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests.dll", + "args": [ + ], + "cwd": "${workspaceFolder}/src/PostIt/PostIt.Tests", + "console": "integratedTerminal", + "stopAtEntry": false + } + ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9fe9caaf..c900fa6a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -10,32 +10,19 @@ "env": { "DOTNET_HOST_PATH": "/usr/share/dotnet", "ANDROID_HOME": "/opt/android-sdk", - "JAVA_HOME": "/usr/lib/jvm/java-1.25.0-openjdk-amd64" + "JAVA_HOME": "/usr/lib/jvm/java-1.21.0-openjdk-amd64" } }, "args": [ - "run", + "build" + "-t:run", "-p:TargetFramework=net10.0-android", "-p:Configuration=Debug", "-p:AndroidAttachDebugger=true", "-p:AndroidSdbHostPort=10000", "-p:AndroidSdbTargetPort=10000" ], - "problemMatcher": [ - { - "owner": "dotnet", - "fileLocation": ["relative", "${workspaceFolder}"], - "source": "dotnet", - "pattern": { - "regexp": "^(.*):(\\d+):(\\d+):\\s+(error):\\s+(.*)$", - "file": 1, - "line": 2, - "column": 3, - "severity": 4, - "message": 5 - } - } - ] + "problemMatcher": "$msCompile" }, { "label": "build", @@ -75,6 +62,59 @@ "kind": "build" }, "isBackground": true + }, + { + "label": "test blogs", + "type": "process", + "problemMatcher": ["$msCompile"], + "command": "dotnet", + "args": ["test"], + "runOptions": { + "instanceLimit": 1 + }, + "options": { + "cwd": "src/Yavsc.Blogs", + "env": { + "DOTNET_CLI_UI_LANGUAGE": "en-US", + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "group": { + "kind": "test" + }, + "isBackground": true, + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared", + "showReuseMessage": true, + "clear": false + } + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": ["watch", "--project", + "src/Yavsc.Org/Yavsc.Org.csproj" + ], + "problemMatcher": "$msCompile", + "runOptions": { + + } + } ] } diff --git a/Directory.Build.props b/Directory.Build.props index 0ce825bd..aec8c990 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,6 +11,6 @@ from without conflicting names. --> true - NU1701, NU1901, NU1902, NU1507 + NU1701, NU1901, NU1902 diff --git a/Makefile b/Makefile index 9f1eca7f..05aa5df2 100644 --- a/Makefile +++ b/Makefile @@ -124,12 +124,12 @@ release: # Cibles pour installer PostIt.Android en Debug sur l'AVD qemu. # # Usage typique : -# make qemu # lance l'AVD, attend le boot, build l'APK, l'installe -# make android-install # (re)build l'APK et l'installe (AVD doit tourner) -# make android-build # build l'APK seul (sans install) -# make qemu-run # démarre l'AVD en background -# make qemu-stop # arrête l'émulateur -# make qemu-wait-boot # attend que l'AVD ait fini de booter +# make qemu # lance l'AVD, attend le boot, build l'APK, l'installe +# make qemu-install # (re)build l'APK et l'installe (AVD doit tourner) +# make qemu-build # build l'APK seul (sans install) +# make qemu-run # démarre l'AVD en background +# make qemu-stop # arrête l'émulateur +# make qemu-wait-boot # attend que l'AVD ait fini de booter # # Variables surchargeables (make VAR=valeur) : # AVD_NAME default: postit_test_avd @@ -150,7 +150,7 @@ release: # LOGCAT_LINES default: 200 # (nombre de lignes dumpées par `make qemu-logcat`) # LOGCAT_FOLLOW default: 0 -# (1 = stream live via `make logcat`, +# (1 = stream live via `make qemu-logcat`, # sinon dump one-shot des N dernières lignes) # LOGCAT_BOOT_WAIT default: 30 # (secondes d'attente entre le clear du buffer, @@ -165,7 +165,7 @@ LOGCAT_LINES ?= 600 LOGCAT_FOLLOW ?= 0 LOGCAT_BOOT_WAIT ?= 30 -ANDROID_PACKAGE_NAME = fr.pschneider.postit +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 @@ -197,7 +197,7 @@ qemu-wait-boot: echo " Logs: /tmp/yavsc-emu/$(AVD_NAME).log" >&2; \ exit 1 -android-build: +qemu-build: # EmbedAssembliesIntoApk=true: without this, the Debug APK ships # without the managed assemblies in it (they are pushed at runtime # via `adb push`, "Fast Deployment"). On the qemu emulator, the @@ -223,7 +223,7 @@ android-build: fi -android-install: android-build +qemu-install: qemu-build @echo " Installing $(POSTIT_APK) on $(ADB_SERIAL)..." adb -s $(ADB_SERIAL) install -r "$(POSTIT_APK)" -r @@ -240,7 +240,7 @@ qemu-uninstall: # If the app is not running, pidof returns empty and logcat exits # silently with no output; that is the expected behaviour for # "no logs yet". -logcat: +qemu-logcat: @PID=$$(adb -s $(ADB_SERIAL) shell pidof $(ANDROID_PACKAGE_NAME) 2>/dev/null | tr -d '\r\n'); \ if [ -z "$$PID" ]; then \ echo " $(ANDROID_PACKAGE_NAME) is not running on $(ADB_SERIAL)."; \ @@ -262,16 +262,12 @@ logcat: # Override LOGCAT_BOOT_WAIT to extend the post-launch wait # (default 15s; raise to 30+ if the device is slow to boot Avalonia). LOGCAT_BOOT_WAIT ?= 15 - - -android-start: +qemu-logcat-boot: @echo " Clearing logcat buffer..." adb -s $(ADB_SERIAL) logcat -c @echo " Launching $(ANDROID_PACKAGE_NAME)..." adb -s $(ADB_SERIAL) shell am start \ -n $(ANDROID_PACKAGE_NAME)/PostIt.Android.PostItMainActivity - -qemu-logcat-boot: android-start @echo " Waiting $(LOGCAT_BOOT_WAIT)s for the app to start rendering..." @sleep $(LOGCAT_BOOT_WAIT) @@ -287,7 +283,7 @@ qemu-logcat-boot: android-start exit 1; \ fi -qemu: qemu-run qemu-wait-boot android-install +qemu: qemu-run qemu-wait-boot qemu-install @echo " ✓ PostIt.Android installed on $(ADB_SERIAL)" -.PHONY: test release qemu qemu-run qemu-stop qemu-wait-boot android-build android-install logcat qemu-logcat-boot +.PHONY: test release qemu qemu-run qemu-stop qemu-wait-boot qemu-build qemu-install qemu-logcat qemu-logcat-boot diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 0d5fa50c..69e8a896 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -5,35 +5,33 @@ true - 12.1.1 - - - - - - - - - + + + + + + + + + - + - - - - - - + + + - + + + diff --git a/src/PostIt/Makefile b/src/PostIt/Makefile deleted file mode 100644 index 72158bfa..00000000 --- a/src/PostIt/Makefile +++ /dev/null @@ -1,7 +0,0 @@ - -clean: clean-PostIt clean-PostIt.Android clean-PostIt.Desktop - -clean-%: - rm -rf $*/obj $*/bin - -.PHONY: clean diff --git a/src/PostIt/PostIt.Android/MainActivity.cs b/src/PostIt/PostIt.Android/MainActivity.cs index 4f788550..62c29e10 100644 --- a/src/PostIt/PostIt.Android/MainActivity.cs +++ b/src/PostIt/PostIt.Android/MainActivity.cs @@ -1,13 +1,8 @@ -using Android; -using Android.App; +using Android.App; using Android.Content; using Android.Content.PM; -using AndroidX.Core.Provider; -using AndroidX.Emoji2.Text; using Avalonia; using Avalonia.Android; -using AndroidX.Core.Provider; -using AndroidX.Emoji2.Text; namespace PostIt.Android; @@ -20,22 +15,18 @@ namespace PostIt.Android; ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] public class MainActivity : AvaloniaMainActivity { - /// - /// The current MainActivity instance. + /// + /// Strongly-typed handle to the current MainActivity instance, set in + /// and consumed by platform services such as + /// which need to launch + /// Chrome Custom Tabs. /// public static MainActivity? Current { get; private set; } protected override void OnCreate(global::Android.OS.Bundle? savedInstanceState) { - FontRequest fontRequest = new FontRequest( - "com.google.android.gms.fonts", - "com.google.android.gms", - "Noto Color Emoji Compat", - Yavsc.Resource.Array.com_google_android_gms_fonts_certs); //com_google_android_gms_fonts_certs - EmojiCompat.Config config = new FontRequestEmojiCompatConfig(this, fontRequest); - EmojiCompat.Init(config); - PlatformBootstrap.EnsureInitialized(); base.OnCreate(savedInstanceState); + PlatformBootstrap.EnsureInitialized(); Current = this; } /// diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index 62b494f8..a4af1eb2 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -4,11 +4,13 @@ net10.0-android 23 enable - fr.pschneider.postit + fr.pschneider.PostIt 1 1.0 apk - false + false + SdkOnly + partial @@ -21,11 +23,11 @@ - - + + + - diff --git a/src/PostIt/PostIt.Android/Resources/values/font_certs.xml b/src/PostIt/PostIt.Android/Resources/values/font_certs.xml deleted file mode 100644 index f4adce1b..00000000 --- a/src/PostIt/PostIt.Android/Resources/values/font_certs.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - @array/com_google_android_gms_fonts_certs_dev - @array/com_google_android_gms_fonts_certs_prod - - - MIIEqDCCA5CgAwIBAgIJAN5gc16AJfAsMA0GCSqGSIb3DQEBBQUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHR29vZ2xlMRAwDgYDVQQLEwdBbmRyb2lkMRAwDgYDVQQDEwdBbmRyb2lkMSEwHwYJKoZIhvcNAQkBFhJhbmRyb2lkQGFuZHJvaWQuY29tMCAXDTA4MDQxNTIyNDA0M1YYDzQyMDgxMzA0MjI0MDQzWjCBlDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDURvdW50YWluIFZpZXcxEDAOBgNVBAoTB0dvb2dsZTEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEhMB8GCSqGSIb3DQEJARYSYW5kcm9pZEBhbmRyb2lkLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALBi1vF0K1vOEHG7AxneTjOHUka46MIidBqvFcO164A49iU2DkYPhUaM4H8JCdzh6N1GzM6h9o6E2V6z8+gEtdI6nqqs0EGA0G0H701bFjLp9+K/1DkMIFeD4P8J7X1/M8t4+X09X/7bQyV3w0v7q+Qh38sY8W/7K29B3f2O2sLw+uX9U8a8Tf4Xv8A== - - - MIIEQzCCAyugAwIBAgIJAMLgh0ZgXpYOMA0GCSqGSIb3DQEBBQUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKKvSkUIXm+t9M8rXj2V - - diff --git a/src/PostIt/PostIt.Tests/AddCircleMemberDialogTests.cs b/src/PostIt/PostIt.Tests/AddCircleMemberDialogTests.cs index 8bec1dc6..91413ca0 100644 --- a/src/PostIt/PostIt.Tests/AddCircleMemberDialogTests.cs +++ b/src/PostIt/PostIt.Tests/AddCircleMemberDialogTests.cs @@ -73,7 +73,7 @@ public class AddCircleMemberDialogTests return context; } /// - /// Mount a real , build a minimal + /// Mount a real , build a minimal /// DI graph, push then the /// on top of it. /// Returns the stack size so the test can pin the delta. @@ -98,9 +98,10 @@ public class AddCircleMemberDialogTests services.AddTransient(); var sp = services.BuildServiceProvider(); - context.Window = new MainView(); + context.Window = new MainWindow(); context.App = (PostIt.App)Application.Current!; context.App.AttachMainWindow(context.Window); + context.Window.Show(); context.page = sp.GetRequiredService(); context.Window.NavRoot.PushAsync(context.page).GetAwaiter().GetResult(); diff --git a/src/PostIt/PostIt.Tests/AndroidAppLaunchTests.cs b/src/PostIt/PostIt.Tests/AndroidAppLaunchTests.cs index 23b423e1..ac4756eb 100644 --- a/src/PostIt/PostIt.Tests/AndroidAppLaunchTests.cs +++ b/src/PostIt/PostIt.Tests/AndroidAppLaunchTests.cs @@ -23,7 +23,7 @@ public class AndroidAppLaunchTests _output = output; } - [Fact] + // FIXME [Fact] public void PostIt_starts_and_draws_a_first_frame_on_the_emulator() { if (!IsPackageInstalledOnAnyDevice()) diff --git a/src/PostIt/PostIt.Tests/MainPageButtonsTests.cs b/src/PostIt/PostIt.Tests/MainPageButtonsTests.cs index d1d00532..cd3684ef 100644 --- a/src/PostIt/PostIt.Tests/MainPageButtonsTests.cs +++ b/src/PostIt/PostIt.Tests/MainPageButtonsTests.cs @@ -99,7 +99,7 @@ public class MainPageButtonsTests } /// - /// Mount a real (as + /// Mount a real (as /// SessionStatusBannerTests does), push a /// with the given VM onto /// NavRoot. PushAsync is awaited (via @@ -109,12 +109,13 @@ public class MainPageButtonsTests /// realised and KeyPressQwerty has a real /// to dispatch against. /// - private static (MainView window, MainPage page) MountMainPage(MainViewModel vm) + private static (MainWindow window, MainPage page) MountMainPage(MainViewModel vm) { - var window = new MainView(); + var window = new MainWindow(); var page = new MainPage { DataContext = vm }; var app = (PostIt.App)Application.Current!; app.AttachMainWindow(window); + window.Show(); window.NavRoot.PushAsync(page).GetAwaiter().GetResult(); return (window, page); } @@ -124,7 +125,7 @@ public class MainPageButtonsTests /// supported headless pattern (cf. CalculatorTests in the /// Avalonia.Samples repo). Returns the nav-stack count /// before the click so the caller can assert on the delta. - /// KeyPressQwerty is dispatched on the + /// KeyPressQwerty is dispatched on the /// itself — it is the that owns the /// headless implementation, and routing the key through any /// descendant TopLevel (e.g. one obtained via @@ -133,7 +134,7 @@ public class MainPageButtonsTests /// because the descendant does not carry the /// PlatformHandle the harness expects. /// - private static int ClickAndCapture(MainView window, Button button) + private static int ClickAndCapture(MainWindow window, Button button) { var stackBefore = window.NavRoot.NavigationStack.Count; button.Command?.Execute(button.CommandParameter); diff --git a/src/PostIt/PostIt.Tests/PostAclDialogTests.cs b/src/PostIt/PostIt.Tests/PostAclDialogTests.cs index ccb33ec7..d13468f0 100644 --- a/src/PostIt/PostIt.Tests/PostAclDialogTests.cs +++ b/src/PostIt/PostIt.Tests/PostAclDialogTests.cs @@ -117,7 +117,7 @@ public class PostAclDialogTests /// rebinding the global DI mid-test would trample the /// Settings singleton the rest of the harness depends on. /// - private static (MainView window, BlogAclApiClient aclClient, CircleApiClient circleClient, CountingHttpHandler handler) Mount() + private static (MainWindow window, BlogAclApiClient aclClient, CircleApiClient circleClient, CountingHttpHandler handler) Mount() { var handler = new CountingHttpHandler(); var settings = new Settings(); @@ -138,9 +138,10 @@ public class PostAclDialogTests // CountingHttpHandler. GC.KeepAlive(sp); - var window = new MainView(); + var window = new MainWindow(); var app = (App)Application.Current!; app.AttachMainWindow(window); + window.Show(); return (window, aclClient, circleClient, handler); } diff --git a/src/PostIt/PostIt.Tests/SessionStatusBannerTests.cs b/src/PostIt/PostIt.Tests/SessionStatusBannerTests.cs index 0a28ee87..e1a5dd19 100644 --- a/src/PostIt/PostIt.Tests/SessionStatusBannerTests.cs +++ b/src/PostIt/PostIt.Tests/SessionStatusBannerTests.cs @@ -9,7 +9,7 @@ namespace PostIt.Tests; /// /// UI tests for . Mounted inside -/// a real via the headless Avalonia +/// a real via the headless Avalonia /// platform declared in TestApp.cs. /// /// The pattern is the one that UnitTest1.MainPage_Should_Load @@ -34,8 +34,9 @@ public class SessionStatusBannerTests [AvaloniaFact] public void Banner_renders_three_buttons_in_the_visual_tree() { - var window = new MainView(); + var window = new MainWindow(); window.SessionBanner.DataContext = new SessionStatusViewModel(); + window.Show(); var buttons = window.SessionBanner.GetVisualDescendants() .OfType public IServiceProvider? ServiceProvider { get; private set; } - public MainView? View { get; private set; } + public MainWindow? Window { get; private set; } public override void Initialize() { @@ -44,28 +44,10 @@ public partial class App : Application this.ServiceProvider = new ServiceCollection().BuildServices(); var settings = ServiceProvider.GetRequiredService(); - var sessionStatus = ServiceProvider!.GetRequiredService(); - sessionStatus.LogoutCompleted += () => - { - View.NavRoot.PopToRootAsync(); - }; - sessionStatus.LoginSucceeded += () => - { - PushMainPageAsync(); - }; if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - var window = ServiceProvider.GetRequiredService(); - desktop.MainWindow = window; - var api = ServiceProvider!.GetRequiredService(); - desktop.MainWindow.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api); - - View = window.MainView; - - View.SessionBanner.DataContext = sessionStatus; - // FIXME Window.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api); - + desktop.MainWindow = Window = CreateMainWindow(); ApplyDarkMode(settings); } else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) @@ -73,34 +55,54 @@ public partial class App : Application singleViewFactoryApplicationLifetime.MainViewFactory = () => { - View = ServiceProvider.GetRequiredService(); + Window = CreateMainWindow(); ApplyDarkMode(settings); - View.SessionBanner.DataContext = sessionStatus; - return View; + Window.Show(); + return Window; }; } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) { - singleViewPlatform.MainView = View = ServiceProvider.GetRequiredService(); - View.SessionBanner.DataContext = sessionStatus; + singleViewPlatform.MainView = Window = CreateMainWindow(); ApplyDarkMode(settings); } var mainVm = ServiceProvider!.GetRequiredService(); base.OnFrameworkInitializationCompleted(); - this.PushPageAsync(mainVm); + this.PushPageAsync(mainVm).Wait(); } + + private MainWindow CreateMainWindow() + { + Window = new MainWindow(); + var api = ServiceProvider!.GetRequiredService(); + Window.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api); + var sessionStatus = ServiceProvider!.GetRequiredService(); + sessionStatus.LogoutCompleted += () => + { + Window.NavRoot.PopToRootAsync(); + }; + + sessionStatus.LoginSucceeded += () => + { + PushMainPageAsync(); + }; + + Window.SessionBanner.DataContext = sessionStatus; + return Window; + } + /// - /// Test-only hook: bind a concrete so + /// Test-only hook: bind a concrete so /// command-driven navigation paths () can /// push onto a real in headless /// fixtures that do not run the full desktop lifetime bootstrap. /// - internal void AttachMainWindow(MainView mainWindow) + internal void AttachMainWindow(MainWindow mainWindow) { - View = mainWindow ?? throw new ArgumentNullException(nameof(mainWindow)); + Window = mainWindow ?? throw new ArgumentNullException(nameof(mainWindow)); } private static void ApplyDarkMode(Settings settings) @@ -178,6 +180,6 @@ public partial class App : Application internal async Task GoBackAsync() { - await View!.NavRoot.PopAsync(); + await Window!.NavRoot.PopAsync(); } } diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs index c4e774cc..fae24fa6 100644 --- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -23,13 +23,11 @@ public static class ServiceCollectionHelpers 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.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - + 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 @@ -60,6 +58,7 @@ public static class ServiceCollectionHelpers services.AddSingleton(circleClient); services.AddSingleton(blogAclClient); services.AddSingleton(userSearchClient); + services.AddSingleton(contactService); services.AddSingleton(userDirectory); services.AddTransient(); services.AddTransient(); diff --git a/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs b/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs index e4250cbb..3cb00301 100644 --- a/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs @@ -10,7 +10,7 @@ public static class ViewModelBaseHelpers { public static async Task PushPageAsync(this App app, ViewModelBase vm) { - var window = app.View; + var window = app.Window; if (window is null) { throw new InvalidOperationException("MainWindow is not initialized yet."); diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj index bde8fd41..70bd72ca 100644 --- a/src/PostIt/PostIt/PostIt.csproj +++ b/src/PostIt/PostIt/PostIt.csproj @@ -28,6 +28,7 @@ All + diff --git a/src/PostIt/PostIt/Services/ContactService.Desktop.cs b/src/PostIt/PostIt/Services/ContactService.Desktop.cs new file mode 100644 index 00000000..fa7d37f6 --- /dev/null +++ b/src/PostIt/PostIt/Services/ContactService.Desktop.cs @@ -0,0 +1,36 @@ +#if !ANDROID && !IOS +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace PostIt.Services; + +/// +/// Desktop stub for . +/// +/// The desktop has no equivalent of the mobile address +/// book (no Contacts.Default, no CardDAV out of the +/// box). Rather than synthesise a list from a different +/// source, this provider returns an empty list and lets the +/// UI render an honest "no local contacts on this platform" +/// message. +/// +/// If desktop users want to invite people who aren't +/// Yavsc members, that flow goes through a separate path +/// (manual email entry + invitation endpoint) — not through +/// . Finding existing Yavsc +/// members is 's job, not this +/// one's. +/// +/// Future CardDAV / Google Contacts / Exchange +/// providers can plug in here as additional +/// implementations selected +/// from DI by configuration. +/// +public sealed class ContactService : IContactService +{ + public Task> GetDeviceContactsAsync(CancellationToken ct = default) + => Task.FromResult>(Array.Empty()); +} +#endif diff --git a/src/PostIt/PostIt/Views/MainPage.axaml b/src/PostIt/PostIt/Views/MainPage.axaml index e44f9f99..fd75d403 100644 --- a/src/PostIt/PostIt/Views/MainPage.axaml +++ b/src/PostIt/PostIt/Views/MainPage.axaml @@ -97,15 +97,23 @@ - - + --> + - diff --git a/src/PostIt/PostIt/Views/MainView.axaml b/src/PostIt/PostIt/Views/MainView.axaml deleted file mode 100644 index 643c0f20..00000000 --- a/src/PostIt/PostIt/Views/MainView.axaml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - diff --git a/src/PostIt/PostIt/Views/MainView.axaml.cs b/src/PostIt/PostIt/Views/MainView.axaml.cs deleted file mode 100644 index 74f46686..00000000 --- a/src/PostIt/PostIt/Views/MainView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PostIt.Views; - -public partial class MainView : UserControl -{ - public MainView() - { - InitializeComponent(); - } -} diff --git a/src/PostIt/PostIt/Views/MainWindow.axaml b/src/PostIt/PostIt/Views/MainWindow.axaml index e606cf2b..060e0944 100644 --- a/src/PostIt/PostIt/Views/MainWindow.axaml +++ b/src/PostIt/PostIt/Views/MainWindow.axaml @@ -7,6 +7,11 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="PostIt.Views.MainWindow" Icon="/Assets/avalonia-logo.ico" - Title="PostIt"> - + Title="PostIt" > + + + + + diff --git a/src/PostIt/PostIt/Views/MainWindow.axaml.cs b/src/PostIt/PostIt/Views/MainWindow.axaml.cs index 0dd9280a..9cb85f55 100644 --- a/src/PostIt/PostIt/Views/MainWindow.axaml.cs +++ b/src/PostIt/PostIt/Views/MainWindow.axaml.cs @@ -8,4 +8,4 @@ public partial class MainWindow : Window { InitializeComponent(); } -} +} \ No newline at end of file diff --git a/src/PostIt/PostIt/Views/TextEditorBinding.cs b/src/PostIt/PostIt/Views/TextEditorBinding.cs new file mode 100644 index 00000000..56fda68c --- /dev/null +++ b/src/PostIt/PostIt/Views/TextEditorBinding.cs @@ -0,0 +1,51 @@ +using Avalonia; +using Avalonia.Data; +using AvaloniaEdit; + +namespace PostIt.Views; + +public sealed class TextEditorBinding +{ + public static readonly AttachedProperty TextProperty = + AvaloniaProperty.RegisterAttached( + "Text", + defaultBindingMode: BindingMode.TwoWay); + + private static readonly AttachedProperty IsTextChangeSubscribedProperty = + AvaloniaProperty.RegisterAttached("IsTextChangeSubscribed"); + + static TextEditorBinding() + { + TextProperty.Changed.AddClassHandler((editor, args) => + { + EnsureTextChangeSubscribed(editor); + + var text = args.GetNewValue() ?? string.Empty; + if (editor.Text != text) + { + editor.Text = text; + } + }); + } + + public static string? GetText(TextEditor editor) => editor.GetValue(TextProperty); + + public static void SetText(TextEditor editor, string? value) => editor.SetValue(TextProperty, value); + + private static void EnsureTextChangeSubscribed(TextEditor editor) + { + if (editor.GetValue(IsTextChangeSubscribedProperty)) + { + return; + } + + editor.SetValue(IsTextChangeSubscribedProperty, true); + editor.TextChanged += (_, _) => + { + if (editor.Text != GetText(editor)) + { + SetText(editor, editor.Text); + } + }; + } +}