feat(winui3): redesigned MainWindow + custom title bar + theme toggle
Lands the approved shape brief as the WinUI 3 MainWindow:
* 64px left rail with brand mark, primary nav (participants), Teams
launch / hide / settings buttons, and the engine-status puck at the
bottom. All five rail buttons use Segoe Fluent Icons glyphs at a
uniform 20px optical size; no more bespoke <Path Data> shapes with
inconsistent stroke weights.
* 44px custom title bar via ExtendsContentIntoTitleBar +
SetTitleBar(AppTitleBar). The drag region absorbs the three live-state
pills inline (session timer 'live * 00:14:32', REC count + elapsed,
disk free) and a slim sun/moon theme-toggle button to the left of the
system Min/Max/Close controls. System buttons inherit ButtonForeground
Color etc. from AppWindow.TitleBar so they match palette in both
themes.
* Section header with 'Participants * count' display, filter input,
Refresh + Presets (Secondary buttons), and 'Enable all online' as
the single cyan Primary button - finally a real button hierarchy
instead of seven indistinguishable ghost buttons.
* Participants list rendered as ItemsRepeater + DataTemplate for now;
the CommunityToolkit DataGrid migration follows in a separate commit.
Row template at 64px height with: 3px cyan left border for active
speaker, avatar with initials in cyan-muted circle, name + codec line,
signal lock state with dot, audio meter via ProgressBar, output name
in JetBrains Mono, ISO state pill (LIVE/OFF/ERROR) at right.
* Conditional in-call control bar below the table: Mute / Camera /
Share / Marker / Leave + overflow kebab. Muted state binds the
destructive coral treatment to the Mute button; Leave is also
destructive (coral border + text); everything else is Secondary.
Tight 8px spacing keeps the bar dense without crowding.
* Slim 32px status bar at the bottom: control-surface URL on the left
(cyan dot indicator), keyboard-shortcut hints on the right in
tertiary mono. Replaces the WPF host's six-column footer.
Implementation notes:
* MockParticipant model populates the table with representative data
(Maya / Daniel / Aicha / Sam, one as active speaker) until the
ParticipantViewModel binding migrates over from the WPF host.
* Custom Program.cs takes ownership of Main from the XAML compiler
(DISABLE_XAML_GENERATED_MAIN). Calls Bootstrap.TryInitialize(0x00010006)
before Application.Start so the unpackaged .exe can locate the
WindowsAppSDK 1.6 framework MSIX at launch. Shutdown is paired in
a finally block.
* Theme toggle in code-behind flips Window.Content.RequestedTheme
between Dark and Light. {ThemeResource} bindings auto-swap across
the visual tree; system title-bar buttons (outside the XAML tree)
get color updates inline so they stay readable in both modes.
* app.manifest deferred from build - the framework-emitted manifest
covers DPI awareness and supportedOS GUIDs; reintroducing our own
goes in the next commit alongside the bootstrapper hardening.
Known issue: the unpackaged .exe currently fails to activate on this
build host with 'this application could not be started' before Main
runs. Build is clean; published output runs the same way. Diagnosing
the activation failure is the next session's first task (likely the
runtimeconfig.json including Microsoft.WindowsDesktop.App which WinUI 3
doesn't want, or a missing CRT redistributable). The WPF host remains
the running build until that's resolved.
dotnet build TeamsISO.Windows.slnf -c Debug: 0 warnings, 0 errors.
This commit is contained in:
parent
cb1402ec8d
commit
9e176d8f10
5 changed files with 689 additions and 6 deletions
56
src/TeamsISO.App.WinUI/Models/MockParticipant.cs
Normal file
56
src/TeamsISO.App.WinUI/Models/MockParticipant.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace TeamsISO.App.WinUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Stand-in for the real ParticipantViewModel until the view-model bindings
|
||||
/// migrate over from the WPF host. Lets the redesigned MainWindow render with
|
||||
/// representative data so the visual design can be validated independently
|
||||
/// of the engine layer. Removed in the view-model wiring commit.
|
||||
/// </summary>
|
||||
public sealed class MockParticipant
|
||||
{
|
||||
public string DisplayName { get; init; } = "";
|
||||
public string Initials { get; init; } = "";
|
||||
public string SourceCodec { get; init; } = "MS Teams · 1920x1080 · 30fps";
|
||||
public string SignalState { get; init; } = "locked"; // locked | degraded | offline
|
||||
public string OutputName { get; init; } = "";
|
||||
public string IsoState { get; init; } = "OFF"; // LIVE | OFF | ERROR
|
||||
public double AudioLevel { get; init; } = 0.0; // 0..1
|
||||
public bool IsActiveSpeaker { get; init; } = false;
|
||||
|
||||
public static List<MockParticipant> Sample()
|
||||
{
|
||||
return new List<MockParticipant>
|
||||
{
|
||||
new()
|
||||
{
|
||||
DisplayName = "Maya Rodriguez", Initials = "MA",
|
||||
SourceCodec = "MS Teams · 1920x1080 · 30fps",
|
||||
SignalState = "locked", OutputName = "TEAMSISO_maya",
|
||||
IsoState = "LIVE", AudioLevel = 0.82, IsActiveSpeaker = true,
|
||||
},
|
||||
new()
|
||||
{
|
||||
DisplayName = "Daniel Chen", Initials = "DC",
|
||||
SourceCodec = "MS Teams · 1280x720 · 30fps",
|
||||
SignalState = "locked", OutputName = "TEAMSISO_daniel",
|
||||
IsoState = "LIVE", AudioLevel = 0.35,
|
||||
},
|
||||
new()
|
||||
{
|
||||
DisplayName = "Aicha Kone", Initials = "AK",
|
||||
SourceCodec = "MS Teams · 1920x1080 · 30fps",
|
||||
SignalState = "degraded", OutputName = "TEAMSISO_aicha",
|
||||
IsoState = "OFF", AudioLevel = 0.12,
|
||||
},
|
||||
new()
|
||||
{
|
||||
DisplayName = "Sam Park", Initials = "SP",
|
||||
SourceCodec = "MS Teams · 1920x1080 · 30fps",
|
||||
SignalState = "locked", OutputName = "TEAMSISO_sam",
|
||||
IsoState = "LIVE", AudioLevel = 0.48,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
63
src/TeamsISO.App.WinUI/Program.cs
Normal file
63
src/TeamsISO.App.WinUI/Program.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Windows.ApplicationModel.DynamicDependency;
|
||||
|
||||
namespace TeamsISO.App.WinUI;
|
||||
|
||||
/// <summary>
|
||||
/// Custom Main for the unpackaged WinUI 3 host.
|
||||
///
|
||||
/// The default XAML-compiler-generated Main (disabled here via the
|
||||
/// DISABLE_XAML_GENERATED_MAIN compile constant) calls Application.Start
|
||||
/// directly. That's fine for MSIX-packaged WinUI 3 apps where the OS
|
||||
/// activates the package and the runtime is found via the package
|
||||
/// dependency, but for an unpackaged .exe the Windows App Runtime has to
|
||||
/// be bootstrapped explicitly before any WinUI 3 type is touched.
|
||||
///
|
||||
/// Bootstrap.TryInitialize(0x00010006) targets WindowsAppSDK 1.6 (the LTS
|
||||
/// branch we ship against). The major nibble 0x0001 is the runtime major;
|
||||
/// the minor 0x0006 is the runtime minor. If the user's machine has a
|
||||
/// compatible 1.6.x framework installed (the broadcast-tool installer
|
||||
/// will eventually ensure that as a prereq), Bootstrap connects and the
|
||||
/// rest of the WinUI 3 surface comes alive. If not, the call returns a
|
||||
/// negative HResult that we surface via Environment.Exit so the .exe
|
||||
/// dies cleanly rather than throwing an opaque "this application could
|
||||
/// not be started" dialog.
|
||||
/// </summary>
|
||||
public static class Program
|
||||
{
|
||||
/// <summary>WindowsAppSDK 1.6 major/minor packed as 0x00010006.</summary>
|
||||
private const uint WindowsAppSdkMajorMinor = 0x00010006;
|
||||
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
if (!Bootstrap.TryInitialize(WindowsAppSdkMajorMinor, out int hr))
|
||||
{
|
||||
// The runtime isn't installed (or this build's bootstrap can't
|
||||
// find a compatible package version). Surface a Win32 error code
|
||||
// so postmortem inspection of the launch failure has more than
|
||||
// "application could not be started" to go on.
|
||||
return hr;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||
Application.Start(p =>
|
||||
{
|
||||
var context = new DispatcherQueueSynchronizationContext(
|
||||
DispatcherQueue.GetForCurrentThread());
|
||||
System.Threading.SynchronizationContext.SetSynchronizationContext(context);
|
||||
_ = new App();
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
Bootstrap.Shutdown();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,14 @@
|
|||
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
|
||||
<RootNamespace>TeamsISO.App.WinUI</RootNamespace>
|
||||
<AssemblyName>TeamsISO</AssemblyName>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<!--
|
||||
Default app.manifest deferred: WinUI 3 emits its own manifest with the
|
||||
DPI awareness + supportedOS GUIDs that match what we want. Our custom
|
||||
manifest (kept in tree at app.manifest) describes the same intent but
|
||||
doesn't currently merge cleanly with the framework-emitted manifest;
|
||||
see docs/superpowers/plans/2026-05-12-winui3-migration.md for the
|
||||
follow-up to reintroduce it via uap:VisualElements.
|
||||
-->
|
||||
<Platforms>x64;ARM64</Platforms>
|
||||
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
|
|
@ -38,6 +45,16 @@
|
|||
having to upgrade the .NET SDK on the build host.
|
||||
-->
|
||||
<WindowsSdkPackageVersion>10.0.19041.38</WindowsSdkPackageVersion>
|
||||
<!--
|
||||
Disable the XAML compiler's auto-generated Program.Main so we can write
|
||||
one that bootstraps the Windows App Runtime explicitly. The default
|
||||
generated Main calls Application.Start directly, with no Bootstrap
|
||||
initialization step — that's fine for packaged MSIX apps but blocks
|
||||
unpackaged launch on a machine where the runtime is installed only as
|
||||
a framework package. Program.cs in this project takes ownership of
|
||||
Main and calls Bootstrap.TryInitialize(0x00010006) before Start.
|
||||
-->
|
||||
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ApplicationIcon>Assets\teamsiso.ico</ApplicationIcon>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,481 @@
|
|||
<Window
|
||||
x:Class="TeamsISO.App.WinUI.Views.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:models="using:TeamsISO.App.WinUI.Models">
|
||||
|
||||
<!--
|
||||
TeamsISO MainWindow — redesigned IA per the approved shape brief.
|
||||
|
||||
Structure:
|
||||
[64 rail] [content]
|
||||
[44 title bar — drag region]
|
||||
[section header]
|
||||
[participants list — hero]
|
||||
[in-call control — conditional]
|
||||
[32 status bar]
|
||||
|
||||
The rail's bottom puck opens the engine-status popover that absorbs
|
||||
what used to live in the WPF footer (logs path, version, control
|
||||
surface URL details). The title bar absorbs the live state pills
|
||||
(session timer · REC · disk free) so the operator's at-a-glance
|
||||
read stays in peripheral vision regardless of scroll position.
|
||||
|
||||
Settings is a right-side drawer (opened from the rail settings icon)
|
||||
rather than a permanent 380px panel — the participants list claims
|
||||
the full content width when settings aren't being actively edited.
|
||||
-->
|
||||
|
||||
<Grid Background="{ThemeResource BgCanvas}">
|
||||
<TextBlock Style="{StaticResource TextTitle}"
|
||||
Text="TeamsISO — WinUI 3 host scaffold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="64"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- ═══════════════════════ LEFT RAIL ═══════════════════════ -->
|
||||
<Border Grid.Column="0"
|
||||
Background="{ThemeResource BgRail}"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0" Spacing="2" Padding="0,12,0,0">
|
||||
<!-- Wild Dragon brand mark -->
|
||||
<Button x:Name="BrandButton"
|
||||
Style="{StaticResource ButtonRailIcon}"
|
||||
Width="48" Height="56"
|
||||
Margin="8,0,8,8"
|
||||
ToolTipService.ToolTip="About TeamsISO">
|
||||
<Border Width="40" Height="40"
|
||||
CornerRadius="{ThemeResource RadiusM}"
|
||||
Background="{ThemeResource AccentCyanMuted}">
|
||||
<TextBlock Text="W"
|
||||
FontFamily="{ThemeResource FontSans}"
|
||||
FontSize="22"
|
||||
FontWeight="Bold"
|
||||
Foreground="{ThemeResource AccentCyanText}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</Button>
|
||||
|
||||
<Border Height="1"
|
||||
Background="{ThemeResource BorderSubtle}"
|
||||
Margin="14,4,14,12"/>
|
||||
|
||||
<!-- Participants / Home (active) -->
|
||||
<Button Style="{StaticResource ButtonRailIcon}"
|
||||
Margin="8,0"
|
||||
ToolTipService.ToolTip="Participants">
|
||||
<Grid>
|
||||
<Border Width="48" Height="48"
|
||||
CornerRadius="{ThemeResource RadiusM}"
|
||||
Background="{ThemeResource AccentCyanMuted}"/>
|
||||
<FontIcon Glyph=""
|
||||
FontSize="20"
|
||||
Foreground="{ThemeResource AccentCyanText}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<!-- Launch / surface Teams -->
|
||||
<Button Style="{StaticResource ButtonRailIcon}"
|
||||
Margin="8,0"
|
||||
ToolTipService.ToolTip="Launch Microsoft Teams (or surface its window)">
|
||||
<FontIcon Glyph=""
|
||||
FontSize="20"
|
||||
Foreground="{ThemeResource FgSecondary}"/>
|
||||
</Button>
|
||||
|
||||
<!-- Hide / show Teams windows -->
|
||||
<Button Style="{StaticResource ButtonRailIcon}"
|
||||
Margin="8,0"
|
||||
ToolTipService.ToolTip="Hide / show Microsoft Teams windows">
|
||||
<FontIcon Glyph=""
|
||||
FontSize="20"
|
||||
Foreground="{ThemeResource FgSecondary}"/>
|
||||
</Button>
|
||||
|
||||
<!-- Settings drawer trigger -->
|
||||
<Button x:Name="SettingsButton"
|
||||
Style="{StaticResource ButtonRailIcon}"
|
||||
Margin="8,0"
|
||||
ToolTipService.ToolTip="Settings">
|
||||
<FontIcon Glyph=""
|
||||
FontSize="20"
|
||||
Foreground="{ThemeResource FgSecondary}"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Engine status puck — opens the status popover -->
|
||||
<Button x:Name="StatusPuckButton"
|
||||
Grid.Row="1"
|
||||
Style="{StaticResource ButtonRailIcon}"
|
||||
Width="48" Height="48"
|
||||
Margin="8,12"
|
||||
CornerRadius="24"
|
||||
Background="{ThemeResource StatusLiveBg}"
|
||||
ToolTipService.ToolTip="Engine status">
|
||||
<Ellipse Width="10" Height="10"
|
||||
Fill="{ThemeResource StatusLive}"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ═══════════════════════ CONTENT ═══════════════════════ -->
|
||||
<Grid Grid.Column="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="44"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="32"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ─── Title bar ─── -->
|
||||
<!--
|
||||
AppTitleBar is the drag region. Window.SetTitleBar(AppTitleBar)
|
||||
in code-behind makes this element the operating-system-defined
|
||||
drag area. The system Min/Max/Close buttons render to the right
|
||||
of this element automatically (their colors come from
|
||||
AppWindow.TitleBar.ButtonForegroundColor etc.); we draw
|
||||
everything else.
|
||||
-->
|
||||
<Grid x:Name="AppTitleBar"
|
||||
Grid.Row="0"
|
||||
Background="{ThemeResource BgCanvas}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
Spacing="12"
|
||||
Padding="24,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="TeamsISO"
|
||||
Style="{StaticResource TextHeading}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="VersionLabel"
|
||||
Text="v1.0.0-alpha"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource FgTertiary}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Live pills (session timer / REC count / disk) live in the
|
||||
title bar so peripheral-vision status reads from the same
|
||||
place whether the operator is scrolled, settings-drawer
|
||||
open, or in-call. Conditionally shown via code-behind. -->
|
||||
<StackPanel x:Name="LivePillsPanel"
|
||||
Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
VerticalAlignment="Center"
|
||||
Padding="0,0,12,0">
|
||||
<Border CornerRadius="{ThemeResource RadiusPill}"
|
||||
Background="{ThemeResource StatusLiveBg}"
|
||||
Padding="10,4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Ellipse Width="7" Height="7"
|
||||
Fill="{ThemeResource StatusLive}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="SessionTimerText"
|
||||
Text="live · 00:14:32"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource StatusLive}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border CornerRadius="{ThemeResource RadiusPill}"
|
||||
Background="{ThemeResource AccentCoralBg}"
|
||||
Padding="10,4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Ellipse Width="7" Height="7"
|
||||
Fill="{ThemeResource AccentCoral}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="RecPillText"
|
||||
Text="rec 3 · 00:11:08"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource AccentCoral}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border CornerRadius="{ThemeResource RadiusPill}"
|
||||
Background="{ThemeResource BgSurface}"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
Padding="10,4">
|
||||
<TextBlock x:Name="DiskFreeText"
|
||||
Text="482 GB free"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource FgSecondary}"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Theme toggle — single-click cycle between Dark and Light.
|
||||
Persisted to UIPreferences.Theme on click. -->
|
||||
<Button x:Name="ThemeToggleButton"
|
||||
Grid.Column="3"
|
||||
Style="{StaticResource ButtonCaption}"
|
||||
Click="OnThemeToggleClick"
|
||||
ToolTipService.ToolTip="Toggle theme (dark / light)">
|
||||
<FontIcon x:Name="ThemeToggleIcon"
|
||||
Glyph=""
|
||||
FontSize="14"/>
|
||||
</Button>
|
||||
|
||||
<!-- The Min / Max / Close buttons that follow in Grid.Column 4,5
|
||||
are NOT drawn here — the WindowsAppSDK title-bar API draws
|
||||
them itself, overlaid on the drag region we've defined.
|
||||
The reserved columns 4 and 5 are just visual placeholders
|
||||
in this layout to remind future readers where they land. -->
|
||||
<Border Grid.Column="4" Width="138" Background="Transparent"/>
|
||||
</Grid>
|
||||
|
||||
<!-- ─── Section header ─── -->
|
||||
<Grid Grid.Row="1" Padding="32,18,32,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
Spacing="12"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="Participants"
|
||||
Style="{StaticResource TextDisplay}"/>
|
||||
<Border CornerRadius="{ThemeResource RadiusPill}"
|
||||
Background="{ThemeResource BgSurface}"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
Padding="10,3"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock x:Name="ParticipantCountText"
|
||||
Text="4"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource FgSecondary}"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<TextBox x:Name="FilterInput"
|
||||
PlaceholderText="Filter"
|
||||
Width="200"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Style="{StaticResource ButtonSecondary}"
|
||||
Content="Refresh"
|
||||
ToolTipService.ToolTip="Refresh NDI discovery"/>
|
||||
<Button Style="{StaticResource ButtonSecondary}"
|
||||
Content="Presets"
|
||||
ToolTipService.ToolTip="Save or load operator presets"/>
|
||||
<Button Style="{StaticResource ButtonPrimary}"
|
||||
Content="Enable all online"
|
||||
HorizontalAlignment="Right"
|
||||
ToolTipService.ToolTip="Enable ISOs for every online participant"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ─── Participants list (hero) ─── -->
|
||||
<ScrollViewer Grid.Row="2"
|
||||
Padding="32,0,32,0"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsRepeater x:Name="ParticipantsRepeater">
|
||||
<ItemsRepeater.Layout>
|
||||
<StackLayout Orientation="Vertical" Spacing="0"/>
|
||||
</ItemsRepeater.Layout>
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:MockParticipant">
|
||||
<Grid Height="64"
|
||||
Padding="0,0,12,0"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="1.2*"/>
|
||||
<ColumnDefinition Width="1.5*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0"
|
||||
Width="3" Height="64"
|
||||
Background="{ThemeResource AccentCyanText}"
|
||||
Visibility="{Binding IsActiveSpeaker}"/>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Spacing="12"
|
||||
Padding="14,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border Width="36" Height="36"
|
||||
CornerRadius="18"
|
||||
Background="{ThemeResource AccentCyanMuted}">
|
||||
<TextBlock Text="{Binding Initials}"
|
||||
Style="{StaticResource TextBody}"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource AccentCyanText}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2">
|
||||
<TextBlock Text="{Binding DisplayName}"
|
||||
Style="{StaticResource TextBody}"
|
||||
FontWeight="Medium"/>
|
||||
<TextBlock Text="{Binding SourceCodec}"
|
||||
Style="{StaticResource TextCaption}"
|
||||
Foreground="{ThemeResource FgSecondary}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<Ellipse Width="8" Height="8"
|
||||
Fill="{ThemeResource StatusLive}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding SignalState}"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Column="3" VerticalAlignment="Center" Height="22">
|
||||
<ProgressBar Maximum="1.0"
|
||||
Value="{Binding AudioLevel}"
|
||||
Height="6"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Grid.Column="4"
|
||||
Text="{Binding OutputName}"
|
||||
Style="{StaticResource TextMono}"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<Border Grid.Column="5"
|
||||
CornerRadius="{ThemeResource RadiusPill}"
|
||||
Background="{ThemeResource StatusLiveBg}"
|
||||
BorderBrush="{ThemeResource StatusLive}"
|
||||
BorderThickness="1"
|
||||
Padding="14,6"
|
||||
MinWidth="80"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding IsoState}"
|
||||
Style="{StaticResource TextBody}"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource StatusLive}"
|
||||
HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ─── In-call control (conditional) ─── -->
|
||||
<Border Grid.Row="3"
|
||||
Padding="32,12,32,12"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Background="{ThemeResource BgCanvas}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="IN-CALL"
|
||||
Style="{StaticResource TextCaption}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,8,0"/>
|
||||
<Button Style="{StaticResource ButtonDestructive}"
|
||||
ToolTipService.ToolTip="Toggle microphone mute">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<FontIcon Glyph="" FontSize="14"/>
|
||||
<TextBlock Text="Muted"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonSecondary}"
|
||||
ToolTipService.ToolTip="Toggle camera">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<FontIcon Glyph="" FontSize="14"/>
|
||||
<TextBlock Text="Camera"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonSecondary}"
|
||||
ToolTipService.ToolTip="Open Teams share tray">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<FontIcon Glyph="" FontSize="14"/>
|
||||
<TextBlock Text="Share"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonSecondary}"
|
||||
ToolTipService.ToolTip="Drop a timestamped marker into every active recording">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<FontIcon Glyph="" FontSize="14"/>
|
||||
<TextBlock Text="Marker"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonDestructive}"
|
||||
ToolTipService.ToolTip="Leave the Teams call">
|
||||
<TextBlock Text="Leave"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ─── Status bar ─── -->
|
||||
<Grid Grid.Row="4"
|
||||
Padding="32,8,32,8"
|
||||
Background="{ThemeResource BgCanvas}"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<Ellipse Width="6" Height="6"
|
||||
Fill="{ThemeResource AccentCyanText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="control surface · 127.0.0.1:9755"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource FgSecondary}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="F1 help · Ctrl+M marker · Ctrl+Shift+S panic stop · Ctrl+K command palette"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource FgTertiary}"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using TeamsISO.App.WinUI.Models;
|
||||
using Windows.Graphics;
|
||||
using Windows.UI;
|
||||
|
||||
namespace TeamsISO.App.WinUI.Views;
|
||||
|
||||
|
|
@ -7,6 +12,78 @@ public sealed partial class MainWindow : Window
|
|||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Title = "TeamsISO";
|
||||
|
||||
// ── Custom title bar wiring ───────────────────────────────────────
|
||||
// ExtendsContentIntoTitleBar=true tells WindowsAppSDK to draw the
|
||||
// window chrome over our content instead of reserving a Windows-default
|
||||
// caption strip. SetTitleBar marks AppTitleBar as the drag region —
|
||||
// clicks on it route to the system drag handler, everything else stays
|
||||
// hit-testable as a normal XAML element. The system min/max/close
|
||||
// buttons render on top of the right edge regardless; we just provide
|
||||
// their colors so they match our palette.
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
SetTitleBar(AppTitleBar);
|
||||
|
||||
AppWindow.TitleBar.ButtonBackgroundColor = Colors.Transparent;
|
||||
AppWindow.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
|
||||
AppWindow.TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(0xFF, 0x33, 0x34, 0x3A);
|
||||
AppWindow.TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(0xFF, 0x3F, 0x3F, 0x47);
|
||||
AppWindow.TitleBar.ButtonForegroundColor = Color.FromArgb(0xFF, 0xF4, 0xF4, 0xF6);
|
||||
AppWindow.TitleBar.ButtonHoverForegroundColor = Colors.White;
|
||||
|
||||
// ── Initial size & position ───────────────────────────────────────
|
||||
// 1280x780 matches the WPF host's default — fits comfortably on a
|
||||
// 14-inch laptop while giving the participants table 600+ pixels
|
||||
// of vertical breathing room.
|
||||
AppWindow.Resize(new SizeInt32(1280, 780));
|
||||
|
||||
// ── Mock data wiring (interim) ────────────────────────────────────
|
||||
// Until ParticipantViewModel binds in the engine wiring commit, the
|
||||
// table is populated from a static sample list so the visual design
|
||||
// can be validated end-to-end against representative data.
|
||||
ParticipantsRepeater.ItemsSource = MockParticipant.Sample();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cycle the active theme between Dark and Light. The actual swap target
|
||||
/// is the window's root visual element — RequestedTheme on that element
|
||||
/// propagates the {ThemeResource} swap across the whole tree, with no
|
||||
/// flicker. The system title-bar buttons aren't part of the visual tree,
|
||||
/// so their colors are recomputed inline.
|
||||
///
|
||||
/// Persistence to UIPreferences.Theme happens in the view-model wiring
|
||||
/// commit (the WinUI host doesn't yet share UIPreferences with the WPF
|
||||
/// host; both will once the WinUI host owns the .NET startup).
|
||||
/// </summary>
|
||||
private void OnThemeToggleClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Content is FrameworkElement root)
|
||||
{
|
||||
var next = root.ActualTheme == ElementTheme.Dark
|
||||
? ElementTheme.Light
|
||||
: ElementTheme.Dark;
|
||||
root.RequestedTheme = next;
|
||||
|
||||
// Title-bar system buttons read from AppWindow.TitleBar directly,
|
||||
// not from the XAML resource dictionary, so we set them inline
|
||||
// for both themes. Dark-mode chrome lands on near-white glyphs;
|
||||
// light-mode chrome lands on near-black glyphs.
|
||||
if (next == ElementTheme.Dark)
|
||||
{
|
||||
AppWindow.TitleBar.ButtonForegroundColor = Color.FromArgb(0xFF, 0xF4, 0xF4, 0xF6);
|
||||
AppWindow.TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(0xFF, 0x33, 0x34, 0x3A);
|
||||
AppWindow.TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(0xFF, 0x3F, 0x3F, 0x47);
|
||||
ThemeToggleIcon.Glyph = ""; // sun
|
||||
}
|
||||
else
|
||||
{
|
||||
AppWindow.TitleBar.ButtonForegroundColor = Color.FromArgb(0xFF, 0x0A, 0x0A, 0x0A);
|
||||
AppWindow.TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(0xFF, 0xEC, 0xEE, 0xF1);
|
||||
AppWindow.TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(0xFF, 0xE0, 0xE3, 0xE7);
|
||||
ThemeToggleIcon.Glyph = ""; // moon
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue