feat: app icon, FPS, drops counter, --version, About dialog, Stop Teams toggle
Six related polish items, all building on tonight's groundwork.
1. App icon: teamsiso.ico generated from dragon-mark.png at 7 sizes (16-256), wired as ApplicationIcon in the WPF csproj, MainWindow.Icon, AboutWindow.Icon, and ARPPRODUCTICON in the WiX MSI. Taskbar / window / Add-Remove-Programs all show the dragon mark now.
2. Running incoming FPS: ring buffer of last 30 frame timestamps in IsoPipeline; ComputeFps() returns moving-average rate. Surfaced on IsoHealthStats.IncomingFps and shown in the Source column of the participants DataGrid as 'WxH · 59.94 fps'. Resets cleanly on every supervisor restart.
3. Drops counter: FrameProcessor.Stats already aggregated FramesDropped (closest-frame strategy when the receiver outpaces the processor) and FramesDuplicated; just plumbed _liveProcessor through IsoPipeline so GetStats() can read them. Exposed in the Live column under the in/out counters as a coral-tinted 'drop N'.
4. Console --version flag: prints engine version (with embedded git SHA), .NET version, OS, NDI runtime banner, expected prefix, exit-code legend, plus a wilddragon.net link. Useful for support tickets.
5. About dialog: chromeless modal with the dragon mark + version / .NET / OS / NDI runtime fields and a link to wilddragon.net. Triggered by clicking the rail logo.
6. Teams launcher Stop toggle: TeamsLauncher gains IsRunning() and StopAll(). The rail's Teams button now toggles — if Teams is up, ask to close all Teams windows via WM_CLOSE; otherwise launch as before. Confirms before stopping so we don't kill the user's call mid-transition.
Tests: 74/74 unit + 9/9 NDI integration green throughout. MSI builds clean and now embeds the dragon icon for ARP.
2026-05-08 13:50:19 -04:00
|
|
|
using System.Diagnostics;
|
2026-05-10 09:41:29 -04:00
|
|
|
using System.IO;
|
feat: app icon, FPS, drops counter, --version, About dialog, Stop Teams toggle
Six related polish items, all building on tonight's groundwork.
1. App icon: teamsiso.ico generated from dragon-mark.png at 7 sizes (16-256), wired as ApplicationIcon in the WPF csproj, MainWindow.Icon, AboutWindow.Icon, and ARPPRODUCTICON in the WiX MSI. Taskbar / window / Add-Remove-Programs all show the dragon mark now.
2. Running incoming FPS: ring buffer of last 30 frame timestamps in IsoPipeline; ComputeFps() returns moving-average rate. Surfaced on IsoHealthStats.IncomingFps and shown in the Source column of the participants DataGrid as 'WxH · 59.94 fps'. Resets cleanly on every supervisor restart.
3. Drops counter: FrameProcessor.Stats already aggregated FramesDropped (closest-frame strategy when the receiver outpaces the processor) and FramesDuplicated; just plumbed _liveProcessor through IsoPipeline so GetStats() can read them. Exposed in the Live column under the in/out counters as a coral-tinted 'drop N'.
4. Console --version flag: prints engine version (with embedded git SHA), .NET version, OS, NDI runtime banner, expected prefix, exit-code legend, plus a wilddragon.net link. Useful for support tickets.
5. About dialog: chromeless modal with the dragon mark + version / .NET / OS / NDI runtime fields and a link to wilddragon.net. Triggered by clicking the rail logo.
6. Teams launcher Stop toggle: TeamsLauncher gains IsRunning() and StopAll(). The rail's Teams button now toggles — if Teams is up, ask to close all Teams windows via WM_CLOSE; otherwise launch as before. Confirms before stopping so we don't kill the user's call mid-transition.
Tests: 74/74 unit + 9/9 NDI integration green throughout. MSI builds clean and now embeds the dragon icon for ARP.
2026-05-08 13:50:19 -04:00
|
|
|
using System.Reflection;
|
|
|
|
|
using System.Runtime.Versioning;
|
|
|
|
|
using System.Windows;
|
|
|
|
|
using System.Windows.Navigation;
|
2026-05-10 09:41:29 -04:00
|
|
|
using TeamsISO.App.Services;
|
feat: app icon, FPS, drops counter, --version, About dialog, Stop Teams toggle
Six related polish items, all building on tonight's groundwork.
1. App icon: teamsiso.ico generated from dragon-mark.png at 7 sizes (16-256), wired as ApplicationIcon in the WPF csproj, MainWindow.Icon, AboutWindow.Icon, and ARPPRODUCTICON in the WiX MSI. Taskbar / window / Add-Remove-Programs all show the dragon mark now.
2. Running incoming FPS: ring buffer of last 30 frame timestamps in IsoPipeline; ComputeFps() returns moving-average rate. Surfaced on IsoHealthStats.IncomingFps and shown in the Source column of the participants DataGrid as 'WxH · 59.94 fps'. Resets cleanly on every supervisor restart.
3. Drops counter: FrameProcessor.Stats already aggregated FramesDropped (closest-frame strategy when the receiver outpaces the processor) and FramesDuplicated; just plumbed _liveProcessor through IsoPipeline so GetStats() can read them. Exposed in the Live column under the in/out counters as a coral-tinted 'drop N'.
4. Console --version flag: prints engine version (with embedded git SHA), .NET version, OS, NDI runtime banner, expected prefix, exit-code legend, plus a wilddragon.net link. Useful for support tickets.
5. About dialog: chromeless modal with the dragon mark + version / .NET / OS / NDI runtime fields and a link to wilddragon.net. Triggered by clicking the rail logo.
6. Teams launcher Stop toggle: TeamsLauncher gains IsRunning() and StopAll(). The rail's Teams button now toggles — if Teams is up, ask to close all Teams windows via WM_CLOSE; otherwise launch as before. Confirms before stopping so we don't kill the user's call mid-transition.
Tests: 74/74 unit + 9/9 NDI integration green throughout. MSI builds clean and now embeds the dragon icon for ARP.
2026-05-08 13:50:19 -04:00
|
|
|
using TeamsISO.Engine.NdiInterop;
|
|
|
|
|
|
|
|
|
|
namespace TeamsISO.App;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Modal "About" dialog. Surfaces enough info that a user filing a support ticket
|
|
|
|
|
/// can paste version + NDI runtime + OS in a single screenshot.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public partial class AboutWindow : Window
|
|
|
|
|
{
|
|
|
|
|
public AboutWindow()
|
|
|
|
|
{
|
|
|
|
|
InitializeComponent();
|
|
|
|
|
PopulateText();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void PopulateText()
|
|
|
|
|
{
|
|
|
|
|
var asm = typeof(App).Assembly;
|
|
|
|
|
var info = asm.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
|
|
|
|
|
?? asm.GetName().Version?.ToString()
|
|
|
|
|
?? "unknown";
|
|
|
|
|
VersionText.Text = info;
|
|
|
|
|
RuntimeText.Text = $".NET {Environment.Version}";
|
|
|
|
|
OsText.Text = Environment.OSVersion.ToString();
|
|
|
|
|
NdiText.Text = TryGetNdiVersion();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[SupportedOSPlatform("windows")]
|
|
|
|
|
private static string TryGetNdiVersion()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using var interop = new NdiInteropPInvoke(
|
|
|
|
|
Microsoft.Extensions.Logging.Abstractions.NullLogger<NdiInteropPInvoke>.Instance);
|
|
|
|
|
return interop.GetRuntimeVersion();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
return $"not initialized ({ex.Message})";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
|
|
|
|
|
2026-05-10 09:41:29 -04:00
|
|
|
/// <summary>
|
|
|
|
|
/// Re-open the first-launch welcome dialog from About so users can revisit
|
|
|
|
|
/// the setup checklist without having to delete the suppression flag file
|
|
|
|
|
/// by hand. The "Don't show again" checkbox in the welcome dialog defaults
|
|
|
|
|
/// to checked so a re-shown welcome won't unset the suppression on close.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void OnShowOnboarding(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
var onboarding = new OnboardingWindow { Owner = this };
|
|
|
|
|
onboarding.ShowDialog();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Quick-jump: open a path in Explorer. Creates the directory if missing
|
|
|
|
|
/// (operator might click "Recordings" before any have been made). Best-
|
|
|
|
|
/// effort — Explorer launch failures don't surface a dialog.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static void OpenInExplorer(string path)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
|
|
|
|
|
Process.Start(new ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = path,
|
|
|
|
|
UseShellExecute = true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// No-op: shell launch failed (path inaccessible / Explorer crashed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnOpenLogs(object sender, RoutedEventArgs e) =>
|
|
|
|
|
OpenInExplorer(Path.Combine(
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
|
|
|
"TeamsISO", "Logs"));
|
|
|
|
|
|
2026-05-14 06:02:40 -04:00
|
|
|
// OnOpenRecordings removed — recording feature axed.
|
2026-05-10 09:41:29 -04:00
|
|
|
|
|
|
|
|
private void OnOpenNotes(object sender, RoutedEventArgs e) =>
|
|
|
|
|
OpenInExplorer(Path.Combine(
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
|
|
|
"TeamsISO", "Notes"));
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Build the diagnostic bundle and tell the operator where it landed. The
|
|
|
|
|
/// bundle is just zipped logs / config / presets — no screenshots, no
|
|
|
|
|
/// memory dumps. Intended to be attached to a bug report.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void OnExportDiagnostics(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var path = DiagnosticsBundle.Export();
|
|
|
|
|
var open = MessageBox.Show(
|
|
|
|
|
this,
|
|
|
|
|
$"Diagnostic bundle written to:\n\n{path}\n\nOpen the containing folder?",
|
|
|
|
|
"TeamsISO — Diagnostics exported",
|
|
|
|
|
MessageBoxButton.YesNo,
|
|
|
|
|
MessageBoxImage.Information);
|
|
|
|
|
if (open == MessageBoxResult.Yes)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
Process.Start(new ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = "explorer.exe",
|
|
|
|
|
Arguments = $"/select,\"{path}\"",
|
|
|
|
|
UseShellExecute = true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
catch { /* shell launch failure is best-effort */ }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
MessageBox.Show(
|
|
|
|
|
this,
|
|
|
|
|
$"Diagnostic export failed.\n\n{ex.Message}",
|
|
|
|
|
"TeamsISO — Diagnostic export",
|
|
|
|
|
MessageBoxButton.OK,
|
|
|
|
|
MessageBoxImage.Warning);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Click handler for "Check for updates". Disables the button while the
|
|
|
|
|
/// HTTP call is in flight (so a second click doesn't spawn parallel
|
|
|
|
|
/// requests), then surfaces the result via MessageBox. On
|
|
|
|
|
/// <see cref="UpdateChecker.UpdateStatus.UpdateAvailable"/> we offer
|
|
|
|
|
/// to open the releases page so the operator can grab the new MSI.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async void OnCheckUpdate(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
UpdateButton.IsEnabled = false;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var result = await UpdateChecker.CheckAsync();
|
|
|
|
|
switch (result.Status)
|
|
|
|
|
{
|
|
|
|
|
case UpdateChecker.UpdateStatus.UpdateAvailable:
|
|
|
|
|
var open = MessageBox.Show(
|
|
|
|
|
this,
|
|
|
|
|
$"{result.Message}\n\n" +
|
|
|
|
|
$"You're on {result.CurrentVersion}; latest is {result.LatestTag}.\n\n" +
|
|
|
|
|
"Open the releases page to download the new MSI?",
|
|
|
|
|
"TeamsISO — Update available",
|
|
|
|
|
MessageBoxButton.YesNo,
|
|
|
|
|
MessageBoxImage.Information);
|
|
|
|
|
if (open == MessageBoxResult.Yes)
|
|
|
|
|
UpdateChecker.OpenReleasesPage();
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case UpdateChecker.UpdateStatus.UpToDate:
|
|
|
|
|
MessageBox.Show(
|
|
|
|
|
this,
|
|
|
|
|
result.Message ?? "You're on the latest release.",
|
|
|
|
|
"TeamsISO — Up to date",
|
|
|
|
|
MessageBoxButton.OK,
|
|
|
|
|
MessageBoxImage.Information);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case UpdateChecker.UpdateStatus.Error:
|
|
|
|
|
default:
|
|
|
|
|
MessageBox.Show(
|
|
|
|
|
this,
|
|
|
|
|
$"Couldn't check for updates.\n\n{result.Message}",
|
|
|
|
|
"TeamsISO — Update check failed",
|
|
|
|
|
MessageBoxButton.OK,
|
|
|
|
|
MessageBoxImage.Warning);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
UpdateButton.IsEnabled = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: app icon, FPS, drops counter, --version, About dialog, Stop Teams toggle
Six related polish items, all building on tonight's groundwork.
1. App icon: teamsiso.ico generated from dragon-mark.png at 7 sizes (16-256), wired as ApplicationIcon in the WPF csproj, MainWindow.Icon, AboutWindow.Icon, and ARPPRODUCTICON in the WiX MSI. Taskbar / window / Add-Remove-Programs all show the dragon mark now.
2. Running incoming FPS: ring buffer of last 30 frame timestamps in IsoPipeline; ComputeFps() returns moving-average rate. Surfaced on IsoHealthStats.IncomingFps and shown in the Source column of the participants DataGrid as 'WxH · 59.94 fps'. Resets cleanly on every supervisor restart.
3. Drops counter: FrameProcessor.Stats already aggregated FramesDropped (closest-frame strategy when the receiver outpaces the processor) and FramesDuplicated; just plumbed _liveProcessor through IsoPipeline so GetStats() can read them. Exposed in the Live column under the in/out counters as a coral-tinted 'drop N'.
4. Console --version flag: prints engine version (with embedded git SHA), .NET version, OS, NDI runtime banner, expected prefix, exit-code legend, plus a wilddragon.net link. Useful for support tickets.
5. About dialog: chromeless modal with the dragon mark + version / .NET / OS / NDI runtime fields and a link to wilddragon.net. Triggered by clicking the rail logo.
6. Teams launcher Stop toggle: TeamsLauncher gains IsRunning() and StopAll(). The rail's Teams button now toggles — if Teams is up, ask to close all Teams windows via WM_CLOSE; otherwise launch as before. Confirms before stopping so we don't kill the user's call mid-transition.
Tests: 74/74 unit + 9/9 NDI integration green throughout. MSI builds clean and now embeds the dragon icon for ARP.
2026-05-08 13:50:19 -04:00
|
|
|
/// <summary>
|
|
|
|
|
/// Open the company site in the default browser. We intentionally use the
|
|
|
|
|
/// shell's URL handler rather than a tab inside the app — this is a
|
|
|
|
|
/// "tell me more" link, not a workflow.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void OnWebsiteClick(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
Process.Start(new ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = "https://wilddragon.net",
|
|
|
|
|
UseShellExecute = true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// best-effort; if shell launch fails the click is a no-op
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|