using TeamsISO.Engine.Controller; using TeamsISO.Engine.Domain; using IsoHealthStats = TeamsISO.Engine.Domain.IsoHealthStats; namespace TeamsISO.App.ViewModels; /// /// Per-row view model for a participant in the participant list. /// Wraps a domain and exposes ISO toggle and naming commands. /// 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); } private long _framesIn; private long _framesOut; private long _framesDropped; private string _incomingResolution = "—"; private string _incomingFps = "—"; /// Number of frames the receiver has captured so far. public long FramesIn { get => _framesIn; set => SetField(ref _framesIn, value); } /// Number of frames the sender has emitted so far. public long FramesOut { get => _framesOut; set => SetField(ref _framesOut, value); } /// Frames dropped by the closest-frame strategy when the receiver outpaces the processor. public long FramesDropped { get => _framesDropped; set => SetField(ref _framesDropped, value); } private string _stateLabel = "—"; private string _stateColor = "Wd.Text.Tertiary"; /// Human-readable pipeline state ("Receiving", "Error", "—"). public string StateLabel { get => _stateLabel; set => SetField(ref _stateLabel, value); } /// Resource key of the brush to color the state pill with. public string StateColor { get => _stateColor; set => SetField(ref _stateColor, value); } /// Source resolution as "WxH", or em-dash when no frames have been seen yet. public string IncomingResolution { get => _incomingResolution; set => SetField(ref _incomingResolution, value); } /// /// Live incoming framerate as "59.94 fps", or em-dash when fewer than 2 frames /// have been observed since the pipeline started. Computed in the engine via a /// 30-frame moving window. /// public string IncomingFps { get => _incomingFps; set => SetField(ref _incomingFps, value); } /// Updates the live stats display from a controller-side snapshot. public void UpdateStats(IsoHealthStats stats) { FramesIn = stats.FramesIn; FramesOut = stats.FramesOut; FramesDropped = stats.FramesDropped; IncomingResolution = stats.IncomingWidth > 0 && stats.IncomingHeight > 0 ? $"{stats.IncomingWidth}×{stats.IncomingHeight}" : "—"; IncomingFps = stats.IncomingFps > 0 ? $"{stats.IncomingFps:0.0} fps" : "—"; (StateLabel, StateColor) = stats.State switch { IsoState.Receiving => ("LIVE", "Wd.Status.Live"), IsoState.Sending => ("LIVE", "Wd.Status.Live"), IsoState.NoSignal => ("NO SIGNAL", "Wd.Status.Warn"), IsoState.Error => ("ERROR", "Wd.Status.Error"), IsoState.Idle => (IsEnabled ? "STARTING" : "—", "Wd.Text.Tertiary"), _ => ("—", "Wd.Text.Tertiary"), }; } 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; } /// Refreshes the underlying participant data (called when the controller emits an updated list). 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; } } }