feat(dock): the LIP-SYNC bay — offset, readout, zero detent
Unit 8. Re-houses the shipped TASK 22 offset in the dock:
- slider −200…+200 ms, snapping to 5 ms ticks (the existing property still clamps and persists)
- readable readout: "0 ms (aligned)" when right, signed otherwise ("+50 ms", "−120 ms")
- presets 0 / +50 / +100; 0 IS the detent — a button is easier to hit than a snap, and "the lip-sync
is right" deserves one click.
Guard: SyncBayTests pins the binding, the readout, the presets and the snapping, and that the
offset stays the persisted property (no second source of truth).
This commit is contained in:
+36
-4
@@ -170,10 +170,42 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border Grid.Column="3" Background="{StaticResource Brush.SurfaceContainerLow}" CornerRadius="{StaticResource Radius.Sm}" Margin="4,0,0,0" Padding="10,6">
|
||||
<StackPanel>
|
||||
<TextBlock Text="LIP-SYNC" FontSize="10" FontWeight="SemiBold" Foreground="{StaticResource Brush.OnSurfaceVariant}"/>
|
||||
<TextBlock x:Name="SyncBayBody" Text="—" FontSize="11" Foreground="{StaticResource Brush.OnSurfaceDisabled}"/>
|
||||
</StackPanel>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Background="{StaticResource Brush.SurfaceContainerHighest}" CornerRadius="{StaticResource Radius.Sm}" Padding="5,1">
|
||||
<TextBlock Text="LIP-SYNC" FontSize="9" FontWeight="Bold" Foreground="{StaticResource Brush.OnSurfaceVariant}"/>
|
||||
</Border>
|
||||
<TextBlock Grid.Column="1" Margin="6,0,0,0" FontSize="10" VerticalAlignment="Center"
|
||||
FontFamily="{StaticResource FontFamily.Mono}" TextAlignment="Right"
|
||||
Foreground="{StaticResource Brush.Tertiary}"
|
||||
Text="{Binding SyncOffsetLabel}"/>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="1" VerticalAlignment="Bottom">
|
||||
<!-- A detent marker would need a custom track; the 0 ms preset is the
|
||||
detent as a button, which is also easier to hit than a snap. -->
|
||||
<Slider Minimum="-200" Maximum="200" SmallChange="5" LargeChange="25"
|
||||
TickFrequency="5" IsSnapToTickEnabled="True"
|
||||
Value="{Binding AudioSyncOffsetMs, Mode=TwoWay}" VerticalAlignment="Center"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,2,0,0" HorizontalAlignment="Right">
|
||||
<Button Content="0" Width="26" Height="16" FontSize="9" Padding="0" Margin="0,0,4,0"
|
||||
Style="{StaticResource YtButtonSecondary}" Click="SyncPreset_Click" Tag="0"/>
|
||||
<Button Content="+50" Width="30" Height="16" FontSize="9" Padding="0" Margin="0,0,4,0"
|
||||
Style="{StaticResource YtButtonSecondary}" Click="SyncPreset_Click" Tag="50"/>
|
||||
<Button Content="+100" Width="34" Height="16" FontSize="9" Padding="0"
|
||||
Style="{StaticResource YtButtonSecondary}" Click="SyncPreset_Click" Tag="100"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -39,6 +39,13 @@ public partial class AudioDock : UserControl
|
||||
vm.OpenMicPickerCommand.Execute(null);
|
||||
}
|
||||
|
||||
private void SyncPreset_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not ViewModels.MainViewModel vm) return;
|
||||
if (sender is Button { Tag: string tag } && int.TryParse(tag, out var ms))
|
||||
vm.SetSyncOffset(ms);
|
||||
}
|
||||
|
||||
private void TraxPlayPause_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is ViewModels.MainViewModel vm) vm.TraxLeftClick();
|
||||
|
||||
@@ -194,11 +194,21 @@ public partial class MainViewModel
|
||||
set
|
||||
{
|
||||
var clamped = Math.Clamp(value, -500, 500);
|
||||
if (SetProperty(ref _audioSyncOffsetMs, clamped))
|
||||
ScheduleSave();
|
||||
if (!SetProperty(ref _audioSyncOffsetMs, clamped)) return;
|
||||
OnPropertyChanged(nameof(SyncOffsetLabel));
|
||||
ScheduleSave();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>"0 ms (aligned)", "+50 ms", "−120 ms" — what the dock shows beside the slider.</summary>
|
||||
public string SyncOffsetLabel => AudioSyncOffsetMs == 0
|
||||
? "0 ms (aligned)"
|
||||
: (AudioSyncOffsetMs > 0 ? "+" : "−") + Math.Abs(AudioSyncOffsetMs) + " ms";
|
||||
|
||||
/// <summary>Preset buttons in the LIP-SYNC bay. 0 is the detent: the offset that means "the
|
||||
/// lip-sync is right", so it deserves one click, not a drag.</summary>
|
||||
internal void SetSyncOffset(int milliseconds) => AudioSyncOffsetMs = milliseconds;
|
||||
|
||||
private void ToggleMicMute()
|
||||
{
|
||||
if (MicMuted)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using Xunit;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>The LIP-SYNC bay re-houses the offset that shipped as TASK 22, with the two things the
|
||||
/// dock asks of it: a readable readout ("0 ms (aligned)") and 0 as a one-click detent. The offset
|
||||
/// itself is clamped and persisted by the existing property — this pins the surface.
|
||||
/// </summary>
|
||||
public class SyncBayTests
|
||||
{
|
||||
[Fact]
|
||||
public void The_lip_sync_bay_uses_the_persisted_offset_with_a_zero_detent()
|
||||
{
|
||||
var dock = SourceAssert.Text("Controls", "AudioDock.xaml");
|
||||
Assert.Contains("Value=\"{Binding AudioSyncOffsetMs, Mode=TwoWay}\"", dock);
|
||||
Assert.Contains("Text=\"{Binding SyncOffsetLabel}\"", dock);
|
||||
Assert.Contains("Click=\"SyncPreset_Click\"", dock);
|
||||
Assert.Contains("Tag=\"0\"", dock);
|
||||
Assert.Contains("Minimum=\"-200\" Maximum=\"200\"", dock);
|
||||
|
||||
// The readout names the good state, and signs the others the way a person reads them.
|
||||
Assert.Matches(@"IsSnapToTickEnabled=""True""", dock);
|
||||
|
||||
var type = typeof(MainViewModel);
|
||||
Assert.NotNull(type.GetProperty("AudioSyncOffsetMs"));
|
||||
Assert.NotNull(type.GetProperty("SyncOffsetLabel"));
|
||||
Assert.NotNull(type.GetMethod("SetSyncOffset", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user