diff --git a/src/TeamsISO.App/App.xaml b/src/TeamsISO.App/App.xaml index 8be975d..abef318 100644 --- a/src/TeamsISO.App/App.xaml +++ b/src/TeamsISO.App/App.xaml @@ -1,5 +1,11 @@ - + + + + + + + diff --git a/src/TeamsISO.App/MainWindow.xaml b/src/TeamsISO.App/MainWindow.xaml index 461b7ff..dda478a 100644 --- a/src/TeamsISO.App/MainWindow.xaml +++ b/src/TeamsISO.App/MainWindow.xaml @@ -3,60 +3,134 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:TeamsISO.App.ViewModels" xmlns:conv="clr-namespace:TeamsISO.App.Converters" - Title="TeamsISO" Height="700" Width="1100" - Background="#202225" Foreground="#E8E8E8"> + Title="TeamsISO" + Height="760" Width="1180" + MinHeight="600" MinWidth="960" + Background="{DynamicResource Stone.Canvas}" + UseLayoutRounding="True" + TextOptions.TextFormattingMode="Ideal" + TextOptions.TextRenderingMode="ClearType"> - - - - - - + + - + - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TeamsISO.App/Themes/StoneTheme.xaml b/src/TeamsISO.App/Themes/StoneTheme.xaml new file mode 100644 index 0000000..12eaae9 --- /dev/null +++ b/src/TeamsISO.App/Themes/StoneTheme.xaml @@ -0,0 +1,491 @@ + + + + + + 4 + 8 + 12 + 16 + 24 + 32 + + + 4 + 6 + 8 + + + + + + + + + + + + + + + + + + + + + + + + + Segoe UI Variable Display, Segoe UI, Manrope, sans-serif + Cascadia Mono, Consolas, monospace + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs b/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs index e92d320..449b9c7 100644 --- a/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs +++ b/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs @@ -4,7 +4,8 @@ using TeamsISO.Engine.Domain; namespace TeamsISO.App.ViewModels; /// -/// Bindings for the global settings panel: framerate, resolution, aspect, audio. +/// Bindings for the global settings panel: framerate, resolution, aspect, audio, +/// NDI groups (discovery + output), and the participant-list hide-Local toggle. /// public sealed class GlobalSettingsViewModel : ObservableObject { @@ -13,6 +14,9 @@ public sealed class GlobalSettingsViewModel : ObservableObject private TargetResolution _resolution; private AspectMode _aspect; private AudioMode _audio; + private string _discoveryGroups; + private string _outputGroups; + private bool _hideLocalSelf = true; public GlobalSettingsViewModel(IIsoController controller) { @@ -22,6 +26,11 @@ public sealed class GlobalSettingsViewModel : ObservableObject _resolution = current.Resolution; _aspect = current.Aspect; _audio = current.Audio; + + var groups = controller.GroupSettings; + _discoveryGroups = groups.DiscoveryGroups ?? string.Empty; + _outputGroups = groups.OutputGroups ?? string.Empty; + ApplyCommand = new AsyncRelayCommand(ApplyAsync); } @@ -35,11 +44,29 @@ public sealed class GlobalSettingsViewModel : ObservableObject public AspectMode Aspect { get => _aspect; set => SetField(ref _aspect, value); } public AudioMode Audio { get => _audio; set => SetField(ref _audio, value); } + /// NDI discovery group(s) — comma-separated. Empty = default (Public). + public string DiscoveryGroups { get => _discoveryGroups; set => SetField(ref _discoveryGroups, value); } + + /// NDI output group(s) — comma-separated. Empty = default (Public). + public string OutputGroups { get => _outputGroups; set => SetField(ref _outputGroups, value); } + + /// + /// Hide the user's own self-preview ("(Local)") from the participants list. + /// On by default — operators rarely want to ISO-route their own preview. + /// Read by when filtering the list it presents. + /// + public bool HideLocalSelf { get => _hideLocalSelf; set => SetField(ref _hideLocalSelf, value); } + public AsyncRelayCommand ApplyCommand { get; } - private Task ApplyAsync() + private async Task ApplyAsync() { var settings = new FrameProcessingSettings(_framerate, _resolution, _aspect, _audio); - return _controller.SetGlobalSettingsAsync(settings, CancellationToken.None); + await _controller.SetGlobalSettingsAsync(settings, CancellationToken.None); + + var groups = new NdiGroupSettings( + DiscoveryGroups: string.IsNullOrWhiteSpace(_discoveryGroups) ? null : _discoveryGroups.Trim(), + OutputGroups: string.IsNullOrWhiteSpace(_outputGroups) ? null : _outputGroups.Trim()); + await _controller.SetGroupSettingsAsync(groups, CancellationToken.None); } } diff --git a/src/TeamsISO.App/ViewModels/MainViewModel.cs b/src/TeamsISO.App/ViewModels/MainViewModel.cs index b7cdfae..79ecb51 100644 --- a/src/TeamsISO.App/ViewModels/MainViewModel.cs +++ b/src/TeamsISO.App/ViewModels/MainViewModel.cs @@ -61,8 +61,14 @@ public sealed class MainViewModel : ObservableObject, IDisposable private void OnParticipantsChanged(IReadOnlyList incoming) { var seenIds = new HashSet(); + var hideLocal = Settings.HideLocalSelf; foreach (var p in incoming) { + // The new Teams client emits a "(Local)" pseudo-participant for the user's + // own preview — operators rarely want it as a routable ISO. Suppress when + // HideLocalSelf is on (default). + if (hideLocal && IsLocalSelf(p)) continue; + seenIds.Add(p.Id); if (_byId.TryGetValue(p.Id, out var vm)) { @@ -76,7 +82,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable } } - // Remove participants no longer present + // Remove participants no longer present (or now hidden by the filter). for (var i = Participants.Count - 1; i >= 0; i--) { var vm = Participants[i]; @@ -88,6 +94,9 @@ public sealed class MainViewModel : ObservableObject, IDisposable } } + private static bool IsLocalSelf(Participant p) => + string.Equals(p.DisplayName, "(Local)", StringComparison.Ordinal); + public void Dispose() { _participantsSub.Dispose();