TASK 18: local (record-only) recording pipeline + top-bar pills/light/Start-End button UX

Encoder dual-output: StreamEnabled/RecordEnabled/RecordPath options; per-output
ffmpeg block (stream ->flv, record ->mp4); throw unless at least one output.

RecordingFile: auto-name ty-<yyyymmdd>-<start hhmm>-0000.mp4, rename-on-stop to
ty-...-<len hhmmss>.mp4 with numeric-suffix on collision. LayoutStore persists
record folder. MainViewModel: REC/ON-AIR pills, status light, single Start<->End
button (replaces Start/Stop variants), StopStream no longer signs out, explicit
session timer. About overlay widened. Sign In label + spacing before Start.

Tests: 244 passed / 246 (2 pre-existing: AudioPipeline gains-mute, RoundClip).
Build 0 warnings. Scope check clean.

Memory-map correction (derived-solution rule, 2026-08-29): image-shrink recipe was
never recorded when first done, so it was re-derived. Added Derived-solution/recipes
rule to AGENTS.md; MyMistakes.md now a permanent recipes registry (records the
verified WPF-imaging shrink recipe); schema.md rows MyMistakes as registry; ai.md
points to the rule. README hero image replaced: 3.2MB screenshot -> 1400x794 ->
188KB docs/ytLlive-preview.jpg (JPEG q82).
This commit is contained in:
2026-08-29 09:02:55 -07:00
parent 1e633f9bd9
commit a9eb360288
20 changed files with 708 additions and 151 deletions
+34 -14
View File
@@ -111,10 +111,23 @@
</TextBlock>
</Button>
<!-- REC sign (center): dark offline, red live, darker red when private -->
<!-- REC / ON-AIR pills (intent) + status signs (reality) -->
<StackPanel Grid.Column="1" Orientation="Horizontal"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Ellipse Width="12" Height="12" VerticalAlignment="Center"
<!-- REC pill: local recording needs no sign-in -->
<TextBlock Text="REC" Foreground="#e94560" FontWeight="Bold" FontSize="12"
VerticalAlignment="Center" Margin="0,0,6,0"/>
<ToggleButton Style="{StaticResource PillToggle}" VerticalAlignment="Center"
IsChecked="{Binding RecordPillOn}"/>
<!-- ON-AIR pill: greyed out until signed in -->
<TextBlock Text="ON-AIR" Foreground="#e94560" FontWeight="Bold" FontSize="12"
VerticalAlignment="Center" Margin="16,0,6,0"/>
<ToggleButton Style="{StaticResource PillToggle}" VerticalAlignment="Center"
IsChecked="{Binding OnAirPillOn}"
IsEnabled="{Binding CanToggleOnAir}"
ToolTip="{Binding AccountStatusLightToolTip}"/>
<!-- Status signs: REC dot + ON-AIR dot -->
<Ellipse Width="12" Height="12" VerticalAlignment="Center" Margin="16,0,0,0"
Fill="{Binding RecDotBrush}" Opacity="{Binding RecDotOpacity}"/>
<TextBlock Text="REC" Style="{StaticResource StatusSignText}"/>
<!-- ON-AIR sign: gray offline, red public, green private -->
@@ -134,7 +147,9 @@
<!-- Three-state top bar: logged-out / idle / live -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<!-- Status light -->
<Ellipse Width="8" Height="8" Margin="0,0,8,0" VerticalAlignment="Center">
<Ellipse Width="8" Height="8" Margin="0,0,8,0" VerticalAlignment="Center"
ToolTip="{Binding AccountStatusLightToolTip}"
x:Name="AccountLight">
<Ellipse.Style>
<Style TargetType="Ellipse">
<Setter Property="Fill" Value="#ef4444"/>
@@ -162,26 +177,30 @@
Visibility="Collapsed"/>
</Grid>
</Border>
<!-- Log In button (disconnected) -->
<Button Content="Log In" Style="{StaticResource YtButton}"
<!-- Sign In button (disconnected + offline) -->
<Button Content="Sign In" Style="{StaticResource YtButton}"
Margin="0,0,12,0"
Command="{Binding StartStreamCommand}"
Visibility="{Binding IsConnected, Converter={StaticResource InverseBoolToVis}}"/>
<!-- Start Stream button (connected + offline) -->
<Button Content="Start Stream" Style="{StaticResource YtButton}"
Visibility="{Binding ShowLogInButton, Converter={StaticResource BoolToVis}}"/>
<!-- Primary Start button (offline): dynamic label from the pills -->
<Button Content="{Binding PrimaryButtonText}" Style="{StaticResource YtButton}"
Command="{Binding StartStreamCommand}"
Visibility="{Binding ShowStartStream, Converter={StaticResource BoolToVis}}">
Visibility="{Binding ShowPrimaryStartButton, Converter={StaticResource BoolToVis}}">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="Choose Record Folder" Command="{Binding ChooseRecordFolderCommand}"/>
<Separator/>
<MenuItem Header="Change Account" Command="{Binding ChangeAccountCommand}"/>
<Separator/>
<MenuItem Header="Logout" Command="{Binding LogoutCommand}"/>
</ContextMenu>
</Button.ContextMenu>
</Button>
<!-- End Stream button (live) -->
<Button Content="End Stream" Style="{StaticResource YtButton}"
<!-- End button (live OR recording) — the only button whose
label changes; "Stop Recording" while recording -->
<Button Content="{Binding EndButtonText}" Style="{StaticResource YtButton}"
Background="#333" Command="{Binding EndStreamCommand}"
Visibility="{Binding IsLive, Converter={StaticResource BoolToVis}}"/>
Visibility="{Binding ShowEndStreamButton, Converter={StaticResource BoolToVis}}"/>
</StackPanel>
</Grid>
</Border>
@@ -1521,9 +1540,9 @@
</Button.Template>
</Button>
<Border Background="#0A0C1A" CornerRadius="8" Width="520" MaxHeight="560"
<Border Background="#0A0C1A" CornerRadius="8" Width="650" MaxHeight="700"
VerticalAlignment="Center" HorizontalAlignment="Center"
Padding="24" BorderBrush="#333" BorderThickness="1">
Padding="28" BorderBrush="#333" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
@@ -1588,6 +1607,7 @@
<!-- About -->
<Grid Grid.Row="1" Margin="0,16,0,0"
Background="#171928"
Visibility="{Binding IsAboutOpen, Converter={StaticResource BoolToVis}}">
<!-- Hub view -->