Compare commits

..

76 commits

Author SHA1 Message Date
6b161b0fb5 Merge pull request 'release/1.0.8-rc3' (#43) from release/1.0.8-rc3 into main
Some checks failed
Dotnet build and test / build (push) Failing after 15m24s
Reviewed-on: #43
2026-08-26 22:49:50 +01:00
671afebc12
Build image version bump
All checks were successful
Dotnet build and test / build (pull_request) Successful in 7m2s
Forgejo Release / release (push) Successful in 6m11s
2026-08-26 22:26:14 +01:00
ad2285b8c5
was breakingthe format
Some checks failed
Dotnet build and test / build (pull_request) Successful in 4m24s
Forgejo Release / release (push) Failing after 1m46s
2026-08-26 20:19:51 +01:00
09c8d79714
debian13!
Some checks failed
Dotnet build and test / build (pull_request) Successful in 9m7s
Forgejo Release / release (push) Failing after 22s
2026-08-26 20:04:39 +01:00
912dd88a02
gives this release a title
Some checks failed
Dotnet build and test / build (pull_request) Failing after 6m4s
Forgejo Release / release (push) Failing after 1s
2026-08-26 19:52:41 +01:00
400db94953
hit the runner
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 19:49:42 +01:00
5d81ab3094
Release 1.0.8-rc3
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 19:43:47 +01:00
45299d14c1
no apk to install
All checks were successful
Dotnet build and test / build (pull_request) Successful in 12m27s
2026-08-26 19:27:43 +01:00
bfcb7cd2ef
disable the Xamarin.UITest
Some checks failed
Dotnet build and test / build (pull_request) Failing after 25s
2026-08-26 19:23:27 +01:00
366cd0eed9
try with vulkan enabled
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 17:22:03 +01:00
29d842a8f9
-cores 1
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 17:18:31 +01:00
98cab22e60
RE_EMULATOR_DISABLE_CPUID_CHECK
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 17:16:47 +01:00
d63f065e03
try host cpu
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 17:15:11 +01:00
99c500d177
start qemu
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 17:13:20 +01:00
b0e70eb198
give a try to the v7
Some checks failed
Dotnet build and test / build (pull_request) Failing after 10s
2026-08-26 17:06:14 +01:00
39400baadb
giving up
Some checks failed
Dotnet build and test / build (pull_request) Failing after 21s
2026-08-26 09:25:23 +01:00
d6b45024dc
kvm-ok is to install (or not?)
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 09:21:44 +01:00
e266c1de6e
syntax
Some checks failed
Dotnet build and test / build (pull_request) Failing after 5s
2026-08-26 09:20:36 +01:00
f285a168bd
settings
Some checks failed
buildAndTest.yml / settings (push) Failing after 0s
buildAndTest.yml / settings (pull_request) Failing after 0s
2026-08-26 09:18:38 +01:00
1e3239498c
-gpu software
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 08:58:46 +01:00
696117ac5a
another try
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 08:56:14 +01:00
eb0cf11703
-cpu host
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 08:50:35 +01:00
1ba114ee33
gpu swiftshader
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 08:45:57 +01:00
f7956b0459
appel direct
Some checks are pending
Dotnet build and test / build (pull_request) Has started running
2026-08-26 08:28:19 +01:00
8003239159
docker image version bump
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 08:08:13 +01:00
8f9c55bf2d
apt-get update && apt-get install
Some checks failed
Dotnet build and test / build (pull_request) Failing after 20m29s
2026-08-26 07:09:10 +01:00
c4f6913fdb
LD_LIBRARY_PATH 2
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 07:05:00 +01:00
7e3b0a72ad
LD_LIBRARY_PATH
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 07:03:03 +01:00
7ccb23ba28
try the direct call
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 06:58:47 +01:00
955d925f33
-no-accel-check
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 06:53:28 +01:00
bbd869ab88
export USER=root
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 06:48:18 +01:00
c338fb33ed
Injecte la ligne parfaite
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 06:44:38 +01:00
016d16fda4
Injecter le groupe KVM
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 06:41:57 +01:00
8978225539
ne rien forcer
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 06:39:22 +01:00
e337e25aa4
Forcer l'appropriation et les droits KVM
Some checks failed
Dotnet build and test / build (pull_request) Failing after 14s
2026-08-26 06:27:04 +01:00
955725d739
Diagnostic système absolu du Runner
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 06:23:22 +01:00
7d1e5763ff
cleanup
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 06:19:42 +01:00
41a1febb24
pour test
Some checks failed
Dotnet build and test / build (pull_request) Failing after 15s
2026-08-26 06:09:16 +01:00
4f9acb17ff
pour test
Some checks failed
Dotnet build and test / build (pull_request) Failing after 19s
2026-08-26 06:07:50 +01:00
af1ca7b1bb
for reading
Some checks failed
Dotnet build and test / build (pull_request) Failing after 32s
2026-08-26 05:44:46 +01:00
bc6861dd26
no kvm
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 05:38:07 +01:00
801839465c
donne l'accès
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 05:25:18 +01:00
1a79ee93eb
avd name changed
Some checks are pending
Dotnet build and test / build (pull_request) Has started running
2026-08-26 05:15:13 +01:00
851920c79e
specify the docker image
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 05:09:24 +01:00
a9c18d6708
emulator
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 04:52:57 +01:00
70ad5100be
yatry
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 03:55:21 +01:00
dab56e508f
hit the AVD
All checks were successful
Dotnet build and test / build (pull_request) Successful in 1h29m2s
2026-08-26 02:24:03 +01:00
9d5667c459
to be clear
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 01:49:57 +01:00
36d58b6870
simpler is better
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-26 01:42:59 +01:00
553bd415a9
comment cleanup
Some checks failed
Dotnet build and test / build (pull_request) Failing after 7m15s
2026-08-26 01:24:19 +01:00
0bb293c6ee
postit_test_avd 2026-08-26 00:20:17 +01:00
a4ae0fde72
should work, Xamarin.UITest installs the app
Some checks failed
Dotnet build and test / build (pull_request) Failing after 10m18s
2026-08-25 23:39:12 +01:00
863c549d51
reinstall when yet installed
Some checks failed
Dotnet build and test / build (pull_request) Failing after 9m12s
2026-08-25 23:17:17 +01:00
14b0cdb6a4
undated 2026-08-25 23:14:50 +01:00
43dd526e4f
fixes the step title 2026-08-25 23:07:19 +01:00
b18f86ed23
fixes the script
Some checks failed
Dotnet build and test / build (pull_request) Failing after 9m22s
2026-08-25 23:06:25 +01:00
d56ad25e6b
fixes the tests
Some checks failed
buildAndTest.yml / fixes the tests (push) Failing after 0s
buildAndTest.yml / fixes the tests (pull_request) Failing after 0s
2026-08-25 23:01:00 +01:00
50a07102cb
Do not fail at attaching developer tools
Some checks failed
Dotnet build and test / build (pull_request) Failing after 10m48s
2026-08-25 21:15:04 +01:00
6d6c4f2a8a
Package renamed impact
Some checks failed
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-25 20:30:39 +01:00
c707b5f29e
Release 1.0.8-rc3 - unstable
Some checks failed
Dotnet build and test / build (pull_request) Failing after 10m56s
Forgejo Release / release (push) Failing after 8m43s
2026-08-25 19:49:12 +01:00
75b298b0f8
Fixes the Android Login process 2026-08-25 19:20:03 +01:00
dd5904f0a7
refacto Makefiles and MainView setup 2026-08-25 14:53:25 +01:00
beeac58628
test are still ok! 2026-08-25 14:16:49 +01:00
a450be6f24
simpler is better 2026-08-24 16:26:42 +01:00
a079c56c08
code format 2026-08-24 16:26:23 +01:00
ce070453b2
It was too much for qemu 2026-08-24 16:25:45 +01:00
7fabee1bd1
restore the TextEditor 2026-08-24 01:43:47 +01:00
f62a020c82
don't wait from App Nav push ok on loggin succeded 2026-08-24 01:23:15 +01:00
88e1499e4b
show the window 2026-08-24 00:30:29 +01:00
246111dee1
Simpler is better 2026-08-24 00:18:07 +01:00
11aaf36789
updated 2026-08-24 00:17:43 +01:00
404bba56ab
restores the legacy behavior 2026-08-24 00:03:14 +01:00
01bc4782a0
changes 2026-08-23 23:30:37 +01:00
bcd72e17d8
Merge branch 'feat/ui-testing' into release/1.0.8-rc3 2026-08-23 23:26:59 +01:00
50de5f412f
chore(release): bump version via gitversion for 1.0.8-rc3 2026-08-23 23:19:48 +01:00
5274d7bdf5 Merge pull request 'release/1.0.8-rc1' (#42) from release/1.0.8-rc1 into main
All checks were successful
Dotnet build and test / build (push) Successful in 7m4s
Reviewed-on: #42
2026-08-23 23:19:06 +01:00
58 changed files with 568 additions and 648 deletions

View file

@ -25,9 +25,9 @@ on:
jobs: jobs:
build: build:
runs-on: docker runs-on: docker
container:
image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v1
steps: steps:
- name: Clone yavsc - name: Clone yavsc
run: | run: |
@ -39,10 +39,9 @@ jobs:
git checkout FETCH_HEAD git checkout FETCH_HEAD
fi fi
git submodule update --init --recursive git submodule update --init --recursive
echo "Checked out at $(git rev-parse HEAD) on $(git branch --show-current 2>/dev/null || echo detached HEAD)" echo "✅ Checked out at $(git rev-parse HEAD) on $(git branch --show-current 2>/dev/null || echo detached HEAD)"
- name: Restore dependencies
run: cd /src/_src && dotnet restore
- name: Build
run: cd /src/_src && dotnet build --no-restore
- name: Test - name: Test
run: cd /src/_src && dotnet test --no-build --verbosity normal run: |
echo "🚀 Lancement des tests..."
cd /src/_src && dotnet test --verbosity normal && echo "✅ Success !"

View file

@ -51,6 +51,8 @@ jobs:
# via l'API REST Forgejo (pas d'actions tierces Node). # via l'API REST Forgejo (pas d'actions tierces Node).
release: release:
runs-on: docker runs-on: docker
container:
image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v1
steps: steps:
- name: Clone du repo au tag demandé - name: Clone du repo au tag demandé
env: env:
@ -194,10 +196,10 @@ jobs:
# la racine du checkout pour que l'étape d'upload le trouve. # la racine du checkout pour que l'étape d'upload le trouve.
run: | run: |
cd /src/_src cd /src/_src
APK=src/PostIt/PostIt.Android/bin/Release/net10.0-android/android-arm64/fr.pschneider.PostIt-Signed.apk APK=src/PostIt/PostIt.Android/bin/Release/net10.0-android/android-arm64/fr.pschneider.postit-Signed.apk
if [[ ! -f "$APK" ]]; then if [[ ! -f "$APK" ]]; then
echo "::error::APK not found at $APK" echo "::error::APK not found at $APK"
ls -la src/PostIt/PostIt.Android/bin/Release/net10.0-android/ 2>/dev/null || true ls -la src/PostIt/PostIt.Android/bin/Release/net10.0-android/android-arm64 2>/dev/null || true
exit 1 exit 1
fi fi
cp "$APK" /src/_src/PostIt.Android.apk cp "$APK" /src/_src/PostIt.Android.apk

View file

@ -40,7 +40,7 @@ jobs:
- name: Extraire l'APK du conteneur Docker - name: Extraire l'APK du conteneur Docker
run: | run: |
docker create --name extractor postit-android docker create --name extractor postit-android
docker cp extractor:/src/src/PostIt/PostIt.Android/bin/Release/net10.0-android/android-arm64/fr.pschneider.PostIt-Signed.apk ./PostIt.Android.apk docker cp extractor:/src/src/PostIt/PostIt.Android/bin/Release/net10.0-android/android-arm64/fr.pschneider.postit-Signed.apk ./PostIt.Android.apk
docker rm extractor docker rm extractor
- name: Téléverser l'APK en tant qu'Artéfact GitHub - name: Téléverser l'APK en tant qu'Artéfact GitHub

18
.vscode/launch.json vendored
View file

@ -5,19 +5,19 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Debug - Android", "name": "Android Debug",
"type": "mono", "type": "mono",
"preLaunchTask": "run-debug-android", "preLaunchTask": "run-debug-android",
"request": "attach", "request": "attach",
"address": "localhost", "address": "localhost",
"port": 10000 "port": 55555
}, },
{ {
"name": "Attach - Android", "name": "Android Attach - Debug",
"type": "mono", "type": "mono",
"request": "attach", "request": "attach",
"address": "localhost", "address": "localhost",
"port": 10000 "port": 55555
}, },
{ {
"name": "API", "name": "API",
@ -26,31 +26,29 @@
"projectPath": "${workspaceFolder}/src/Api/Api.csproj" "projectPath": "${workspaceFolder}/src/Api/Api.csproj"
}, },
{ {
"name": "Yavsc.Org", "name": "Yavsc Org",
"type": "dotnet", "type": "dotnet",
"request": "launch", "request": "launch",
"projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj", "projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj",
}, },
{ {
"name": "Yavsc.Blogs", "name": "Yavsc Blogs",
"type": "dotnet", "type": "dotnet",
"request": "launch", "request": "launch",
"projectPath": "${workspaceFolder}/src/Yavsc.Blogs/Yavsc.Blogs.csproj" "projectPath": "${workspaceFolder}/src/Yavsc.Blogs/Yavsc.Blogs.csproj"
}, },
{ {
"name": "PostIt", "name": "PostIt Desktop",
"type": "dotnet", "type": "dotnet",
"request": "launch", "request": "launch",
"projectPath": "${workspaceFolder}/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj", "projectPath": "${workspaceFolder}/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj",
}, },
{ {
"name": "Test PostIt.Android launch (Xamarin.UITest)", "name": "Test PostIt.Android launch (Xamarin.UITest)",
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/src/PostIt/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests.dll", "program": "${workspaceFolder}/src/PostIt/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests.dll",
"args": [ "args": [],
],
"cwd": "${workspaceFolder}/src/PostIt/PostIt.Tests", "cwd": "${workspaceFolder}/src/PostIt/PostIt.Tests",
"console": "integratedTerminal", "console": "integratedTerminal",
"stopAtEntry": false "stopAtEntry": false

View file

@ -5,6 +5,7 @@
"appsettings", "appsettings",
"asciidoctor", "asciidoctor",
"ASPNETCORE", "ASPNETCORE",
"Avalonia",
"Configurabilité", "Configurabilité",
"Cratie", "Cratie",
"DESTDIR", "DESTDIR",
@ -15,6 +16,7 @@
"Hsts", "Hsts",
"Newtonsoft", "Newtonsoft",
"Npgsql", "Npgsql",
"Oidc",
"PKCE", "PKCE",
"postit", "postit",
"pschneider", "pschneider",

78
.vscode/tasks.json vendored
View file

@ -10,19 +10,32 @@
"env": { "env": {
"DOTNET_HOST_PATH": "/usr/share/dotnet", "DOTNET_HOST_PATH": "/usr/share/dotnet",
"ANDROID_HOME": "/opt/android-sdk", "ANDROID_HOME": "/opt/android-sdk",
"JAVA_HOME": "/usr/lib/jvm/java-1.21.0-openjdk-amd64" "JAVA_HOME": "/usr/lib/jvm/java-1.25.0-openjdk-amd64"
} }
}, },
"args": [ "args": [
"build" "run",
"-t:run",
"-p:TargetFramework=net10.0-android", "-p:TargetFramework=net10.0-android",
"-p:Configuration=Debug", "-p:Configuration=Debug",
"-p:AndroidAttachDebugger=true", "-p:AndroidAttachDebugger=true",
"-p:AndroidSdbHostPort=10000", "-p:AndroidSdbHostPort=55555",
"-p:AndroidSdbTargetPort=10000" "-p:AndroidSdbTargetPort=55555"
], ],
"problemMatcher": "$msCompile" "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
}
}
]
}, },
{ {
"label": "build", "label": "build",
@ -62,59 +75,6 @@
"kind": "build" "kind": "build"
}, },
"isBackground": true "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": {
}
} }
] ]
} }

View file

@ -16,6 +16,24 @@ Cette convention est partagée avec le dépôt
[`postit-debian`](https://forgejo.pschneider.fr/notazof/postit-debian) [`postit-debian`](https://forgejo.pschneider.fr/notazof/postit-debian)
pour la production des paquets `.deb`. pour la production des paquets `.deb`.
## [1.0.8-rc3] - unstable
### Added
* a code cleanup,
* a first Xamarin.UITest is successful, but disabled, because breaking the actual CI process,
* Android app starts, the login process succeeds
### Changed
L'identifiant de l'application client Android a changé, il passe en minuscules :
`fr.pschneider.postit`
### Fixed
nothing, [TODO] a bug persists laoding a blog post from PostIt, ACL sould come along with and should not need any "Refresh" button.
Today, assigning ACL succeeds the first time, the following times result in a 409
## [1.0.8-rc1] - unstable ## [1.0.8-rc1] - unstable
### Added ### Added

View file

@ -11,6 +11,6 @@
from without conflicting names. from without conflicting names.
--> -->
<UseProjectNamespaceForGitVersionInformation>true</UseProjectNamespaceForGitVersionInformation> <UseProjectNamespaceForGitVersionInformation>true</UseProjectNamespaceForGitVersionInformation>
<NoWarn>NU1701, NU1901, NU1902</NoWarn> <NoWarn>NU1701, NU1901, NU1902, NU1507</NoWarn>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

166
Makefile
View file

@ -121,168 +121,4 @@ release:
git push -u origin "$$BRANCH"; \ git push -u origin "$$BRANCH"; \
echo "==> Terminé. Branche $$BRANCH live sur origin." echo "==> Terminé. Branche $$BRANCH live sur origin."
# Cibles pour installer PostIt.Android en Debug sur l'AVD qemu. .PHONY: test release
#
# Usage typique :
# 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
# (l'AVD doit être listé par `avdmanager list avd`)
# ADB_SERIAL default: emulator-5554
# (port standard du premier émulateur lancé)
# ANDROID_HOME default: /opt/android-sdk
# (le SDK Android local; doit contenir
# emulator/emulator et platform-tools/adb)
# POSTIT_RID default: android-x64
# (doit matcher l'ABI de l'AVD; `avdmanager list avd`
# affiche la ligne Tag/ABI)
# EMU_HEADLESS default: 0
# (1 = lancer l'émulateur sans fenêtre, pour scripter)
# CONFIG surcharge la variable CONFIG globale (Debug par
# défaut dans ce Makefile). Passer à Release pour
# un APK optimisé et signé release.
# LOGCAT_LINES default: 200
# (nombre de lignes dumpées par `make qemu-logcat`)
# LOGCAT_FOLLOW default: 0
# (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,
# le `am start`, et le dump final dans
# `make qemu-logcat-boot`)
AVD_NAME ?= postit_test_avd
ADB_SERIAL ?= emulator-5554
ANDROID_HOME ?= /opt/android-sdk
POSTIT_RID ?= android-x64
EMU_HEADLESS ?= 0
LOGCAT_LINES ?= 600
LOGCAT_FOLLOW ?= 0
LOGCAT_BOOT_WAIT ?= 20
ANDROID_PACKAGE_NAME = fr.pschneider.PostIt
POSTIT_ANDROID_CSPROJ := src/PostIt/PostIt.Android/PostIt.Android.csproj
POSTIT_APK_DIR := src/PostIt/PostIt.Android/bin/$(CONFIG)/net10.0-android/$(POSTIT_RID)
POSTIT_APK := $(POSTIT_APK_DIR)/$(ANDROID_PACKAGE_NAME)-Signed.apk
qemu-run:
@echo " Starting AVD $(AVD_NAME) on $(ADB_SERIAL)..."
@mkdir -p /tmp/yavsc-emu
@EMU_ARGS=""; \
if [ "$(EMU_HEADLESS)" = "1" ]; then EMU_ARGS="-no-window -no-audio"; fi; \
$(ANDROID_HOME)/emulator/emulator -avd $(AVD_NAME) $$EMU_ARGS \
>/tmp/yavsc-emu/$(AVD_NAME).log 2>&1 & \
echo " emulator PID: $$!"
qemu-stop:
adb -s $(ADB_SERIAL) emu kill
qemu-wait-boot:
@echo " Waiting for $(ADB_SERIAL) to finish booting..."
adb -s $(ADB_SERIAL) wait-for-device
@for i in $$(seq 1 180); do \
BOOTED=$$(adb -s $(ADB_SERIAL) shell getprop sys.boot_completed 2>/dev/null | tr -d '\r\n'); \
if [ "$$BOOTED" = "1" ]; then \
echo " ✓ booted in $${i}s"; \
exit 0; \
fi; \
sleep 1; \
done; \
echo " ERROR: device did not boot within 180s." >&2; \
echo " Logs: /tmp/yavsc-emu/$(AVD_NAME).log" >&2; \
exit 1
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
# runtime cannot find them in `files/.__override__/<rid>/` and
# aborts at startup with "No assemblies found in '.__override__'"
# (monodroid-glue.cc:757, SIGABRT). Forcing this property on
# packages the .dlls into the APK as `assemblies/<rid>/` so the
# runtime reads them directly.
#
# The Xamarin.Android SDK property is `EmbedAssembliesIntoApk`,
# not `AndroidEnableFastDeployment` (which exists in older
# templates but is a no-op in the .NET 10 SDK).
dotnet build $(POSTIT_ANDROID_CSPROJ) \
-c $(CONFIG) \
-p:RuntimeIdentifier=$(POSTIT_RID) \
-p:EmbedAssembliesIntoApk=true \
--nologo
@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
qemu-uninstall:
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 $(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.
# 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".
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)."; \
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 $(ANDROID_PACKAGE_NAME); \
else \
adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID $(ANDROID_PACKAGE_NAME); \
fi
# Clear logcat, launch PostIt.Android, then dump everything that was
# emitted during the startup window. Targets the "démarrage KO" case
# where the process starts but Avalonia never renders a frame — the
# logcat trace from process start to first frame is what diagnoses it.
#
# 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
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
@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'); \
if [ -n "$$PID" ]; then \
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)"; \
adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES); \
exit 1; \
fi
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 qemu-build qemu-install qemu-logcat qemu-logcat-boot

View file

@ -5,33 +5,35 @@
<!-- https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management --> <!-- https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management -->
<PropertyGroup> <PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AvaloniaVersionBase>12.1.1</AvaloniaVersionBase>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<!-- Avalonia packages --> <!-- Avalonia packages -->
<!-- Important: keep version in sync! --> <!-- Important: keep version in sync! -->
<PackageVersion Include="Avalonia" Version="12.1.1" /> <PackageVersion Include="Avalonia" Version="$(AvaloniaVersionBase)" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.1" /> <PackageVersion Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersionBase)" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.1.1" /> <PackageVersion Include="Avalonia.Desktop" Version="$(AvaloniaVersionBase)" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.1" /> <PackageVersion Include="Avalonia.Browser" Version="$(AvaloniaVersionBase)" />
<PackageVersion Include="Avalonia.Browser" Version="12.1.1" /> <PackageVersion Include="Avalonia.Android" Version="$(AvaloniaVersionBase)" />
<PackageVersion Include="Avalonia.Android" Version="12.1.1" /> <PackageVersion Include="Avalonia.Headless" Version="$(AvaloniaVersionBase)" />
<PackageVersion Include="Avalonia.Headless" Version="12.1.1" /> <PackageVersion Include="Avalonia.Headless.Xunit" Version="$(AvaloniaVersionBase)" />
<PackageVersion Include="Avalonia.Headless.Xunit" Version="12.1.1" /> <PackageVersion Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersionBase)" />
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Material.Avalonia" Version="3.19.0" /> <PackageVersion Include="Material.Avalonia" Version="3.19.0" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" /> <PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersionBase)" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" /> <PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageVersion Include="Xamarin.AndroidX.Browser" Version="1.10.0.1" /> <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.Core.SplashScreen" Version="1.2.0" />
<PackageVersion Include="Xamarin.AndroidX.Lifecycle.Common" Version="2.11.0.1" />
<PackageVersion Include="Xamarin.AndroidX.Lifecycle.Runtime" Version="2.10.0.1" />
<PackageVersion Include="Xamarin.AndroidX.Lifecycle.Common" Version="2.10.0.1" />
<PackageVersion Include="Xamarin.UITest" Version="4.4.2" /> <PackageVersion Include="Xamarin.UITest" Version="4.4.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" />
<PackageVersion Include="Microsoft.Maui.Essentials" Version="10.0.100" />
</ItemGroup> </ItemGroup>
</Project> </Project>

