From 549bae3b5ff057f5200ba876ad25649ff15c787d Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 27 Sep 2026 03:44:34 +0100 Subject: [PATCH 01/21] should fix the CI --- .forgejo/workflows/buildAndTest.yml | 5 ++++- CHANGELOG.md | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index 55d0700f8..e1e936204 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -52,7 +52,10 @@ jobs: - name: install dependencies run: | echo "📦 Installing dependencies..." - apt install -y texlive-binaries texlive-luatex texlive-pictures texlive-fonts-extra + apt install -y texlive-binaries texlive-luatex texlive-pictures \ + texlive-fonts-extra texlive-lang-french texlive-lang-english \ + texlive-lang-portuguese texlive-lang-spanish \ + texlive-latex-recommended texlive-latex-extra - name: Test artifacts: name: test-results diff --git a/CHANGELOG.md b/CHANGELOG.md index e36d75dd3..844d0846a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # Changelog -## [1.0.8-rc19] - Blogs UX +## [1.0.8-rc19] - unstable + +[PostIt] Blogs UX ### Fixed From edac506c4902e9d14f81bf2529ac6d6df393bd67 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 27 Sep 2026 22:22:50 +0100 Subject: [PATCH 02/21] Build image version bump --- .forgejo/workflows/buildAndTest.yml | 9 +-------- .forgejo/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index e1e936204..0a05b8277 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -27,7 +27,7 @@ jobs: build: runs-on: docker container: - image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v2 + image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v3 steps: - name: Clone yavsc run: | @@ -49,13 +49,6 @@ jobs: run: | echo "🏗️ Building the solution..." cd /src/_src && dotnet build --verbosity normal - - name: install dependencies - run: | - echo "📦 Installing dependencies..." - apt install -y texlive-binaries texlive-luatex texlive-pictures \ - texlive-fonts-extra texlive-lang-french texlive-lang-english \ - texlive-lang-portuguese texlive-lang-spanish \ - texlive-latex-recommended texlive-latex-extra - name: Test artifacts: name: test-results diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 7d0670a17..75c3becbd 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -52,7 +52,7 @@ jobs: release: runs-on: docker container: - image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v2 + image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v3 steps: - name: Clone du repo au tag demandé env: From 1e1b75190654856c5629567858be0d6a99ac87cf Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 27 Sep 2026 23:42:52 +0100 Subject: [PATCH 03/21] localization + AudiRecorder --- src/PostIt/Directory.Packages.props | 3 + src/PostIt/PostIt/App.axaml.cs | 16 +++-- src/PostIt/PostIt/Interfaces/IPCMProcessor.cs | 6 ++ src/PostIt/PostIt/Lang/Resources.Designer.cs | 72 +++++++++++++++++++ src/PostIt/PostIt/Lang/Resources.en-EN.resx | 5 ++ src/PostIt/PostIt/Lang/Resources.pt-PT.resx | 5 ++ src/PostIt/PostIt/Lang/Resources.resx | 5 ++ src/PostIt/PostIt/PostIt.csproj | 21 +++++- src/PostIt/PostIt/Services/AudioRecorder.cs | 67 +++++++++++++++++ .../PostIt/ViewModels/Settings/Settings.cs | 13 +++- src/PostIt/PostIt/Views/Layout/HomePage.axaml | 3 +- 11 files changed, 205 insertions(+), 11 deletions(-) create mode 100644 src/PostIt/PostIt/Interfaces/IPCMProcessor.cs create mode 100644 src/PostIt/PostIt/Lang/Resources.Designer.cs create mode 100644 src/PostIt/PostIt/Lang/Resources.en-EN.resx create mode 100644 src/PostIt/PostIt/Lang/Resources.pt-PT.resx create mode 100644 src/PostIt/PostIt/Lang/Resources.resx create mode 100644 src/PostIt/PostIt/Services/AudioRecorder.cs diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 42d09cd55..c9e77bf1d 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -16,6 +16,7 @@ + @@ -23,6 +24,8 @@ + + diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 9a04ffd03..0c4f0a7ca 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -11,6 +11,7 @@ using PostIt.Services; using PostIt.ViewModels; using PostIt.Views; using PostIt.Helpers; +using System.Globalization; namespace PostIt; @@ -36,6 +37,7 @@ public partial class App : Application public override void Initialize() { AvaloniaXamlLoader.Load(this); + } public override void OnFrameworkInitializationCompleted() @@ -65,7 +67,7 @@ public partial class App : Application { singleViewPlatform.MainView = View = ServiceProvider!.GetRequiredService(); ConfigureRootView(View); - ApplyDarkMode(settings); + ApplyStettings(settings); } public void InitializeActivityLifetime(Settings settings, IActivityApplicationLifetime singleViewFactoryApplicationLifetime) @@ -74,7 +76,7 @@ public partial class App : Application () => { this.ConfigureRootView(ServiceProvider!.GetRequiredService()); - ApplyDarkMode(settings); + ApplyStettings(settings); return View!; }; } @@ -85,7 +87,7 @@ public partial class App : Application desktop.MainWindow = window; this.ConfigureRootView(window.MainView); - ApplyDarkMode(settings); + ApplyStettings(settings); } public void ConfigureRootView(MainView rootView) @@ -126,10 +128,12 @@ public partial class App : Application View = mainView ?? throw new ArgumentNullException(nameof(mainView)); } - private static void ApplyDarkMode(Settings settings) + private static void ApplyStettings(Settings settings) { - Application.Current!.RequestedThemeVariant = - settings.DarkMode ? ThemeVariant.Dark : ThemeVariant.Light; + if (!string.IsNullOrWhiteSpace(settings.SelectedLanguage)) + Yavsc.Lang.Resources.Culture = new CultureInfo(settings.SelectedLanguage); + Current!.RequestedThemeVariant = + settings.DarkMode ? ThemeVariant.Dark : ThemeVariant.Light; } /// diff --git a/src/PostIt/PostIt/Interfaces/IPCMProcessor.cs b/src/PostIt/PostIt/Interfaces/IPCMProcessor.cs new file mode 100644 index 000000000..e8e61070e --- /dev/null +++ b/src/PostIt/PostIt/Interfaces/IPCMProcessor.cs @@ -0,0 +1,6 @@ +using System; + +public interface IPCMProcessor : IDisposable +{ + void Process(short[] audioFrame); +} diff --git a/src/PostIt/PostIt/Lang/Resources.Designer.cs b/src/PostIt/PostIt/Lang/Resources.Designer.cs new file mode 100644 index 000000000..5537f5924 --- /dev/null +++ b/src/PostIt/PostIt/Lang/Resources.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Yavsc.Lang { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Yavsc.Lang.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Bienvenue dans PostIt!. + /// + public static string GreetingText { + get { + return ResourceManager.GetString("GreetingText", resourceCulture); + } + } + } +} diff --git a/src/PostIt/PostIt/Lang/Resources.en-EN.resx b/src/PostIt/PostIt/Lang/Resources.en-EN.resx new file mode 100644 index 000000000..6128bca8d --- /dev/null +++ b/src/PostIt/PostIt/Lang/Resources.en-EN.resx @@ -0,0 +1,5 @@ + + + Welcome to PostIt! + + diff --git a/src/PostIt/PostIt/Lang/Resources.pt-PT.resx b/src/PostIt/PostIt/Lang/Resources.pt-PT.resx new file mode 100644 index 000000000..049c25316 --- /dev/null +++ b/src/PostIt/PostIt/Lang/Resources.pt-PT.resx @@ -0,0 +1,5 @@ + + + Bemvindo em PostIt! + + diff --git a/src/PostIt/PostIt/Lang/Resources.resx b/src/PostIt/PostIt/Lang/Resources.resx new file mode 100644 index 000000000..ae18ff220 --- /dev/null +++ b/src/PostIt/PostIt/Lang/Resources.resx @@ -0,0 +1,5 @@ + + + Bienvenue dans PostIt! + + diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj index 9cdf71299..5057bde8c 100644 --- a/src/PostIt/PostIt/PostIt.csproj +++ b/src/PostIt/PostIt/PostIt.csproj @@ -7,6 +7,8 @@ 1.1.0.0 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e 1.1.0-estimate.1 + true + @@ -30,6 +32,7 @@ None All + @@ -37,9 +40,25 @@ + + - \ No newline at end of file + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + + + + True + True + Lang/Resources.resx + + + diff --git a/src/PostIt/PostIt/Services/AudioRecorder.cs b/src/PostIt/PostIt/Services/AudioRecorder.cs new file mode 100644 index 000000000..43e7f5c1b --- /dev/null +++ b/src/PostIt/PostIt/Services/AudioRecorder.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading; +using Pv; + +public class AudioRecorder : IDisposable +{ + private int frameLength; + PvRecorder recorder; + + Thread thread; + short[] audioFrame; + IPCMProcessor processor; + + public bool processing { get; private set; } + + public AudioRecorder(IPCMProcessor processor, int frameLength=512, int deviceIndex = -1) + { + this.frameLength = frameLength; + this.recorder = PvRecorder.Create(frameLength: frameLength, deviceIndex: deviceIndex); + this.audioFrame = new short[frameLength]; + this.processor=processor; + } + + public static string [] GetAvailableDevices() + { + return PvRecorder.GetAvailableDevices(); + } + + public void Start() + { + recorder.Start(); + thread = new Thread(ThreadLoop); + thread.Start(); + } + + public void ThreadLoop() + { + while (recorder.IsRecording) + { + if (thread.ThreadState != ThreadState.Running) + { + break; + } + if (processing) + { + Thread.Sleep(10); + continue; + } + processing = true; + audioFrame = recorder.Read(); + processor.Process(audioFrame); + processing = false; + } + } + + public void Stop() + { + thread.Interrupt(); + thread.Join(); + recorder.Stop(); + thread = null; + } + public void Dispose() + { + recorder.Dispose(); + } +} diff --git a/src/PostIt/PostIt/ViewModels/Settings/Settings.cs b/src/PostIt/PostIt/ViewModels/Settings/Settings.cs index 5576f01cd..e92bf9385 100644 --- a/src/PostIt/PostIt/ViewModels/Settings/Settings.cs +++ b/src/PostIt/PostIt/ViewModels/Settings/Settings.cs @@ -8,6 +8,7 @@ using System.IO; using System.Net.Http; using System.Text.Json; using System.Text.Json.Serialization; +using System.Globalization; [assembly: InternalsVisibleTo("PostIt.Tests")] @@ -44,6 +45,7 @@ public partial class Settings : ViewModelBase public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Pret."); + [JsonIgnore] public bool Loaded { get; private set; } = false; @@ -59,8 +61,16 @@ public partial class Settings : ViewModelBase /// . /// [ObservableProperty] + [JsonIgnore] public partial bool IsDirty { get; private set; } = false; + [JsonIgnore] + public override bool CanNavigateNext { get => false; protected set => throw new System.NotImplementedException(); } + + [JsonIgnore] + public override bool CanNavigatePrevious { get => true; protected set => throw new System.NotImplementedException(); } + + public string SelectedLanguage { get; internal set; } = CultureInfo.CurrentUICulture.Name; /// /// Guards every mutation of the observable state. [ObservableProperty] @@ -532,7 +542,4 @@ public partial class Settings : ViewModelBase /// partial void OnIsDirtyChanged(bool value) => SaveCommand.NotifyCanExecuteChanged(); - - public override bool CanNavigateNext { get => false; protected set => throw new System.NotImplementedException(); } - public override bool CanNavigatePrevious { get => true; protected set => throw new System.NotImplementedException(); } } diff --git a/src/PostIt/PostIt/Views/Layout/HomePage.axaml b/src/PostIt/PostIt/Views/Layout/HomePage.axaml index 31dd5851b..4172cb159 100644 --- a/src/PostIt/PostIt/Views/Layout/HomePage.axaml +++ b/src/PostIt/PostIt/Views/Layout/HomePage.axaml @@ -1,6 +1,7 @@ @@ -10,7 +11,7 @@ - From eacc4697c9fa314e1d3ca902169b0a59c1737857 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 28 Sep 2026 00:43:17 +0100 Subject: [PATCH 04/21] build image version bump --- .forgejo/workflows/buildAndTest.yml | 2 +- .forgejo/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index 0a05b8277..54274d95e 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -27,7 +27,7 @@ jobs: build: runs-on: docker container: - image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v3 + image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v4 steps: - name: Clone yavsc run: | diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 75c3becbd..de61bd43d 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -52,7 +52,7 @@ jobs: release: runs-on: docker container: - image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v3 + image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v4 steps: - name: Clone du repo au tag demandé env: From 68b224323bda12cdd6707a823bb736aba5122ef8 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Tue, 29 Sep 2026 01:26:37 +0100 Subject: [PATCH 05/21] Live.Avalonia integration --- src/PostIt/Directory.Packages.props | 2 + .../Properties/AndroidManifest.xml | 5 +- .../Resources/network_security_config.xml | 6 + src/PostIt/PostIt/App.axaml.cs | 63 +++++++- .../Helpers/ServiceCollectionHelpers.cs | 6 +- .../Layout => Models}/StatusNotice.cs | 2 +- src/PostIt/PostIt/PostIt.csproj | 3 + src/PostIt/PostIt/ViewLocator.cs | 3 + .../PostIt/ViewModels/Chat/ChatRoomInfo.cs | 11 ++ .../PostIt/ViewModels/Chat/ChatViewModel.cs | 135 ++++++++++++++++++ .../ViewModels/Layout/HomePageViewModel.cs | 13 ++ .../PostIt/Views/Blogs/BlogsPage.axaml.cs | 7 +- src/PostIt/PostIt/Views/Chat/ChatPage..axaml | 60 ++++++++ .../PostIt/Views/Chat/ChatPage.axaml.cs | 10 ++ src/PostIt/PostIt/Views/Layout/HomePage.axaml | 5 + 15 files changed, 321 insertions(+), 10 deletions(-) create mode 100644 src/PostIt/PostIt.Android/Resources/network_security_config.xml rename src/PostIt/PostIt/{ViewModels/Layout => Models}/StatusNotice.cs (94%) create mode 100644 src/PostIt/PostIt/ViewModels/Chat/ChatRoomInfo.cs create mode 100644 src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs create mode 100644 src/PostIt/PostIt/Views/Chat/ChatPage..axaml create mode 100644 src/PostIt/PostIt/Views/Chat/ChatPage.axaml.cs diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index c9e77bf1d..386d8924e 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -17,12 +17,14 @@ + + diff --git a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml index 8793aae8a..46f0bc9bb 100644 --- a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml +++ b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml @@ -1,6 +1,9 @@  - + diff --git a/src/PostIt/PostIt.Android/Resources/network_security_config.xml b/src/PostIt/PostIt.Android/Resources/network_security_config.xml new file mode 100644 index 000000000..6d2c97559 --- /dev/null +++ b/src/PostIt/PostIt.Android/Resources/network_security_config.xml @@ -0,0 +1,6 @@ + + + + 10.0.2.2 + + diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 0c4f0a7ca..4769fedf3 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -12,10 +12,14 @@ using PostIt.ViewModels; using PostIt.Views; using PostIt.Helpers; using System.Globalization; +using Live.Avalonia; +using System.Diagnostics; +using ReactiveUI; +using System.Reactive; namespace PostIt; -public partial class App : Application +public partial class App : Application, ILiveView { private int _bootStarted; public string? CliConfigFileSpecification {  get; private set; } @@ -38,13 +42,29 @@ public partial class App : Application { AvaloniaXamlLoader.Load(this); +#if DEBUG + this.AttachDeveloperTools(); +#endif } public override void OnFrameworkInitializationCompleted() { + +#if DEBUG + if (Debugger.IsAttached && !IsProduction()) + { + // Debugging requires pdb loading etc, so we disable live reloading + // during a test run with an attached debugger. + var window = new Window(); + window.Content = CreateView(window); + window.Show(); + } + else +#endif + { if (TryHandOffCustomSchemeUrl()) return; - this.ServiceProvider = new ServiceCollection().BuildServices(); + this.ServiceProvider = new ServiceCollection().BuildPostItServices(); var settings = ServiceProvider.GetRequiredService(); if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) @@ -60,9 +80,30 @@ public partial class App : Application InitializeSingleViewLifetime(settings, singleViewPlatform); } + } + +#if DEBUG + // Here we subscribe to ReactiveUI default exception handler to avoid app + // termination in case if we do something wrong in our view models. See: + // https://www.reactiveui.net/docs/handbook/default-exception-handler/ + // + // In case if you are using another MV* framework, please refer to its + // documentation explaining global exception handling. + RxApp.DefaultExceptionHandler = Observer.Create(Console.WriteLine); +#endif + base.OnFrameworkInitializationCompleted(); } + private bool IsProduction() + { +#if DEBUG + return false; +#else + return true; +#endif + } + public void InitializeSingleViewLifetime(Settings settings, ISingleViewApplicationLifetime singleViewPlatform) { singleViewPlatform.MainView = View = ServiceProvider!.GetRequiredService(); @@ -213,4 +254,22 @@ public partial class App : Application { this.CliConfigFileSpecification = configFile; } + + public object CreateView(Window window) + { + if (this.ServiceProvider==null) + { + var serviceCollection = new ServiceCollection(); + this.ServiceProvider = serviceCollection.BuildPostItServices(); + } + if (window.DataContext == null) + window.DataContext = ServiceProvider!.GetRequiredService(); + + // The AppView class will inherit the DataContext + // of the window. The AppView class can be a + // UserControl, a Grid, a TextBlock, whatever. + this.View = ServiceProvider!.GetRequiredService(); + this.ConfigureRootView(this.View); + return this.View; + } } diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs index f6fcc3e3b..55dad555b 100644 --- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -2,8 +2,10 @@ using System; using Microsoft.Extensions.DependencyInjection; using PostIt.Services; using PostIt.ViewModels; +using PostIt.ViewModels.Chat; using PostIt.Views; using PostIt.Views.Blogs; +using PostIt.Views.Chat; using PostIt.Views.Commands; using Yavsc.Api.Client; @@ -11,7 +13,7 @@ namespace PostIt.Helpers; public static class ServiceCollectionHelpers { - public static IServiceProvider BuildServices(this ServiceCollection services) + public static IServiceProvider BuildPostItServices(this ServiceCollection services) { var settings = new Settings(); settings.Load(); @@ -40,6 +42,7 @@ public static class ServiceCollectionHelpers services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // SettingsPage is a singleton: there must be one and only one // instance of the settings UI for the lifetime of the app. @@ -59,6 +62,7 @@ public static class ServiceCollectionHelpers services.AddSingleton(); services.AddSingleton(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/PostIt/PostIt/ViewModels/Layout/StatusNotice.cs b/src/PostIt/PostIt/Models/StatusNotice.cs similarity index 94% rename from src/PostIt/PostIt/ViewModels/Layout/StatusNotice.cs rename to src/PostIt/PostIt/Models/StatusNotice.cs index c87af390c..c50ac3503 100644 --- a/src/PostIt/PostIt/ViewModels/Layout/StatusNotice.cs +++ b/src/PostIt/PostIt/Models/StatusNotice.cs @@ -16,7 +16,7 @@ public sealed class StatusNotice public string BorderBrush { get; } public string Foreground { get; } - private StatusNotice(string message, StatusSeverity severity) + public StatusNotice(string message, StatusSeverity severity) { Message = string.IsNullOrWhiteSpace(message) ? "Pret." : message; Severity = severity; diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj index 5057bde8c..89525f190 100644 --- a/src/PostIt/PostIt/PostIt.csproj +++ b/src/PostIt/PostIt/PostIt.csproj @@ -33,11 +33,13 @@ All + + @@ -60,5 +62,6 @@ True Lang/Resources.resx + diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs index ae4662072..ffaaadc66 100644 --- a/src/PostIt/PostIt/ViewLocator.cs +++ b/src/PostIt/PostIt/ViewLocator.cs @@ -5,8 +5,10 @@ using Avalonia.Controls.Templates; using Microsoft.Extensions.DependencyInjection; using PostIt.ViewModels; using PostIt.ViewModels.Commands; +using PostIt.ViewModels.Chat; using PostIt.Views; using PostIt.Views.Blogs; +using PostIt.Views.Chat; using PostIt.Views.Commands; namespace PostIt; @@ -58,6 +60,7 @@ public class ViewLocator : IDataTemplate EstimateListPageViewModel => services.GetRequiredService(), EstimateValidationPageViewModel => services.GetRequiredService(), MyFilesViewModel => services.GetRequiredService(), + ChatViewModel => services.GetRequiredService(), null => new TextBlock { Text = "No view for " }, _ => new TextBlock { Text = $"No view for {data.GetType().Name}" } }; diff --git a/src/PostIt/PostIt/ViewModels/Chat/ChatRoomInfo.cs b/src/PostIt/PostIt/ViewModels/Chat/ChatRoomInfo.cs new file mode 100644 index 000000000..548371157 --- /dev/null +++ b/src/PostIt/PostIt/ViewModels/Chat/ChatRoomInfo.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace PostIt.ViewModels.Chat +{ + // DTO minimal calqué sur votre logique serveur ChatRoomInfo + public class ChatRoomInfo + { + public string? Name { get; set; } + public List Users { get; set; } = new(); + } +} diff --git a/src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs b/src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs new file mode 100644 index 000000000..85392cfb1 --- /dev/null +++ b/src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.AspNetCore.SignalR.Client; + +namespace PostIt.ViewModels.Chat +{ + + public partial class ChatViewModel : ViewModelBase + { + private HubConnection? _connection; + + [ObservableProperty] + public partial string ServerUrl { get; protected set; } = "http://localhost:5000/chathub"; + + + [ObservableProperty] + public partial string CurrentRoom { get; set; } = "General"; + + [ObservableProperty] + public partial string NickName { get; set; } = ""; + + [ObservableProperty] + public partial StatusNotice Status { get; set; } + = new StatusNotice("Déconnecté", StatusSeverity.Info); + + [ObservableProperty] + public partial bool IsConnected { get; set; } = false; + + // Listes dynamiques pour l'UI + public ObservableCollection Messages { get; } = new(); + public ObservableCollection ConnectedUsers { get; } = new(); + + // Commandes pour les boutons + [RelayCommand] + public async Task ConnectCommand() => await ConnectAsync(); + + [RelayCommand] + public async Task JoinRoomCommand() => await JoinRoomAsync(); + + [RelayCommand] + public async Task ChangeNickCommand() => await ChangeNickAsync(); + + public override bool CanNavigateNext { get; protected set; } = false; + public override bool CanNavigatePrevious { get; protected set; } = true; + + private async Task ConnectAsync() + { + try + { + Status = new StatusNotice("Connexion en cours...", StatusSeverity.Info); + + _connection = new HubConnectionBuilder() + .WithUrl(ServerUrl) + .WithAutomaticReconnect() + .Build(); + + // 1. Ecoute des notifications globales (notifyUser) + _connection.On("notifyUser", (type, user, msg) => + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + Messages.Add($"[{type}] {user}: {msg}")); + }); + + // 2. Ecoute des notifications de salon (notifyRoom) + _connection.On("notifyRoom", (type, room, user) => + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + Messages.Add($"*{user} a interagi avec le salon {room} (Action: {type})"); + if (type == "UserJoin" && !ConnectedUsers.Contains(user)) + ConnectedUsers.Add(user); + }); + }); + + // 3. Réception de l'état complet suite au Join réussi (joint) + _connection.On("joint", (roomInfo) => + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + Messages.Add($"Salon rejoint avec succès : {roomInfo.Name}"); + ConnectedUsers.Clear(); + foreach (var u in roomInfo.Users) ConnectedUsers.Add(u); + }); + }); + + await _connection.StartAsync(); + IsConnected = true; + Status = new StatusNotice("Connecté au Hub SignalR", StatusSeverity.Info); + } + catch (Exception ex) + { + Status = new StatusNotice($"Erreur : {ex.Message}", StatusSeverity.Error); + IsConnected = false; + } + } + + private async Task JoinRoomAsync() + { + if (_connection == null || string.IsNullOrWhiteSpace(CurrentRoom)) return; + + try + { + // Appel de la méthode publique 'Join' du Hub (renvoie un ChatRoomInfo) + var info = await _connection.InvokeAsync("Join", CurrentRoom); + if (info == null) + { + Messages.Add("Erreur lors de la validation du salon par le serveur."); + } + } + catch (Exception ex) + { + Messages.Add($"Erreur Join: {ex.Message}"); + } + } + + private async Task ChangeNickAsync() + { + if (_connection == null || string.IsNullOrWhiteSpace(NickName)) return; + + try + { + // Appel de la méthode publique 'Nick' du Hub (void) + await _connection.InvokeAsync("Nick", NickName); + Messages.Add($"Demande de changement de pseudo vers '{NickName}' envoyée."); + } + catch (Exception ex) + { + Messages.Add($"Erreur Nick: {ex.Message}"); + } + } + } +} diff --git a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs index 2c138a27d..19ed05833 100644 --- a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; using PostIt.Helpers; using PostIt.Services; +using PostIt.ViewModels.Chat; using Yavsc.Api.Client; namespace PostIt.ViewModels; @@ -36,7 +37,11 @@ public class HomePageViewModel : ViewModelBase OpenProviderEstimates = new AsyncRelayCommand(() => OpenEstimateListAsync(EstimateListPerspective.Provider)); OpenBlogs = new AsyncRelayCommand(App.PushBlogsPageAsync); OpenMyFiles = new AsyncRelayCommand(OpenMyFilesAsync); + OpenChat = new AsyncRelayCommand(OpenChatAsync); } + + + public IAsyncRelayCommand OpenChat { get; } public IAsyncRelayCommand OpenBlogs { get; } public IAsyncRelayCommand OpenActivities { get; } public IAsyncRelayCommand OpenProviderRequests { get; } @@ -103,6 +108,14 @@ public class HomePageViewModel : ViewModelBase await app.PushPageAsync(vm); } + private async Task OpenChatAsync() + { + var app = (App?)Application.Current; + var vm = app!.ServiceProvider?.GetRequiredService(); + await app.PushPageAsync(vm!); + + } + /// /// Ouvre la page « Mes fichiers » (espace de stockage personnel), /// servie par le host Blogs via api/v1/fs. diff --git a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs b/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs index 89743d0ad..629727870 100644 --- a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs +++ b/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs @@ -5,15 +5,12 @@ using Avalonia.Platform.Storage; using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.Controls.Platform; -using Avalonia.Input; -using System; -using Avalonia; namespace PostIt.Views.Blogs; public partial class BlogsPage : ContentPage { - private IInputPane _inputPane; + private IInputPane? _inputPane; public BlogsPage() { @@ -28,7 +25,7 @@ public partial class BlogsPage : ContentPage var topLevel = TopLevel.GetTopLevel(this); if (topLevel != null) { - _inputPane = topLevel.InputPane; + _inputPane = topLevel!.InputPane; if (_inputPane != null) { // S'abonne aux changements d'état du clavier virtuel diff --git a/src/PostIt/PostIt/Views/Chat/ChatPage..axaml b/src/PostIt/PostIt/Views/Chat/ChatPage..axaml new file mode 100644 index 000000000..927187de8 --- /dev/null +++ b/src/PostIt/PostIt/Views/Chat/ChatPage..axaml @@ -0,0 +1,60 @@ + + + + + + + + +