Onboarding step + Open /ui button + recording duration in footer
Some checks failed
CI / build-and-test (push) Failing after 30s
Some checks failed
CI / build-and-test (push) Failing after 30s
Three small UX wins:
1. Onboarding gained step 5 ('Run Teams headless') and step 6 ('Drive from another machine') so new operators discover the auto-launch/auto-hide + LAN-reachable workflows. Existing 'where things live' step renumbered to 7.
2. Settings → DISPLAY → Control surface URL row gains an Open button next to Copy that fires the URL into the default browser via Process.Start with UseShellExecute. Operators previewing how the embedded /ui control panel looks on a phone/tablet no longer need to copy-paste manually.
3. Recording badge in footer now shows 'REC 3 · 12:45' instead of just 'REC 3'. RecordingElapsed VM property maintains a separate timer from the session timer because recording can start AFTER the meeting begins; operators tracking 'how long has the archive copy been rolling' need that distinct duration.
This commit is contained in:
parent
3f71a4f29a
commit
acc569dd24
4 changed files with 132 additions and 3 deletions
|
|
@ -370,6 +370,8 @@
|
|||
VerticalAlignment="Center">
|
||||
<Run Text="REC "/>
|
||||
<Run Text="{Binding ActiveRecordingCount, Mode=OneWay}"/>
|
||||
<Run Text=" · "/>
|
||||
<Run Text="{Binding RecordingElapsed, Mode=OneWay}"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
|
|
@ -1367,14 +1369,24 @@
|
|||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding Settings.ControlSurfaceUrl}"
|
||||
Style="{StaticResource Wd.Text.Mono}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource Wd.Accent.Cyan}"
|
||||
VerticalAlignment="Center"/>
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<Button Grid.Column="1"
|
||||
Style="{StaticResource Wd.Button.Ghost}"
|
||||
Content="Open"
|
||||
Command="{Binding Settings.OpenControlSurfaceCommand}"
|
||||
Padding="10,2"
|
||||
Margin="0,0,4,0"
|
||||
FontSize="11"
|
||||
ToolTip="Open this URL in your default browser. Useful for previewing how the control panel looks before pointing a phone or second-monitor at it."/>
|
||||
<Button Grid.Column="2"
|
||||
Style="{StaticResource Wd.Button.Ghost}"
|
||||
Content="Copy"
|
||||
Command="{Binding Settings.CopyControlSurfaceUrlCommand}"
|
||||
|
|
|
|||
|
|
@ -184,8 +184,8 @@
|
|||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Step 5 — Where things live -->
|
||||
<Border Style="{StaticResource Wd.Card}" Padding="16">
|
||||
<!-- Step 5 — Headless Teams ("I only see TeamsISO") -->
|
||||
<Border Style="{StaticResource Wd.Card}" Padding="16" Margin="0,0,0,12">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<Border Width="22" Height="22"
|
||||
|
|
@ -200,6 +200,62 @@
|
|||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Text="Run Teams headless (optional)"
|
||||
Style="{StaticResource Wd.Text.Heading}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock TextWrapping="Wrap"
|
||||
Style="{StaticResource Wd.Text.Body}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource Wd.Text.Secondary}"
|
||||
Text="To use TeamsISO as your only window: tick both 'Launch Microsoft Teams on TeamsISO startup' and 'Auto-hide Teams windows when launched' under Settings → DISPLAY. Teams runs in the background; the IN-CALL bar shows the meeting state (READY / IN CALL · meeting title), the Mute/Camera/Share/Leave buttons drive Teams via UIAutomation, and the URL paste-box joins meetings directly. Use the eye-icon button in the left rail to manually restore Teams' windows when you need them."/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Step 6 — Where things live -->
|
||||
<Border Style="{StaticResource Wd.Card}" Padding="16" Margin="0,0,0,12">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<Border Width="22" Height="22"
|
||||
CornerRadius="11"
|
||||
Background="{DynamicResource Wd.Accent.CyanMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0">
|
||||
<TextBlock Text="6"
|
||||
Foreground="{DynamicResource Wd.Accent.Cyan}"
|
||||
FontFamily="{StaticResource Wd.Font.Mono}"
|
||||
FontSize="12"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Text="Drive from another machine (optional)"
|
||||
Style="{StaticResource Wd.Text.Heading}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock TextWrapping="Wrap"
|
||||
Style="{StaticResource Wd.Text.Body}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource Wd.Text.Secondary}"
|
||||
Text="For headless host PC + thin-client setups: tick 'Control surface' then 'LAN-reachable' under DISPLAY. TeamsISO listens on http://<your-lan-ip>:9755/ui — open that URL from any browser on the LAN. First-time use needs a one-shot 'netsh http add urlacl url=http://+:9755/ user=Everyone' in an Administrator PowerShell."/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Step 7 — Where things live -->
|
||||
<Border Style="{StaticResource Wd.Card}" Padding="16">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<Border Width="22" Height="22"
|
||||
CornerRadius="11"
|
||||
Background="{DynamicResource Wd.Accent.CyanMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0">
|
||||
<TextBlock Text="7"
|
||||
Foreground="{DynamicResource Wd.Accent.Cyan}"
|
||||
FontFamily="{StaticResource Wd.Font.Mono}"
|
||||
FontSize="12"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Text="If something breaks…"
|
||||
Style="{StaticResource Wd.Text.Heading}"
|
||||
VerticalAlignment="Center"/>
|
||||
|
|
|
|||
|
|
@ -95,8 +95,35 @@ public sealed class GlobalSettingsViewModel : ObservableObject
|
|||
// Clipboard occasionally errors when something else has it locked.
|
||||
}
|
||||
});
|
||||
OpenControlSurfaceCommand = new RelayCommand(() =>
|
||||
{
|
||||
// Hands the URL off to the OS shell so the user's default browser
|
||||
// opens it. Operators previewing how the control panel looks on
|
||||
// their phone / tablet / second monitor would otherwise have to
|
||||
// copy-paste the URL — this is a one-click preview.
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = ControlSurfaceUrl,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_toast?.Warn($"Couldn't open: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open the embedded HTML control panel (the <c>/ui</c> endpoint) in the
|
||||
/// default browser. Enabled regardless of whether the control surface is
|
||||
/// running — if it isn't, the browser will show a connection error, which
|
||||
/// is informative; operators learn the surface needs to be enabled first.
|
||||
/// </summary>
|
||||
public RelayCommand OpenControlSurfaceCommand { get; }
|
||||
|
||||
private void ResetOutputDefaults()
|
||||
{
|
||||
var confirm = MessageBox.Show(
|
||||
|
|
|
|||
|
|
@ -206,6 +206,21 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
|||
}
|
||||
private int _activeRecordingCount;
|
||||
|
||||
/// <summary>
|
||||
/// Elapsed time since recording started this session, formatted as
|
||||
/// "MM:SS" (or "HH:MM:SS" past an hour). Empty when nothing is
|
||||
/// recording. Resets when all recordings stop, restarts on the next
|
||||
/// rec-on transition. Useful for operators tracking "how long has the
|
||||
/// show been rolling".
|
||||
/// </summary>
|
||||
public string RecordingElapsed
|
||||
{
|
||||
get => _recordingElapsed;
|
||||
private set => SetField(ref _recordingElapsed, value);
|
||||
}
|
||||
private string _recordingElapsed = string.Empty;
|
||||
private DateTimeOffset? _recordingStartedAt;
|
||||
|
||||
/// <summary>True when the REST control surface (or OSC bridge, or both) is listening.</summary>
|
||||
public bool IsControlSurfaceRunning
|
||||
{
|
||||
|
|
@ -585,6 +600,25 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
|||
ActiveRecordingCount = _controller.RecordingEnabled ? enabledCount : 0;
|
||||
IsRecording = ActiveRecordingCount > 0;
|
||||
|
||||
// Recording session timer — independent of the global session timer
|
||||
// since recording can start AFTER the meeting begins (or vice versa)
|
||||
// and operators want to know exactly how long the archive copy has
|
||||
// been rolling. Resets to null when all recordings stop, so the
|
||||
// next rec-on transition starts the timer from 00:00.
|
||||
if (IsRecording)
|
||||
{
|
||||
_recordingStartedAt ??= DateTimeOffset.UtcNow;
|
||||
var recElapsed = DateTimeOffset.UtcNow - _recordingStartedAt.Value;
|
||||
RecordingElapsed = recElapsed.TotalHours >= 1
|
||||
? $"{(int)recElapsed.TotalHours:D2}:{recElapsed.Minutes:D2}:{recElapsed.Seconds:D2}"
|
||||
: $"{recElapsed.Minutes:D2}:{recElapsed.Seconds:D2}";
|
||||
}
|
||||
else
|
||||
{
|
||||
_recordingStartedAt = null;
|
||||
RecordingElapsed = string.Empty;
|
||||
}
|
||||
|
||||
// Session timer — start on first ISO going live, reset when none are
|
||||
// live anymore. Subsequent enables after a full-zero gap restart the
|
||||
// timer rather than resuming, which is the operator's mental model:
|
||||
|
|
|
|||
Loading…
Reference in a new issue