178
src/PostIt/Makefile Normal file
View file

@ -0,0 +1,178 @@
# 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
#
# Variables surchargeables (make VAR=valeur) :
# AVD_NAME default: postit_test_avd
# (l'AVD doit être listé par `avdmanager list avd`)
# ADB_SERIAL default: emulator-5554
# (port standard du premier émulateur lancé)
# ANDROID_HOME default: /opt/android-sdk
# (le SDK Android local; doit contenir
# emulator/emulator et platform-tools/adb)
# POSTIT_RID default: android-x64
# (doit matcher l'ABI de l'AVD; `avdmanager list avd`
# affiche la ligne Tag/ABI)
# EMU_HEADLESS default: 0
# (1 = lancer l'émulateur sans fenêtre, pour scripter)
# CONFIG surcharge la variable CONFIG globale (Debug par
# défaut dans ce Makefile). Passer à Release pour
# un APK optimisé et signé release.
# LOGCAT_LINES default: 200
# (nombre de lignes dumpées par `make qemu-logcat`)
# LOGCAT_FOLLOW default: 0
# (1 = stream live via `make logcat`,
# sinon dump one-shot des N dernières lignes)
# LOGCAT_BOOT_WAIT default: 30
# (secondes d'attente entre le clear du buffer,
# le `am start`, et le dump final dans
# `make qemu-logcat-boot`)
AVD_NAME ?= postit_test_avd
ADB_SERIAL ?= emulator-5554
ANDROID_HOME ?= /opt/android-sdk
POSTIT_RID ?= android-x64
EMU_HEADLESS ?= 0
LOGCAT_LINES ?= 600
LOGCAT_FOLLOW ?= 0
LOGCAT_BOOT_WAIT ?= 30
ANDROID_PACKAGE_NAME = fr.pschneider.postit
POSTIT_ANDROID_CSPROJ := PostIt.Android/PostIt.Android.csproj
POSTIT_APK_DIR := PostIt.Android/bin/$(CONFIG)/net10.0-android/$(POSTIT_RID)
POSTIT_APK := $(POSTIT_APK_DIR)/$(ANDROID_PACKAGE_NAME)-Signed.apk
clean: clean-PostIt clean-PostIt.Android clean-PostIt.Desktop
clean-%:
rm -rf $*/obj $*/bin
qemu-run:
@echo " Starting AVD $(AVD_NAME) on $(ADB_SERIAL)..."
@mkdir -p /tmp/yavsc-emu
@EMU_ARGS=""; \
if [ "$(EMU_HEADLESS)" = "1" ]; then EMU_ARGS="-no-window -no-audio"; fi; \
$(ANDROID_HOME)/emulator/emulator -avd $(AVD_NAME) $$EMU_ARGS \
>/tmp/yavsc-emu/$(AVD_NAME).log 2>&1 & \
echo " ✅ Started emulator PID: $$!"
qemu-stop:
adb -s $(ADB_SERIAL) emu kill
echo " ✅ Stopped emulator"
qemu-wait-boot:
@echo " Waiting for $(ADB_SERIAL) to finish booting..."
adb -s $(ADB_SERIAL) wait-for-device
@for i in $$(seq 1 180); do \
BOOTED=$$(adb -s $(ADB_SERIAL) shell getprop sys.boot_completed 2>/dev/null | tr -d '\r\n'); \
if [ "$$BOOTED" = "1" ]; then \
echo " ✓ booted in $${i}s"; \
exit 0; \
fi; \
sleep 1; \
done; \
echo " 👿 ERROR: device did not boot within 180s." >&2; \
echo " Logs: /tmp/yavsc-emu/$(AVD_NAME).log" >&2; \
exit 1
android-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
# runtime cannot find them in `files/.__override__/<rid>/` and
# aborts at startup with "No assemblies found in '.__override__'"
# (monodroid-glue.cc:757, SIGABRT). Forcing this property on
# packages the .dlls into the APK as `assemblies/<rid>/` so the
# runtime reads them directly.
#
# The Xamarin.Android SDK property is `EmbedAssembliesIntoApk`,
# not `AndroidEnableFastDeployment` (which exists in older
# templates but is a no-op in the .NET 10 SDK).
dotnet build $(POSTIT_ANDROID_CSPROJ) \
-c $(CONFIG) \
-p:RuntimeIdentifier=$(POSTIT_RID) \
-p:EmbedAssembliesIntoApk=true \
--nologo
@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
android-install: android-build
@echo " Installing $(POSTIT_APK) on $(ADB_SERIAL)..."
adb -s $(ADB_SERIAL) install -r "$(POSTIT_APK)" -r
@echo " ✅ PostIt.Android installed on $(ADB_SERIAL)"
qemu-uninstall:
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 $(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.
# 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:
@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)."; \
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 $(ANDROID_PACKAGE_NAME); \
else \
adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID $(ANDROID_PACKAGE_NAME); \
fi
# Clear logcat, launch PostIt.Android, then dump everything that was
# emitted during the startup window. Targets the "démarrage KO" case
# where the process starts but Avalonia never renders a frame — the
# logcat trace from process start to first frame is what diagnoses it.
#
# 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:
@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
@echo "$(ANDROID_PACKAGE_NAME) started on $(ADB_SERIAL)"
qemu-logcat-boot: android-start
@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'); \
if [ -n "$$PID" ]; then \
echo " ✅ (PID $$PID at dump time)"; \
sleep 10; \
adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID; \
else \
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 android-install
.PHONY: clean qemu qemu-run qemu-stop qemu-wait-boot android-build android-install logcat qemu-logcat-boot

View file

@ -1,8 +1,11 @@
using Android.App; 
using Android.App;
using Android.Content; using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Avalonia; using AndroidX.Core.Provider;
using AndroidX.Emoji2.Text;
using Avalonia.Android; using Avalonia.Android;
using PostIt.Droid.Services;
namespace PostIt.Android; namespace PostIt.Android;
@ -16,17 +19,21 @@ namespace PostIt.Android;
public class MainActivity : AvaloniaMainActivity public class MainActivity : AvaloniaMainActivity
{ {
/// <summary> /// <summary>
/// Strongly-typed handle to the current MainActivity instance, set in /// The current MainActivity instance.
/// <see cref="OnCreate"/> and consumed by platform services such as
/// <see cref="Services.AndroidSystemBrowser"/> which need to launch
/// Chrome Custom Tabs.
/// </summary> /// </summary>
public static MainActivity? Current { get; private set; } public static MainActivity? Current { get; private set; }
protected override void OnCreate(global::Android.OS.Bundle? savedInstanceState) 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.InitPlatform();
base.OnCreate(savedInstanceState); base.OnCreate(savedInstanceState);
PlatformBootstrap.EnsureInitialized();
Current = this; Current = this;
} }
/// <summary> /// <summary>
@ -41,7 +48,13 @@ public class MainActivity : AvaloniaMainActivity
protected override void OnNewIntent(Intent? intent) protected override void OnNewIntent(Intent? intent)
{ {
base.OnNewIntent(intent); base.OnNewIntent(intent);
if (intent is not null) AndroidOidcCallbackSink.Handle(intent);
var url = intent?.DataString;
if (!string.IsNullOrEmpty(url) && url.StartsWith("postit://callback"))
{
OidcCallbackManager.SetResult(url);
}
} }
internal static class AndroidOidcCallbackSink internal static class AndroidOidcCallbackSink

View file

@ -12,14 +12,9 @@ namespace PostIt.Android;
/// </summary> /// </summary>
internal static class PlatformBootstrap internal static class PlatformBootstrap
{ {
private static int _initialized; internal static void InitPlatform()
internal static void EnsureInitialized()
{ {
if (System.Threading.Interlocked.Exchange(ref _initialized, 1) != 0)
return;
Platform.DefaultRedirectUri = ViewModels.Settings.AndroidRedirectUri;
Platform.CreateBrowser = () => Platform.CreateBrowser = () =>
{ {
var activity = MainActivity.Current; var activity = MainActivity.Current;

View file

@ -4,13 +4,11 @@
<TargetFramework>net10.0-android</TargetFramework> <TargetFramework>net10.0-android</TargetFramework>
<SupportedOSPlatformVersion>23</SupportedOSPlatformVersion> <SupportedOSPlatformVersion>23</SupportedOSPlatformVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ApplicationId>fr.pschneider.PostIt</ApplicationId> <ApplicationId>fr.pschneider.postit</ApplicationId>
<ApplicationVersion>1</ApplicationVersion> <ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<AndroidPackageFormat>apk</AndroidPackageFormat> <AndroidPackageFormat>apk</AndroidPackageFormat>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot> <AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<AndroidLinkMode>SdkOnly</AndroidLinkMode>
<TrimMode>partial</TrimMode>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -23,11 +21,11 @@
<PackageReference Include="Avalonia.Android" /> <PackageReference Include="Avalonia.Android" />
<PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" /> <PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" />
<PackageReference Include="Xamarin.AndroidX.Browser" /> <PackageReference Include="Xamarin.AndroidX.Browser" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Runtime" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Common" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\PostIt\PostIt.csproj" /> <ProjectReference Include="..\PostIt\PostIt.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -2,31 +2,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application android:label="PostIt" android:icon="@drawable/Icon"> <application android:label="PostIt" android:icon="@drawable/Icon">
<!--
Deep-link receiver for the OIDC Authorization Code + PKCE flow.
After the user authenticates in the system browser, the OP
redirects to android://postit-signin?... and Android forwards
the Intent to the MainActivity (configured SingleTask so the
existing instance receives OnNewIntent rather than spawning a
new one).
The host value (postit-signin) MUST match the
AndroidRedirectUri constant in PostIt/Settings/Settings.cs and
the corresponding RedirectUri registered for the 'postit'
client in IdentityServer (Yavsc.Org ConfigurationDb).
-->
<activity-alias
android:name="PostIt.Android.OidcCallbackActivity"
android:targetActivity="PostIt.Android.PostItMainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="android" android:host="postit-signin" />
</intent-filter>
</activity-alias>
</application> </application>
</manifest> </manifest>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="com_google_android_gms_fonts_certs">
<item>@array/com_google_android_gms_fonts_certs_dev</item>
<item>@array/com_google_android_gms_fonts_certs_prod</item>
</array>
<string-array name="com_google_android_gms_fonts_certs_dev">
<item>MIIEqDCCA5CgAwIBAgIJAN5gc16AJfAsMA0GCSqGSIb3DQEBBQUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHR29vZ2xlMRAwDgYDVQQLEwdBbmRyb2lkMRAwDgYDVQQDEwdBbmRyb2lkMSEwHwYJKoZIhvcNAQkBFhJhbmRyb2lkQGFuZHJvaWQuY29tMCAXDTA4MDQxNTIyNDA0M1YYDzQyMDgxMzA0MjI0MDQzWjCBlDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDURvdW50YWluIFZpZXcxEDAOBgNVBAoTB0dvb2dsZTEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEhMB8GCSqGSIb3DQEJARYSYW5kcm9pZEBhbmRyb2lkLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALBi1vF0K1vOEHG7AxneTjOHUka46MIidBqvFcO164A49iU2DkYPhUaM4H8JCdzh6N1GzM6h9o6E2V6z8+gEtdI6nqqs0EGA0G0H701bFjLp9+K/1DkMIFeD4P8J7X1/M8t4+X09X/7bQyV3w0v7q+Qh38sY8W/7K29B3f2O2sLw+uX9U8a8Tf4Xv8A==</item>
</string-array>
<string-array name="com_google_android_gms_fonts_certs_prod">
<item>MIIEQzCCAyugAwIBAgIJAMLgh0ZgXpYOMA0GCSqGSIb3DQEBBQUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKKvSkUIXm+t9M8rXj2V</item>
</string-array>
</resources>

View file

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Android.App; using Android.App;
using AndroidX.Browser.CustomTabs; using AndroidX.Browser.CustomTabs;
using IdentityModel.OidcClient.Browser; using IdentityModel.OidcClient.Browser;
using PostIt.Droid.Services;
namespace PostIt.Android.Services; namespace PostIt.Android.Services;
@ -35,9 +36,14 @@ public sealed class AndroidSystemBrowser : IBrowser
}; };
} }
var uri = global::Android.Net.Uri.Parse(options.StartUrl)!; // 1. Enregistrez la tâche avant de lancer le Custom Tab
var callbackTask = OidcCallbackManager.RegisterCallback(cancellationToken);
var callbackTask = MainActivity.AndroidOidcCallbackSink.AwaitNextCallbackAsync(); // 2. LANCEZ VOTRE CUSTOM TAB ICI (via AndroidX.Browser.CustomTabs)
// ... code pour ouvrir l'URL d'authentification ...
var uri = global::Android.Net.Uri.Parse(options.StartUrl)!;
var tabsIntent = new CustomTabsIntent.Builder() var tabsIntent = new CustomTabsIntent.Builder()
.SetShowTitle(true)! .SetShowTitle(true)!

View file

@ -0,0 +1,21 @@
using System.Threading;
using System.Threading.Tasks;
namespace PostIt.Droid.Services;
public static class OidcCallbackManager
{
private static TaskCompletionSource<string>? _tcs;
public static Task<string> RegisterCallback(CancellationToken cancellationToken)
{
_tcs = new TaskCompletionSource<string>();
cancellationToken.Register(() => _tcs.TrySetCanceled());
return _tcs.Task;
}
public static void SetResult(string url)
{
_tcs?.TrySetResult(url);
}
}

View file

@ -0,0 +1,35 @@
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using PostIt.Droid.Services;
namespace PostIt.Android;
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true)]
[IntentFilter(new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataScheme = "postit", // Remplacez par votre schéma personnalisé (ex: yavsc ou postit)
DataHost = "callback")] // Correspond à postit://callback
public class WebAuthenticationCallbackActivity : Activity
{
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
// Capturer l'URL de redirection OIDC
var url = Intent?.DataString;
if (!string.IsNullOrEmpty(url))
{
// Transmettre l'URL au gestionnaire partagé pour compléter la Task
OidcCallbackManager.SetResult(url);
}
// Fermer cette activité transparente et ramener l'application au premier plan
var intent = new Intent(this, typeof(MainActivity));
intent.AddFlags(ActivityFlags.ClearTop | ActivityFlags.SingleTop);
StartActivity(intent);
Finish();
}
}

View file

@ -6,7 +6,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion> <FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3</InformationalVersion> <InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<Version>1.1.0-beta.1</Version> <Version>1.1.0-beta.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,29 +0,0 @@
using PostIt.Services;
namespace PostIt.Desktop;
/// <summary>
/// One-shot platform bootstrap. Called from <c>Program.Main</c> so that
/// the shared OIDC login path sees a working <c>IBrowser</c> — the
/// custom-scheme browser that hands the OIDC callback off to the
/// running instance through the named pipe. Desktop builds do NOT use
/// a loopback HTTP listener: the <c>postit://</c> scheme is registered
/// with the OS at install time and the browser is whatever the user
/// has configured to open it.
/// </summary>
internal static class PlatformBootstrap
{
private static int _initialized;
internal static void EnsureInitialized()
{
if (System.Threading.Interlocked.Exchange(ref _initialized, 1) != 0)
return;
// Use the custom-scheme redirect on Desktop. Loopback is only
// a fallback for platforms that cannot register postit://
// (see Settings.DefaultLoopbackRedirectUri for that path).
Platform.DefaultRedirectUri = AuthenticationSettings.DefaultDesktopRedirectUri;
Platform.CustomScheme = "postit";
}
}

View file

@ -7,7 +7,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion> <FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3</InformationalVersion> <InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<Version>1.1.0-beta.1</Version> <Version>1.1.0-beta.1</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View file

@ -12,8 +12,6 @@ sealed class Program
[STAThread] [STAThread]
public static void Main(string[] args) public static void Main(string[] args)
{ {
PlatformBootstrap.EnsureInitialized();
// Short-circuit 2nd-instance launches (OS handing us the // Short-circuit 2nd-instance launches (OS handing us the
// postit://callback URL) BEFORE Avalonia spins up a window. // postit://callback URL) BEFORE Avalonia spins up a window.
// If we let Avalonia initialise, the new MainWindow flashes // If we let Avalonia initialise, the new MainWindow flashes

View file

@ -73,7 +73,7 @@ public class AddCircleMemberDialogTests
return context; return context;
} }
/// <summary> /// <summary>
/// Mount a real <see cref="MainWindow"/>, build a minimal /// Mount a real <see cref="MainView"/>, build a minimal
/// DI graph, push <see cref="CirclesPage"/> then the /// DI graph, push <see cref="CirclesPage"/> then the
/// <see cref="AddCircleMemberDialog"/> on top of it. /// <see cref="AddCircleMemberDialog"/> on top of it.
/// Returns the stack size so the test can pin the delta. /// Returns the stack size so the test can pin the delta.
@ -98,12 +98,9 @@ public class AddCircleMemberDialogTests
services.AddTransient<AddCircleMemberDialogViewModel>(); services.AddTransient<AddCircleMemberDialogViewModel>();
var sp = services.BuildServiceProvider(); var sp = services.BuildServiceProvider();
context.Window = new MainWindow(); context.Window = new MainView();
context.App = (PostIt.App)Application.Current!; context.App = (PostIt.App)Application.Current!;
context.App.DataTemplates.Clear();
context.App.DataTemplates.Add(new ViewLocator(sp));
context.App.AttachMainWindow(context.Window); context.App.AttachMainWindow(context.Window);
context.Window.Show();
context.page = sp.GetRequiredService<CirclesPage>(); context.page = sp.GetRequiredService<CirclesPage>();
context.Window.NavRoot.PushAsync(context.page).GetAwaiter().GetResult(); context.Window.NavRoot.PushAsync(context.page).GetAwaiter().GetResult();

View file

