refacts and PostIt Nav

This commit is contained in:
Paul Schneider 2026-06-11 00:14:51 +01:00
commit e98bba4247
34 changed files with 315 additions and 200 deletions

View file

@ -10,6 +10,7 @@
<PackageVersion Include="Microsoft.AspNetCore.Http.Features" Version="5.0.17" /> <PackageVersion Include="Microsoft.AspNetCore.Http.Features" Version="5.0.17" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.9" /> <PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="10.0.9" /> <PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.DataAnnotations" Version="2.3.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.9" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.9" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" />
@ -24,7 +25,6 @@
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.9" /> <PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.9" />
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="10.0.9" /> <PackageVersion Include="System.Security.Cryptography.Pkcs" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication" Version="10.0.8" /> <PackageVersion Include="Microsoft.AspNetCore.Authentication" Version="10.0.8" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" /> <PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" />
<PackageVersion Include="Microsoft.AspNetCore.Antiforgery" Version="2.3.11" /> <PackageVersion Include="Microsoft.AspNetCore.Antiforgery" Version="2.3.11" />
<PackageVersion Include="Microsoft.AspNetCore.Razor" Version="2.3.0" /> <PackageVersion Include="Microsoft.AspNetCore.Razor" Version="2.3.0" />
@ -34,10 +34,8 @@
<PackageVersion Include="Microsoft.Extensions.CommandLineUtils" Version="1.1.1" /> <PackageVersion Include="Microsoft.Extensions.CommandLineUtils" Version="1.1.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="10.0.2" /> <PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="10.0.2" />
<PackageVersion Include="Google.Apis.Calendar.v3" Version="1.74.0.4154" /> <PackageVersion Include="Google.Apis.Calendar.v3" Version="1.74.0.4154" />
<PackageVersion Include="Google.Apis.Compute.v1" Version="1.75.0.4157" /> <PackageVersion Include="Google.Apis.Compute.v1" Version="1.75.0.4157" />
<PackageVersion Include="IdentityModel.AspNetCore" Version="4.3.0" /> <PackageVersion Include="IdentityModel.AspNetCore" Version="4.3.0" />
<PackageVersion Include="IdentityModel.OidcClient" Version="6.0.0" /> <PackageVersion Include="IdentityModel.OidcClient" Version="6.0.0" />
<PackageVersion Include="HigginsSoft.IdentityServer8" Version="8.0.5-preview-net9" /> <PackageVersion Include="HigginsSoft.IdentityServer8" Version="8.0.5-preview-net9" />
@ -46,12 +44,9 @@
<PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework.Storage" Version="8.0.5-preview-net9" /> <PackageVersion Include="HigginsSoft.IdentityServer8.EntityFramework.Storage" Version="8.0.5-preview-net9" />
<PackageVersion Include="HigginsSoft.IdentityServer8.Security" Version="8.0.5-preview-net9" /> <PackageVersion Include="HigginsSoft.IdentityServer8.Security" Version="8.0.5-preview-net9" />
<PackageVersion Include="HigginsSoft.IdentityServer8.Storage" Version="8.0.5-preview-net9" /> <PackageVersion Include="HigginsSoft.IdentityServer8.Storage" Version="8.0.5-preview-net9" />
<PackageVersion Include="Anthropic.SDK" Version="5.10.0" /> <PackageVersion Include="Anthropic.SDK" Version="5.10.0" />
<PackageVersion Include="AsciiDocSharp" Version="0.2.0" /> <PackageVersion Include="AsciiDocSharp" Version="0.2.0" />
<PackageVersion Include="AsciiDocSharp.Converters.Html" Version="0.2.0" /> <PackageVersion Include="AsciiDocSharp.Converters.Html" Version="0.2.0" />
<PackageVersion Include="Avalonia" Version="12.0.4" /> <PackageVersion Include="Avalonia" Version="12.0.4" />
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" /> <PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Avalonia.Desktop" Version="12.0.4" /> <PackageVersion Include="Avalonia.Desktop" Version="12.0.4" />
@ -60,16 +55,14 @@
<PackageVersion Include="Avalonia.Browser" Version="12.0.4" /> <PackageVersion Include="Avalonia.Browser" Version="12.0.4" />
<PackageVersion Include="Avalonia.Android" Version="12.0.4" /> <PackageVersion Include="Avalonia.Android" Version="12.0.4" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.2" /> <PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.2" />
<PackageVersion Include="bootstrap" Version="5.3.8" /> <PackageVersion Include="bootstrap" Version="5.3.8" />
<PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.2.0" /> <PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.2.0" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" /> <PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" /> <PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="Magick.NET-Q8-AnyCPU" Version="14.14.0" /> <PackageVersion Include="Magick.NET-Q8-AnyCPU" Version="14.14.0" />
<PackageVersion Include="MailKit" Version="4.17.0" /> <PackageVersion Include="MailKit" Version="4.17.0" />
<PackageVersion Include="MimeKit" Version="4.17.0" /> <PackageVersion Include="MimeKit" Version="4.17.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" /> <PackageVersion Include="Json.NET" Version="1.0.33" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" /> <PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
<PackageVersion Include="PayPalMerchantSDK" Version="2.16.250" /> <PackageVersion Include="PayPalMerchantSDK" Version="2.16.250" />
<PackageVersion Include="pazof.rules" Version="1.1.3" /> <PackageVersion Include="pazof.rules" Version="1.1.3" />
@ -81,6 +74,5 @@
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.1" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.1" />
<PackageVersion Include="xunit" Version="2.9.3" /> <PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" /> <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -27,18 +27,18 @@ public partial class App : Application
{ {
desktop.MainWindow = new MainWindow desktop.MainWindow = new MainWindow
{ {
DataContext = new MainViewModel() DataContext = new MainPageViewModel()
}; };
} }
else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
{ {
singleViewFactoryApplicationLifetime.MainViewFactory = () => new MainView { DataContext = new MainViewModel() }; singleViewFactoryApplicationLifetime.MainViewFactory = () => new MainPage { DataContext = new MainPageViewModel() };
} }
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{ {
singleViewPlatform.MainView = new MainView singleViewPlatform.MainView = new MainPage
{ {
DataContext = new MainViewModel() DataContext = new MainPageViewModel()
}; };
} }

View file

@ -26,7 +26,7 @@ public partial class Settings : ObservableObject
[ObservableProperty] [ObservableProperty]
public partial string[] Scopes { get; set; } public partial string[] Scopes { get; set; }
internal OidcClientOptions GetOidcClientOptions() internal OidcClientOptions GetOidcClientOptions()
{ {
@ -40,40 +40,43 @@ public partial class Settings : ObservableObject
} }
internal async Task Load(IStorageProvider storageProvider) internal async Task Load()
{ {
var configFile = await storageProvider.TryGetFileFromPathAsync( String configPath =
Path.Combine(AppContext.BaseDirectory, SettingsFileName)); Path.Combine(AppContext.BaseDirectory, SettingsFileName);
if (configFile is null) FileInfo configFileInfo = new FileInfo(configPath);
if (!configFileInfo.Exists)
{ {
Console.Error.WriteLine("🩎 No settings file found."); Console.Error.WriteLine($"🩎 Settings file not found at {configFileInfo.FullName}");
return; // no settings file return; // no settings file
} }
try {
using var stream = await configFile.OpenReadAsync();
Console.WriteLine($"🔎 Loading settings from {configFileInfo.FullName}");
using var reader = new StreamReader( stream); try
var json = await reader.ReadToEndAsync(); {
if (string.IsNullOrWhiteSpace(json)) using var stream = configFileInfo.OpenRead();
{ using var reader = new StreamReader(stream);
Console.Error.WriteLine("🩎 Settings file is empty."); var json = await reader.ReadToEndAsync();
return ; if (string.IsNullOrWhiteSpace(json))
} {
Console.Error.WriteLine("🩎 Settings file is empty.");
return;
}
var settings = JsonSerializer.Deserialize<Settings>(json); var settings = JsonSerializer.Deserialize<Settings>(json);
if (settings is null)
{
Console.Error.WriteLine("🩎 Settings file is invalid.");
return ;
}
this.Authentication = settings.Authentication;
this.DarkMode = settings.DarkMode;
this.ApiUrl = settings.ApiUrl;
this.Scopes = settings.Scopes;
if (settings is null)
{
Console.Error.WriteLine("🩎 Settings file is invalid.");
return;
}
this.Authentication = settings.Authentication;
this.DarkMode = settings.DarkMode;
this.ApiUrl = settings.ApiUrl;
this.Scopes = settings.Scopes;
} }
catch (Exception ex) catch (Exception ex)

View file

@ -2,36 +2,34 @@ using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Microsoft.Extensions.DependencyInjection;
using PostIt.ViewModels; using PostIt.ViewModels;
using PostIt.Views;
namespace PostIt; namespace PostIt;
/// <summary> /// <summary>
/// Given a view model, returns the corresponding view if possible. /// Given a view model, returns the corresponding view if possible.
/// </summary> /// </summary>
[RequiresUnreferencedCode(
"Default implementation of ViewLocator involves reflection which may be trimmed away.",
Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")]
public class ViewLocator : IDataTemplate public class ViewLocator : IDataTemplate
{ {
public Control? Build(object? param) private readonly IServiceProvider _services;
public ViewLocator(IServiceProvider services)
{ {
if (param is null) _services = services;
return null; }
var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); public Control Build(object data)
var type = Type.GetType(name); {
return data switch
if (type != null)
{ {
return (Control)Activator.CreateInstance(type)!; MainPageViewModel => _services.GetRequiredService<MainPage>(),
} SettingsPageViewModel => _services.GetRequiredService<SettingsPage>(),
_ => new TextBlock { Text = $"No view for {data.GetType().Name}" }
return new TextBlock { Text = "Not Found: " + name }; };
} }
public bool Match(object? data) public bool Match(object data) => data is ViewModelBase;
{
return data is ViewModelBase;
}
} }

View file

@ -0,0 +1,15 @@
namespace PostIt.ViewModels;
public class HomePageViewModel : ViewModelBase
{
private string _welcomeText = "Welcome to PostIt!";
public string WelcomeText
{
get => _welcomeText;
set => SetProperty(ref _welcomeText, value);
}
public override bool CanNavigateNext { get => true; protected set => throw new System.NotImplementedException(); }
public override bool CanNavigatePrevious { get => false; protected set => throw new System.NotImplementedException(); }
}

View file

@ -15,9 +15,15 @@ using Avalonia.Styling;
namespace PostIt.ViewModels; namespace PostIt.ViewModels;
public partial class MainViewModel : ViewModelBase public partial class MainPageViewModel : ViewModelBase
{ {
[ObservableProperty]
public partial string Title { get; set; }
[ObservableProperty]
public partial ViewModelBase? CurrentViewModel { get; set; }
public SettingsPageViewModel SettingsModel { get; }
[ObservableProperty] [ObservableProperty]
public partial string StatusMessage { get; set; } public partial string StatusMessage { get; set; }
@ -34,18 +40,20 @@ public partial class MainViewModel : ViewModelBase
public partial ObservableCollection<BlogPost> FilteredPosts { get; set; } public partial ObservableCollection<BlogPost> FilteredPosts { get; set; }
[ObservableProperty] [ObservableProperty]
public partial BlogPost? SelectedPost{ get; set; } public partial BlogPost? SelectedPost { get; set; }
[ObservableProperty] [ObservableProperty]
public partial bool IsBusy{ get; set; } public partial bool IsBusy { get; set; }
[ObservableProperty] [ObservableProperty]
ThemeVariant themeVariant = ThemeVariant.Default; ThemeVariant themeVariant = ThemeVariant.Default;
[ObservableProperty] [ObservableProperty]
public partial Settings Settings { get; private set; } public partial Settings Settings { get; private set; }
public override bool CanNavigateNext { get => throw new NotImplementedException(); protected set => throw new NotImplementedException(); }
public override bool CanNavigatePrevious { get => throw new NotImplementedException(); protected set => throw new NotImplementedException(); }
public MainViewModel() public MainPageViewModel()
{ {
SearchText = string.Empty; SearchText = string.Empty;
Posts = new ObservableCollection<BlogPost>(); Posts = new ObservableCollection<BlogPost>();
@ -55,6 +63,9 @@ public partial class MainViewModel : ViewModelBase
IsBusy = false; IsBusy = false;
StatusMessage = "Ready"; StatusMessage = "Ready";
Settings = new Settings(); Settings = new Settings();
Title = "PostIt";
CurrentViewModel = this;
SettingsModel = new SettingsPageViewModel();
} }
partial void OnSearchTextChanged(string value) partial void OnSearchTextChanged(string value)
@ -105,7 +116,7 @@ public partial class MainViewModel : ViewModelBase
try try
{ {
var loginWin = new PostIt.Views.LoginWindow(); var loginWin = new PostIt.Views.LoginPage();
var result = await loginWin.StartLoginAsync(Settings.GetOidcClientOptions()); var result = await loginWin.StartLoginAsync(Settings.GetOidcClientOptions());
if (result is not null && !result.IsError && !string.IsNullOrWhiteSpace(result.AccessToken)) if (result is not null && !result.IsError && !string.IsNullOrWhiteSpace(result.AccessToken))
@ -202,6 +213,12 @@ public partial class MainViewModel : ViewModelBase
StatusMessage = "New blog post ready."; StatusMessage = "New blog post ready.";
} }
[RelayCommand]
internal void OpenSettings()
{
// Appeler la méthode OpenSettings de la vue MainWindow
CurrentViewModel = SettingsModel;
}
private async Task RefreshPostsAsync() private async Task RefreshPostsAsync()
{ {
@ -299,6 +316,7 @@ public partial class MainViewModel : ViewModelBase
return payload; return payload;
} }
private static readonly JsonSerializerOptions JsonOptions = new() private static readonly JsonSerializerOptions JsonOptions = new()
{ {
PropertyNameCaseInsensitive = true PropertyNameCaseInsensitive = true

View file

@ -2,7 +2,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
namespace PostIt.ViewModels; namespace PostIt.ViewModels;
public partial class SettingsViewModel : ViewModelBase public partial class SettingsPageViewModel : ViewModelBase
{ {
[ObservableProperty] [ObservableProperty]
public partial bool DarkMode { get; set; } public partial bool DarkMode { get; set; }
@ -15,5 +15,6 @@ public partial class SettingsViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
public partial string ClientSecret { get; set; } public partial string ClientSecret { get; set; }
public override bool CanNavigateNext { get => false; protected set => throw new System.NotImplementedException(); }
public override bool CanNavigatePrevious { get => true; protected set => throw new System.NotImplementedException(); }
} }

View file

@ -5,4 +5,15 @@ namespace PostIt.ViewModels;
public abstract partial class ViewModelBase : ObservableObject public abstract partial class ViewModelBase : ObservableObject
{ {
/// <summary>
/// Gets if the user can navigate to the next page
/// </summary>
public abstract bool CanNavigateNext { get; protected set; }
/// <summary>
/// Gets if the user can navigate to the previous page
/// </summary>
public abstract bool CanNavigatePrevious { get; protected set; }
} }

View file

@ -0,0 +1,16 @@
<ContentPage xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PostIt.Views.HomePage"
Header="Home">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<TextBlock Text="Welcome to the Home Page"
FontSize="22"
FontWeight="SemiBold"
HorizontalAlignment="Center"/>
<Button Content="Login"
Click="OnLoginClick"
HorizontalAlignment="Center"/>
</StackPanel>
</ContentPage>

View file

@ -0,0 +1,23 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using System.Threading.Tasks;
namespace PostIt.Views;
public partial class HomePage : ContentPage
{
public HomePage()
{
InitializeComponent();
}
private async void OnLoginClick(object? sender, RoutedEventArgs e)
{
if (Navigation is not null)
await Navigation.PushModalAsync(new LoginPage());
}
}

View file

@ -0,0 +1,37 @@
<ContentPage xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PostIt.Views.LoginPage"
Header="Login">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="20">
<TextBlock Text="Sign In"
FontSize="24"
HorizontalAlignment="Center"/>
<StackPanel Spacing="4">
<TextBlock Text="Email"/>
<TextBox Name="EmailBox"
PlaceholderText="Enter your email"/>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Text="Password"/>
<TextBox Name="PasswordBox"
PlaceholderText="Enter your password"
PasswordChar="•"/>
</StackPanel>
<Button Content="Login"
Click="OnLoginClick"/>
<Button Content="Cancel"
Click="OnCancelClick"/>
<TextBlock Name="StatusText" Text="Starting authentication..." />
<ProgressBar IsIndeterminate="True" />
</StackPanel>
</ContentPage>

View file

@ -0,0 +1,51 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using IdentityModel.OidcClient;
using PostIt.Services;
namespace PostIt.Views;
public partial class LoginPage : ContentPage
{
private TaskCompletionSource<LoginResult?> _tcs = new();
public Settings Settings { get; }
public LoginPage()
{
this.Settings = new Settings();
InitializeComponent();
}
public async Task<LoginResult?> StartLoginAsync(OidcClientOptions options)
{
try
{
Settings.Load().Wait();
var client = new OidcClient(options);
var loginResult = await client.LoginAsync(new LoginRequest());
return loginResult;
}
catch (Exception)
{
return null;
}
}
private async void OnLoginClick(object? sender, RoutedEventArgs e)
{
// This is where you specify your actual login auth logic
if (Navigation is not null)
await Navigation.PopModalAsync();
}
private async void OnCancelClick(object? sender, RoutedEventArgs e)
{
// Cancel button dismisses all open modals
if (Navigation is not null)
await Navigation.PopAllModalsAsync();
}
}

View file

@ -1,13 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PostIt.Views.LoginWindow"
Width="480" Height="160" WindowStartupLocation="CenterOwner"
Title="Login">
<StackPanel Margin="12" Spacing="12">
<TextBlock Name="StatusText" Text="Starting authentication..." />
<ProgressBar IsIndeterminate="True" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Name="CancelButton" Content="Cancel" />
</StackPanel>
</StackPanel>
</Window>

View file

@ -1,33 +0,0 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using IdentityModel.OidcClient;
using PostIt.Services;
namespace PostIt.Views;
public partial class LoginWindow : Window
{
private TaskCompletionSource<LoginResult?> _tcs = new();
public LoginWindow()
{
InitializeComponent();
CancelButton.Click += (_, __) => Close(null);
}
public async Task<LoginResult?> StartLoginAsync(OidcClientOptions options)
{
try
{
var client = new OidcClient(options);
var loginResult = await client.LoginAsync(new LoginRequest());
return loginResult;
}
catch (Exception)
{
return null;
}
}
}

View file

@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui" <NavigationPage xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@ -6,18 +6,17 @@
xmlns:models="using:PostIt.Models" xmlns:models="using:PostIt.Models"
xmlns:views="using:PostIt.Views" xmlns:views="using:PostIt.Views"
xmlns:AvaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit" xmlns:AvaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="600" mc:Ignorable="d"
x:Class="PostIt.Views.MainView" x:Class="PostIt.Views.MainPage"
x:DataType="vm:MainViewModel"> x:DataType="vm:MainPageViewModel" HorizontalAlignment="Center" VerticalAlignment="Center" >
<Design.DataContext> <Design.DataContext>
<vm:MainViewModel /> <vm:MainPageViewModel />
</Design.DataContext> </Design.DataContext>
<StackPanel Margin="12" Spacing="12"> <StackPanel Margin="12" Spacing="12" HorizontalAlignment="Center" >
<TextBlock Text="PostIt Blog API Interface" FontSize="20" FontWeight="Bold" /> <TextBlock Text="PostIt Blog API Interface" FontSize="20" FontWeight="Bold" />
<StackPanel Orientation="Horizontal" Spacing="8" >
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Command="{Binding Login}" Content="Acquire token" /> <Button Command="{Binding Login}" Content="Acquire token" />
<Button Command="{Binding LoadPosts}" Content="Load posts" /> <Button Command="{Binding LoadPosts}" Content="Load posts" />
<Button Command="{Binding Search}" Content="Filter" /> <Button Command="{Binding Search}" Content="Filter" />
@ -26,7 +25,6 @@
<Button Command="{Binding Delete}" Content="Delete" /> <Button Command="{Binding Delete}" Content="Delete" />
</StackPanel> </StackPanel>
<Grid ColumnDefinitions="2*,3*" RowDefinitions="*" ColumnSpacing="12">
<Border BorderBrush="Gray" BorderThickness="1" Padding="8"> <Border BorderBrush="Gray" BorderThickness="1" Padding="8">
<ListBox ItemsSource="{Binding FilteredPosts}" SelectedItem="{Binding SelectedPost, Mode=TwoWay}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <ListBox ItemsSource="{Binding FilteredPosts}" SelectedItem="{Binding SelectedPost, Mode=TwoWay}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
@ -56,6 +54,6 @@
<TextBlock Text="{Binding StatusMessage}" Foreground="Gray" /> <TextBlock Text="{Binding StatusMessage}" Foreground="Gray" />
</StackPanel> </StackPanel>
</Border> </Border>
</Grid>
</StackPanel> </StackPanel>
</UserControl> </NavigationPage>

View file

@ -4,9 +4,9 @@ using Avalonia.Controls;
namespace PostIt.Views; namespace PostIt.Views;
public partial class MainView : UserControl public partial class MainPage : NavigationPage
{ {
public MainView() public MainPage()
{ {
InitializeComponent(); InitializeComponent();
} }

View file

@ -0,0 +1,26 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PostIt.ViewModels;
public partial class MainWindowViewModel : ObservableObject
{
// Stocke le ViewModel de la page actuellement affichée
[ObservableProperty]
private object? _currentPage;
// Instances des pages pour éviter de les recréer à chaque fois (Optionnel)
private readonly HomePageViewModel _homePage = new();
private readonly SettingsPageViewModel _settingsPage = new();
public MainWindowViewModel()
{
// Définir la page de démarrage par défaut
CurrentPage = _homePage;
}
[RelayCommand]
private void NavigateToHome() => CurrentPage = _homePage;
[RelayCommand]
private void NavigateToSettings() => CurrentPage = _settingsPage;
}

View file

@ -1,13 +1,14 @@
<Window xmlns="https://github.com/avaloniaui" <Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:PostIt.ViewModels" xmlns:vm="using:PostIt.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="using:PostIt.Views" xmlns:views="using:PostIt.Views"
x:DataType="vm:MainPageViewModel"
x:Class="PostIt.Views.MainWindow" x:Class="PostIt.Views.MainWindow"
x:DataType="vm:MainViewModel"
Icon="/Assets/yavsc-logo.ico"
Title="PostIt"
> >
<views:MainView x:Name="MainView"/>
<!-- Association des ViewModels aux Vues graphiques (UserControls) -->
<NavigationPage>
<views:HomePage/>
</NavigationPage>
</Window> </Window>

View file

@ -1,35 +1,11 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Styling;
using PostIt.ViewModels;
using System;
using System.Threading.Tasks;
namespace PostIt.Views; namespace PostIt.Views;
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
internal Settings Settings { get; private set; }
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
this.Settings = new Settings();
} }
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is MainViewModel vm)
{
Task.Run(async () => await this.Settings.Load(this.StorageProvider)).Wait();
this.RequestedThemeVariant = Settings.DarkMode ? ThemeVariant.Dark : ThemeVariant.Light;
MainView.DataContext = new MainViewModel();
vm.Settings.Load(this.StorageProvider).Wait();
}
}
} }

View file

@ -1,11 +1,10 @@
<Window <ContentPage
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="cl:avalonia.Controls" xmlns:controls="cl:avalonia.Controls"
x:Class="PostIt.Views.SettingsView" x:Class="PostIt.Views.SettingsPage"
xmlns:vm="using:PostIt.ViewModels" xmlns:vm="using:PostIt.ViewModels"
x:DataType="vm:SettingsViewModel" x:DataType="vm:SettingsPageViewModel"
Title="Settings"
Width="400" Width="400"
Height="300"> Height="300">
<Grid> <Grid>
@ -26,4 +25,4 @@
<TextBlock Grid.Row="4" Text="ClientSecret"/> <TextBlock Grid.Row="4" Text="ClientSecret"/>
<TextBox Grid.Row="5" x:Name="ClientSecretTextBox" Text="{Binding ClientSecret, Mode=TwoWay}"/> <TextBox Grid.Row="5" x:Name="ClientSecretTextBox" Text="{Binding ClientSecret, Mode=TwoWay}"/>
</Grid> </Grid>
</Window> </ContentPage>

View file

@ -3,9 +3,9 @@
using Avalonia.Controls; using Avalonia.Controls;
namespace PostIt.Views; namespace PostIt.Views;
public partial class SettingsView: Window public partial class SettingsPage: ContentPage
{ {
public SettingsView() public SettingsPage()
{ {
InitializeComponent(); InitializeComponent();
} }

View file

@ -7,8 +7,8 @@ namespace Yavsc
{ {
public interface IBlogPostPayLoad public interface IBlogPostPayLoad
{ {
string? Article { get; set; } string Article { get; set; }
string? Photo { get; set; } string Photo { get; set; }
} }
public interface IBlogPost : IBlogPostPayLoad, ITrackedEntity, IIdentified<long>, ITitle public interface IBlogPost : IBlogPostPayLoad, ITrackedEntity, IIdentified<long>, ITitle

View file

@ -7,10 +7,12 @@ namespace Yavsc.Models.FileSystem
{ {
[ValidRemoteUserFilePath] [ValidRemoteUserFilePath]
[StringLength(512)] [StringLength(512)]
[Required]
public required string Id { get; set; } public required string Id { get; set; }
[StringLength(512)] [StringLength(512)]
[ValidRemoteUserFilePath] [ValidRemoteUserFilePath]
[Required]
public required string To { get; set; } public required string To { get; set; }
} }

View file

@ -1,5 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json; using System.Text.Json.Serialization;
namespace Yavsc.Abstract.IT { namespace Yavsc.Abstract.IT {
@ -10,7 +10,7 @@ public class CiBuildSettings
/// The global process environment variables /// The global process environment variables
/// </summary> /// </summary>
/// <value></value> /// <value></value>
[JsonProperty("env")] [JsonPropertyName("env")]
public string[] Environment { get; set; } public string[] Environment { get; set; }
/// <summary> /// <summary>
@ -18,7 +18,7 @@ public class CiBuildSettings
/// </summary> /// </summary>
/// <value></value> /// <value></value>
[Required] [Required]
[JsonPropertyAttribute("build")] [JsonPropertyName("build")]
public CommandPipe Build { get; set; } public CommandPipe Build { get; set; }
/// <summary> /// <summary>
@ -27,7 +27,7 @@ public class CiBuildSettings
/// must end ok in order to launch the build. /// must end ok in order to launch the build.
/// </summary> /// </summary>
/// <value></value> /// <value></value>
[JsonPropertyAttribute("prepare")] [JsonPropertyName("prepare")]
public CommandPipe Prepare { get; set; } public CommandPipe Prepare { get; set; }
/// <summary> /// <summary>
@ -37,14 +37,14 @@ public class CiBuildSettings
/// only fired on successful build. /// only fired on successful build.
/// </summary> /// </summary>
/// <value></value> /// <value></value>
[JsonPropertyAttribute("post_build")] [JsonPropertyName("post_build")]
public CommandPipe PostBuild { get; set; } public CommandPipe PostBuild { get; set; }
/// <summary> /// <summary>
/// Additional emails, as dest of notifications /// Additional emails, as dest of notifications
/// </summary> /// </summary>
/// <value></value> /// <value></value>
[JsonPropertyAttribute("emails")] [JsonPropertyName("emails")]
public string[] Emails { get; set; } public string[] Emails { get; set; }
} }

View file

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Diagnostics; using System.Diagnostics;
using Newtonsoft.Json; using System.Text.Json.Serialization;
namespace Yavsc.Abstract.IT namespace Yavsc.Abstract.IT
{ {
@ -13,17 +13,17 @@ namespace Yavsc.Abstract.IT
public class Command public class Command
{ {
[Required] [Required]
[JsonPropertyAttribute("path")] [JsonPropertyName("path")]
public string Path { get; set; } public string Path { get; set; }
[JsonPropertyAttribute("args")] [JsonPropertyName("args")]
public string[] Args { get; set; } public string[] Args { get; set; }
/// <summary> /// <summary>
/// Specific variables for this process /// Specific variables for this process
/// </summary> /// </summary>
/// <value></value> /// <value></value>
[JsonPropertyAttribute("env")] [JsonPropertyName("env")]
public string[] Environment { get; set; } public string[] Environment { get; set; }
public virtual Process Start(string workingDir=null, bool redirectInput=false, bool redirectOutput=false) public virtual Process Start(string workingDir=null, bool redirectInput=false, bool redirectOutput=false)

View file

@ -1,8 +1,6 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics; using System.Diagnostics;
using System.Threading.Tasks; using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace Yavsc.Abstract.IT namespace Yavsc.Abstract.IT
{ {
@ -10,11 +8,11 @@ namespace Yavsc.Abstract.IT
{ {
[JsonPropertyAttribute("pipe")] [JsonPropertyName("pipe")]
public Command[] Pipe { get; set; } public Command[] Pipe { get; set; }
[JsonPropertyAttribute("working_dir")] [JsonPropertyName("working_dir")]
public string WorkingDir { get; set; } public string WorkingDir { get; set; }
public virtual int Run() public virtual int Run()

View file

@ -20,7 +20,7 @@ namespace Yavsc.Models.IT.Fixing
ErrorMessageResourceName="TitleSizeError")] ErrorMessageResourceName="TitleSizeError")]
public string Title { get; set; } public string Title { get; set; }
[YaStringLength(10240, [StringLength(10240,
ErrorMessageResourceType=typeof(Yavsc.Models.IT.Fixing.Bug), ErrorMessageResourceType=typeof(Yavsc.Models.IT.Fixing.Bug),
ErrorMessageResourceName="DescSizeError")] ErrorMessageResourceName="DescSizeError")]
public string Description { get; set; } public string Description { get; set; }

View file

@ -5,8 +5,8 @@ namespace Yavsc
public interface ITrackedEntity public interface ITrackedEntity
{ {
DateTime DateCreated { get; set; } DateTime DateCreated { get; set; }
string? UserCreated { get; set; } string UserCreated { get; set; }
DateTime DateModified { get; set; } DateTime DateModified { get; set; }
string? UserModified { get; set; } string UserModified { get; set; }
} }
} }

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net9.0</TargetFrameworks> <TargetFrameworks>net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Description> A shared model for a little client/server app, dealing about establishing some contract, between some human client and provider. <Description> A shared model for a little client/server app, dealing about establishing some contract, between some human client and provider.
</Description> </Description>
@ -8,8 +8,6 @@
<RepositoryUrl>https://github.com/pazof/yavsc</RepositoryUrl> <RepositoryUrl>https://github.com/pazof/yavsc</RepositoryUrl>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<Library>true</Library> <Library>true</Library>
<LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>
</Project> </Project>

View file

@ -40,7 +40,7 @@ namespace cli.Commands
}); });
loginCommand.OnExecute(async () => loginCommand.OnExecute(async () =>
{ {
string? authHostName = Program.AppConfiguration.GetRequiredSection("ConnectionSettings:ServerApi")["Authority"]; string authHostName = Program.AppConfiguration.GetRequiredSection("ConnectionSettings:ServerApi")["Authority"];
throw new NotImplementedException(); throw new NotImplementedException();
/* /*

View file

@ -5,7 +5,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
internal class Program internal class Program
{ {
public static IHost? AppHost { get; private set; } public static IHost AppHost { get; private set; }
public static IConfigurationRoot AppConfiguration { get; private set; } public static IConfigurationRoot AppConfiguration { get; private set; }
public static IHostEnvironment AppEnvironment { get; private set; } public static IHostEnvironment AppEnvironment { get; private set; }

View file

@ -13,7 +13,6 @@
<PackageReference Include="Microsoft.Extensions.Configuration" /> <PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Hosting" /> <PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Extensions.Logging" /> <PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Yavsc.Abstract\Yavsc.Abstract.csproj" /> <ProjectReference Include="..\Yavsc.Abstract\Yavsc.Abstract.csproj" />

View file

@ -18,7 +18,7 @@ public class PostItViewModelTests
[Fact] [Fact]
public void SearchCommand_filters_posts_by_title_article_or_author() public void SearchCommand_filters_posts_by_title_article_or_author()
{ {
var viewModel = new MainViewModel(); var viewModel = new MainPageViewModel();
viewModel.Posts.Add(new BlogPost { Id = 1, Title = "First post", Article = "Hello world", AuthorId = "alice" }); viewModel.Posts.Add(new BlogPost { Id = 1, Title = "First post", Article = "Hello world", AuthorId = "alice" });
viewModel.Posts.Add(new BlogPost { Id = 2, Title = "Second post", Article = "Nothing here", AuthorId = "bob" }); viewModel.Posts.Add(new BlogPost { Id = 2, Title = "Second post", Article = "Nothing here", AuthorId = "bob" });

View file

@ -21,7 +21,6 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="Microsoft.Extensions.Options" /> <PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="xunit" /> <PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" /> <PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup> </ItemGroup>