postit: persist settings save and apply ApiUrl changes without restart #51

Merged
notazof merged 25 commits from feat/estimate into main 2026-09-06 21:05:58 +01:00
12 changed files with 159 additions and 40 deletions
Showing only changes of commit c1c060799d - Show all commits

postit: unify action status bar with severity across settings/activities/circles/rdv

Paul Schneider 2026-09-06 14:52:50 +01:00
Signed by: notazof
GPG key ID: 1DD5D838E5343B06

View file

@ -0,0 +1,22 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:PostIt.ViewModels"
x:Class="PostIt.Controls.StatusBar"
x:DataType="vm:StatusNotice">
<Border Background="{Binding Background}"
BorderBrush="{Binding BorderBrush}"
BorderThickness="1"
CornerRadius="6"
Padding="8">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding Glyph}"
FontWeight="Bold"
Foreground="{Binding Foreground}"
VerticalAlignment="Top"/>
<TextBlock Text="{Binding Message}"
Foreground="{Binding Foreground}"
TextWrapping="Wrap"
VerticalAlignment="Center"/>
</StackPanel>
</Border>
</UserControl>

View file

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

View file

@ -43,6 +43,14 @@ public partial class ActivitiesPageViewModel : ViewModelBase
[ObservableProperty]
public partial string StatusMessage { get; set; } = "Choisissez une activité.";
[ObservableProperty]
public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Choisissez une activité.");
partial void OnStatusMessageChanged(string value)
{
ActionStatus = StatusNotice.FromMessage(value);
}
public ActivityInfo? CurrentActivity => SelectedSpecialization ?? SelectedActivity;
public string SelectedActivityLabel => SelectedActivity?.Name ?? "(aucune activité)";
public string CurrentActivityLabel => CurrentActivity?.Name ?? "(aucune)";

View file

@ -65,6 +65,14 @@ public partial class CirclesPageViewModel : ViewModelBase
[ObservableProperty]
public partial string StatusMessage { get; set; } = string.Empty;
[ObservableProperty]
public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Pret.");
partial void OnStatusMessageChanged(string value)
{
ActionStatus = StatusNotice.FromMessage(value);
}
public CirclesPageViewModel(CircleApiClient client)
{

View file

@ -23,6 +23,9 @@ public abstract partial class BillingCommandPageViewModel : RemoteViewModelBase
[ObservableProperty]
public partial string StatusMessage { get; set; }
[ObservableProperty]
public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Pret.");
[ObservableProperty]
public partial string Reason { get; set; } = string.Empty;
@ -89,6 +92,11 @@ public abstract partial class BillingCommandPageViewModel : RemoteViewModelBase
OnPropertyChanged(nameof(CanUseCurrentLocation));
}
partial void OnStatusMessageChanged(string value)
{
ActionStatus = StatusNotice.FromMessage(value);
}
public async Task InitializeAsync(BillingQueryDetailsDto? existingQuery = null)
{
await LoadAsync();

View file

@ -31,6 +31,19 @@ public partial class Settings : ViewModelBase
[ObservableProperty]
public partial string SearchText { get; set; } = string.Empty;
[ObservableProperty]
public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Pret.");
public void SetActionStatus(string message, StatusSeverity severity = StatusSeverity.Info)
{
ActionStatus = severity switch
{
StatusSeverity.Error => StatusNotice.Error(message),
StatusSeverity.Warning => StatusNotice.Warning(message),
_ => StatusNotice.Info(message),
};
}
/// <summary>
/// Catch top-level mutations: the four ObservableProperty
/// setters above all funnel through here, and we flip
@ -406,6 +419,8 @@ public partial class Settings : ViewModelBase
[RelayCommand(CanExecute = nameof(CanSave))]
public void Save()
{
SetActionStatus("Enregistrement des parametres...", StatusSeverity.Info);
var configDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"PostIt");
@ -425,10 +440,13 @@ public partial class Settings : ViewModelBase
File.SetUnixFileMode(configPath,
UnixFileMode.UserRead | UnixFileMode.UserWrite);
IsDirty = false;
SetActionStatus("Parametres sauvegardes.", StatusSeverity.Info);
Console.WriteLine($"💾 Settings saved to {configPath}");
}
catch (Exception ex)
{
SetActionStatus($"Echec sauvegarde parametres: {ex.Message}", StatusSeverity.Error);
Console.Error.WriteLine($"🩎 Error saving settings to {configPath}: {ex.Message}");
throw;
}

View file

@ -0,0 +1,58 @@
namespace PostIt.ViewModels;
public enum StatusSeverity
{
Info,
Warning,
Error
}
public sealed class StatusNotice
{
public string Message { get; }
public StatusSeverity Severity { get; }
public string Glyph { get; }
public string Background { get; }
public string BorderBrush { get; }
public string Foreground { get; }
private StatusNotice(string message, StatusSeverity severity)
{
Message = string.IsNullOrWhiteSpace(message) ? "Pret." : message;
Severity = severity;
(Glyph, Background, BorderBrush, Foreground) = severity switch
{
StatusSeverity.Error => ("!", "#FDECEA", "#C62828", "#7F1D1D"),
StatusSeverity.Warning => ("~", "#FFF8E1", "#E6A700", "#7C4A03"),
_ => ("i", "#E8F0FE", "#5B8DEF", "#1E3A8A"),
};
}
public static StatusNotice Info(string message) => new(message, StatusSeverity.Info);
public static StatusNotice Warning(string message) => new(message, StatusSeverity.Warning);
public static StatusNotice Error(string message) => new(message, StatusSeverity.Error);
public static StatusNotice FromMessage(string? message)
{
if (string.IsNullOrWhiteSpace(message))
{
return Info("Pret.");
}
var text = message.Trim();
var lower = text.ToLowerInvariant();
if (lower.StartsWith("erreur") || lower.StartsWith("echec") || lower.StartsWith("impossible"))
{
return Error(text);
}
if (lower.Contains("refuse") || lower.Contains("annule") || lower.Contains("obligatoire") || lower.Contains("deja"))
{
return Warning(text);
}
return Info(text);
}
}

View file

@ -1,6 +1,7 @@
<ContentPage xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:PostIt.ViewModels"
xmlns:postitControls="using:PostIt.Controls"
xmlns:wf="using:Yavsc.Abstract.Workflow"
x:Class="PostIt.Views.ActivitiesPage"
x:DataType="vm:ActivitiesPageViewModel"
@ -131,16 +132,8 @@
</Grid>
<Grid Grid.Row="2" ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0"
Text="{Binding StatusMessage}"
IsReadOnly="True"
AcceptsReturn="True"
TextWrapping="Wrap"
BorderThickness="0"
BorderBrush="Transparent"
Padding="0"
Background="Transparent"
VerticalAlignment="Center" />
<postitControls:StatusBar Grid.Column="0"
DataContext="{Binding ActionStatus}" />
<ProgressBar Grid.Column="1" Width="120" IsIndeterminate="True" IsVisible="{Binding IsBusy}" />
</Grid>
</Grid>

View file

@ -1,6 +1,7 @@
<ContentPage
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:postitControls="using:PostIt.Controls"
x:Class="PostIt.Views.CirclesPage"
xmlns:vm="using:PostIt.ViewModels"
xmlns:dtos="using:Yavsc.Api.Client.Dtos"
@ -101,8 +102,8 @@
<!-- Status bar -->
<Grid Grid.Row="2" ColumnDefinitions="*,Auto" Margin="12,0,12,12">
<TextBlock Grid.Column="0" Text="{Binding StatusMessage}"
VerticalAlignment="Center"/>
<postitControls:StatusBar Grid.Column="0"
DataContext="{Binding ActionStatus}" />
<ProgressBar Grid.Column="1" IsIndeterminate="True"
IsVisible="{Binding IsBusy}"
Width="120"/>

View file

@ -1,6 +1,7 @@
<ContentPage xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cvm="using:PostIt.ViewModels.Commands"
xmlns:postitControls="using:PostIt.Controls"
x:Class="PostIt.Views.Commands.RdvPage"
x:DataType="cvm:RdvViewModel"
Header="Commande billing">
@ -71,16 +72,8 @@
Content="{Binding SubmitLabel}"
Command="{Binding SubmitCommand}"
IsEnabled="{Binding IsSupported}" />
<TextBox Grid.Column="2"
Text="{Binding StatusMessage}"
IsReadOnly="True"
AcceptsReturn="True"
TextWrapping="Wrap"
BorderThickness="0"
BorderBrush="Transparent"
Padding="0"
Background="Transparent"
VerticalAlignment="Center" />
<postitControls:StatusBar Grid.Column="2"
DataContext="{Binding ActionStatus}" />
<ProgressBar Grid.Column="3"
Width="120"
IsIndeterminate="True"

View file

@ -2,6 +2,7 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="cl:avalonia.Controls"
xmlns:postitControls="using:PostIt.Controls"
x:Class="PostIt.Views.SettingsPage"
xmlns:vm="using:PostIt.ViewModels"
x:DataType="vm:Settings"
@ -51,9 +52,7 @@
Opacity="0.8"
TextWrapping="Wrap" />
<TextBlock x:Name="AvatarUploadStatusText"
Text=""
TextWrapping="Wrap" />
<postitControls:StatusBar DataContext="{Binding ActionStatus}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="ChooseAvatarButton"

View file

@ -6,10 +6,10 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Microsoft.Extensions.DependencyInjection;
using PostIt.Services;
using PostIt.ViewModels;
namespace PostIt.Views;
public partial class SettingsPage: ContentPage
@ -28,12 +28,12 @@ public partial class SettingsPage: ContentPage
{
var statusVersion = Interlocked.Increment(ref _avatarStatusVersion);
ChooseAvatarButton.IsEnabled = false;
SetAvatarStatus("Sélection d'un fichier avatar...", Brushes.Gray);
SetAvatarStatus("Selection d'un fichier avatar...", StatusSeverity.Info);
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel is null)
{
SetAvatarStatus("Impossible d'accéder à la fenêtre active.", Brushes.IndianRed);
SetAvatarStatus("Impossible d'acceder a la fenetre active.", StatusSeverity.Error);
ChooseAvatarButton.IsEnabled = true;
return;
}
@ -54,27 +54,28 @@ public partial class SettingsPage: ContentPage
var file = files.FirstOrDefault();
if (file is null)
{
SetAvatarStatus("Upload annulé.", Brushes.Gray);
SetAvatarStatus("Upload annule.", StatusSeverity.Warning);
ChooseAvatarButton.IsEnabled = true;
return;
}
try
{
SetAvatarStatus("Upload avatar en cours...", Brushes.Gray);
SetAvatarStatus("Upload avatar en cours...", StatusSeverity.Info);
await using var stream = await file.OpenReadAsync();
var api = ((App)App.Current!).ServiceProvider!.GetRequiredService<YavscApiClient>();
var message = await api.SetAvatarAsync(stream, file.Name, GetMimeType(file.Name));
SetAvatarStatus(string.IsNullOrWhiteSpace(message)
? "Avatar mis à jour."
: message, Brushes.ForestGreen);
? "Avatar mis a jour."
: message,
StatusSeverity.Info);
_ = ClearSuccessStatusLaterAsync(statusVersion);
}
catch (Exception ex)
{
SetAvatarStatus($"Échec upload avatar: {ex.Message}", Brushes.IndianRed);
SetAvatarStatus($"Echec upload avatar: {ex.Message}", StatusSeverity.Error);
Console.Error.WriteLine($"🩎 Avatar upload failed: {ex.Message}");
}
finally
@ -89,16 +90,15 @@ public partial class SettingsPage: ContentPage
if (statusVersion != _avatarStatusVersion)
return;
if (AvatarUploadStatusText.Foreground == Brushes.ForestGreen)
{
SetAvatarStatus($"Avatar prêt. Formats supportés: {AcceptedAvatarFormats}. Taille max: {MaxAvatarSizeMegabytes} MB.", Brushes.Gray);
}
SetAvatarStatus($"Avatar pret. Formats supportes: {AcceptedAvatarFormats}. Taille max: {MaxAvatarSizeMegabytes} MB.", StatusSeverity.Info);
}
private void SetAvatarStatus(string message, IBrush color)
private void SetAvatarStatus(string message, StatusSeverity severity)
{
AvatarUploadStatusText.Foreground = color;
AvatarUploadStatusText.Text = message;
if (DataContext is Settings settings)
{
settings.SetActionStatus(message, severity);
}
}
private static string GetMimeType(string fileName)