@ -14,7 +14,7 @@ namespace PostIt.Tests;
/// </summary> /// </summary>
public class AndroidAppLaunchTests public class AndroidAppLaunchTests
{ {
private const string PackageName = "fr.pschneider.PostIt"; private const string PackageName = "fr.pschneider.postit";
private readonly ITestOutputHelper _output; private readonly ITestOutputHelper _output;
@ -23,7 +23,8 @@ public class AndroidAppLaunchTests
_output = output; _output = output;
} }
// FIXME [Fact] // https://twosixtech.com/blog/integrating-docker-and-adb/
// FIXME ala hosted shared resource adb server - [Fact]
public void PostIt_starts_and_draws_a_first_frame_on_the_emulator() public void PostIt_starts_and_draws_a_first_frame_on_the_emulator()
{ {
if (!IsPackageInstalledOnAnyDevice()) if (!IsPackageInstalledOnAnyDevice())

View file

@ -99,7 +99,7 @@ public class MainPageButtonsTests
} }
/// <summary> /// <summary>
/// Mount a real <see cref="MainWindow"/> (as /// Mount a real <see cref="MainView"/> (as
/// <c>SessionStatusBannerTests</c> does), push a /// <c>SessionStatusBannerTests</c> does), push a
/// <see cref="MainPage"/> with the given VM onto /// <see cref="MainPage"/> with the given VM onto
/// <c>NavRoot</c>. <c>PushAsync</c> is awaited (via /// <c>NavRoot</c>. <c>PushAsync</c> is awaited (via
@ -109,18 +109,12 @@ public class MainPageButtonsTests
/// realised and <c>KeyPressQwerty</c> has a real /// realised and <c>KeyPressQwerty</c> has a real
/// <see cref="TopLevel"/> to dispatch against. /// <see cref="TopLevel"/> to dispatch against.
/// </summary> /// </summary>
private static (MainWindow window, MainPage page) MountMainPage(MainViewModel vm) private static (MainView window, MainPage page) MountMainPage(MainViewModel vm)
{ {
var window = new MainWindow(); var window = new MainView();
var page = new MainPage { DataContext = vm }; var page = new MainPage { DataContext = vm };
var app = (PostIt.App)Application.Current!; var app = (PostIt.App)Application.Current!;
if (vm.Services is not null)
{
app.DataTemplates.Clear();
app.DataTemplates.Add(new ViewLocator(vm.Services));
}
app.AttachMainWindow(window); app.AttachMainWindow(window);
window.Show();
window.NavRoot.PushAsync(page).GetAwaiter().GetResult(); window.NavRoot.PushAsync(page).GetAwaiter().GetResult();
return (window, page); return (window, page);
} }
@ -130,7 +124,7 @@ public class MainPageButtonsTests
/// supported headless pattern (cf. CalculatorTests in the /// supported headless pattern (cf. CalculatorTests in the
/// Avalonia.Samples repo). Returns the nav-stack count /// Avalonia.Samples repo). Returns the nav-stack count
/// before the click so the caller can assert on the delta. /// before the click so the caller can assert on the delta.
/// KeyPressQwerty is dispatched on the <see cref="MainWindow"/> /// KeyPressQwerty is dispatched on the <see cref="MainView"/>
/// itself — it is the <see cref="TopLevel"/> that owns the /// itself — it is the <see cref="TopLevel"/> that owns the
/// headless implementation, and routing the key through any /// headless implementation, and routing the key through any
/// descendant TopLevel (e.g. one obtained via /// descendant TopLevel (e.g. one obtained via
@ -139,7 +133,7 @@ public class MainPageButtonsTests
/// because the descendant does not carry the /// because the descendant does not carry the
/// <c>PlatformHandle</c> the harness expects. /// <c>PlatformHandle</c> the harness expects.
/// </summary> /// </summary>
private static int ClickAndCapture(MainWindow window, Button button) private static int ClickAndCapture(MainView window, Button button)
{ {
var stackBefore = window.NavRoot.NavigationStack.Count; var stackBefore = window.NavRoot.NavigationStack.Count;
button.Command?.Execute(button.CommandParameter); button.Command?.Execute(button.CommandParameter);

View file

@ -117,7 +117,7 @@ public class PostAclDialogTests
/// rebinding the global DI mid-test would trample the /// rebinding the global DI mid-test would trample the
/// Settings singleton the rest of the harness depends on. /// Settings singleton the rest of the harness depends on.
/// </summary> /// </summary>
private static (MainWindow window, BlogAclApiClient aclClient, CircleApiClient circleClient, CountingHttpHandler handler) Mount() private static (MainView window, BlogAclApiClient aclClient, CircleApiClient circleClient, CountingHttpHandler handler) Mount()
{ {
var handler = new CountingHttpHandler(); var handler = new CountingHttpHandler();
var settings = new Settings(); var settings = new Settings();
@ -138,12 +138,9 @@ public class PostAclDialogTests
// CountingHttpHandler. // CountingHttpHandler.
GC.KeepAlive(sp); GC.KeepAlive(sp);
var window = new MainWindow(); var window = new MainView();
var app = (App)Application.Current!; var app = (App)Application.Current!;
app.DataTemplates.Clear();
app.DataTemplates.Add(new ViewLocator(sp));
app.AttachMainWindow(window); app.AttachMainWindow(window);
window.Show();
return (window, aclClient, circleClient, handler); return (window, aclClient, circleClient, handler);
} }

View file

@ -8,7 +8,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion> <FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3</InformationalVersion> <InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<Version>1.1.0-beta.1</Version> <Version>1.1.0-beta.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -9,7 +9,7 @@ namespace PostIt.Tests;
/// <summary> /// <summary>
/// UI tests for <see cref="SessionStatusBanner"/>. Mounted inside /// UI tests for <see cref="SessionStatusBanner"/>. Mounted inside
/// a real <see cref="MainWindow"/> via the headless Avalonia /// a real <see cref="MainView"/> via the headless Avalonia
/// platform declared in <c>TestApp.cs</c>. /// platform declared in <c>TestApp.cs</c>.
/// ///
/// <para>The pattern is the one that <c>UnitTest1.MainPage_Should_Load</c> /// <para>The pattern is the one that <c>UnitTest1.MainPage_Should_Load</c>
@ -34,11 +34,10 @@ public class SessionStatusBannerTests
[AvaloniaFact] [AvaloniaFact]
public void Banner_renders_three_buttons_in_the_visual_tree() public void Banner_renders_three_buttons_in_the_visual_tree()
{ {
var window = new MainWindow(); MainWindow window = new MainWindow();
window.SessionBanner.DataContext = new SessionStatusViewModel();
window.Show(); window.Show();
var buttons = window.SessionBanner.GetVisualDescendants() var buttons = window.GetVisualDescendants()
.OfType<Button>() .OfType<Button>()
.ToList(); .ToList();
@ -46,7 +45,7 @@ public class SessionStatusBannerTests
// déconnecter, Se connecter, Paramètres. If any one is // déconnecter, Se connecter, Paramètres. If any one is
// missing, the user has no way to trigger the // missing, the user has no way to trigger the
// corresponding navigation event. // corresponding navigation event.
Assert.Equal(3, buttons.Count); Assert.Equal(4, buttons.Count);
Assert.Contains(buttons, b => b.Content as string == "Se déconnecter"); Assert.Contains(buttons, b => b.Content as string == "Se déconnecter");
Assert.Contains(buttons, b => b.Content as string == "Se connecter"); Assert.Contains(buttons, b => b.Content as string == "Se connecter");
Assert.Contains(buttons, b => b.Content as string == "Paramètres"); Assert.Contains(buttons, b => b.Content as string == "Paramètres");
@ -55,13 +54,10 @@ public class SessionStatusBannerTests
[AvaloniaFact] [AvaloniaFact]
public void Banner_login_button_is_visible_when_logged_out() public void Banner_login_button_is_visible_when_logged_out()
{ {
var window = new MainWindow(); MainWindow window = new MainWindow();
var vm = new SessionStatusViewModel();
Assert.True(vm.IsLoggedOut); // VM default
window.SessionBanner.DataContext = vm;
window.Show(); window.Show();
var login = window.SessionBanner.GetVisualDescendants() var login = window.GetVisualDescendants()
.OfType<Button>() .OfType<Button>()
.Single(b => b.Content as string == "Se connecter"); .Single(b => b.Content as string == "Se connecter");
@ -73,13 +69,13 @@ public class SessionStatusBannerTests
[AvaloniaFact] [AvaloniaFact]
public void Banner_logout_button_is_hidden_when_logged_out() public void Banner_logout_button_is_hidden_when_logged_out()
{ {
var window = new MainWindow(); SessionStatusBanner banner = CreateBanner();
var vm = new SessionStatusViewModel();
Assert.False(vm.IsLoggedIn); // VM default
window.SessionBanner.DataContext = vm;
window.Show();
var logout = window.SessionBanner.GetVisualDescendants() Assert.False((banner.DataContext as SessionStatusViewModel)!
.IsLoggedIn); // VM default
var logout = banner.GetVisualDescendants()
.OfType<Button>() .OfType<Button>()
.Single(b => b.Content as string == "Se déconnecter"); .Single(b => b.Content as string == "Se déconnecter");
@ -89,11 +85,9 @@ public class SessionStatusBannerTests
[AvaloniaFact] [AvaloniaFact]
public void Banner_settings_button_is_visible_regardless_of_session() public void Banner_settings_button_is_visible_regardless_of_session()
{ {
var window = new MainWindow(); SessionStatusBanner banner = CreateBanner();
window.SessionBanner.DataContext = new SessionStatusViewModel();
window.Show();
var settings = window.SessionBanner.GetVisualDescendants() var settings = banner.GetVisualDescendants()
.OfType<Button>() .OfType<Button>()
.Single(b => b.Content as string == "Paramètres"); .Single(b => b.Content as string == "Paramètres");
@ -103,14 +97,21 @@ public class SessionStatusBannerTests
Assert.True(settings.IsVisible); Assert.True(settings.IsVisible);
} }
private static SessionStatusBanner CreateBanner()
{
MainWindow window = new MainWindow();
window.Show();
var banner = window.MainView.SessionBanner;
var status = new SessionStatusViewModel();
banner.DataContext = status;
return banner;
}
[AvaloniaFact] [AvaloniaFact]
public void Banner_session_label_reflects_DataContext() public void Banner_session_label_reflects_DataContext()
{ {
var window = new MainWindow(); SessionStatusBanner banner = CreateBanner();
window.SessionBanner.DataContext = new SessionStatusViewModel(); var label = banner.GetVisualDescendants()
window.Show();
var label = window.SessionBanner.GetVisualDescendants()
.OfType<TextBlock>() .OfType<TextBlock>()
.First(t => t.Text == "Déconnecté" || t.Text == "Connecté"); .First(t => t.Text == "Déconnecté" || t.Text == "Connecté");

View file

@ -85,7 +85,7 @@ public class SettingsLoadTests
bool flip = ((workerId + i) & 1) == 0; bool flip = ((workerId + i) & 1) == 0;
settings.DarkMode = flip; settings.DarkMode = flip;
settings.Authentication.RedirectUri = settings.Authentication.RedirectUri =
global::AuthenticationSettings.DefaultDesktopRedirectUri; global::AuthenticationSettings.DesktopRedirectUri;
settings.BusinessApiUrl = flip settings.BusinessApiUrl = flip
? "https://a.example.test/api/v1/" ? "https://a.example.test/api/v1/"

View file

@ -4,7 +4,7 @@ namespace PostIt.Tests;
internal class TestAppContext internal class TestAppContext
{ {
public MainWindow? Window {get; set; } public MainView? Window {get; set; }
public CirclesPage? page {get; set; } public CirclesPage? page {get; set; }
public AddCircleMemberDialog? dialog { get; set; } public AddCircleMemberDialog? dialog { get; set; }
public App? App { get; internal set; } public App? App { get; internal set; }

View file

@ -8,8 +8,7 @@ public class MainPageTests
[AvaloniaFact] [AvaloniaFact]
public void MainPage_Should_Load() public void MainPage_Should_Load()
{ {
var window = new MainWindow(); var window = new MainView();
window.Show();
Assert.NotNull(window); Assert.NotNull(window);
} }
} }

View file

@ -11,6 +11,5 @@
<Application.Styles> <Application.Styles>
<FluentTheme /> <FluentTheme />
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
</Application.Styles> </Application.Styles>
</Application> </Application>

View file

@ -1,5 +1,4 @@
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
@ -28,14 +27,11 @@ public partial class App : Application
/// </summary> /// </summary>
public IServiceProvider? ServiceProvider { get; private set; } public IServiceProvider? ServiceProvider { get; private set; }
public MainWindow? Window { get; private set; } public MainView? View { get; private set; }
public override void Initialize() public override void Initialize()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
#if DEBUG
this.AttachDeveloperTools();
#endif
} }
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
@ -47,7 +43,13 @@ public partial class App : Application
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
desktop.MainWindow = CreateMainWindow(); var window = ServiceProvider.GetRequiredService<MainWindow>();
desktop.MainWindow = window;
var api = ServiceProvider!.GetRequiredService<YavscApiClient>();
desktop.MainWindow.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api);
View = window.MainView;
this.ConfigureRootView(window.MainView);
ApplyDarkMode(settings); ApplyDarkMode(settings);
} }
else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
@ -55,53 +57,53 @@ public partial class App : Application
singleViewFactoryApplicationLifetime.MainViewFactory = singleViewFactoryApplicationLifetime.MainViewFactory =
() => () =>
{ {
Window = CreateMainWindow(); View = ServiceProvider.GetRequiredService<MainView>();
this.ConfigureRootView(View);
ApplyDarkMode(settings); ApplyDarkMode(settings);
return Window; return View;
}; };
} }
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{ {
singleViewPlatform.MainView = CreateMainWindow(); singleViewPlatform.MainView = View = ServiceProvider.GetRequiredService<MainView>();
ConfigureRootView(View);
ApplyDarkMode(settings); ApplyDarkMode(settings);
} }
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }
private void ConfigureRootView(MainView rootView)
private MainWindow CreateMainWindow()
{ {
Window = new MainWindow();
var api = ServiceProvider!.GetRequiredService<YavscApiClient>(); var api = ServiceProvider!.GetRequiredService<YavscApiClient>();
Window.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api);
// Déclencher le Boot lors du chargement du contrôle à l'écran
rootView.AttachedToVisualTree += async (_, _) => await BootAsync(this.ServiceProvider!, api);
var sessionStatus = ServiceProvider!.GetRequiredService<SessionStatusViewModel>(); var sessionStatus = ServiceProvider!.GetRequiredService<SessionStatusViewModel>();
sessionStatus.LogoutCompleted += () => sessionStatus.LogoutCompleted += () =>
{ {
Window.NavRoot.PopToRootAsync(); // Remplacer Window.NavRoot par rootView.NavRoot
rootView.NavRoot.PopToRootAsync();
}; };
sessionStatus.LoginSucceeded += () => sessionStatus.LoginSucceeded += async () =>
{ {
PushMainPageAsync().Wait(); await PushMainPageAsync();
}; };
var homeVm = ServiceProvider!.GetRequiredService<HomePageViewModel>(); rootView.SessionBanner.DataContext = sessionStatus;
this.PushPageAsync(homeVm).Wait();
Window.SessionBanner.DataContext = sessionStatus;
return Window;
} }
/// <summary> /// <summary>
/// Test-only hook: bind a concrete <see cref="MainWindow"/> so /// Test-only hook: bind a concrete <see cref="MainView"/> so
/// command-driven navigation paths (<see cref="PushPage"/>) can /// command-driven navigation paths (<see cref="PushPage"/>) can
/// push onto a real <see cref="NavigationPage"/> in headless /// push onto a real <see cref="NavigationPage"/> in headless
/// fixtures that do not run the full desktop lifetime bootstrap. /// fixtures that do not run the full desktop lifetime bootstrap.
/// </summary> /// </summary>
internal void AttachMainWindow(MainWindow mainWindow) internal void AttachMainWindow(MainView mainView)
{ {
Window = mainWindow ?? throw new ArgumentNullException(nameof(mainWindow)); View = mainView ?? throw new ArgumentNullException(nameof(mainView));
} }
private static void ApplyDarkMode(Settings settings) private static void ApplyDarkMode(Settings settings)
@ -125,6 +127,9 @@ public partial class App : Application
var refreshed = await api.TrySilentLoginAsync().ConfigureAwait(true); var refreshed = await api.TrySilentLoginAsync().ConfigureAwait(true);
var sessionStatus = provider.GetRequiredService<SessionStatusViewModel>(); var sessionStatus = provider.GetRequiredService<SessionStatusViewModel>();
sessionStatus.Refresh(); sessionStatus.Refresh();
var homePage = provider.GetRequiredService<HomePageViewModel>();
var app = (App)Current!;
await app.PushPageAsync(homePage);
if (!refreshed) return; if (!refreshed) return;
await PushMainPageAsync().ConfigureAwait(true); await PushMainPageAsync().ConfigureAwait(true);
@ -179,6 +184,6 @@ public partial class App : Application
internal async Task GoBackAsync() internal async Task GoBackAsync()
{ {
await Window!.NavRoot.PopAsync(); await View!.NavRoot.PopAsync();
} }
} }

View file

@ -23,11 +23,13 @@ public static class ServiceCollectionHelpers
var circleClient = new CircleApiClient(api, settings.BlogsApiUrl); var circleClient = new CircleApiClient(api, settings.BlogsApiUrl);
var blogAclClient = new BlogAclApiClient(api, settings.BlogsApiUrl); var blogAclClient = new BlogAclApiClient(api, settings.BlogsApiUrl);
var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl); var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl);
var contactService = new ContactService();
var userDirectory = new UserDirectory(userSearchClient); var userDirectory = new UserDirectory(userSearchClient);
// Vues // Vues
services.AddTransient<MainPage>(); services.AddSingleton<MainView>();
services.AddSingleton<MainPage>();
services.AddSingleton<MainWindow>();
// SettingsPage is a singleton: there must be one and only one // SettingsPage is a singleton: there must be one and only one
// instance of the settings UI for the lifetime of the app. // instance of the settings UI for the lifetime of the app.
// This guarantees that (a) the bindings always reflect the // This guarantees that (a) the bindings always reflect the
@ -58,7 +60,6 @@ public static class ServiceCollectionHelpers
services.AddSingleton(circleClient); services.AddSingleton(circleClient);
services.AddSingleton(blogAclClient); services.AddSingleton(blogAclClient);
services.AddSingleton(userSearchClient); services.AddSingleton(userSearchClient);
services.AddSingleton<IContactService>(contactService);
services.AddSingleton<IUserDirectory>(userDirectory); services.AddSingleton<IUserDirectory>(userDirectory);
services.AddTransient<MainViewModel>(); services.AddTransient<MainViewModel>();
services.AddTransient<HomePageViewModel>(); services.AddTransient<HomePageViewModel>();

View file

@ -10,7 +10,8 @@ public static class ViewModelBaseHelpers
{ {
public static async Task PushPageAsync(this App app, ViewModelBase vm) public static async Task PushPageAsync(this App app, ViewModelBase vm)
{ {
if (app.Window is null) var window = app.View;
if (window is null)
{ {
throw new InvalidOperationException("MainWindow is not initialized yet."); throw new InvalidOperationException("MainWindow is not initialized yet.");
} }
@ -39,12 +40,12 @@ public static class ViewModelBaseHelpers
page.DataContext = vm; page.DataContext = vm;
// Avoid stacking the same singleton page twice (e.g. SettingsPage). // Avoid stacking the same singleton page twice (e.g. SettingsPage).
var stack = app.Window.NavRoot.NavigationStack; var stack = window.NavRoot.NavigationStack;
if (stack.Count > 0 && ReferenceEquals(stack[stack.Count - 1], page)) if (stack.Count > 0 && ReferenceEquals(stack[stack.Count - 1], page))
{ {
return; return;
} }
await app.Window.NavRoot.PushAsync(page); await window.NavRoot.PushAsync(page);
} }
} }

View file

@ -28,7 +28,6 @@
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets> <PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" /> <PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="Avalonia.AvaloniaEdit" />
<PackageReference Include="IdentityModel.OidcClient" /> <PackageReference Include="IdentityModel.OidcClient" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup> </ItemGroup>

View file

@ -1,36 +0,0 @@
#if !ANDROID && !IOS
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace PostIt.Services;
/// <summary>
/// Desktop stub for <see cref="IContactService"/>.
///
/// <para>The desktop has no equivalent of the mobile address
/// book (no <c>Contacts.Default</c>, 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.</para>
///
/// <para>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
/// <see cref="IContactService"/>. Finding existing Yavsc
/// members is <see cref="IUserDirectory"/>'s job, not this
/// one's.</para>
///
/// <para>Future CardDAV / Google Contacts / Exchange
/// providers can plug in here as additional
/// <see cref="IContactService"/> implementations selected
/// from DI by configuration.</para>
/// </summary>
public sealed class ContactService : IContactService
{
public Task<IReadOnlyList<ContactDto>> GetDeviceContactsAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<ContactDto>>(Array.Empty<ContactDto>());
}
#endif

View file

@ -21,14 +21,14 @@ public static class Platform
/// override this property at startup (e.g. PostIt.Android sets /// override this property at startup (e.g. PostIt.Android sets
/// it to <c>android://postit-signin</c>). /// it to <c>android://postit-signin</c>).
/// </summary> /// </summary>
public static string DefaultRedirectUri { get; set; } = "postit://callback"; public const string RedirectUri = "postit://callback";
/// <summary> /// <summary>
/// Scheme prefix the <see cref="CustomSchemeBrowser"/> matches /// Scheme prefix the <see cref="CustomSchemeBrowser"/> matches
/// against <c>BrowserOptions.EndUrl</c>. Overridable for apps /// against <c>BrowserOptions.EndUrl</c>. Overridable for apps
/// that want to register their own scheme. /// that want to register their own scheme.
/// </summary> /// </summary>
public static string CustomScheme { get; set; } = "postit"; public const string CustomScheme = "postit";
/// <summary> /// <summary>
/// Constructs a fresh <see cref="IBrowser"/> for the running platform. /// Constructs a fresh <see cref="IBrowser"/> for the running platform.

View file

@ -10,7 +10,7 @@ public partial class AuthenticationSettings : ObservableObject
/// hand-off in <see cref="PostIt.Services.SingleInstance"/> /// hand-off in <see cref="PostIt.Services.SingleInstance"/>
/// (RFC 8252 §7.1). Production Desktop builds use this. /// (RFC 8252 §7.1). Production Desktop builds use this.
/// </summary> /// </summary>
public const string DefaultDesktopRedirectUri = "postit://callback"; public const string DesktopRedirectUri = "postit://callback";
/// <summary> /// <summary>
/// Redirect URI used by the Android app. The corresponding IntentFilter /// Redirect URI used by the Android app. The corresponding IntentFilter
@ -34,15 +34,19 @@ public partial class AuthenticationSettings : ObservableObject
[ObservableProperty] [ObservableProperty]
public partial string[] Scopes { get; set; } public partial string[] Scopes { get; set; }
/// <summary> /// <summary>
/// OAuth redirect URI. Defaults to <see cref="DefaultDesktopRedirectUri"/> /// OAuth redirect URI. Defaults to <see cref="DesktopRedirectUri"/>
/// (custom URI scheme) which is the right answer for desktop /// (custom URI scheme) which is the right answer for desktop
/// production builds. Mobile platforms must set this to /// production builds. Mobile platforms must set this to
/// <see cref="AndroidRedirectUri"/> before calling <c>LoginAsync</c>. /// <see cref="AndroidRedirectUri"/> before calling <c>LoginAsync</c>.
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty]
public partial string RedirectUri { get; set; } = DefaultDesktopRedirectUri; public partial string RedirectUri { get; set; }
#if ANDROID
= AndroidRedirectUri;
#else
= DesktopRedirectUri;
#endif
/// <summary> /// <summary>
/// Space-separated view of <see cref="Scopes"/>. Exists for the /// Space-separated view of <see cref="Scopes"/>. Exists for the

View file

