From d282e1b0f801283c983f7de026dcd6a6de0f168b Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Fri, 15 May 2026 11:15:00 -0400 Subject: [PATCH] feat(wpf): v2 task 39+40 - studio table redesign + Ctrl+K command palette Task 39: 5-column participants table - state LED, name+codec caption, 5-bar audio meter, mono output name, ISO pill. Row height 52, full-row active-speaker tint (no left stripe). New converter LevelThresholdConverter, OutputName property on ParticipantViewModel. Task 40: Ctrl+K / Ctrl+P command palette - chromeless centered floating window, fuzzy Contains match across Label/Category/Keywords, arrow nav, Enter invoke, Esc close. Quick/Teams/Network/App categories cover top operator verbs and theme switching. Also: log startup exceptions to Serilog before the modal MessageBox fires - much better triage signal than user-pasted dialog text. --- src/TeamsISO.App/App.xaml.cs | 7 + .../Converters/LevelThresholdConverter.cs | 42 ++++ .../Converters/NullToCollapsedConverter.cs | 24 ++ src/TeamsISO.App/MainWindow.xaml | 182 +++++++++++++-- src/TeamsISO.App/MainWindow.xaml.cs | 20 +- src/TeamsISO.App/Themes/WildDragonTheme.xaml | 11 +- .../ViewModels/CommandPaletteViewModel.cs | 210 ++++++++++++++++++ src/TeamsISO.App/ViewModels/MainViewModel.cs | 19 ++ .../ViewModels/ParticipantViewModel.cs | 20 +- .../Views/CommandPaletteWindow.xaml | 198 +++++++++++++++++ .../Views/CommandPaletteWindow.xaml.cs | 84 +++++++ 11 files changed, 784 insertions(+), 33 deletions(-) create mode 100644 src/TeamsISO.App/Converters/LevelThresholdConverter.cs create mode 100644 src/TeamsISO.App/Converters/NullToCollapsedConverter.cs create mode 100644 src/TeamsISO.App/ViewModels/CommandPaletteViewModel.cs create mode 100644 src/TeamsISO.App/Views/CommandPaletteWindow.xaml create mode 100644 src/TeamsISO.App/Views/CommandPaletteWindow.xaml.cs diff --git a/src/TeamsISO.App/App.xaml.cs b/src/TeamsISO.App/App.xaml.cs index e2b0837..7cdf4ab 100644 --- a/src/TeamsISO.App/App.xaml.cs +++ b/src/TeamsISO.App/App.xaml.cs @@ -286,6 +286,13 @@ public partial class App : Application } catch (Exception ex) { + // Log the full exception (incl. stack + inner) to Serilog BEFORE the + // modal MessageBox fires — diagnostic logs are far more useful than a + // user-pasted "TeamsISO failed to start..." line when triaging a + // startup crash. The logger may itself have been the failure target + // so guard the call. + try { _loggerFactory?.CreateLogger().LogCritical(ex, "OnStartup failed before main loop"); } + catch { /* defensive */ } MessageBox.Show( "TeamsISO failed to start.\n\nDetails: " + ex, "TeamsISO — startup error", diff --git a/src/TeamsISO.App/Converters/LevelThresholdConverter.cs b/src/TeamsISO.App/Converters/LevelThresholdConverter.cs new file mode 100644 index 0000000..f53630d --- /dev/null +++ b/src/TeamsISO.App/Converters/LevelThresholdConverter.cs @@ -0,0 +1,42 @@ +using System.Globalization; +using System.Windows.Data; + +namespace TeamsISO.App.Converters; + +/// +/// Maps an audio level (0.0–1.0) to an opacity for a single audio-meter +/// segment. The XAML binds five copies, each with a different +/// threshold (0.2, 0.4, 0.6, +/// 0.8, 1.0). A segment renders at full opacity when the live level +/// exceeds its threshold; below that it dims to a faint silhouette so the +/// inactive segments still read as "the meter has 5 steps" rather than +/// blank space. +/// +/// Designed for the v2 "Studio Terminal" participants table's audio meter. +/// Broadcast engineers expect instantaneous (non-averaged) bars; the +/// converter is stateless and trusts the caller to push raw levels. +/// +public sealed class LevelThresholdConverter : IValueConverter +{ + /// Opacity for an above-threshold segment. Defaults to 1.0. + public double ActiveOpacity { get; set; } = 1.0; + + /// Opacity for a below-threshold segment. Defaults to 0.18 — visible enough to read the segment shape but clearly off. + public double InactiveOpacity { get; set; } = 0.18; + + public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture) + { + var level = value switch + { + double d => d, + float f => f, + _ => 0.0, + }; + if (!double.TryParse(parameter?.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var threshold)) + threshold = 1.0; + return level >= threshold ? ActiveOpacity : InactiveOpacity; + } + + public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => + System.Windows.Data.Binding.DoNothing; +} diff --git a/src/TeamsISO.App/Converters/NullToCollapsedConverter.cs b/src/TeamsISO.App/Converters/NullToCollapsedConverter.cs new file mode 100644 index 0000000..e24a8c9 --- /dev/null +++ b/src/TeamsISO.App/Converters/NullToCollapsedConverter.cs @@ -0,0 +1,24 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace TeamsISO.App.Converters; + +/// +/// Tiny utility converter — null or empty string ⇒ Collapsed, otherwise +/// Visible. Used by the v2 command palette's optional shortcut chip +/// (e.g. "Ctrl+R") so rows without a bound shortcut don't render the +/// empty pill outline. +/// +public sealed class NullToCollapsedConverter : IValueConverter +{ + public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture) + { + if (value is null) return Visibility.Collapsed; + if (value is string s && string.IsNullOrEmpty(s)) return Visibility.Collapsed; + return Visibility.Visible; + } + + public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => + System.Windows.Data.Binding.DoNothing; +} diff --git a/src/TeamsISO.App/MainWindow.xaml b/src/TeamsISO.App/MainWindow.xaml index 93668c7..4aee607 100644 --- a/src/TeamsISO.App/MainWindow.xaml +++ b/src/TeamsISO.App/MainWindow.xaml @@ -38,6 +38,7 @@ TrueValue="Collapsed" FalseValue="Visible"/> + @@ -45,10 +46,11 @@ - - + + + @@ -405,10 +407,31 @@ - + + RowHeight="52"> - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +