feat(ui): add MVVM helpers and per-component view models (Participant, GlobalSettings, AlertBanner)
Some checks failed
CI / build-and-test (push) Has been cancelled
Some checks failed
CI / build-and-test (push) Has been cancelled
This commit is contained in:
parent
2c6fbdf861
commit
fbb73bcf04
6 changed files with 250 additions and 2 deletions
|
|
@ -5,8 +5,9 @@
|
|||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<RootNamespace>TeamsISO.App</RootNamespace>
|
||||
<!-- WPF only builds on Windows. Non-Windows CI skips this project. -->
|
||||
<BuildOnNonWindows Condition="'$(OS)' != 'Windows_NT'">false</BuildOnNonWindows>
|
||||
<AssemblyName>TeamsISO</AssemblyName>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
<ApplicationIcon></ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
31
src/TeamsISO.App/ViewModels/AlertBannerViewModel.cs
Normal file
31
src/TeamsISO.App/ViewModels/AlertBannerViewModel.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using TeamsISO.Engine.Domain;
|
||||
|
||||
namespace TeamsISO.App.ViewModels;
|
||||
|
||||
public sealed class AlertBannerViewModel : ObservableObject
|
||||
{
|
||||
private EngineAlert? _current;
|
||||
|
||||
public EngineAlert? Current
|
||||
{
|
||||
get => _current;
|
||||
set
|
||||
{
|
||||
if (SetField(ref _current, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(IsVisible));
|
||||
OnPropertyChanged(nameof(Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsVisible => _current is not null;
|
||||
public string Message => _current?.Message ?? string.Empty;
|
||||
|
||||
public RelayCommand DismissCommand { get; }
|
||||
|
||||
public AlertBannerViewModel()
|
||||
{
|
||||
DismissCommand = new RelayCommand(() => Current = null);
|
||||
}
|
||||
}
|
||||
45
src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs
Normal file
45
src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
using TeamsISO.Engine.Controller;
|
||||
using TeamsISO.Engine.Domain;
|
||||
|
||||
namespace TeamsISO.App.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Bindings for the global settings panel: framerate, resolution, aspect, audio.
|
||||
/// </summary>
|
||||
public sealed class GlobalSettingsViewModel : ObservableObject
|
||||
{
|
||||
private readonly IIsoController _controller;
|
||||
private TargetFramerate _framerate;
|
||||
private TargetResolution _resolution;
|
||||
private AspectMode _aspect;
|
||||
private AudioMode _audio;
|
||||
|
||||
public GlobalSettingsViewModel(IIsoController controller)
|
||||
{
|
||||
_controller = controller;
|
||||
var current = controller.GlobalSettings;
|
||||
_framerate = current.Framerate;
|
||||
_resolution = current.Resolution;
|
||||
_aspect = current.Aspect;
|
||||
_audio = current.Audio;
|
||||
ApplyCommand = new AsyncRelayCommand(ApplyAsync);
|
||||
}
|
||||
|
||||
public IEnumerable<TargetFramerate> AvailableFramerates => Enum.GetValues<TargetFramerate>();
|
||||
public IEnumerable<TargetResolution> AvailableResolutions => Enum.GetValues<TargetResolution>();
|
||||
public IEnumerable<AspectMode> AvailableAspectModes => Enum.GetValues<AspectMode>();
|
||||
public IEnumerable<AudioMode> AvailableAudioModes => Enum.GetValues<AudioMode>();
|
||||
|
||||
public TargetFramerate Framerate { get => _framerate; set => SetField(ref _framerate, value); }
|
||||
public TargetResolution Resolution { get => _resolution; set => SetField(ref _resolution, value); }
|
||||
public AspectMode Aspect { get => _aspect; set => SetField(ref _aspect, value); }
|
||||
public AudioMode Audio { get => _audio; set => SetField(ref _audio, value); }
|
||||
|
||||
public AsyncRelayCommand ApplyCommand { get; }
|
||||
|
||||
private Task ApplyAsync()
|
||||
{
|
||||
var settings = new FrameProcessingSettings(_framerate, _resolution, _aspect, _audio);
|
||||
return _controller.SetGlobalSettingsAsync(settings, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
23
src/TeamsISO.App/ViewModels/ObservableObject.cs
Normal file
23
src/TeamsISO.App/ViewModels/ObservableObject.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace TeamsISO.App.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal MVVM base class implementing <see cref="INotifyPropertyChanged"/>.
|
||||
/// </summary>
|
||||
public abstract class ObservableObject : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
|
||||
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
90
src/TeamsISO.App/ViewModels/ParticipantViewModel.cs
Normal file
90
src/TeamsISO.App/ViewModels/ParticipantViewModel.cs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
using TeamsISO.Engine.Controller;
|
||||
using TeamsISO.Engine.Domain;
|
||||
|
||||
namespace TeamsISO.App.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Per-row view model for a participant in the participant list.
|
||||
/// Wraps a domain <see cref="Participant"/> and exposes ISO toggle and naming commands.
|
||||
/// </summary>
|
||||
public sealed class ParticipantViewModel : ObservableObject
|
||||
{
|
||||
private readonly IIsoController _controller;
|
||||
private Participant _participant;
|
||||
private bool _isEnabled;
|
||||
private bool _isProcessing;
|
||||
private string _customName;
|
||||
|
||||
public ParticipantViewModel(IIsoController controller, Participant participant)
|
||||
{
|
||||
_controller = controller;
|
||||
_participant = participant;
|
||||
_customName = string.Empty;
|
||||
ToggleIsoCommand = new AsyncRelayCommand(ToggleIsoAsync, () => !_isProcessing);
|
||||
}
|
||||
|
||||
public Guid Id => _participant.Id;
|
||||
public string DisplayName => _participant.DisplayName;
|
||||
public string SourceMachine => _participant.CurrentSource?.MachineName ?? "(disconnected)";
|
||||
public string SourceFullName => _participant.CurrentSource?.FullName ?? "(disconnected)";
|
||||
public bool IsOnline => _participant.CurrentSource is not null;
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => _isEnabled;
|
||||
set => SetField(ref _isEnabled, value);
|
||||
}
|
||||
|
||||
public bool IsProcessing
|
||||
{
|
||||
get => _isProcessing;
|
||||
private set
|
||||
{
|
||||
if (SetField(ref _isProcessing, value))
|
||||
ToggleIsoCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string CustomName
|
||||
{
|
||||
get => _customName;
|
||||
set => SetField(ref _customName, value);
|
||||
}
|
||||
|
||||
public AsyncRelayCommand ToggleIsoCommand { get; }
|
||||
|
||||
/// <summary>Refreshes the underlying participant data (called when the controller emits an updated list).</summary>
|
||||
public void Update(Participant updated)
|
||||
{
|
||||
_participant = updated;
|
||||
OnPropertyChanged(nameof(DisplayName));
|
||||
OnPropertyChanged(nameof(SourceMachine));
|
||||
OnPropertyChanged(nameof(SourceFullName));
|
||||
OnPropertyChanged(nameof(IsOnline));
|
||||
}
|
||||
|
||||
private async Task ToggleIsoAsync()
|
||||
{
|
||||
IsProcessing = true;
|
||||
try
|
||||
{
|
||||
if (IsEnabled)
|
||||
{
|
||||
await _controller.DisableIsoAsync(Id, CancellationToken.None);
|
||||
IsEnabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
await _controller.EnableIsoAsync(
|
||||
Id,
|
||||
string.IsNullOrWhiteSpace(_customName) ? null : _customName,
|
||||
CancellationToken.None);
|
||||
IsEnabled = true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsProcessing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/TeamsISO.App/ViewModels/RelayCommand.cs
Normal file
58
src/TeamsISO.App/ViewModels/RelayCommand.cs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
using System.Windows.Input;
|
||||
|
||||
namespace TeamsISO.App.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Synchronous command that delegates execution to an <see cref="Action"/>.
|
||||
/// </summary>
|
||||
public sealed class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action _execute;
|
||||
private readonly Func<bool>? _canExecute;
|
||||
|
||||
public RelayCommand(Action execute, Func<bool>? canExecute = null)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
|
||||
public void Execute(object? parameter) => _execute();
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async command that suppresses re-entrancy while running.
|
||||
/// </summary>
|
||||
public sealed class AsyncRelayCommand : ICommand
|
||||
{
|
||||
private readonly Func<Task> _execute;
|
||||
private readonly Func<bool>? _canExecute;
|
||||
private bool _isRunning;
|
||||
|
||||
public AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object? parameter) => !_isRunning && (_canExecute?.Invoke() ?? true);
|
||||
|
||||
public async void Execute(object? parameter)
|
||||
{
|
||||
if (_isRunning) return;
|
||||
_isRunning = true;
|
||||
RaiseCanExecuteChanged();
|
||||
try { await _execute(); }
|
||||
finally
|
||||
{
|
||||
_isRunning = false;
|
||||
RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
Loading…
Reference in a new issue