@ -16,12 +16,6 @@ namespace PostIt;
Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")] Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")]
public class ViewLocator : IDataTemplate public class ViewLocator : IDataTemplate
{ {
private readonly IServiceProvider _services;
public ViewLocator(IServiceProvider services)
{
_services = services;
}
public Control Build(object? data) public Control Build(object? data)
{ {
@ -38,15 +32,17 @@ public class ViewLocator : IDataTemplate
private Control BuildCore(object? data) private Control BuildCore(object? data)
{ {
var app = App.Current as App;
var services = app!.ServiceProvider!;
return data switch return data switch
{ {
MainViewModel => _services.GetRequiredService<MainPage>(), MainViewModel => services.GetRequiredService<MainPage>(),
Settings => _services.GetRequiredService<SettingsPage>(), Settings => services.GetRequiredService<SettingsPage>(),
HomePageViewModel => _services.GetRequiredService<HomePage>(), HomePageViewModel => services.GetRequiredService<HomePage>(),
SignaturePageViewModel => _services.GetRequiredService<SignaturePage>(), SignaturePageViewModel => services.GetRequiredService<SignaturePage>(),
AddCircleMemberDialogViewModel => _services.GetRequiredService<AddCircleMemberDialog>(), AddCircleMemberDialogViewModel => services.GetRequiredService<AddCircleMemberDialog>(),
CirclesPageViewModel => _services.GetRequiredService<CirclesPage>(), CirclesPageViewModel => services.GetRequiredService<CirclesPage>(),
PostAclDialogViewModel => _services.GetRequiredService<PostAclDialog>(), PostAclDialogViewModel => services.GetRequiredService<PostAclDialog>(),
null => new TextBlock { Text = "No view for <null>" }, null => new TextBlock { Text = "No view for <null>" },
_ => new TextBlock { Text = $"No view for {data.GetType().Name}" } _ => new TextBlock { Text = $"No view for {data.GetType().Name}" }
}; };

View file

@ -2,13 +2,11 @@ using System.Runtime.CompilerServices;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using IdentityModel.OidcClient; using IdentityModel.OidcClient;
using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Text.Json; using System.Text.Json;
using System.Threading;
[assembly: InternalsVisibleTo("PostIt.Tests")] [assembly: InternalsVisibleTo("PostIt.Tests")]
@ -18,37 +16,6 @@ public partial class Settings : ViewModelBase
{ {
const string SettingsFileName = "postit-settings.json"; const string SettingsFileName = "postit-settings.json";
/// <summary>
/// Redirect URI used by the Android app. The corresponding IntentFilter
/// in <c>PostIt.Android/Properties/AndroidManifest.xml</c> must match.
/// </summary>
public const string AndroidRedirectUri = "android://postit-signin";
/// <summary>
/// Process-wide canonical <see cref="Settings"/> instance, wired up
/// at application boot by <see cref="App.OnFrameworkInitializationCompleted"/>
/// through <see cref="BindToServiceProvider"/>. The hybrid pattern:
/// <list type="bullet">
/// <item><description>The static <c>Current</c> reference gives
/// ViewModels a non-DI way to reach the same instance (and lets
/// the framework bindings push notifications through one stable
/// <see cref="ObservableObject"/>).</description></item>
/// <item><description>Tests that want to exercise a clean
/// instance still call <c>new Settings()</c>; <c>Current</c>
/// stays null in those contexts because <see cref="BindToServiceProvider"/>
/// is never invoked.</description></item>
/// <item><description>Reads (<see cref="GetCurrent"/>) are
/// thread-safe and never allocate; mutations always go through
/// the DI-resolved singleton so two threads cannot each register
/// a different "current" Settings.</description></item>
/// </list>
/// </summary>
private static Settings? s_current;
[ObservableProperty] [ObservableProperty]
public partial AuthenticationSettings Authentication { get; set; } = new(); public partial AuthenticationSettings Authentication { get; set; } = new();
@ -336,7 +303,7 @@ public partial class Settings : ViewModelBase
this.Authentication.ClientId = string.IsNullOrWhiteSpace(settings.Authentication.ClientId) ? this.Authentication.ClientId = string.IsNullOrWhiteSpace(settings.Authentication.ClientId) ?
AuthenticationSettings.DefaultClientId : settings.Authentication.ClientId; AuthenticationSettings.DefaultClientId : settings.Authentication.ClientId;
this.Authentication.RedirectUri = string.IsNullOrWhiteSpace(settings.Authentication.RedirectUri) ? this.Authentication.RedirectUri = string.IsNullOrWhiteSpace(settings.Authentication.RedirectUri) ?
AuthenticationSettings.DefaultDesktopRedirectUri : settings.Authentication.RedirectUri; AuthenticationSettings.DesktopRedirectUri : settings.Authentication.RedirectUri;
if (settings.Authentication.Scopes is null || settings.Authentication.Scopes.Length == 0) if (settings.Authentication.Scopes is null || settings.Authentication.Scopes.Length == 0)
{ {
settings.Authentication.Scopes = AuthenticationSettings.DefaultScopes; settings.Authentication.Scopes = AuthenticationSettings.DefaultScopes;
@ -377,7 +344,7 @@ public partial class Settings : ViewModelBase
{ {
Authority = AuthenticationSettings.DefaultAuthority, Authority = AuthenticationSettings.DefaultAuthority,
ClientId = AuthenticationSettings.DefaultClientId, ClientId = AuthenticationSettings.DefaultClientId,
RedirectUri = AuthenticationSettings.DefaultDesktopRedirectUri, RedirectUri = AuthenticationSettings.DesktopRedirectUri,
Scopes = AuthenticationSettings.DefaultScopes Scopes = AuthenticationSettings.DefaultScopes
}; };
this.DarkMode = false; this.DarkMode = false;

View file

@ -97,16 +97,15 @@
<TextBlock Grid.Row="0" Text="Post detail" FontWeight="SemiBold" /> <TextBlock Grid.Row="0" Text="Post detail" FontWeight="SemiBold" />
<TextBox Grid.Row="1" Text="{Binding DraftTitle, Mode=TwoWay}" PlaceholderText="Title" /> <TextBox Grid.Row="1" Text="{Binding DraftTitle, Mode=TwoWay}" PlaceholderText="Title" />
<AvaloniaEdit:TextEditor Grid.Row="2" <TextBox Grid.Row="2" PlaceholderText="Write something here !"
views:TextEditorBinding.Text="{Binding DraftArticle, Mode=TwoWay}" Text="{Binding DraftArticle, Mode=TwoWay}"
ShowLineNumbers="True"
FontFamily="Cascadia Code, Consolas, Menlo, Monospace"
MinHeight="320" MinHeight="320"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch">
VerticalScrollBarVisibility="Auto" </TextBox>
HorizontalScrollBarVisibility="Auto" />
<TextBlock Grid.Row="3" Text="{Binding StatusMessage}" Foreground="Gray" /> <TextBlock Grid.Row="3" Text="{Binding StatusMessage}" Foreground="Gray" />
</Grid> </Grid>
</Border> </Border>
</Grid> </Grid>

View file

@ -0,0 +1,22 @@
<UserControl 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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="PostIt.Views.MainView"
x:DataType="vm:MainViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainViewModel />
</Design.DataContext>
<DockPanel LastChildFill="True">
<views:SessionStatusBanner
x:Name="SessionBanner"
DockPanel.Dock="Bottom"/>
<NavigationPage x:Name="NavRoot"/>
</DockPanel>
</UserControl>

View file

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace PostIt.Views;
public partial class MainView : UserControl
{
public MainView()
{
InitializeComponent();
}
}

View file

@ -8,10 +8,5 @@
x:Class="PostIt.Views.MainWindow" x:Class="PostIt.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico" Icon="/Assets/avalonia-logo.ico"
Title="PostIt"> Title="PostIt">
<views:MainView x:Name="MainView" />
<DockPanel LastChildFill="True">
<views:SessionStatusBanner x:Name="SessionBanner"
DockPanel.Dock="Bottom"/>
<NavigationPage x:Name="NavRoot"/>
</DockPanel>
</Window> </Window>

View file

@ -1,51 +0,0 @@
using Avalonia;
using Avalonia.Data;
using AvaloniaEdit;
namespace PostIt.Views;
public sealed class TextEditorBinding
{
public static readonly AttachedProperty<string?> TextProperty =
AvaloniaProperty.RegisterAttached<TextEditorBinding, TextEditor, string?>(
"Text",
defaultBindingMode: BindingMode.TwoWay);
private static readonly AttachedProperty<bool> IsTextChangeSubscribedProperty =
AvaloniaProperty.RegisterAttached<TextEditorBinding, TextEditor, bool>("IsTextChangeSubscribed");
static TextEditorBinding()
{
TextProperty.Changed.AddClassHandler<TextEditor>((editor, args) =>
{
EnsureTextChangeSubscribed(editor);
var text = args.GetNewValue<string?>() ?? 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);
}
};
}
}

View file

@ -11,7 +11,7 @@
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion> <FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3</InformationalVersion> <InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<Version>1.1.0-beta.1</Version> <Version>1.1.0-beta.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -17,7 +17,7 @@
<Library>true</Library> <Library>true</Library>
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion> <FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3</InformationalVersion> <InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<Version>1.1.0-beta.1</Version> <Version>1.1.0-beta.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -7,7 +7,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion> <FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3</InformationalVersion> <InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<Version>1.1.0-beta.1</Version> <Version>1.1.0-beta.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -9,7 +9,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion> <FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3</InformationalVersion> <InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<Version>1.1.0-beta.1</Version> <Version>1.1.0-beta.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -8,7 +8,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion> <FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3</InformationalVersion> <InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<Version>1.1.0-beta.1</Version> <Version>1.1.0-beta.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -11,7 +11,7 @@
<RunSettingsFilePath>$(MSBuildProjectDirectory)\test.runsettings</RunSettingsFilePath> <RunSettingsFilePath>$(MSBuildProjectDirectory)\test.runsettings</RunSettingsFilePath>
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion> <FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3</InformationalVersion> <InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<Version>1.1.0-beta.1</Version> <Version>1.1.0-beta.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -9,7 +9,7 @@
<RepositoryUrl>https://github.com/pazof/yavsc</RepositoryUrl> <RepositoryUrl>https://github.com/pazof/yavsc</RepositoryUrl>
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion> <FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3</InformationalVersion> <InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<Version>1.1.0-beta.1</Version> <Version>1.1.0-beta.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -9,7 +9,7 @@
<Library>true</Library> <Library>true</Library>
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion> <FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3</InformationalVersion> <InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<Version>1.1.0-beta.1</Version> <Version>1.1.0-beta.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -14,7 +14,7 @@
--> -->
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion> <FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3</InformationalVersion> <InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<Version>1.1.0-beta.1</Version> <Version>1.1.0-beta.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -7,7 +7,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion> <FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+1.Branch.release-1.0.8-rc1.Sha.1167169aa89e1bf25290e9a152d27b357a500ab3</InformationalVersion> <InformationalVersion>1.1.0-beta.1+49.Branch.release-1.0.8-rc3.Sha.5274d7bdf5d14406748a2063b5ae15191aac4359</InformationalVersion>
<Version>1.1.0-beta.1</Version> <Version>1.1.0-beta.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>