An activity interface
This commit is contained in:
parent
ce5d3ce810
commit
8d7bb8f6b6
29 changed files with 810 additions and 10 deletions
113
src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs
Normal file
113
src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
using System.Net.Http;
|
||||
using PostIt.ViewModels;
|
||||
using Yavsc.Abstract.Workflow;
|
||||
using Yavsc.Api.Client;
|
||||
|
||||
namespace PostIt.Tests;
|
||||
|
||||
public class ActivitiesPageViewModelTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ActivityApiClient_uses_business_absolute_paths()
|
||||
{
|
||||
var api = new StubActivityApi();
|
||||
var client = new ActivityApiClient(api, "https://business.example/api/v1/");
|
||||
|
||||
await client.GetCatalogAsync("brush", TestContext.Current.CancellationToken);
|
||||
await client.GetPerformersAsync("brush-pro", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal("https://business.example/api/v1/activity/catalog?parentCode=brush", api.Paths[0]);
|
||||
Assert.Equal("https://business.example/api/v1/activity/brush-pro/performers", api.Paths[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RefreshAsync_loads_first_activity_then_specialization_performers()
|
||||
{
|
||||
var api = new StubActivityApi();
|
||||
var client = new ActivityApiClient(api, "https://business.example/api/v1/");
|
||||
var vm = new ActivitiesPageViewModel(client);
|
||||
|
||||
await vm.RefreshAsync();
|
||||
|
||||
Assert.Equal("brush", vm.SelectedActivity?.Code);
|
||||
Assert.Single(vm.Specializations);
|
||||
Assert.Equal("brush", vm.CurrentActivity?.Code);
|
||||
Assert.Single(vm.Performers);
|
||||
Assert.Equal("Alice", vm.Performers[0].UserName);
|
||||
|
||||
await vm.ShowSpecializationAsync(vm.Specializations[0]);
|
||||
|
||||
Assert.Equal("brush-pro", vm.CurrentActivity?.Code);
|
||||
Assert.Single(vm.Performers);
|
||||
Assert.Equal("Bob", vm.Performers[0].UserName);
|
||||
Assert.Contains("brush pro", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
await vm.ShowSpecializationAsync(null);
|
||||
|
||||
Assert.Equal("brush", vm.CurrentActivity?.Code);
|
||||
Assert.Single(vm.Performers);
|
||||
Assert.Equal("Alice", vm.Performers[0].UserName);
|
||||
}
|
||||
|
||||
private sealed class StubActivityApi : IYavscApiClient
|
||||
{
|
||||
public HttpClient Http { get; } = new();
|
||||
public List<string> Paths { get; } = new();
|
||||
|
||||
public Task<T> CallAsync<T>(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
|
||||
{
|
||||
Paths.Add(path);
|
||||
|
||||
if (typeof(T) == typeof(List<ActivityBrowseItemDto>))
|
||||
{
|
||||
var activities = new List<ActivityBrowseItemDto>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Code = "brush",
|
||||
Name = "Brush",
|
||||
Description = "Coiffure à domicile",
|
||||
PerformerCount = 1,
|
||||
Children = new List<ActivityBrowseItemDto>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Code = "brush-pro",
|
||||
Name = "Brush Pro",
|
||||
Description = "Spécialisation premium",
|
||||
ParentCode = "brush",
|
||||
PerformerCount = 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return Task.FromResult((T)(object)activities);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(List<ActivityPerformerDto>))
|
||||
{
|
||||
var performers = path.EndsWith("brush-pro/performers", StringComparison.Ordinal)
|
||||
? new List<ActivityPerformerDto>
|
||||
{
|
||||
new() { PerformerId = "pro-2", UserName = "Bob", ActivityCode = "brush-pro", ActivityName = "Brush Pro" }
|
||||
}
|
||||
: new List<ActivityPerformerDto>
|
||||
{
|
||||
new() { PerformerId = "pro-1", UserName = "Alice", ActivityCode = "brush", ActivityName = "Brush" }
|
||||
};
|
||||
|
||||
return Task.FromResult((T)(object)performers);
|
||||
}
|
||||
|
||||
return Task.FromResult(default(T)!);
|
||||
}
|
||||
|
||||
public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
|
||||
{
|
||||
Paths.Add(path);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -94,6 +94,26 @@ public class SignaturePadControlTests
|
|||
Assert.NotEqual(first.Strokes, third.Strokes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingStroke_is_exposed_only_while_capturing()
|
||||
{
|
||||
var pad = new SignaturePadControl();
|
||||
|
||||
Assert.Empty(pad.PendingStroke);
|
||||
|
||||
pad.BeginCaptureForTest();
|
||||
pad.AppendPointForTest(1_000, 2_000);
|
||||
pad.AppendPointForTest(3_000, 4_000);
|
||||
|
||||
Assert.Equal(new[] { 1_000, 2_000, 3_000, 4_000 }, pad.PendingStroke);
|
||||
Assert.Equal(new[] { 1_000, 2_000, 3_000, 4_000 }, pad.Strokes);
|
||||
|
||||
pad.SealStrokeForTest();
|
||||
|
||||
Assert.Empty(pad.PendingStroke);
|
||||
Assert.Equal(new[] { 2, 1_000, 2_000, 3_000, 4_000 }, pad.Strokes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_empties_buffer_and_raises_redraw()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -212,6 +212,17 @@ public class SignaturePadControl : TemplatedControl
|
|||
_pendingPoints++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test hook: mark the control as actively capturing so tests
|
||||
/// can exercise the live-preview path without synthetic pointer
|
||||
/// events.
|
||||
/// </summary>
|
||||
internal void BeginCaptureForTest()
|
||||
{
|
||||
_capturing = true;
|
||||
_pendingPoints = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test hook: seal the currently-pending stroke with a length
|
||||
/// prefix. Mirrors what <see cref="OnCaptureReleased"/> does at
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public static class ServiceCollectionHelpers
|
|||
var circleClient = new CircleApiClient(api, settings.BlogsApiUrl);
|
||||
var blogAclClient = new BlogAclApiClient(api, settings.BlogsApiUrl);
|
||||
var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl);
|
||||
var activityClient = new ActivityApiClient(api, settings.BusinessApiUrl);
|
||||
var userDirectory = new UserDirectory(userSearchClient);
|
||||
|
||||
// Vues
|
||||
|
|
@ -45,6 +46,7 @@ public static class ServiceCollectionHelpers
|
|||
services.AddSingleton<HomePage>();
|
||||
services.AddSingleton<SignaturePage>();
|
||||
services.AddSingleton<CirclesPage>();
|
||||
services.AddSingleton<ActivitiesPage>();
|
||||
// ViewModels
|
||||
services.AddSingleton(settings);
|
||||
services.AddSingleton<YavscApiClient>(api);
|
||||
|
|
@ -52,10 +54,12 @@ public static class ServiceCollectionHelpers
|
|||
services.AddSingleton(circleClient);
|
||||
services.AddSingleton(blogAclClient);
|
||||
services.AddSingleton(userSearchClient);
|
||||
services.AddSingleton(activityClient);
|
||||
services.AddSingleton<IUserDirectory>(userDirectory);
|
||||
services.AddSingleton<HomePageViewModel>();
|
||||
services.AddSingleton<SignaturePageViewModel>();
|
||||
services.AddSingleton<CirclesPageViewModel>();
|
||||
services.AddSingleton<ActivitiesPageViewModel>();
|
||||
|
||||
// Dialogs (modal-light pages): the ViewLocator resolves
|
||||
// them when a caller pushes a PostAclDialogViewModel or
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ public class ViewLocator : IDataTemplate
|
|||
MainViewModel => services.GetRequiredService<MainPage>(),
|
||||
Settings => services.GetRequiredService<SettingsPage>(),
|
||||
HomePageViewModel => services.GetRequiredService<HomePage>(),
|
||||
ActivitiesPageViewModel => services.GetRequiredService<ActivitiesPage>(),
|
||||
SignaturePageViewModel => services.GetRequiredService<SignaturePage>(),
|
||||
AddCircleMemberDialogViewModel => services.GetRequiredService<AddCircleMemberDialog>(),
|
||||
CirclesPageViewModel => services.GetRequiredService<CirclesPage>(),
|
||||
|
|
|
|||
219
src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs
Normal file
219
src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Yavsc.Abstract.Workflow;
|
||||
using Yavsc.Api.Client;
|
||||
|
||||
namespace PostIt.ViewModels;
|
||||
|
||||
public partial class ActivitiesPageViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ActivityApiClient _client;
|
||||
private bool _syncingSelection;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ActivityBrowseItemDto> Activities { get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ActivityBrowseItemDto? SelectedActivity { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ActivityBrowseItemDto> Specializations { get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ActivityBrowseItemDto? SelectedSpecialization { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ActivityPerformerDto> Performers { get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsBusy { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string StatusMessage { get; set; } = "Choisissez une activité.";
|
||||
|
||||
public ActivityBrowseItemDto? CurrentActivity => SelectedSpecialization ?? SelectedActivity;
|
||||
public string SelectedActivityLabel => SelectedActivity?.Name ?? "(aucune activité)";
|
||||
public string CurrentActivityLabel => CurrentActivity?.Name ?? "(aucune)";
|
||||
|
||||
public override bool CanNavigateNext
|
||||
{
|
||||
get => false;
|
||||
protected set { _ = value; }
|
||||
}
|
||||
|
||||
public override bool CanNavigatePrevious
|
||||
{
|
||||
get => true;
|
||||
protected set { _ = value; }
|
||||
}
|
||||
|
||||
public ActivitiesPageViewModel(ActivityApiClient client)
|
||||
{
|
||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
partial void OnSelectedActivityChanged(ActivityBrowseItemDto? value)
|
||||
{
|
||||
if (_syncingSelection) return;
|
||||
_ = ShowActivitySafeAsync(value);
|
||||
}
|
||||
|
||||
partial void OnSelectedSpecializationChanged(ActivityBrowseItemDto? value)
|
||||
{
|
||||
if (_syncingSelection) return;
|
||||
_ = ShowSpecializationSafeAsync(value);
|
||||
}
|
||||
|
||||
private async Task ShowActivitySafeAsync(ActivityBrowseItemDto? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ShowActivityAsync(value);
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||
{
|
||||
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Erreur: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowSpecializationSafeAsync(ActivityBrowseItemDto? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ShowSpecializationAsync(value);
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||
{
|
||||
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Erreur: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var list = await _client.GetCatalogAsync();
|
||||
Activities = new ObservableCollection<ActivityBrowseItemDto>(list ?? new());
|
||||
|
||||
var first = Activities.FirstOrDefault();
|
||||
await ShowActivityAsync(first);
|
||||
if (first is null)
|
||||
{
|
||||
StatusMessage = "Aucune activité disponible.";
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||
{
|
||||
Activities = new ObservableCollection<ActivityBrowseItemDto>();
|
||||
Specializations = new ObservableCollection<ActivityBrowseItemDto>();
|
||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
||||
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Activities = new ObservableCollection<ActivityBrowseItemDto>();
|
||||
Specializations = new ObservableCollection<ActivityBrowseItemDto>();
|
||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
||||
StatusMessage = $"Erreur: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ShowActivityAsync(ActivityBrowseItemDto? activity)
|
||||
{
|
||||
_syncingSelection = true;
|
||||
try
|
||||
{
|
||||
SelectedActivity = activity;
|
||||
SelectedSpecialization = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_syncingSelection = false;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(CurrentActivity));
|
||||
OnPropertyChanged(nameof(SelectedActivityLabel));
|
||||
OnPropertyChanged(nameof(CurrentActivityLabel));
|
||||
Specializations = new ObservableCollection<ActivityBrowseItemDto>(activity?.Children ?? new());
|
||||
|
||||
if (activity is null)
|
||||
{
|
||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadPerformersAsync(activity);
|
||||
}
|
||||
|
||||
public async Task ShowSpecializationAsync(ActivityBrowseItemDto? specialization)
|
||||
{
|
||||
_syncingSelection = true;
|
||||
try
|
||||
{
|
||||
SelectedSpecialization = specialization;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_syncingSelection = false;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(CurrentActivity));
|
||||
OnPropertyChanged(nameof(CurrentActivityLabel));
|
||||
|
||||
if (specialization is null)
|
||||
{
|
||||
if (SelectedActivity is not null)
|
||||
{
|
||||
await LoadPerformersAsync(SelectedActivity);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadPerformersAsync(specialization);
|
||||
}
|
||||
|
||||
private async Task LoadPerformersAsync(ActivityBrowseItemDto activity)
|
||||
{
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var list = await _client.GetPerformersAsync(activity.Code);
|
||||
Performers = new ObservableCollection<ActivityPerformerDto>(list ?? new());
|
||||
StatusMessage = $"{activity.Name} · {Performers.Count} prestataire(s)";
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||
{
|
||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
||||
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
||||
StatusMessage = $"Erreur: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PostIt.Helpers;
|
||||
using PostIt.Services;
|
||||
namespace PostIt.ViewModels;
|
||||
|
||||
|
|
@ -24,8 +29,25 @@ public class HomePageViewModel : ViewModelBase
|
|||
Settings = settings;
|
||||
SessionStatus = sessionStatus;
|
||||
|
||||
OpenActivities = new AsyncRelayCommand(OpenActivitiesAsync);
|
||||
|
||||
}
|
||||
public RelayCommand OpenBlogs { get; set; } = new RelayCommand(() => App.PushMainPageAsync());
|
||||
public IAsyncRelayCommand OpenBlogs { get; } = new AsyncRelayCommand(App.PushMainPageAsync);
|
||||
public IAsyncRelayCommand OpenActivities { get; }
|
||||
|
||||
private async Task OpenActivitiesAsync()
|
||||
{
|
||||
var app = (App?)Application.Current;
|
||||
var vm = app?.ServiceProvider?.GetRequiredService<ActivitiesPageViewModel>();
|
||||
if (app is null || vm is null)
|
||||
{
|
||||
throw new InvalidOperationException("Activities page is not available.");
|
||||
}
|
||||
|
||||
await vm.RefreshAsync();
|
||||
await app.PushPageAsync(vm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Avalonia designer constructor. Builds a self-contained VM
|
||||
/// with a freshly-constructed Settings so the XAML preview can
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public partial class Settings : ViewModelBase
|
|||
public partial string BlogsApiUrl { get; set; } = "https://blogs.pschneider.fr/api/v1/";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string BusinessApiUrl { get; set; } = "https://business.pschneider.fr/api/v1/";
|
||||
public partial string BusinessApiUrl { get; set; } = "https://api.pschneider.fr/api/v1/";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SearchText { get; set; } = string.Empty;
|
||||
|
|
@ -154,7 +154,10 @@ public partial class Settings : ViewModelBase
|
|||
{
|
||||
"openid", // OIDC: required for the id_token
|
||||
"profile", // OIDC: standard profile claims
|
||||
"offline_access" // OIDC: required to receive a refresh_token
|
||||
"offline_access", // OIDC: required to receive a refresh_token
|
||||
"blogs",
|
||||
"api"
|
||||
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -297,8 +300,15 @@ public partial class Settings : ViewModelBase
|
|||
// → our overridden dispatcher-safe marshaller below.
|
||||
else lock (_mutationGate)
|
||||
{
|
||||
var legacyApiUrl = TryReadLegacyApiUrl(json);
|
||||
this.Authentication = settings.Authentication;
|
||||
this.DarkMode = settings.DarkMode;
|
||||
this.BlogsApiUrl = !string.IsNullOrWhiteSpace(settings.BlogsApiUrl)
|
||||
? settings.BlogsApiUrl
|
||||
: legacyApiUrl ?? this.BlogsApiUrl;
|
||||
this.BusinessApiUrl = !string.IsNullOrWhiteSpace(settings.BusinessApiUrl)
|
||||
? settings.BusinessApiUrl
|
||||
: this.BusinessApiUrl;
|
||||
this.SearchText = settings.SearchText ?? string.Empty;
|
||||
if (!(settings.Authentication is null))
|
||||
{
|
||||
|
|
@ -343,6 +353,26 @@ public partial class Settings : ViewModelBase
|
|||
}
|
||||
}
|
||||
|
||||
private static string? TryReadLegacyApiUrl(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.TryGetProperty("ApiUrl", out var apiUrl)
|
||||
&& apiUrl.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return apiUrl.GetString();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore legacy payload parse errors: normal deserialization
|
||||
// already reports actionable diagnostics to the caller.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void UseDefaultSettings()
|
||||
{
|
||||
this.Authentication = new AuthenticationSettings
|
||||
|
|
@ -353,6 +383,8 @@ public partial class Settings : ViewModelBase
|
|||
Scopes = AuthenticationSettings.DefaultScopes
|
||||
};
|
||||
this.DarkMode = false;
|
||||
this.BlogsApiUrl = "https://blogs.pschneider.fr/api/v1/";
|
||||
this.BusinessApiUrl = "https://api.pschneider.fr/api/v1/";
|
||||
this.SearchText = string.Empty;
|
||||
}
|
||||
|
||||
|
|
|
|||
79
src/PostIt/PostIt/Views/ActivitiesPage.axaml
Normal file
79
src/PostIt/PostIt/Views/ActivitiesPage.axaml
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<ContentPage xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:PostIt.ViewModels"
|
||||
xmlns:wf="using:Yavsc.Abstract.Workflow"
|
||||
x:Class="PostIt.Views.ActivitiesPage"
|
||||
x:DataType="vm:ActivitiesPageViewModel"
|
||||
Header="Activités">
|
||||
<Grid RowDefinitions="Auto,*,Auto" Margin="12">
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="Rafraîchir" Command="{Binding RefreshCommand}" />
|
||||
<TextBlock Text="Catalogue des activités" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,12,0,12" ColumnDefinitions="*,16,*,16,*">
|
||||
<Grid Grid.Column="0" RowDefinitions="Auto,*">
|
||||
<TextBlock Grid.Row="0" Text="Activités" FontWeight="Bold" Margin="0,0,0,8" />
|
||||
<ListBox Grid.Row="1"
|
||||
ItemsSource="{Binding Activities}"
|
||||
SelectedItem="{Binding SelectedActivity, Mode=TwoWay}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="wf:ActivityBrowseItemDto">
|
||||
<StackPanel Spacing="2" Margin="0,0,0,8">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
|
||||
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" FontSize="11" Opacity="0.7" />
|
||||
<TextBlock Text="{Binding PerformerCount, StringFormat='Prestataires: {0}'}"
|
||||
FontSize="11" Opacity="0.6" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Column="2" RowDefinitions="Auto,Auto,*">
|
||||
<TextBlock Grid.Row="0" Text="Spécialisations" FontWeight="Bold" Margin="0,0,0,8" />
|
||||
<TextBlock Grid.Row="1"
|
||||
Text="{Binding SelectedActivityLabel, StringFormat='Pour : {0}'}"
|
||||
FontSize="11" Opacity="0.7" Margin="0,0,0,8" />
|
||||
<ListBox Grid.Row="2"
|
||||
ItemsSource="{Binding Specializations}"
|
||||
SelectedItem="{Binding SelectedSpecialization, Mode=TwoWay}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="wf:ActivityBrowseItemDto">
|
||||
<StackPanel Spacing="2" Margin="0,0,0,8">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
|
||||
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" FontSize="11" Opacity="0.7" />
|
||||
<TextBlock Text="{Binding PerformerCount, StringFormat='Prestataires: {0}'}"
|
||||
FontSize="11" Opacity="0.6" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Column="4" RowDefinitions="Auto,Auto,*">
|
||||
<TextBlock Grid.Row="0" Text="Prestataires" FontWeight="Bold" Margin="0,0,0,8" />
|
||||
<TextBlock Grid.Row="1"
|
||||
Text="{Binding CurrentActivityLabel, StringFormat='Activité affichée : {0}'}"
|
||||
FontSize="11" Opacity="0.7" Margin="0,0,0,8" />
|
||||
<ListBox Grid.Row="2" ItemsSource="{Binding Performers}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="wf:ActivityPerformerDto">
|
||||
<StackPanel Spacing="2" Margin="0,0,0,8">
|
||||
<TextBlock Text="{Binding UserName}" FontWeight="Bold" />
|
||||
<TextBlock Text="{Binding WebSite}" FontSize="11" Opacity="0.7" />
|
||||
<TextBlock Text="{Binding ExtraActivityCount, StringFormat='Autres spécialisations: {0}'}"
|
||||
FontSize="11" Opacity="0.6" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="2" ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding StatusMessage}" VerticalAlignment="Center" />
|
||||
<ProgressBar Grid.Column="1" Width="120" IsIndeterminate="True" IsVisible="{Binding IsBusy}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ContentPage>
|
||||
17
src/PostIt/PostIt/Views/ActivitiesPage.axaml.cs
Normal file
17
src/PostIt/PostIt/Views/ActivitiesPage.axaml.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace PostIt.Views;
|
||||
|
||||
public partial class ActivitiesPage : ContentPage
|
||||
{
|
||||
public ActivitiesPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,5 +18,9 @@
|
|||
Command="{Binding OpenBlogs}"
|
||||
HorizontalAlignment="Center"
|
||||
IsEnabled="{Binding SessionStatus.IsLoggedIn}"/>
|
||||
<Button Content="Parcourir les activités"
|
||||
Command="{Binding OpenActivities}"
|
||||
HorizontalAlignment="Center"
|
||||
IsEnabled="{Binding SessionStatus.IsLoggedIn}"/>
|
||||
</StackPanel>
|
||||
</ContentPage>
|
||||
|
|
|
|||
|
|
@ -62,9 +62,16 @@ public partial class SignaturePage : ContentPage
|
|||
var h = PadFrame.Bounds.Height;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
var pending = _control.PendingStroke;
|
||||
var strokes = _control.Strokes;
|
||||
int sealedCount = strokes.Count - pending.Count;
|
||||
if (sealedCount < 0)
|
||||
{
|
||||
sealedCount = 0;
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
while (i < strokes.Count)
|
||||
while (i < sealedCount)
|
||||
{
|
||||
int k = strokes[i];
|
||||
if (k <= 0) break;
|
||||
|
|
@ -88,7 +95,6 @@ public partial class SignaturePage : ContentPage
|
|||
InkLayer.Children.Add(poly);
|
||||
}
|
||||
|
||||
var pending = _control.PendingStroke;
|
||||
if (pending.Count > 0)
|
||||
{
|
||||
var poly = new Polyline
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue