diff --git a/.dockerignore b/.dockerignore index 444995d0..45b7c22b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,6 @@ **/bin/ **/obj/ **/.playwright/ -.git/ .vs/ .github/ # Exclure uniquement les dossiers de sortie de compilation @@ -14,5 +13,4 @@ test/*/obj/ # Exclure les caches lourds **/.playwright/ -.git/ .vs/ diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ae9780df..48d3b4d1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -59,7 +59,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` diff --git a/.vscode/launch.json b/.vscode/launch.json index c374bc6b..76dc08d5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,7 @@ "name": "Yavsc.Org", "type": "dotnet", "request": "launch", - "projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj" + "projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj", }, { "name": "Yavsc.Blogs", diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 00000000..7ca6ed4b --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,11 @@ +{ + "servers": { + "openclaw": { + "type": "stdio", + "command": "/home/paul/.nvm/versions/node/v22.23.0/bin/node", + "args": [ + "/home/paul/Workspace/tools/openclaw-mcp-server.js" + ] + } + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 0a4785b9..6e22cb5e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,5 +26,17 @@ "cSpell.language": "fr,en", "makefile.configureOnOpen": false, "search.useGlobalIgnoreFiles": true, - "search.useParentIgnoreFiles": true + "search.useParentIgnoreFiles": true, + "chat.mcp.serverSampling": { + "yavsc/.vscode/mcp.json: openclaw": { + "allowedModels": [ + "copilot/auto", + "copilotcli/claude-haiku-4.5", + "copilotcli/gpt-4.1", + "copilotcli/gpt-5-mini", + "copilotcli/mai-code-1-flash-picker", + "copilotcli/gpt-5.3-codex" + ] + } + } } diff --git a/Directory.Packages.props b/Directory.Packages.props index b1ed6926..021c5e53 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,20 +2,11 @@ true - - - - + + + @@ -24,7 +15,7 @@ - + diff --git a/Dockerfile b/Dockerfile index e534f8ad..88b11683 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ COPY . . # (3) Source NuGet interne (Letsencrypt, certificat auto-signé côté # serveur, justifié par build privé). -RUN dotnet nuget add source https://isn.pschneider.fr/v3/index.json --allow-insecure-connections +RUN dotnet nuget add source https://isn.pschneider.fr/api/v3/index.json --allow-insecure-connections # (4) Restore RUN dotnet restore diff --git a/Dockerfile.backend b/Dockerfile.backend index 86df9ccd..76ad9ea0 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -26,7 +26,7 @@ COPY src/PostIt/PostIt.Desktop/*.csproj ./src/PostIt/PostIt.Desktop/ COPY . . # 3. Restauration des dépendances avec vos workloads actifs -RUN dotnet nuget add source https://isn.pschneider.fr/v3/index.json --allow-insecure-connections +RUN dotnet nuget add source https://isn.pschneider.fr/api/v3/index.json # 4. Restauration des dépendances pour tous les projets RUN dotnet restore diff --git a/Makefile b/Makefile index a4922a3d..2683c542 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,8 @@ include .env all: dotnet build --nologo -clean: - dotnet clean +clean: + dotnet clean -c $(CONFIG) src/Yavsc/bin/output/wwwroot: dotnet --project src/Yavsc.Org/Yavsc.Org.csproj publish @@ -31,7 +31,7 @@ src/Yavsc.Server/bin/$(CONFIG)/$(FRAMEWORK)/Yavsc.Server.dll: src/Yavsc/bin/$(CONFIG)/$(FRAMEWORK)/Yavsc.dll: dotnet build -p:Configuration=$(CONFIG) --project src/Yavsc.Org/Yavsc.Org.csproj -$(DESTDIR): +$(DESTDIR): mkdir $(DESTDIR) install: $(DESTDIR) diff --git a/README.md b/README.md index 1f1e7ce5..d1ed912a 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,13 @@ d'abord `appsettings-org.json` du serveur ; sinon, laisse-le en place. (utilisateur, mot de passe, hôte, base). Privilégier `dotnet user-secrets` ou des variables d'environnement `ASPNETCORE_*` plutôt qu'un mot de passe en clair dans le fichier. +- Au démarrage, Yavsc.Org applique automatiquement ses migrations EF + Core. Sur cette base de code, EF Core 10 peut encore lever un + `PendingModelChangesWarning` malgré des migrations et snapshots déjà + alignés ; ce faux positif est ignoré sur les contextes PostgreSQL pour + éviter un démarrage inutilement en mode dégradé. Si une erreur de + migration apparaît encore en production, elle doit être traitée comme + une vraie divergence de schéma ou de connexion. - `Smtp.*` — hôte, port, identifiants SMTP pour l'envoi d'e-mails transactionnels. - `Authentication.PayPal.*` et `Authentication.Google.*` — clés d'API diff --git a/doc/dev-tracking/client-editor-overhaul.md b/doc/dev-tracking/client-editor-overhaul.md deleted file mode 100644 index 0231e6aa..00000000 --- a/doc/dev-tracking/client-editor-overhaul.md +++ /dev/null @@ -1,278 +0,0 @@ -# Client editor overhaul — Yavsc.Org administration - -## Goal - -Bring the OAuth2 client administration UI (`/Client/Edit/{id}` and friends) -in Yavsc.Org to feature parity with the IdentityServer8 `Client` entity -model. Today the editor only exposes a handful of scalar fields and a few -single-line inputs for collections; the bulk of the entity and its -related collections are unreachable from the UI. - -## Inventory — current state - -### Properties exposed by `Views/Client/Edit.cshtml` - -| Field | Type | Notes | -| ------------------------ | ----------- | ---------------------------------- | -| `ClientId` | string | hidden, identifier | -| `Enabled` | bool | checkbox | -| `ClientName` | string | display name | -| `FrontChannelLogoutUri` | string | only front-channel, no back-channel | -| `RedirectUris` | collection | rendered as a single text input | -| `IdentityTokenLifetime` | int | seconds | -| `AbsoluteRefreshTokenLifetime` | int | seconds | -| `ClientSecrets` | collection | rendered as a single text input | -| `AccessTokenType` | enum | dropdown (custom `SetAppTypesInputValues`) | - -### Properties of `IdentityServer8.EntityFramework.Entities.Client` **NOT** in the editor - -Core scalars (16 fields missing): - -- `Description` -- `ClientUri` -- `LogoUri` -- `RequireConsent` -- `RequirePkce` -- `RequireRequestObject` -- `RequireClientSecret` -- `AllowPlainTextPkce` -- `AllowOfflineAccess` -- `AllowRememberConsent` -- `AlwaysIncludeUserClaimsInIdToken` -- `AlwaysSendClientClaims` -- `AuthorizationCodeLifetime` -- `BackChannelLogoutUri` -- `BackChannelLogoutSessionRequired` -- `CibaLifetime` -- `ClientClaimsPrefix` -- `ConsentLifetime` -- `Created` -- `DeviceCodeLifetime` -- `EnableLocalLogin` -- `Enabled` -- `FrontChannelLogoutSessionRequired` -- `IncludeJwtId` -- `LastAccessed` -- `LogoUri` -- `NonEditable` -- `PairwiseSubjectSalt` -- `PollingInterval` -- `ProtocolType` -- `RefreshTokenExpiration` -- `RefreshTokenUsage` -- `SlidingRefreshTokenLifetime` -- `UpdateAccessTokenClaimsOnRefresh` -- `Updated` -- `UserCodeType` -- `UserSsoLifetime` - -Collections (8 missing — currently either not exposed at all, or jammed -into a single-line text input that doesn't work for an IEnumerable): - -- `AllowedGrantTypes` → `ClientGrantType` (GrantType) -- `AllowedScopes` → `ClientScope` (Scope) -- `RedirectUris` → `ClientRedirectUri` (RedirectUri) — exposed but broken -- `PostLogoutRedirectUris` → `ClientPostLogoutRedirectUri` (PostLogoutRedirectUri) -- `AllowedCorsOrigins` → `ClientCorsOrigin` (Origin) -- `IdentityProviderRestrictions` → `ClientIdPRestriction` (Provider) -- `Claims` → `ClientClaim` (Type, Value) -- `Properties` → `ClientProperty` (Key, Value) -- `ClientSecrets` → `ClientSecret` (Type, Value, Description, Created, Expiration) — exposed but broken -- `AllowedSigningAlgorithms` → scalar string collection on Client itself - -## Pages to add - -Pattern: one Razor page per collection under -`Views/Client/Edit{Collection}.cshtml`. Each page lists existing rows, -offers an "Add" form with the relevant fields, and a per-row -remove button. The main `Edit.cshtml` becomes a hub page with links -to each subpage plus the scalar fields it already has. - -| Page | Route | Form fields | -| ------------------------------------- | ------------------------------------------ | ------------------------------------------------- | -| `Edit.cshtml` | `GET /Client/Edit/{id}` (existing) | scalar fields + nav links | -| `EditRedirectUris.cshtml` | `GET /Client/EditRedirectUris/{id}` | `RedirectUri` | -| `EditPostLogoutRedirectUris.cshtml` | `GET /Client/EditPostLogoutRedirectUris/{id}` | `PostLogoutRedirectUri` | -| `EditScopes.cshtml` | `GET /Client/EditScopes/{id}` | `Scope` (with select of known scopes) | -| `EditGrantTypes.cshtml` | `GET /Client/EditGrantTypes/{id}` | `GrantType` (with select of known types) | -| `EditCorsOrigins.cshtml` | `GET /Client/EditCorsOrigins/{id}` | `Origin` | -| `EditIdPRestrictions.cshtml` | `GET /Client/EditIdPRestrictions/{id}` | `Provider` | -| `EditClaims.cshtml` | `GET /Client/EditClaims/{id}` | `Type`, `Value` | -| `EditProperties.cshtml` | `GET /Client/EditProperties/{id}` | `Key`, `Value` | -| `EditSecrets.cshtml` (replacement) | `GET /Client/EditSecrets/{id}` | `Type`, `Value`, `Description`, `Expiration` | - -Partial view `_EditableList.cshtml` factored once and consumed by all -of the above. - -## Controller actions to add - -For each collection `Foo`: - -- `GET EditFoo(int id)` — load the client, render the page -- `POST AddFoo(int id, …)` — append a row, redirect to `EditFoo` -- `POST RemoveFoo(int id, int rowId)` — delete a row, redirect - -## Verification - -- `dotnet build src/Yavsc.Org/Yavsc.Org.csproj` → 0 errors -- No tests in `Yavsc.Org.Tests` exercise the controller today (per - `find … -name "ClientController*" -not -path "*/bin/*"`). Smoke-test - by logging in as admin, hitting `/Client/Edit/1`, then each - `Edit*/1` page, and verifying the add/remove POSTs. -- Existing seed flow (`MigratePostItClientToPublic` in - `HostingExtensions.cs`) must keep working — the editor changes are - additive, not destructive. - -## Out of scope - -- Tests (no MVC test infrastructure currently exists for this controller) -- Migration of existing collection fields (the broken `RedirectUris` - text input will simply be replaced by the new subpage) -- Per-collection authorization policies (the controller is already - `[Authorize("AdministratorOnly")]`) -- Client cloning / templating / JSON import-export - -## Status - -2026-06-21 16:04 — kickoff. Inventory done. Pages not yet started. - -2026-06-21 16:11 — first delivery, **build does not compile by design** -(per Paul: "Tu peux même me laisser un travail qui ne compile -pas"). The structural work is done; the residual errors are easy -fixes Paul will do in a debug session. - -Files added (working tree, not yet committed): - -- `src/Yavsc.Org/Controllers/Administration/ClientController.Collections.cs` - — partial class with the per-collection GET / Add / Remove actions. -- `src/Yavsc.Org/Views/Client/EditRedirectUris.cshtml` -- `src/Yavsc.Org/Views/Client/EditPostLogoutRedirectUris.cshtml` -- `src/Yavsc.Org/Views/Client/EditScopes.cshtml` -- `src/Yavsc.Org/Views/Client/EditGrantTypes.cshtml` -- `src/Yavsc.Org/Views/Client/EditCorsOrigins.cshtml` -- `src/Yavsc.Org/Views/Client/EditIdPRestrictions.cshtml` -- `src/Yavsc.Org/Views/Client/EditClaims.cshtml` -- `src/Yavsc.Org/Views/Client/EditProperties.cshtml` -- `src/Yavsc.Org/Views/Client/EditSecrets.cshtml` -- `src/Yavsc.Org/Views/Client/_EditableStringList.cshtml` - — partial consumed by the single-string-field collection pages. - -Files modified: - -- `src/Yavsc.Org/Controllers/Administration/ClientController.cs` - — `class` → `partial class`; the `Edit(int id)` GET now uses - `LoadClientAsync` to load all navigations (so the new Edit.cshtml - can render counts in its nav links). -- `src/Yavsc.Org/Views/Client/Edit.cshtml` - — significantly enriched: nav links to the 9 sub-pages, all the - scalar fields split into fieldsets (Security, Logout, Tokens, - Device / CIBA, Tokens-extra), ClientId / Id hidden. - -### Known residual compile errors (4 errors total) - -Paul is fixing these in a debug session. The structure is sound; the -errors are missing properties on the `Client` entity, a Razor -nullable quirk, and a `Localizer` injection miss. - -1. `Edit.cshtml:249` — `PairwiseSubjectSalt` doesn't exist on - `IdentityServer8.EntityFramework.Entities.Client`. **Fix**: drop - the field from Edit.cshtml; IdentityServer8 likely uses a - different property name (e.g. on a related entity) or doesn't - expose it. -2. `Edit.cshtml:221` — `CibaLifetime` doesn't exist on `Client`. - **Fix**: same as above. CIBA flow may be configured elsewhere - (resource-level) or via a different property. -3. `ClientController.Collections.cs` lines 181, 217, 253, 304 — - `Localizer` is not available in the partial class. **Fix**: inject - `IStringLocalizer` via the constructor, or - inline the strings ("BothTypeAndValueRequired", "KeyRequired", - "ValueRequired", "SecretValueRequired"). -4. `EditSecrets.cshtml:44` — `s.Expiration?.ToString("u")` on a - `DateTime?`. **Fix**: just `s.Expiration?.ToString("u")` works - if you write `s.Expiration.Value.ToString("u")`, or use - `(s.Expiration is null ? "" : s.Expiration.Value.ToString("u"))`, - or `s.Expiration?.ToString("u") ?? string.Empty`. - -### Suggested next session - -Once the 4 compile errors are fixed and the pages render: - -1. Smoke test by logging in as admin, hitting `/Client/Edit/1`, - then each `Edit*/1` page, and verifying add/remove POSTs. -2. Add a confirmation prompt (or 2-step form) for Remove actions — - removing a Redirect URI is destructive and one click is too easy. -3. Wire up some collection-level validation (e.g. redirect URI must - be a valid URL) at the controller level. -4. Add tests — the project doesn't have MVC test infrastructure - today; consider adding a `Yavsc.Org.Tests` project that drives - the controller via `WebApplicationFactory`. - - -## Test bootstrap notes (session of 2026-06-21 17:00+) - -When adding new integration tests against `WebServerFixture`: - -1. **Skip `/Account/Login` roundtrip.** The fixture ships without - `MapRazorPages()` (commented out in `HostingExtensions.ConfigurePipeline`), - so `/Identity/Account/Login` is 404, and the custom - `/Account/Login` route requires a complex antiforgery dance. - Instead, build a `ClaimsPrincipal` for the test user via - `UserManager` + `IUserClaimsPrincipalFactory`, - then call `IAuthenticationService.SignInAsync` on a synthetic - `DefaultHttpContext` and replay the resulting `Set-Cookie` header - into the test `HttpClient`. See - `ClientControllerCollectionTests.IssueIdentityCookie`. - -2. **Create the `Administrator` role before assigning it.** ASP.NET - Identity stores roles in `AspNetRoles`; there is no automatic seed. - The constant name is `YavscConstants.AdminGroupName` = `"Administrator"`. - Use `RoleManager.CreateAsync(new IdentityRole("Administrator"))` - before `AddToRoleAsync`. - -3. **Use `InMemory` connection string to bypass the prod signing-cert - requirement.** `HostingExtensions.AddIdentityServer` requires a - PEM cert unless `builder.Environment.IsDevelopment()` OR - `UsesInMemoryProvider(connectionString)`. The fixture already - uses `InMemory`, so `AddDeveloperSigningCredential()` is called - automatically — but only after we wired this check in (see - commit history). - -4. **Field-name gotchas** (from disassembling HigginsSoft - IdentityServer8.EntityFramework.Entities.Client 8.0.5-preview-net9): - - `PairWiseSubjectSalt` (capital W on "Wise"), not `PairwiseSubjectSalt`. - - `CibaLifetime` and `PollingInterval` do NOT exist on `Client` in - this version. - - `ConsentLifetime` and `UserSsoLifetime` are `int?`. - -5. **`MapStaticAssets()` fails on test projects.** Calling - `MapStaticAssets()` resolves a manifest file - (`.staticwebassets.endpoints.json`) that test projects - don't produce. Skip when `WebRootPath` points at the test - assembly directory. - -6. **Routing 404 on /Client/Edit/{id} via WebServerFixture.** As of - this session, the GET endpoint returns 404 even with admin - header. The route mapping is intact - (`MapDefaultControllerRoute()`), so this is likely an MVC - convention routing issue with the - `Controllers/Administration/` subdirectory. To investigate - next session: log middleware pipeline or hit `/Client` index - first to see if any Client route resolves. - -7. **`MapStaticAssets()` is unconditional in prod, but blocks tests.** - `WebApplication.CreateBuilder` defaults `ContentRootPath` to - `AppContext.BaseDirectory`. In test runs that resolves to - `src/Yavsc.Org.Tests/bin/Debug/net10.0/`, where - `Yavsc.Org.Tests.staticwebassets.endpoints.json` doesn't exist - (it's generated only by projects with the Web SDK). The - `app.MapStaticAssets()` call inside `ConfigurePipeline` then - throws and the fixture fails to start — taking every test in - the `[Collection("Yavsc Server")]` down with it. - - This is a pre-existing fragility of the WebServerFixture that - the new test work surfaced. Fixing it cleanly requires either: - (a) moving the test project to the Web SDK so it produces its - own manifest, (b) copying the manifest at build time via an - MSBuild target, or (c) routing `MapStaticAssets` through an - assembly-resolution fallback. None attempted in this session — - recorded for next session. diff --git a/src/PostIt.Tests/FakeAuthorizingBrowser.cs b/src/PostIt.Tests/FakeAuthorizingBrowser.cs index 10311500..4748425a 100644 --- a/src/PostIt.Tests/FakeAuthorizingBrowser.cs +++ b/src/PostIt.Tests/FakeAuthorizingBrowser.cs @@ -10,7 +10,7 @@ namespace PostIt.Tests; /// URL emitted by OidcClient, extracts its state, and returns a /// BrowserResult that mimics the OIDC redirect-with-code callback. /// -/// The paired 's token endpoint accepts +/// The paired 's token endpoint accepts /// any authorization code, so we don't need to mint a real one here. /// public sealed class FakeAuthorizingBrowser diff --git a/src/PostIt.Tests/LoginPageViewModelTests.cs b/src/PostIt.Tests/LoginPageViewModelTests.cs index e469c011..7a8b579f 100644 --- a/src/PostIt.Tests/LoginPageViewModelTests.cs +++ b/src/PostIt.Tests/LoginPageViewModelTests.cs @@ -14,7 +14,7 @@ public class LoginPageViewModelTests // short-circuits the system browser. The authority signs its // access_token with RS256; the fake browser captures the redirect // URI so the authority can complete the token exchange. - using var authority = await OidcStubAuthority.StartAsync(); + using var authority = await OIDCStubAuthority.StartAsync(); var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri); var settings = new PostIt.Settings @@ -96,7 +96,7 @@ public class LoginPageViewModelTests // double slash before /.well-known/openid-configuration. The // stub advertises itself without the trailing slash; OidcClient // must bridge. - using var authority = await OidcStubAuthority.StartAsync(); + using var authority = await OIDCStubAuthority.StartAsync(); var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri); var settings = new PostIt.Settings @@ -254,4 +254,4 @@ public class LoginPageViewModelTests "https://yavsc.example.com/.well-known/openid-configuration", vm.StatusMessage); } -} \ No newline at end of file +} diff --git a/src/PostIt.Tests/OidcStubAuthority.cs b/src/PostIt.Tests/OidcStubAuthority.cs index 552db6b0..3c6552fb 100644 --- a/src/PostIt.Tests/OidcStubAuthority.cs +++ b/src/PostIt.Tests/OidcStubAuthority.cs @@ -20,7 +20,7 @@ namespace PostIt.Tests; /// the browser intercepts the authorize redirect, the server completes /// the token exchange. /// -public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable +public sealed class OIDCStubAuthority : IAsyncDisposable, IDisposable { private readonly HttpListener _listener; private readonly RSA _rsa; @@ -30,7 +30,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable public string Issuer { get; } public string LoopbackRedirectUri { get; } - private OidcStubAuthority(HttpListener listener, RSA rsa, string kid, string issuer, string loopback) + private OIDCStubAuthority(HttpListener listener, RSA rsa, string kid, string issuer, string loopback) { _listener = listener; _rsa = rsa; @@ -39,7 +39,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable LoopbackRedirectUri = loopback; } - public static async Task StartAsync() + public static async Task StartAsync() { // Pick a free loopback port. var port = GetFreePort(); @@ -53,7 +53,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable var rsa = RSA.Create(2048); var kid = "test-key-1"; - var authority = new OidcStubAuthority(listener, rsa, kid, prefix.TrimEnd('/'), loopback); + var authority = new OIDCStubAuthority(listener, rsa, kid, prefix.TrimEnd('/'), loopback); _ = Task.Run(() => authority.AcceptLoopAsync(authority._cts.Token)); return authority; } diff --git a/src/PostIt.Tests/SignaturePadControlTests.cs b/src/PostIt.Tests/SignaturePadControlTests.cs new file mode 100644 index 00000000..691ae547 --- /dev/null +++ b/src/PostIt.Tests/SignaturePadControlTests.cs @@ -0,0 +1,188 @@ +using System; +using System.Linq; +using PostIt.Controls; +using PostIt.Models; +using Xunit; + +namespace PostIt.Tests; + +/// +/// Targeted tests for and +/// . +/// +/// The control exposes internal test hooks so we can drive +/// the buffer without standing up a headless XAML tree just to +/// deliver synthetic pointer events. The headless surface is used +/// only to assert that the control's pointer handlers are wired +/// when a template is applied; see +/// . +/// +public class SignaturePadControlTests +{ + // --- SignaturePadData (pure) --------------------------------------- + + [Fact] + public void Data_empty_array_is_empty() + { + var d = new SignaturePadData(Array.Empty()); + Assert.True(d.IsEmpty); + Assert.Equal(0, d.StrokeCount); + } + + [Fact] + public void Data_single_dot_is_one_stroke_with_k_equals_one() + { + var d = new SignaturePadData(new[] { 1, 5_000, 5_000 }); + Assert.False(d.IsEmpty); + Assert.Equal(1, d.StrokeCount); + } + + [Fact] + public void Data_two_strokes_are_independent() + { + var d = new SignaturePadData(new[] + { + 2, 100, 100, 200, 200, + 1, 9_000, 9_000, + }); + Assert.Equal(2, d.StrokeCount); + } + + [Fact] + public void Data_malformed_payload_does_not_throw_on_read() + { + // k=0 at the head would underflow the walker. The reader + // short-circuits instead of throwing. + var d = new SignaturePadData(new[] { 0, 1, 2, 3 }); + Assert.Equal(0, d.StrokeCount); + } + + [Fact] + public void Data_constructor_rejects_null() + { + Assert.Throws(() => new SignaturePadData(null!)); + } + + // --- SignaturePadControl (buffer / events) ------------------------- + + [Fact] + public void New_control_has_empty_buffer() + { + var pad = new SignaturePadControl(); + Assert.Empty(pad.Strokes); + Assert.True(pad.Snapshot().IsEmpty); + } + + [Fact] + public void Snapshot_returns_a_distinct_array_each_call() + { + var pad = new SignaturePadControl(); + pad.AppendPointForTest(1_000, 2_000); + pad.AppendPointForTest(3_000, 4_000); + pad.SealStrokeForTest(); + + var first = pad.Snapshot(); + var second = pad.Snapshot(); + + // Distinct array instances — the consumer of the first + // snapshot can hold onto it after the control mutates. + Assert.NotSame(first.Strokes, second.Strokes); + // Same logical content (no mutation in between). + Assert.Equal(first.Strokes, second.Strokes); + + pad.AppendPointForTest(5_000, 6_000); + pad.SealStrokeForTest(); + + var third = pad.Snapshot(); + Assert.NotEqual(first.Strokes, third.Strokes); + } + + [Fact] + public void Clear_empties_buffer_and_raises_redraw() + { + var pad = new SignaturePadControl(); + pad.AppendPointForTest(1, 1); + pad.SealStrokeForTest(); + Assert.NotEmpty(pad.Strokes); + + int redraws = 0; + pad.RedrawRequested += (_, _) => redraws++; + pad.Clear(); + + Assert.Empty(pad.Strokes); + Assert.True(pad.Snapshot().IsEmpty); + Assert.Equal(1, redraws); + } + + [Fact] + public void SealStrokeForTest_raises_redraw() + { + var pad = new SignaturePadControl(); + int redraws = 0; + pad.RedrawRequested += (_, _) => redraws++; + pad.AppendPointForTest(1, 1); + pad.AppendPointForTest(2, 2); + pad.SealStrokeForTest(); + Assert.Equal(1, redraws); + } + + [Fact] + public void SealStrokeForTest_with_no_pending_points_is_a_no_op() + { + var pad = new SignaturePadControl(); + int redraws = 0; + pad.RedrawRequested += (_, _) => redraws++; + pad.SealStrokeForTest(); + Assert.Equal(0, redraws); + } + + [Fact] + public void Two_sealed_strokes_produce_two_length_prefixes() + { + var pad = new SignaturePadControl(); + // Stroke 0: one point. + pad.AppendPointForTest(1_000, 1_000); + pad.SealStrokeForTest(); + // Stroke 1: two points. + pad.AppendPointForTest(2_000, 2_000); + pad.AppendPointForTest(3_000, 3_000); + pad.SealStrokeForTest(); + + var s = pad.Strokes; + // Layout: [k0, x0, y0, k1, x1, y1, x2, y2] + Assert.Equal(1, s[0]); + Assert.Equal(1_000, s[1]); + Assert.Equal(1_000, s[2]); + Assert.Equal(2, s[3]); + Assert.Equal(2_000, s[4]); + Assert.Equal(2_000, s[5]); + Assert.Equal(3_000, s[6]); + Assert.Equal(3_000, s[7]); + } + + [Fact] + public void StrokeCompleted_fires_on_seal() + { + var pad = new SignaturePadControl(); + int events = 0; + pad.StrokeCompleted += (_, _) => events++; + pad.AppendPointForTest(1, 1); + pad.SealStrokeForTest(); + pad.AppendPointForTest(2, 2); + pad.SealStrokeForTest(); + Assert.Equal(2, events); + } + + [Fact] + public void StrokeCompleted_carries_a_snapshot_with_k_count() + { + var pad = new SignaturePadControl(); + SignaturePadData? captured = null; + pad.StrokeCompleted += (_, d) => captured = d; + pad.AppendPointForTest(1, 1); + pad.AppendPointForTest(2, 2); + pad.SealStrokeForTest(); + Assert.NotNull(captured); + Assert.Equal(1, captured!.StrokeCount); + } +} diff --git a/src/PostIt.Tests/SignaturePageViewModelTests.cs b/src/PostIt.Tests/SignaturePageViewModelTests.cs new file mode 100644 index 00000000..37f17a58 --- /dev/null +++ b/src/PostIt.Tests/SignaturePageViewModelTests.cs @@ -0,0 +1,163 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using PostIt.Controls; +using PostIt.ViewModels; +using Xunit; + +namespace PostIt.Tests; + +/// +/// Tests for : the contract +/// between the page's view model and the . +/// The view (XAML + code-behind rendering) is not tested here — the +/// control is render-agnostic, and the rendering is plain Polyline +/// reconstruction that we'll exercise manually in PostIt.Desktop. +/// +public class SignaturePageViewModelTests +{ + [Fact] + public void Default_constructor_uses_default_dimensions() + { + var vm = new SignaturePageViewModel(); + Assert.Equal(SignaturePageViewModel.DefaultWidth, vm.Width); + Assert.Equal(SignaturePageViewModel.DefaultHeight, vm.Height); + } + + [Fact] + public void Constructor_rejects_non_positive_dimensions() + { + Assert.Throws( + () => new SignaturePageViewModel(0, 100)); + Assert.Throws( + () => new SignaturePageViewModel(100, 0)); + Assert.Throws( + () => new SignaturePageViewModel(-1, 100)); + } + + [Fact] + public void Attach_then_Detach_is_idempotent() + { + var vm = new SignaturePageViewModel(); + var pad = new SignaturePadControl(); + vm.Attach(pad); + vm.Detach(); + // Second detach is a no-op: must not throw. + vm.Detach(); + } + + [Fact] + public void Attach_rejects_null() + { + var vm = new SignaturePageViewModel(); + Assert.Throws(() => vm.Attach(null!)); + } + + [Fact] + public void StrokeCompleted_updates_status_and_counts() + { + var vm = new SignaturePageViewModel(); + var pad = new SignaturePadControl(); + vm.Attach(pad); + + // Drive the control via the test hooks so we don't depend + // on Avalonia pointer events. + pad.AppendPointForTest(1_000, 1_000); + pad.AppendPointForTest(2_000, 2_000); + pad.SealStrokeForTest(); + + Assert.Equal(1, vm.StrokeCount); + Assert.Equal(2, vm.PointCount); + Assert.Contains("1 trait", vm.StatusMessage); + } + + [Fact] + public void Clear_resets_counts_and_buffer() + { + var vm = new SignaturePageViewModel(); + var pad = new SignaturePadControl(); + vm.Attach(pad); + + pad.AppendPointForTest(1, 1); + pad.SealStrokeForTest(); + Assert.Equal(1, vm.StrokeCount); + + vm.Clear(); + + Assert.Equal(0, vm.StrokeCount); + Assert.Equal(0, vm.PointCount); + Assert.Empty(pad.Strokes); + Assert.Contains("Effacé", vm.StatusMessage); + } + + [Fact] + public async Task CaptureAsync_on_empty_buffer_reports_and_writes_nothing() + { + var vm = new SignaturePageViewModel(); + var pad = new SignaturePadControl(); + vm.Attach(pad); + + await vm.CaptureAsync(); + + Assert.Contains("Rien", vm.StatusMessage); + Assert.Null(vm.LastCapturedPath); + } + + [Fact] + public async Task CaptureAsync_writes_a_yavsc_signature_v1_file() + { + // The VM uses Environment.SpecialFolder.LocalApplicationData, + // which we cannot redirect per-call without a constructor + // seam. We test the produced file's structure rather than + // its text formatting, because System.Text.Json's pretty- + // printer is not part of the contract we're locking down. + var vm = new SignaturePageViewModel(); + var pad = new SignaturePadControl(); + vm.Attach(pad); + + pad.AppendPointForTest(1_000, 2_000); + pad.AppendPointForTest(3_000, 4_000); + pad.SealStrokeForTest(); + + await vm.CaptureAsync(); + + Assert.NotNull(vm.LastCapturedPath); + Assert.True(File.Exists(vm.LastCapturedPath!), $"file missing: {vm.LastCapturedPath}"); + + using var doc = JsonDocument.Parse(File.ReadAllText(vm.LastCapturedPath!)); + var root = doc.RootElement; + + Assert.Equal("yavsc.signature/v1", root.GetProperty("format").GetString()); + Assert.Equal(10_000, root.GetProperty("coordinateMax").GetInt32()); + Assert.Equal(1, root.GetProperty("strokeCount").GetInt32()); + + var strokes = root.GetProperty("strokes"); + Assert.Equal(JsonValueKind.Array, strokes.ValueKind); + // [k=2, x0, y0, x1, y1] + Assert.Equal(5, strokes.GetArrayLength()); + Assert.Equal(2, strokes[0].GetInt32()); // k (2 points) + Assert.Equal(1_000, strokes[1].GetInt32()); // x0 + Assert.Equal(2_000, strokes[2].GetInt32()); // y0 + Assert.Equal(3_000, strokes[3].GetInt32()); // x1 + Assert.Equal(4_000, strokes[4].GetInt32()); // y1 + } + + [Fact] + public async Task CaptureAsync_creates_directory_if_missing() + { + var vm = new SignaturePageViewModel(); + var pad = new SignaturePadControl(); + vm.Attach(pad); + pad.AppendPointForTest(1, 1); + pad.SealStrokeForTest(); + + // The directory must exist after the call (CreateDirectory + // in the VM handles this). + await vm.CaptureAsync(); + + var dir = Path.GetDirectoryName(vm.LastCapturedPath!); + Assert.NotNull(dir); + Assert.True(Directory.Exists(dir), $"directory missing: {dir}"); + } +} diff --git a/src/PostIt.Tests/YavscApiClientTests.cs b/src/PostIt.Tests/YavscApiClientTests.cs index 1617c1b6..03e2fa3f 100644 --- a/src/PostIt.Tests/YavscApiClientTests.cs +++ b/src/PostIt.Tests/YavscApiClientTests.cs @@ -20,7 +20,7 @@ namespace PostIt.Tests; /// End-to-end coverage of : silent /// refresh on a near-expiry access token, 401-driven refresh + retry, /// and persistence of the token bundle via . -/// Uses the project's for the IdP and +/// Uses the project's for the IdP and /// a tiny in-process HTTP listener for the API server side. /// public class YavscApiClientTests @@ -45,7 +45,7 @@ public class YavscApiClientTests // in-memory access token as expired and re-run a call. The // refresh path must rotate the refresh token transparently // and the API call must succeed with the new token. - using var authority = await OidcStubAuthority.StartAsync(); + using var authority = await OIDCStubAuthority.StartAsync(); using var apiServer = new StubApiServer(); await apiServer.StartAsync(); @@ -63,7 +63,7 @@ public class YavscApiClientTests var reloaded = new YavscApiClient(settings, new TokenStore(tokensPath)); var posts = await reloaded.CallAsync>( - HttpMethod.Get, "posts"); + HttpMethod.Get, "posts", TestContext.Current.CancellationToken); Assert.NotNull(posts); Assert.NotEmpty(posts); @@ -85,7 +85,7 @@ public class YavscApiClientTests { // API server returns 401 on the first request, 200 on the next. // YavscApiClient must refresh, then retry exactly once. - using var authority = await OidcStubAuthority.StartAsync(); + using var authority = await OIDCStubAuthority.StartAsync(); using var apiServer = new StubApiServer(forceFirstRequest: true); await apiServer.StartAsync(); @@ -97,7 +97,7 @@ public class YavscApiClientTests settings, authority, tokensPath); var posts = await client.CallAsync>( - HttpMethod.Get, "posts"); + HttpMethod.Get, "posts", TestContext.Current.CancellationToken); Assert.NotEmpty(posts); Assert.Equal(2, apiServer.RequestCount); @@ -125,14 +125,15 @@ public class YavscApiClientTests var client = new YavscApiClient(settings, new TokenStore(Path.Combine( Path.GetTempPath(), $"postit-tests-noop-{Guid.NewGuid():N}.json"))); - await Assert.ThrowsAsync(() => - client.CallAsync(HttpMethod.Get, "posts")); + await Assert.ThrowsAsync( + () => + client.CallAsync(HttpMethod.Get, "posts", TestContext.Current.CancellationToken)); } [Fact] public async Task HasValidSession_is_true_after_login() { - using var authority = await OidcStubAuthority.StartAsync(); + using var authority = await OIDCStubAuthority.StartAsync(); using var apiServer = new StubApiServer(); await apiServer.StartAsync(); @@ -154,7 +155,7 @@ public class YavscApiClientTests // --- helpers -------------------------------------------------------- - private static PostIt.Settings BuildSettings(OidcStubAuthority authority, string apiBaseUrl) => new() + private static PostIt.Settings BuildSettings(OIDCStubAuthority authority, string apiBaseUrl) => new() { Authentication = new AuthenticationSettings { @@ -167,7 +168,7 @@ public class YavscApiClientTests }; private static async Task LoginAndPersistAsync( - PostIt.Settings settings, OidcStubAuthority authority, string tokensPath) + PostIt.Settings settings, OIDCStubAuthority authority, string tokensPath) { var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri); var client = new YavscApiClient(settings, new TokenStore(tokensPath)); @@ -181,7 +182,7 @@ public class YavscApiClientTests /// /// YavscApiClient.LoginInteractiveAsync delegates to /// Platform.CreateBrowser. We can't override that static cleanly - /// from xunit.v3, so we rebuild the call by re-routing the + /// from XUnit.v3, so we rebuild the call by re-routing the /// Platform.CreateBrowser delegate for the duration of the call. /// private static async Task LoginWithBrowserAsync( @@ -214,7 +215,7 @@ public class YavscApiClientTests File.WriteAllText(tokensPath, JsonSerializer.Serialize(record)); } - // --- OidcLoginPhase progress tests --------------------------------- + // --- OIDCLoginPhase progress tests --------------------------------- /// /// Collecting Progress is documented to capture reports @@ -225,7 +226,7 @@ public class YavscApiClientTests [Fact] public async Task LoginInteractiveAsync_reports_Discovering_then_Success() { - using var authority = await OidcStubAuthority.StartAsync(); + using var authority = await OIDCStubAuthority.StartAsync(); using var apiServer = new StubApiServer(); await apiServer.StartAsync(); @@ -234,18 +235,18 @@ public class YavscApiClientTests var client = new YavscApiClient(settings, new TokenStore(tokensPath)); var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri); - var reported = new System.Collections.Generic.List(); - var progress = new SyncProgress(reported); + var reported = new System.Collections.Generic.List(); + var progress = new SyncProgress(reported); try { await LoginWithBrowserAsync(client, browser.CreateBrowser(), progress); // SyncProgress captures reports synchronously — no flush needed. - Assert.Contains(OidcLoginPhase.Discovering, reported); - Assert.Contains(OidcLoginPhase.OpeningBrowser, reported); - Assert.Contains(OidcLoginPhase.ExchangingCode, reported); - Assert.Equal(OidcLoginPhase.Success, Last(reported)); + Assert.Contains(OIDCLoginPhase.Discovering, reported); + Assert.Contains(OIDCLoginPhase.OpeningBrowser, reported); + Assert.Contains(OIDCLoginPhase.ExchangingCode, reported); + Assert.Equal(OIDCLoginPhase.Success, Last(reported)); } finally { @@ -256,24 +257,24 @@ public class YavscApiClientTests [Fact] public async Task LoginInteractiveAsync_reports_Error_when_browser_missing() { - using var authority = await OidcStubAuthority.StartAsync(); + using var authority = await OIDCStubAuthority.StartAsync(); using var apiServer = new StubApiServer(); await apiServer.StartAsync(); var settings = BuildSettings(authority, apiServer.BaseUrl); var client = new YavscApiClient(settings, new TokenStore(TokensPath())); - var reported = new System.Collections.Generic.List(); - var progress = new SyncProgress(reported); + var reported = new System.Collections.Generic.List(); + var progress = new SyncProgress(reported); var original = Platform.CreateBrowser; try { Platform.CreateBrowser = () => null; // simulate no browser wired up await Assert.ThrowsAsync( - () => client.LoginInteractiveAsync(progress)); + () => client.LoginInteractiveAsync(progress, TestContext.Current.CancellationToken)); // SyncProgress captures reports synchronously — no flush needed. - Assert.Equal(OidcLoginPhase.Error, Last(reported)); + Assert.Equal(OIDCLoginPhase.Error, Last(reported)); } finally { @@ -284,7 +285,7 @@ public class YavscApiClientTests [Fact] public async Task TrySilentLoginAsync_returns_false_when_no_bundle_on_disk() { - using var authority = await OidcStubAuthority.StartAsync(); + using var authority = await OIDCStubAuthority.StartAsync(); using var apiServer = new StubApiServer(); await apiServer.StartAsync(); @@ -293,7 +294,7 @@ public class YavscApiClientTests // Tokens file deliberately doesn't exist. var client = new YavscApiClient(settings, new TokenStore(tokensPath)); - var ok = await client.TrySilentLoginAsync(); + var ok = await client.TrySilentLoginAsync(null, TestContext.Current.CancellationToken); Assert.False(ok); Assert.False(client.HasValidSession); } @@ -301,7 +302,7 @@ public class YavscApiClientTests [Fact] public async Task TrySilentLoginAsync_returns_true_when_access_token_still_valid() { - using var authority = await OidcStubAuthority.StartAsync(); + using var authority = await OIDCStubAuthority.StartAsync(); using var apiServer = new StubApiServer(); await apiServer.StartAsync(); @@ -314,7 +315,7 @@ public class YavscApiClientTests { await LoginWithBrowserAsync(client, browser.CreateBrowser()); // Login fresh → access token is far from expiry. - var ok = await client.TrySilentLoginAsync(); + var ok = await client.TrySilentLoginAsync(null, TestContext.Current.CancellationToken); Assert.True(ok); Assert.True(client.HasValidSession); } @@ -327,7 +328,7 @@ public class YavscApiClientTests [Fact] public async Task TrySilentLoginAsync_returns_true_when_refresh_succeeds() { - using var authority = await OidcStubAuthority.StartAsync(); + using var authority = await OIDCStubAuthority.StartAsync(); using var apiServer = new StubApiServer(); await apiServer.StartAsync(); @@ -351,13 +352,13 @@ public class YavscApiClientTests // matches the disk: access expired, refresh still good. var client = new YavscApiClient(settings, store); - var reported = new System.Collections.Generic.List(); - var progress = new SyncProgress(reported); + var reported = new System.Collections.Generic.List(); + var progress = new SyncProgress(reported); - var ok = await client.TrySilentLoginAsync(progress); + var ok = await client.TrySilentLoginAsync(progress, TestContext.Current.CancellationToken); Assert.True(ok, "silent refresh should succeed via the stub authority."); - Assert.Contains(OidcLoginPhase.ExchangingCode, reported); - Assert.Equal(OidcLoginPhase.Success, Last(reported)); + Assert.Contains(OIDCLoginPhase.ExchangingCode, reported); + Assert.Equal(OIDCLoginPhase.Success, Last(reported)); } finally { @@ -430,7 +431,7 @@ public class YavscApiClientTests /// overload stays for tests that don't care about phase events. /// private static async Task LoginWithBrowserAsync( - YavscApiClient client, IBrowser browser, IProgress? progress = null) + YavscApiClient client, IBrowser browser, IProgress? progress = null) { var original = Platform.CreateBrowser; try diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 65ce8b26..134ae42f 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -63,6 +63,7 @@ public partial class App : Application services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // ViewModels services.AddSingleton(settings); @@ -72,6 +73,7 @@ public partial class App : Application services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Persistent session banner: one instance for the lifetime of // the app so the same VM survives page navigation. diff --git a/src/PostIt/PostIt/Controls/SignaturePadControl.cs b/src/PostIt/PostIt/Controls/SignaturePadControl.cs new file mode 100644 index 00000000..87949d30 --- /dev/null +++ b/src/PostIt/PostIt/Controls/SignaturePadControl.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using PostIt.Models; + +namespace PostIt.Controls; + +/// +/// Pointer-driven capture surface that records a signature as a list +/// of strokes, each stroke being a length-prefixed sequence of (x, y) +/// coordinates normalised to [0, CoordinateMax]. +/// +/// The control is render-agnostic: it does not draw anything. The +/// host view templates a (typically a +/// Border) as PART_CaptureArea for pointer capture, +/// and binds a separate visual layer (e.g. a Canvas) to +/// for redraw. Keeping the control headless of +/// rendering makes it usable from a headless test where no +/// composition happens. +/// +/// Wire format (see ): +/// int[] = [k0, x0, y0, ..., k1, x0, y0, ...] +/// with x, y ∈ [0, 10_000]. +/// +/// Threading: pointer events are dispatched on the UI thread, which +/// is the only thread that ever mutates . The +/// buffer is safe to read from any thread as long as no read +/// straddles a pointer event — for cross-thread transfer use +/// , which copies. +/// +public class SignaturePadControl : TemplatedControl +{ + /// + /// Styled property pointing at the + /// that receives pointer events. Set it in the control's + /// template (PART_CaptureArea). + /// + public static readonly StyledProperty CaptureAreaProperty = + AvaloniaProperty.Register(nameof(CaptureArea)); + + public InputElement? CaptureArea + { + get => GetValue(CaptureAreaProperty); + set => SetValue(CaptureAreaProperty, value); + } + + /// + /// Captured strokes in wire form. Exposed as a read-only view + /// over the internal buffer. The buffer only mutates on the UI + /// thread, between pointer events. + /// + public IReadOnlyList Strokes => _strokes; + + /// + /// Raised when the user finishes a stroke (pointer release). + /// The argument is a snapshot of the buffer at release time. + /// + public event EventHandler? StrokeCompleted; + + /// + /// Raised when the buffer changes: at the end of every stroke + /// and on . Mid-stroke points do not raise + /// this event (pointer-move is too dense); bind a separate + /// visual layer if you need a live preview. + /// + public event EventHandler? RedrawRequested; + + private readonly List _strokes = new(capacity: 256); + private int _pendingPoints; // number of (x, y) pairs awaiting a length prefix + private bool _capturing; + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + if (CaptureArea is { } previous) + { + previous.PointerPressed -= OnCapturePressed; + previous.PointerMoved -= OnCaptureMoved; + previous.PointerReleased -= OnCaptureReleased; + } + + if (CaptureArea is { } area) + { + area.PointerPressed += OnCapturePressed; + area.PointerMoved += OnCaptureMoved; + area.PointerReleased += OnCaptureReleased; + } + } + + private void OnCapturePressed(object? sender, PointerPressedEventArgs e) + { + if (!e.GetCurrentPoint(CaptureArea).Properties.IsLeftButtonPressed) return; + e.Pointer.Capture(CaptureArea); + _capturing = true; + _pendingPoints = 0; + AppendPoint(e.GetPosition(CaptureArea)); + } + + private void OnCaptureMoved(object? sender, PointerEventArgs e) + { + if (!_capturing) return; + AppendPoint(e.GetPosition(CaptureArea)); + } + + private void OnCaptureReleased(object? sender, PointerReleasedEventArgs e) + { + if (!_capturing) return; + AppendPoint(e.GetPosition(CaptureArea)); + _capturing = false; + + if (_pendingPoints == 0) + { + // Press + immediate release without movement yields no + // point at all (the press fired AppendPoint, so this + // branch is unreachable — kept for clarity if a future + // change skips the press append). + return; + } + + // Seal the current stroke by inserting its length at the + // head of its slice. The slice is the trailing + // 2 * _pendingPoints entries. + int sliceStart = _strokes.Count - 2 * _pendingPoints; + _strokes.Insert(sliceStart, _pendingPoints); + _pendingPoints = 0; + + StrokeCompleted?.Invoke(this, Snapshot()); + RedrawRequested?.Invoke(this, EventArgs.Empty); + } + + private void AppendPoint(Point p) + { + var (nx, ny) = Normalise(p); + _strokes.Add(nx); + _strokes.Add(ny); + _pendingPoints++; + } + + private (int x, int y) Normalise(Point p) + { + if (CaptureArea is null) return (0, 0); + var bounds = CaptureArea.Bounds; + double w = bounds.Width; + double h = bounds.Height; + if (w <= 0 || h <= 0) return (0, 0); + int nx = (int)Math.Round(Math.Clamp(p.X / w, 0.0, 1.0) * SignaturePadData.CoordinateMax); + int ny = (int)Math.Round(Math.Clamp(p.Y / h, 0.0, 1.0) * SignaturePadData.CoordinateMax); + return (nx, ny); + } + + /// + /// Forget every captured stroke. Raises . + /// + public void Clear() + { + _strokes.Clear(); + _pendingPoints = 0; + _capturing = false; + RedrawRequested?.Invoke(this, EventArgs.Empty); + } + + /// + /// Defensive copy of the current buffer wrapped in a + /// . Cheap; call only when the + /// view needs to ship the data off (e.g. to a backend). + /// + public SignaturePadData Snapshot() => new(_strokes.ToArray()); + + // --- Test-only surface (visible to PostIt.Tests) ------------------- + + /// + /// Test hook: append a single normalised point without going + /// through the pointer pipeline. Does not raise + /// . + /// + internal void AppendPointForTest(int x, int y) + { + _strokes.Add(x); + _strokes.Add(y); + _pendingPoints++; + } + + /// + /// Test hook: seal the currently-pending stroke with a length + /// prefix. Mirrors what does at + /// pointer release time, including the + /// and + /// events, so test scenarios observe the same notification + /// contract as production. Idempotent: a second call without + /// intermediate appends is a no-op. + /// + internal void SealStrokeForTest() + { + if (_pendingPoints == 0) return; + int sliceStart = _strokes.Count - 2 * _pendingPoints; + _strokes.Insert(sliceStart, _pendingPoints); + _pendingPoints = 0; + StrokeCompleted?.Invoke(this, Snapshot()); + RedrawRequested?.Invoke(this, EventArgs.Empty); + } +} diff --git a/src/PostIt/PostIt/Models/SignaturePadData.cs b/src/PostIt/PostIt/Models/SignaturePadData.cs new file mode 100644 index 00000000..11568eac --- /dev/null +++ b/src/PostIt/PostIt/Models/SignaturePadData.cs @@ -0,0 +1,103 @@ +namespace PostIt.Models; + +/// +/// Serialized form of a signature captured by +/// . +/// +/// Wire format (length-prefixed, normalised): +/// +/// int[] = [k0, x00, y00, x01, y01, ..., x0_{k0-1}, y0_{k0-1}, +/// k1, x10, y10, x11, y11, ..., x1_{k1-1}, y1_{k1-1}, +/// ...] +/// +/// +/// k_i — number of (x, y) pairs in stroke i. +/// x, y — coordinates normalised to [0, CoordinateMax] +/// (inclusive) on the control's client area. +/// is 10_000 by default — a 4-decimal fixed-point fraction of +/// the surface, which is enough to discriminate 0.01% of the diagonal +/// on any reasonable screen and stays well inside int. +/// Total array length is even: each stroke contributes +/// 1 + 2 * k_i integers, and 1 + 2k is always odd. +/// Sum of 1 + 2k_i over strokes is therefore odd * N, which +/// is odd when N is odd and even when N is even — so the overall +/// "size pair" property is not enforced, only the per-stroke shape +/// is. If the consumer needs a strictly even total, pad the last +/// stroke with a duplicate terminal point (or use +/// to drop the array entirely). +/// +/// +/// Empty signature (no strokes) is represented by an empty array +/// (length 0). A single dot — pen down + pen up at the same point — +/// is a single stroke with k = 1: [1, x, y]. +/// +public sealed class SignaturePadData +{ + /// + /// Upper bound of normalised coordinates. 10_000 means a + /// surface unit is represented as 0.0001 of the whole. + /// + public const int CoordinateMax = 10_000; + + /// + /// Raw payload. See for the layout. + /// Never null; an empty array means "no strokes". + /// + public int[] Strokes { get; } + + public SignaturePadData(int[] strokes) + { + if (strokes is null) throw new System.ArgumentNullException(nameof(strokes)); + Strokes = strokes; + } + + /// True if no stroke has been captured. + public bool IsEmpty => Strokes.Length == 0; + + /// + /// Number of distinct strokes (pen-down / pen-up cycles). + /// Returns 0 when is true. + /// + public int StrokeCount + { + get + { + if (Strokes.Length == 0) return 0; + int n = 0; + int i = 0; + while (i < Strokes.Length) + { + int k = Strokes[i]; + // Defensive: a malformed entry is treated as 0 so we + // never throw on read. The capture side never produces + // these, this is only for robustness on the wire. + if (k <= 0) return n; + i += 1 + 2 * k; + n++; + } + return n; + } + } + + /// + /// Total number of (x, y) pairs across all strokes. Useful + /// for sanity-checks and for displaying capture density + /// without re-walking the wire format. + /// + public int PointCount + { + get + { + int n = 0; + int i = 0; + while (i < Strokes.Length) + { + int k = Strokes[i]; + if (k <= 0) break; + n += k; + i += 1 + 2 * k; + } + return n; + } + } +} diff --git a/src/PostIt/PostIt/Services/OidcLoginPhase.cs b/src/PostIt/PostIt/Services/OidcLoginPhase.cs index 0add3d76..c4787489 100644 --- a/src/PostIt/PostIt/Services/OidcLoginPhase.cs +++ b/src/PostIt/PostIt/Services/OidcLoginPhase.cs @@ -12,7 +12,7 @@ namespace PostIt.Services; /// The set is deliberately small: each value is a milestone an /// operator can grep for in logs / StatusMessage, not a heartbeat. /// -public enum OidcLoginPhase +public enum OIDCLoginPhase { /// No login in flight (or login has settled). Idle, diff --git a/src/PostIt/PostIt/Services/YavscApiClient.cs b/src/PostIt/PostIt/Services/YavscApiClient.cs index 6710ebb8..0a42773b 100644 --- a/src/PostIt/PostIt/Services/YavscApiClient.cs +++ b/src/PostIt/PostIt/Services/YavscApiClient.cs @@ -91,15 +91,15 @@ public class YavscApiClient : IAsyncDisposable /// for the human /// text (URLs, error detail). public async Task LoginInteractiveAsync( - IProgress? progress = null, + IProgress? progress = null, CancellationToken ct = default) { - progress?.Report(OidcLoginPhase.Discovering); + progress?.Report(OIDCLoginPhase.Discovering); var browser = Platform.CreateBrowser?.Invoke(); if (browser is null) { - progress?.Report(OidcLoginPhase.Error); + progress?.Report(OIDCLoginPhase.Error); throw new InvalidOperationException("No browser is available on this platform."); } @@ -114,20 +114,20 @@ public class YavscApiClient : IAsyncDisposable // the moment we ask the browser to open (covers the entire // user-driven window including the AwaitingCallback wait), and // the moment we trade the code for tokens. - progress?.Report(OidcLoginPhase.OpeningBrowser); + progress?.Report(OIDCLoginPhase.OpeningBrowser); var result = await client.LoginAsync(new LoginRequest(), ct).ConfigureAwait(false); if (result.IsError) { - progress?.Report(OidcLoginPhase.Error); + progress?.Report(OIDCLoginPhase.Error); throw new InvalidOperationException($"OIDC login failed: {result.Error}"); } - progress?.Report(OidcLoginPhase.ExchangingCode); + progress?.Report(OIDCLoginPhase.ExchangingCode); if (string.IsNullOrEmpty(result.RefreshToken)) { - progress?.Report(OidcLoginPhase.Error); + progress?.Report(OIDCLoginPhase.Error); throw new InvalidOperationException( "Missing refresh_token — vérifie le scope 'offline_access'."); } @@ -139,7 +139,7 @@ public class YavscApiClient : IAsyncDisposable IdToken: result.IdentityToken); _store.Save(_tokens); - progress?.Report(OidcLoginPhase.Success); + progress?.Report(OIDCLoginPhase.Success); } /// @@ -152,7 +152,7 @@ public class YavscApiClient : IAsyncDisposable /// phase and returns false so the UI can keep going. /// public async Task TrySilentLoginAsync( - IProgress? progress = null, + IProgress? progress = null, CancellationToken ct = default) { if (!HasValidSession) return false; @@ -161,7 +161,7 @@ public class YavscApiClient : IAsyncDisposable // Access token still has plenty of life — nothing to do. if (_tokens.AccessTokenExpiresAt - DateTimeOffset.UtcNow > RefreshSkew) { - progress?.Report(OidcLoginPhase.Success); + progress?.Report(OIDCLoginPhase.Success); return true; } @@ -172,19 +172,19 @@ public class YavscApiClient : IAsyncDisposable // the user back to the login page. try { - progress?.Report(OidcLoginPhase.ExchangingCode); + progress?.Report(OIDCLoginPhase.ExchangingCode); await ForceRefreshAsync(ct).ConfigureAwait(false); - progress?.Report(OidcLoginPhase.Success); + progress?.Report(OIDCLoginPhase.Success); return true; } catch (RefreshFailedException) { - progress?.Report(OidcLoginPhase.Idle); + progress?.Report(OIDCLoginPhase.Idle); return false; } catch { - progress?.Report(OidcLoginPhase.Idle); + progress?.Report(OIDCLoginPhase.Idle); return false; } } @@ -205,6 +205,16 @@ public class YavscApiClient : IAsyncDisposable return dto!; } + /// + /// Call a JSON endpoint with no request body while still allowing a + /// positional cancellation token argument. + /// + public Task CallAsync( + HttpMethod method, + string path, + CancellationToken ct) + => CallAsync(method, path, body: null, ct); + /// Call an endpoint that returns no useful body (DELETE, etc.). public async Task CallAsync( HttpMethod method, @@ -216,6 +226,16 @@ public class YavscApiClient : IAsyncDisposable response.EnsureSuccessStatusCode(); } + /// + /// Call an endpoint with no request body while still allowing a + /// positional cancellation token argument. + /// + public Task CallAsync( + HttpMethod method, + string path, + CancellationToken ct) + => CallAsync(method, path, body: null, ct); + private async Task SendAsync( HttpMethod method, string path, object? body, CancellationToken ct) { diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs index 9a05aa84..e6d0e91a 100644 --- a/src/PostIt/PostIt/ViewLocator.cs +++ b/src/PostIt/PostIt/ViewLocator.cs @@ -29,6 +29,7 @@ public class ViewLocator : IDataTemplate SettingsPageViewModel => _services.GetRequiredService(), LoginPageViewModel => _services.GetRequiredService(), HomePageViewModel => _services.GetRequiredService(), + SignaturePageViewModel => _services.GetRequiredService(), _ => new TextBlock { Text = $"No view for {data.GetType().Name}" } }; } diff --git a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs index 009d67f2..a6ed595d 100644 --- a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs @@ -105,8 +105,8 @@ public partial class LoginPageViewModel : ViewModelBase /// callback hand-off: when AwaitingCallback never resolves, /// the OS never re-launched PostIt with the postit:// URL. /// - private OidcLoginPhase _phase = OidcLoginPhase.Idle; - public OidcLoginPhase Phase + private OIDCLoginPhase _phase = OIDCLoginPhase.Idle; + public OIDCLoginPhase Phase { get => _phase; private set @@ -122,13 +122,13 @@ public partial class LoginPageViewModel : ViewModelBase /// public string PhaseLabel => _phase switch { - OidcLoginPhase.Idle => "En attente", - OidcLoginPhase.Discovering => "Découverte OIDC…", - OidcLoginPhase.OpeningBrowser => "Ouverture du navigateur…", - OidcLoginPhase.AwaitingCallback => "En attente du callback postit://…", - OidcLoginPhase.ExchangingCode => "Échange du code contre les jetons…", - OidcLoginPhase.Success => "Connecté", - OidcLoginPhase.Error => "Erreur", + OIDCLoginPhase.Idle => "En attente", + OIDCLoginPhase.Discovering => "Découverte OIDC…", + OIDCLoginPhase.OpeningBrowser => "Ouverture du navigateur…", + OIDCLoginPhase.AwaitingCallback => "En attente du callback postit://…", + OIDCLoginPhase.ExchangingCode => "Échange du code contre les jetons…", + OIDCLoginPhase.Success => "Connecté", + OIDCLoginPhase.Error => "Erreur", _ => _phase.ToString(), }; @@ -275,7 +275,7 @@ public partial class LoginPageViewModel : ViewModelBase // The progress sink drives Phase / PhaseLabel; StatusMessage // keeps the text detail (URLs, error messages). Same // underlying flow, two views. - var progress = new Progress(p => Phase = p); + var progress = new Progress(p => Phase = p); await LoginInteractiveCoreAsync(_api, progress); IsBusy = false; @@ -299,7 +299,7 @@ public partial class LoginPageViewModel : ViewModelBase /// private async Task LoginInteractiveCoreAsync( YavscApiClient api, - IProgress? progress = null) + IProgress? progress = null) { var original = Platform.CreateBrowser; try diff --git a/src/PostIt/PostIt/ViewModels/SignaturePageViewModel.cs b/src/PostIt/PostIt/ViewModels/SignaturePageViewModel.cs new file mode 100644 index 00000000..b4b37974 --- /dev/null +++ b/src/PostIt/PostIt/ViewModels/SignaturePageViewModel.cs @@ -0,0 +1,185 @@ +using System; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using PostIt.Controls; +using PostIt.Models; + +namespace PostIt.ViewModels; + +/// +/// Backing state for . +/// +/// The page exists to produce a +/// (length-prefixed normalised int[]) from a human signature drawn +/// with the mouse (Desktop) or finger (touch / Android). The page +/// is a recipient of an external trigger — a SignalR push from +/// Yavsc.Org telling PostIt "a devis has been sent, sign here" — +/// so it intentionally has no first-class entry point in +/// . The only "open" affordance today is a +/// dev-only shortcut on the blog editor, marked for removal once +/// the SignalR handler lands. +/// +/// Output path is the platform-friendly per-user data directory +/// (XDG_DATA_HOME / AppData / NSDocumentDirectory on iOS). Files +/// are JSON, one per capture, named +/// signature-{yyyyMMdd-HHmmssfff}.json. This is a stop-gap +/// until the Yavsc.Org endpoint exists; the contract there will +/// be POST /api/signature/{devisId} with this same payload. +/// +public partial class SignaturePageViewModel : ViewModelBase +{ + /// + /// Default capture surface, in DIPs. 3:1 ratio matches a + /// signature line at the bottom of an A4 contract. + /// + public const double DefaultWidth = 600; + public const double DefaultHeight = 200; + + [ObservableProperty] + public partial string StatusMessage { get; set; } = "Prêt."; + + [ObservableProperty] + public partial int StrokeCount { get; set; } + + [ObservableProperty] + public partial int PointCount { get; set; } + + [ObservableProperty] + public partial string? LastCapturedPath { get; set; } + + public double Width { get; } + public double Height { get; } + + private SignaturePadControl? _control; + + public override bool CanNavigateNext + { + get => false; + protected set { _ = value; } + } + + public override bool CanNavigatePrevious + { + get => true; + protected set { _ = value; } + } + + public SignaturePageViewModel() + : this(DefaultWidth, DefaultHeight) + { + } + + public SignaturePageViewModel(double width, double height) + { + if (width <= 0) throw new ArgumentOutOfRangeException(nameof(width)); + if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height)); + Width = width; + Height = height; + } + + /// + /// Bind a freshly-constructed (or re-templated) control to this + /// VM. Called from the view's code-behind once the control has + /// been added to the visual tree and its template applied (so + /// is wired). + /// + public void Attach(SignaturePadControl control) + { + if (control is null) throw new ArgumentNullException(nameof(control)); + Detach(); + _control = control; + _control.RedrawRequested += OnRedraw; + _control.StrokeCompleted += OnStrokeCompleted; + RefreshCounts(); + } + + public void Detach() + { + if (_control is null) return; + _control.RedrawRequested -= OnRedraw; + _control.StrokeCompleted -= OnStrokeCompleted; + _control = null; + } + + private void OnStrokeCompleted(object? sender, SignaturePadData data) + { + StatusMessage = $"Trait terminé. {data.StrokeCount} trait(s)."; + RefreshCounts(); + } + + private void OnRedraw(object? sender, EventArgs e) => RefreshCounts(); + + private void RefreshCounts() + { + if (_control is null) return; + var snap = _control.Snapshot(); + StrokeCount = snap.StrokeCount; + PointCount = snap.PointCount; + } + + [RelayCommand] + public void Clear() + { + _control?.Clear(); + StatusMessage = "Effacé."; + RefreshCounts(); + } + + [RelayCommand] + public async Task CaptureAsync() + { + if (_control is null) + { + StatusMessage = "Contrôle non attaché."; + return; + } + + var data = _control.Snapshot(); + if (data.IsEmpty) + { + StatusMessage = "Rien à capturer."; + return; + } + + try + { + var path = WriteCapture(data); + LastCapturedPath = path; + StatusMessage = $"Capture enregistrée: {path}"; + } + catch (Exception ex) + { + StatusMessage = $"Erreur: {ex.Message}"; + } + await Task.CompletedTask; + } + + private static string WriteCapture(SignaturePadData data) + { + var dir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "PostIt", "signatures"); + Directory.CreateDirectory(dir); + + var fileName = $"signature-{DateTime.UtcNow:yyyyMMdd-HHmmssfff}.json"; + var path = Path.Combine(dir, fileName); + + var payload = new + { + format = "yavsc.signature/v1", + coordinateMax = SignaturePadData.CoordinateMax, + capturedAtUtc = DateTime.UtcNow, + strokes = data.Strokes, + strokeCount = data.StrokeCount, + }; + File.WriteAllText( + path, + JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }), + Encoding.UTF8); + return path; + } +} diff --git a/src/PostIt/PostIt/Views/MainPage.axaml b/src/PostIt/PostIt/Views/MainPage.axaml index b9e0308d..c09c2f7a 100644 --- a/src/PostIt/PostIt/Views/MainPage.axaml +++ b/src/PostIt/PostIt/Views/MainPage.axaml @@ -35,6 +35,17 @@ - + \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Account/ForgotPasswordConfirmation.cshtml b/src/Yavsc.Org/Views/Account/ForgotPasswordConfirmation.cshtml index d1e50615..4faaf2d0 100644 --- a/src/Yavsc.Org/Views/Account/ForgotPasswordConfirmation.cshtml +++ b/src/Yavsc.Org/Views/Account/ForgotPasswordConfirmation.cshtml @@ -1,5 +1,7 @@ @model string +@{ + ViewBag.Title = Localizer["Account"]; +}

Check your mail box!

- diff --git a/src/Yavsc.Org/Views/Account/LoggedOut.cshtml b/src/Yavsc.Org/Views/Account/LoggedOut.cshtml index dc9fbf7a..82c653c7 100644 --- a/src/Yavsc.Org/Views/Account/LoggedOut.cshtml +++ b/src/Yavsc.Org/Views/Account/LoggedOut.cshtml @@ -1,4 +1,8 @@ -@model LoggedOutViewModel +@{ + ViewBag.Title = Localizer["Account"]; +} + +@model LoggedOutViewModel @{ // set this so the layout rendering sees an anonymous user diff --git a/src/Yavsc.Org/Views/Account/Login.cshtml b/src/Yavsc.Org/Views/Account/Login.cshtml index b0ee8e88..54df2e3e 100644 --- a/src/Yavsc.Org/Views/Account/Login.cshtml +++ b/src/Yavsc.Org/Views/Account/Login.cshtml @@ -1,4 +1,7 @@ @model LoginViewModel +@{ + ViewBag.Title = Localizer["Account"]; +} - + \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Account/Logout.cshtml b/src/Yavsc.Org/Views/Account/Logout.cshtml index b49dee09..893807f7 100644 --- a/src/Yavsc.Org/Views/Account/Logout.cshtml +++ b/src/Yavsc.Org/Views/Account/Logout.cshtml @@ -1,4 +1,8 @@ -@model LogoutViewModel +@{ + ViewBag.Title = Localizer["Account"]; +} + +@model LogoutViewModel
diff --git a/src/Yavsc.Org/Views/Account/Register.cshtml b/src/Yavsc.Org/Views/Account/Register.cshtml index 388b2ecd..92a7f34e 100644 --- a/src/Yavsc.Org/Views/Account/Register.cshtml +++ b/src/Yavsc.Org/Views/Account/Register.cshtml @@ -1,8 +1,11 @@ @model RegisterModel +@{ + ViewBag.Title = Localizer["Account"]; +}
@Html.EditorForModel() -
+ \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Account/ResetPassword.cshtml b/src/Yavsc.Org/Views/Account/ResetPassword.cshtml index 5f64c727..0cdbb2e3 100644 --- a/src/Yavsc.Org/Views/Account/ResetPassword.cshtml +++ b/src/Yavsc.Org/Views/Account/ResetPassword.cshtml @@ -1,4 +1,7 @@ @model ResetPasswordViewModel +@{ + ViewBag.Title = Localizer["Account"]; +}

Your email : @Model.Email

@@ -15,4 +18,4 @@ -
+ \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Account/Signin.cshtml b/src/Yavsc.Org/Views/Account/Signin.cshtml index 079882b3..871c0c74 100644 --- a/src/Yavsc.Org/Views/Account/Signin.cshtml +++ b/src/Yavsc.Org/Views/Account/Signin.cshtml @@ -1,4 +1,7 @@ @model SignInModel +@{ + ViewBag.Title = Localizer["Account"]; +} -
+
\ No newline at end of file diff --git a/src/Yavsc.Org/Views/Administration/Haircut.cshtml b/src/Yavsc.Org/Views/Administration/Haircut.cshtml index a59ab801..0165665c 100644 --- a/src/Yavsc.Org/Views/Administration/Haircut.cshtml +++ b/src/Yavsc.Org/Views/Administration/Haircut.cshtml @@ -1,4 +1,7 @@ @model HaircutAdminViewModel +@{ + ViewBag.Title = Localizer["Administration"]; +} Gestion des couleurs diff --git a/src/Yavsc.Org/Views/Administration/Role.cshtml b/src/Yavsc.Org/Views/Administration/Role.cshtml index 68a5710f..e541f75a 100644 --- a/src/Yavsc.Org/Views/Administration/Role.cshtml +++ b/src/Yavsc.Org/Views/Administration/Role.cshtml @@ -21,7 +21,19 @@ @foreach (var user in Model.Users) { - avatar + @if (user.UserId==User.GetUserId()) { + You + } + @if (SiteSettings.Value.Admin.EMail == user.Email) { + Admin + } + @if (SiteSettings.Value.Owner.EMail == user.Email) { + Owner + } + @if (!String.IsNullOrWhiteSpace(user.Avatar)) + { + avatar + } @user.UserName <@(user.Email)> diff --git a/src/Yavsc.Org/Views/ApiScope/Create.cshtml b/src/Yavsc.Org/Views/ApiScope/Create.cshtml index 5d3b9b8b..8297251d 100644 --- a/src/Yavsc.Org/Views/ApiScope/Create.cshtml +++ b/src/Yavsc.Org/Views/ApiScope/Create.cshtml @@ -1,20 +1,9 @@ @model IdentityServer8.EntityFramework.Entities.ApiScope - @{ - Layout = null; + ViewBag.Title = Localizer["ApiScope"]; } - - - - - - Create - - -

ApiScope

-
@@ -64,6 +53,3 @@ - - - diff --git a/src/Yavsc.Org/Views/ApiScope/Delete.cshtml b/src/Yavsc.Org/Views/ApiScope/Delete.cshtml index 166e6110..abb3d26b 100644 --- a/src/Yavsc.Org/Views/ApiScope/Delete.cshtml +++ b/src/Yavsc.Org/Views/ApiScope/Delete.cshtml @@ -1,18 +1,8 @@ @model Yavsc.Models.YavscApiScope - @{ - Layout = null; + ViewBag.Title = Localizer["ApiScope"]; } - - - - - - Delete - - -

Are you sure you want to delete this?

YavscApiScope

@@ -61,12 +51,9 @@ @Html.DisplayFor(model => model.ShowInDiscoveryDocument) - + | Back to List -
- - diff --git a/src/Yavsc.Org/Views/ApiScope/Details.cshtml b/src/Yavsc.Org/Views/ApiScope/Details.cshtml index 6c904ce3..9275a114 100644 --- a/src/Yavsc.Org/Views/ApiScope/Details.cshtml +++ b/src/Yavsc.Org/Views/ApiScope/Details.cshtml @@ -1,17 +1,8 @@ @model Yavsc.Models.YavscApiScope - @{ - Layout = null; + ViewBag.Title = Localizer["ApiScope"]; } - - - - - - Details - -

YavscApiScope

@@ -65,5 +56,3 @@ Edit | Back to List
- - diff --git a/src/Yavsc.Org/Views/ApiScope/Edit.cshtml b/src/Yavsc.Org/Views/ApiScope/Edit.cshtml index 34b111d4..1466435b 100644 --- a/src/Yavsc.Org/Views/ApiScope/Edit.cshtml +++ b/src/Yavsc.Org/Views/ApiScope/Edit.cshtml @@ -1,17 +1,8 @@ @model Yavsc.Models.YavscApiScope - @{ - Layout = null; + ViewBag.Title = Localizer["ApiScope"]; } - - - - - - Edit - -

YavscApiScope


@@ -65,6 +56,3 @@ - - - diff --git a/src/Yavsc.Org/Views/ApiScope/Index.cshtml b/src/Yavsc.Org/Views/ApiScope/Index.cshtml index 2d3bd6c7..2c64b5c1 100644 --- a/src/Yavsc.Org/Views/ApiScope/Index.cshtml +++ b/src/Yavsc.Org/Views/ApiScope/Index.cshtml @@ -1,17 +1,10 @@ @model IEnumerable @{ - Layout = null; + ViewBag.Title = Localizer["ApiScope"]; + } - - - - - - Index - -

Create New

@@ -75,5 +68,3 @@ } - - diff --git a/src/Yavsc.Org/Views/Client/Create.cshtml b/src/Yavsc.Org/Views/Client/Create.cshtml index 76fece36..3bdb03ab 100644 --- a/src/Yavsc.Org/Views/Client/Create.cshtml +++ b/src/Yavsc.Org/Views/Client/Create.cshtml @@ -1,4 +1,7 @@ @model Client +@{ + ViewBag.Title = Localizer["Client"]; +}

@Localizer["Create"]

@@ -77,4 +80,3 @@ - diff --git a/src/Yavsc.Org/Views/Client/Delete.cshtml b/src/Yavsc.Org/Views/Client/Delete.cshtml index bba5a1b5..f2e68d8e 100644 --- a/src/Yavsc.Org/Views/Client/Delete.cshtml +++ b/src/Yavsc.Org/Views/Client/Delete.cshtml @@ -1,4 +1,7 @@ @model Client +@{ + ViewBag.Title = Localizer["Client"]; +}

@Localizer["Delete"]

@@ -63,4 +66,4 @@ @Localizer["Back to List"]
-
+ \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Client/Details.cshtml b/src/Yavsc.Org/Views/Client/Details.cshtml index b39a3f70..770042be 100644 --- a/src/Yavsc.Org/Views/Client/Details.cshtml +++ b/src/Yavsc.Org/Views/Client/Details.cshtml @@ -2,7 +2,12 @@ @using Microsoft.AspNetCore.Html @using System.Text +@{ + ViewBag.Title = Localizer["Client"]; +} + @functions { + // Lifetimes in IdentityServer are stored as seconds (int). Render them // in a human-friendly way: "5 min", "2 h", "30 d", "—" for 0/negative. // Zero typically means "use the global default" — show it as such so diff --git a/src/Yavsc.Org/Views/Client/Edit.cshtml b/src/Yavsc.Org/Views/Client/Edit.cshtml index a1245061..5b9e107d 100644 --- a/src/Yavsc.Org/Views/Client/Edit.cshtml +++ b/src/Yavsc.Org/Views/Client/Edit.cshtml @@ -1,4 +1,7 @@ @model Client +@{ + ViewBag.Title = Localizer["Client"]; +}

@Localizer["Edit"]

@@ -303,4 +306,4 @@ + \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Client/EditClaims.cshtml b/src/Yavsc.Org/Views/Client/EditClaims.cshtml index bf156b09..9877019b 100644 --- a/src/Yavsc.Org/Views/Client/EditClaims.cshtml +++ b/src/Yavsc.Org/Views/Client/EditClaims.cshtml @@ -1,4 +1,8 @@ @model IEnumerable +@{ + ViewBag.Title = Localizer["Client"]; +} + @{ ViewData["Title"] = "Edit Client Claims"; var clientId = Model.FirstOrDefault()?.ClientId ?? 0; @@ -61,4 +65,4 @@

Back to client -

+

\ No newline at end of file diff --git a/src/Yavsc.Org/Views/Client/EditCorsOrigins.cshtml b/src/Yavsc.Org/Views/Client/EditCorsOrigins.cshtml index 035c130a..7e613b35 100644 --- a/src/Yavsc.Org/Views/Client/EditCorsOrigins.cshtml +++ b/src/Yavsc.Org/Views/Client/EditCorsOrigins.cshtml @@ -1,4 +1,8 @@ @model IEnumerable +@{ + ViewBag.Title = Localizer["Client"]; +} + @{ ViewData["Title"] = "Edit Allowed CORS Origins"; ViewData["addAction"] = "AddCorsOrigin"; @@ -23,4 +27,4 @@

Back to client -

+

\ No newline at end of file diff --git a/src/Yavsc.Org/Views/Client/EditGrantTypes.cshtml b/src/Yavsc.Org/Views/Client/EditGrantTypes.cshtml index 6f1a1739..139d779a 100644 --- a/src/Yavsc.Org/Views/Client/EditGrantTypes.cshtml +++ b/src/Yavsc.Org/Views/Client/EditGrantTypes.cshtml @@ -1,4 +1,8 @@ @model IEnumerable +@{ + ViewBag.Title = Localizer["Client"]; +} + @{ ViewData["Title"] = "Edit Allowed Grant Types"; ViewData["addAction"] = "AddGrantType"; @@ -24,4 +28,4 @@

Back to client -

+

\ No newline at end of file diff --git a/src/Yavsc.Org/Views/Client/EditIdPRestrictions.cshtml b/src/Yavsc.Org/Views/Client/EditIdPRestrictions.cshtml index a0032598..5f9d1160 100644 --- a/src/Yavsc.Org/Views/Client/EditIdPRestrictions.cshtml +++ b/src/Yavsc.Org/Views/Client/EditIdPRestrictions.cshtml @@ -1,4 +1,8 @@ @model IEnumerable +@{ + ViewBag.Title = Localizer["Client"]; +} + @{ ViewData["Title"] = "Edit Identity Provider Restrictions"; ViewData["addAction"] = "AddIdPRestriction"; @@ -21,4 +25,4 @@

Back to client -

+

\ No newline at end of file diff --git a/src/Yavsc.Org/Views/Client/EditPostLogoutRedirectUris.cshtml b/src/Yavsc.Org/Views/Client/EditPostLogoutRedirectUris.cshtml index f80d2953..9282d298 100644 --- a/src/Yavsc.Org/Views/Client/EditPostLogoutRedirectUris.cshtml +++ b/src/Yavsc.Org/Views/Client/EditPostLogoutRedirectUris.cshtml @@ -1,4 +1,8 @@ @model IEnumerable +@{ + ViewBag.Title = Localizer["Client"]; +} + @{ ViewData["Title"] = "Edit Post-logout Redirect URIs"; ViewData["addAction"] = "AddPostLogoutRedirectUri"; @@ -22,4 +26,4 @@

Back to client -

+

\ No newline at end of file diff --git a/src/Yavsc.Org/Views/Client/EditProperties.cshtml b/src/Yavsc.Org/Views/Client/EditProperties.cshtml index e067d487..6017bc34 100644 --- a/src/Yavsc.Org/Views/Client/EditProperties.cshtml +++ b/src/Yavsc.Org/Views/Client/EditProperties.cshtml @@ -1,4 +1,8 @@ @model IEnumerable +@{ + ViewBag.Title = Localizer["Client"]; +} + @{ ViewData["Title"] = "Edit Client Properties"; var clientId = Model.FirstOrDefault()?.ClientId ?? 0; @@ -62,4 +66,4 @@

Back to client -

+

\ No newline at end of file diff --git a/src/Yavsc.Org/Views/Client/EditRedirectUris.cshtml b/src/Yavsc.Org/Views/Client/EditRedirectUris.cshtml index fe936624..4a6467a2 100644 --- a/src/Yavsc.Org/Views/Client/EditRedirectUris.cshtml +++ b/src/Yavsc.Org/Views/Client/EditRedirectUris.cshtml @@ -1,4 +1,8 @@ @model IEnumerable +@{ + ViewBag.Title = Localizer["Client"]; +} + @{ ViewData["Title"] = "Edit Redirect URIs"; ViewData["addAction"] = "AddRedirectUri"; @@ -22,4 +26,4 @@

Back to client -

+

\ No newline at end of file diff --git a/src/Yavsc.Org/Views/Client/EditScopes.cshtml b/src/Yavsc.Org/Views/Client/EditScopes.cshtml index 0e9e0608..62978f5c 100644 --- a/src/Yavsc.Org/Views/Client/EditScopes.cshtml +++ b/src/Yavsc.Org/Views/Client/EditScopes.cshtml @@ -1,4 +1,8 @@ @model IEnumerable +@{ + ViewBag.Title = Localizer["Client"]; +} + @{ ViewData["Title"] = "Edit Allowed Scopes"; ViewData["addAction"] = "AddScope"; @@ -22,4 +26,4 @@

Back to client -

+

\ No newline at end of file diff --git a/src/Yavsc.Org/Views/Client/EditSecrets.cshtml b/src/Yavsc.Org/Views/Client/EditSecrets.cshtml index 61a23cb3..e7ce748d 100644 --- a/src/Yavsc.Org/Views/Client/EditSecrets.cshtml +++ b/src/Yavsc.Org/Views/Client/EditSecrets.cshtml @@ -1,4 +1,8 @@ @model IEnumerable +@{ + ViewBag.Title = Localizer["Client"]; +} + @{ ViewData["Title"] = "Edit Client Secrets"; var clientId = Model.FirstOrDefault()?.ClientId ?? 0; @@ -80,4 +84,4 @@

Back to client Regenerate a single secret -

+

\ No newline at end of file diff --git a/src/Yavsc.Org/Views/Client/Index.cshtml b/src/Yavsc.Org/Views/Client/Index.cshtml index f91f71dc..347ee263 100644 --- a/src/Yavsc.Org/Views/Client/Index.cshtml +++ b/src/Yavsc.Org/Views/Client/Index.cshtml @@ -1,6 +1,10 @@ @using IdentityServer8.Models; @model IEnumerable +@{ + ViewBag.Title = Localizer["Client"]; +} +

@Localizer["Index"]

@@ -10,7 +14,7 @@ @foreach (var item in Model) {

- +
Identifier
and activation
@@ -36,7 +40,7 @@
- +
Urls
@@ -56,7 +60,7 @@
@Html.DisplayNameFor(model => model.AbsoluteRefreshTokenLifetime)
@Html.DisplayFor(model => item.AbsoluteRefreshTokenLifetime)
- +
@@ -78,7 +82,7 @@
@Enum.GetName(typeof(AccessTokenType), item.AccessTokenType) - +
@@ -86,6 +90,6 @@ Edit Details Delete - + } diff --git a/src/Yavsc.Org/Views/Client/RegenerateSecret.cshtml b/src/Yavsc.Org/Views/Client/RegenerateSecret.cshtml index b0fd554d..cc1c1b4f 100644 --- a/src/Yavsc.Org/Views/Client/RegenerateSecret.cshtml +++ b/src/Yavsc.Org/Views/Client/RegenerateSecret.cshtml @@ -1,4 +1,7 @@ @model Client +@{ + ViewBag.Title = Localizer["Client"]; +}

@Localizer["Regenerate client secret"]

@@ -36,4 +39,4 @@ @Html.AntiForgeryToken() @Localizer["Cancel"] - + \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Client/ShowSecret.cshtml b/src/Yavsc.Org/Views/Client/ShowSecret.cshtml index 5256e327..3d8ff78a 100644 --- a/src/Yavsc.Org/Views/Client/ShowSecret.cshtml +++ b/src/Yavsc.Org/Views/Client/ShowSecret.cshtml @@ -1,3 +1,7 @@ +@{ + ViewBag.Title = Localizer["Client"]; +} + @{ var secret = ViewBag.NewClientSecret as string; var expiresAt = ViewBag.NewClientSecretExpiresAt as string; diff --git a/src/Yavsc.Org/Views/CommandForms/Index.cshtml b/src/Yavsc.Org/Views/CommandForms/Index.cshtml index ede4b71d..975f131f 100644 --- a/src/Yavsc.Org/Views/CommandForms/Index.cshtml +++ b/src/Yavsc.Org/Views/CommandForms/Index.cshtml @@ -1,10 +1,10 @@ @model IEnumerable @{ - ViewBag.Title = "Index"; + ViewBag.Title = Localizer["CommandForms"]; } -

Index

+

@ViewBag.Title

Create New @@ -22,7 +22,7 @@ - + @foreach (var item in Model) { diff --git a/src/Yavsc.Org/Views/Device/Success.cshtml b/src/Yavsc.Org/Views/Device/Success.cshtml index 050dd91c..7ba766d3 100644 --- a/src/Yavsc.Org/Views/Device/Success.cshtml +++ b/src/Yavsc.Org/Views/Device/Success.cshtml @@ -1,7 +1,10 @@ +@{ + ViewBag.Title = Localizer["Device"]; +}

Success

You have successfully authorized the device

-
+ \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Device/UserCodeCapture.cshtml b/src/Yavsc.Org/Views/Device/UserCodeCapture.cshtml index 6d412613..6357c752 100644 --- a/src/Yavsc.Org/Views/Device/UserCodeCapture.cshtml +++ b/src/Yavsc.Org/Views/Device/UserCodeCapture.cshtml @@ -1,4 +1,7 @@ @model string +@{ + ViewBag.Title = Localizer["Device"]; +}
@@ -20,4 +23,4 @@
- + \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Device/UserCodeConfirmation.cshtml b/src/Yavsc.Org/Views/Device/UserCodeConfirmation.cshtml index e1d3b196..ef60ba1c 100644 --- a/src/Yavsc.Org/Views/Device/UserCodeConfirmation.cshtml +++ b/src/Yavsc.Org/Views/Device/UserCodeConfirmation.cshtml @@ -1,4 +1,7 @@ @model DeviceAuthorizationViewModel +@{ + ViewBag.Title = Localizer["Device"]; +}
@@ -105,4 +108,4 @@
- + \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Diagnostics/Index.cshtml b/src/Yavsc.Org/Views/Diagnostics/Index.cshtml index e939c0d8..b7bf7a36 100644 --- a/src/Yavsc.Org/Views/Diagnostics/Index.cshtml +++ b/src/Yavsc.Org/Views/Diagnostics/Index.cshtml @@ -1,5 +1,9 @@ @model DiagnosticsViewModel +@{ + ViewBag.Title = Localizer["Diagnostics"]; +} +

Authentication Cookie

@@ -22,7 +26,7 @@
- +
diff --git a/src/Yavsc.Org/Views/Do/Yavsc.Models.Booking.MusicianSettingsEditor.fr.cshtml b/src/Yavsc.Org/Views/Do/Yavsc.Models.Booking.MusicianSettingsEditor.fr.cshtml index 0b9b6b32..2bfa0ae5 100644 --- a/src/Yavsc.Org/Views/Do/Yavsc.Models.Booking.MusicianSettingsEditor.fr.cshtml +++ b/src/Yavsc.Org/Views/Do/Yavsc.Models.Booking.MusicianSettingsEditor.fr.cshtml @@ -1,4 +1,7 @@ @model Instrumentation +@{ + ViewBag.Title = Localizer["Do"]; +}

@Model.Tool.Name

@@ -6,4 +9,4 @@ Remove Instrument
-
+ \ No newline at end of file diff --git a/src/Yavsc.Org/Views/FrontOffice/BillAcquitment.cshtml b/src/Yavsc.Org/Views/FrontOffice/BillAcquitment.cshtml index e69de29b..8777a809 100644 --- a/src/Yavsc.Org/Views/FrontOffice/BillAcquitment.cshtml +++ b/src/Yavsc.Org/Views/FrontOffice/BillAcquitment.cshtml @@ -0,0 +1,4 @@ +@{ + ViewBag.Title = Localizer["FrontOffice"]; +} + diff --git a/src/Yavsc.Org/Views/FrontOffice/BillValidation.cshtml b/src/Yavsc.Org/Views/FrontOffice/BillValidation.cshtml index e69de29b..8777a809 100644 --- a/src/Yavsc.Org/Views/FrontOffice/BillValidation.cshtml +++ b/src/Yavsc.Org/Views/FrontOffice/BillValidation.cshtml @@ -0,0 +1,4 @@ +@{ + ViewBag.Title = Localizer["FrontOffice"]; +} + diff --git a/src/Yavsc.Org/Views/FrontOffice/EstimateClientValidation.cshtml b/src/Yavsc.Org/Views/FrontOffice/EstimateClientValidation.cshtml index e69de29b..8777a809 100644 --- a/src/Yavsc.Org/Views/FrontOffice/EstimateClientValidation.cshtml +++ b/src/Yavsc.Org/Views/FrontOffice/EstimateClientValidation.cshtml @@ -0,0 +1,4 @@ +@{ + ViewBag.Title = Localizer["FrontOffice"]; +} + diff --git a/src/Yavsc.Org/Views/FrontOffice/EstimateProValidation.cshtml b/src/Yavsc.Org/Views/FrontOffice/EstimateProValidation.cshtml index e69de29b..8777a809 100644 --- a/src/Yavsc.Org/Views/FrontOffice/EstimateProValidation.cshtml +++ b/src/Yavsc.Org/Views/FrontOffice/EstimateProValidation.cshtml @@ -0,0 +1,4 @@ +@{ + ViewBag.Title = Localizer["FrontOffice"]; +} + diff --git a/src/Yavsc.Org/Views/FrontOffice/Index.cshtml b/src/Yavsc.Org/Views/FrontOffice/Index.cshtml index e563532d..ed4c9038 100644 --- a/src/Yavsc.Org/Views/FrontOffice/Index.cshtml +++ b/src/Yavsc.Org/Views/FrontOffice/Index.cshtml @@ -13,7 +13,7 @@
EstimateToSignAsPro
-
@Html.DisplayFor(m=>m.EstimateToSignAsProCount) +
@Html.DisplayFor(m=>m.EstimateToHonorAsProCount)
EstimateToSignAsCli @@ -21,11 +21,6 @@
@Html.DisplayFor(m=>m.EstimateToSignAsCliCount)
-
BillToSignAsPro -
-
@Html.DisplayFor(m=>m.BillToSignAsProCount) -
-
BillToSignAsCli
@Html.DisplayFor(m=>m.BillToSignAsCliCount) diff --git a/src/Yavsc.Org/Views/FrontOffice/Yavsc.Models.Haircut.BrusherProfile.cshtml b/src/Yavsc.Org/Views/FrontOffice/Yavsc.Models.Haircut.BrusherProfile.cshtml index 5fe3cc17..9b22e772 100644 --- a/src/Yavsc.Org/Views/FrontOffice/Yavsc.Models.Haircut.BrusherProfile.cshtml +++ b/src/Yavsc.Org/Views/FrontOffice/Yavsc.Models.Haircut.BrusherProfile.cshtml @@ -1,4 +1,7 @@ @model Yavsc.Models.Haircut.BrusherProfile +@{ + ViewBag.Title = Localizer["FrontOffice"]; +}
Disponibilités diff --git a/src/Yavsc.Org/Views/FrontOffice/Yavsc.Models.Musical.Profiles.Instrumentation.cshtml b/src/Yavsc.Org/Views/FrontOffice/Yavsc.Models.Musical.Profiles.Instrumentation.cshtml index f4b2ec80..1dd077bc 100644 --- a/src/Yavsc.Org/Views/FrontOffice/Yavsc.Models.Musical.Profiles.Instrumentation.cshtml +++ b/src/Yavsc.Org/Views/FrontOffice/Yavsc.Models.Musical.Profiles.Instrumentation.cshtml @@ -1,5 +1,8 @@ @model Yavsc.Models.Musical.Profiles.Instrumentation +@{ + ViewBag.Title = Localizer["FrontOffice"]; +} @if (Model == null) {

NoInstrument

@@ -17,4 +20,4 @@
-} +} \ No newline at end of file diff --git a/src/Yavsc.Org/Views/FrontOffice/Yavsc.Models.Workflow.Profiles.FormationSettings.cshtml b/src/Yavsc.Org/Views/FrontOffice/Yavsc.Models.Workflow.Profiles.FormationSettings.cshtml index 139c4eb5..906f130a 100644 --- a/src/Yavsc.Org/Views/FrontOffice/Yavsc.Models.Workflow.Profiles.FormationSettings.cshtml +++ b/src/Yavsc.Org/Views/FrontOffice/Yavsc.Models.Workflow.Profiles.FormationSettings.cshtml @@ -1,4 +1,8 @@ @model Yavsc.Models.Workflow.Profiles.FormationSettings +@{ + ViewBag.Title = Localizer["FrontOffice"]; +} + @if (Model != null && Model.CoWorking != null) {
Partenariat
@@ -10,4 +14,4 @@ }
-} +} \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Grants/Index.cshtml b/src/Yavsc.Org/Views/Grants/Index.cshtml index a60f2261..7fc509e4 100644 --- a/src/Yavsc.Org/Views/Grants/Index.cshtml +++ b/src/Yavsc.Org/Views/Grants/Index.cshtml @@ -1,5 +1,9 @@ @model GrantsViewModel +@{ + ViewBag.Title = Localizer["Grants"]; +} +

Client Application Permissions

@@ -39,13 +43,13 @@
- +
    @if (grant.Description != null) {
  • @grant.Description -
  • + }
  • @grant.Created.ToString("yyyy-MM-dd") @@ -84,4 +88,4 @@
} } -
\ No newline at end of file + diff --git a/src/Yavsc.Org/Views/HairCutCommand/BrusherProfileScript.cshtml b/src/Yavsc.Org/Views/HairCutCommand/BrusherProfileScript.cshtml index 7458e15c..e26397e5 100644 --- a/src/Yavsc.Org/Views/HairCutCommand/BrusherProfileScript.cshtml +++ b/src/Yavsc.Org/Views/HairCutCommand/BrusherProfileScript.cshtml @@ -1,4 +1,7 @@ @model BrusherProfile +@{ + ViewBag.Title = Localizer["HairCutCommand"]; +} + \ No newline at end of file diff --git a/src/Yavsc.Org/Views/HairCutCommand/CommandConfirmation.cshtml b/src/Yavsc.Org/Views/HairCutCommand/CommandConfirmation.cshtml index ecdd8876..4031b7f0 100644 --- a/src/Yavsc.Org/Views/HairCutCommand/CommandConfirmation.cshtml +++ b/src/Yavsc.Org/Views/HairCutCommand/CommandConfirmation.cshtml @@ -54,7 +54,7 @@
@Html.DisplayNameFor(m => m.Location)
@if (Model.Location == null) { -

Pas de lieu convenu ...

+

Pas de lieu convenu ...

} else { @Html.DisplayFor(m => m.Location) @@ -62,7 +62,7 @@
Notification
-
@if (ViewBag.GooglePayload !=null) +
@if (ViewBag.GooglePayload !=null) { @if (ViewBag.GooglePayload.success>0) {

GCM Notifications sent

@@ -85,10 +85,10 @@
@await Component.InvokeAsync("Bill", Model)
- -
@Html.DisplayNameFor(m=>m.Regularisation)
+ +
@Html.DisplayNameFor(m=>m.Regularization)
@await Component.InvokeAsync("PayPalButton", Model)
- + diff --git a/src/Yavsc.Org/Views/Home/About.cshtml b/src/Yavsc.Org/Views/Home/About.cshtml index dbde9bc5..39062cb2 100755 --- a/src/Yavsc.Org/Views/Home/About.cshtml +++ b/src/Yavsc.Org/Views/Home/About.cshtml @@ -1,4 +1,7 @@ @using System.Diagnostics +@{ + ViewBag.Title = Localizer["Home"]; +}

@SiteSettings.Value.Title - À Propos

@@ -67,4 +70,4 @@ Il a accès à la connaissance des journées connues comme libres des artistes p De plus, le droit de retrait est permanent et sa mise en oeuvre immédiate. Les artistes comme les clients peuvent demander leur désinscription, qui désactive immédiatement les publications associées à leurs informations, et programme la suppression complète de ces dites informations dans les quinze jours à compter de la demande, sauf demande contradictoire. L'opération est annulable, jusqu'à deux semaines après sa programmation. - + \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Home/About.pt.cshtml b/src/Yavsc.Org/Views/Home/About.pt.cshtml index 85b4d291..cca60a94 100755 --- a/src/Yavsc.Org/Views/Home/About.pt.cshtml +++ b/src/Yavsc.Org/Views/Home/About.pt.cshtml @@ -1,3 +1,7 @@ +@{ + ViewBag.Title = Localizer["Home"]; +} +

@SiteSettings.Value.Title - objetivo

diff --git a/src/Yavsc.Org/Views/Home/AboutIdentityServer.cshtml b/src/Yavsc.Org/Views/Home/AboutIdentityServer.cshtml index b34a19c8..b77f2032 100644 --- a/src/Yavsc.Org/Views/Home/AboutIdentityServer.cshtml +++ b/src/Yavsc.Org/Views/Home/AboutIdentityServer.cshtml @@ -1,4 +1,7 @@ @using System.Diagnostics +@{ + ViewBag.Title = Localizer["Home"]; +} @{ var version = FileVersionInfo.GetVersionInfo(typeof(IdentityServer8.Hosting.IdentityServerMiddleware).Assembly.Location).ProductVersion.Split('+').First(); @@ -29,4 +32,4 @@ and ready to use samples. - + \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Home/Basket.cshtml b/src/Yavsc.Org/Views/Home/Basket.cshtml index d9882ae3..120b96d3 100644 --- a/src/Yavsc.Org/Views/Home/Basket.cshtml +++ b/src/Yavsc.Org/Views/Home/Basket.cshtml @@ -1,3 +1,7 @@ +@{ + ViewBag.Title = Localizer["Home"]; +} + @Model BasketView
    diff --git a/src/Yavsc.Org/Views/Home/Contact.cshtml b/src/Yavsc.Org/Views/Home/Contact.cshtml index d7ae2fa7..2488e2a0 100755 --- a/src/Yavsc.Org/Views/Home/Contact.cshtml +++ b/src/Yavsc.Org/Views/Home/Contact.cshtml @@ -1,5 +1,9 @@ @using Microsoft.IdentityModel.Protocols.Configuration @model SiteSettings +@{ + ViewBag.Title = Localizer["Home"]; +} +

    Contact

    @Model.Owner.Name

    @@ -9,4 +13,4 @@
    Support: @(Model.Admin.Name)<@(Model.Admin.EMail)>
    Marketing: @(Model.Owner.Name)<@(Model.Owner.EMail)> -
    + \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Home/Privacy.cshtml b/src/Yavsc.Org/Views/Home/Privacy.cshtml index 2f76522b..34ae1803 100644 --- a/src/Yavsc.Org/Views/Home/Privacy.cshtml +++ b/src/Yavsc.Org/Views/Home/Privacy.cshtml @@ -1,3 +1,6 @@ +@{ + ViewBag.Title = Localizer["Home"]; +} = La confidentialité @@ -10,4 +13,4 @@ ne sont transmis à personne. Seul le système et son link:Contact[possesseur] o De plus, le droit de retrait est permanent et sa mise en oeuvre link:/Account/Delete[immédiate]. - + \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Manage/DoDirectCredit.cshtml b/src/Yavsc.Org/Views/Manage/DoDirectCredit.cshtml index c51ad586..b6a21b88 100644 --- a/src/Yavsc.Org/Views/Manage/DoDirectCredit.cshtml +++ b/src/Yavsc.Org/Views/Manage/DoDirectCredit.cshtml @@ -1,5 +1,9 @@ @model DoDirectCreditViewModel +@{ + ViewBag.Title = Localizer["Manage"]; +} +
    @@ -124,4 +128,4 @@ -
    + \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Manage/ProfileEMailUsage.cshtml b/src/Yavsc.Org/Views/Manage/ProfileEMailUsage.cshtml index 008897fe..95e70fb0 100644 --- a/src/Yavsc.Org/Views/Manage/ProfileEMailUsage.cshtml +++ b/src/Yavsc.Org/Views/Manage/ProfileEMailUsage.cshtml @@ -1,4 +1,7 @@ @model Yavsc.ViewModels.Manage.ProfileEMailUsageViewModel +@{ + ViewBag.Title = Localizer["Manage"]; +}
    @@ -17,4 +20,4 @@ -
    + \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Manage/SetActivity.cshtml b/src/Yavsc.Org/Views/Manage/SetActivity.cshtml index 41866484..48690359 100644 --- a/src/Yavsc.Org/Views/Manage/SetActivity.cshtml +++ b/src/Yavsc.Org/Views/Manage/SetActivity.cshtml @@ -1,5 +1,5 @@ -@model PerformerProfile -@{ ViewBag.Title = "Your performer profile"; } +@model PerformerProfile +@{ ViewBag.Title = "Your performer profile"; } @section header {