TASK 18: manual rename modal for finished recordings

RenameRecordingDialog (themed, mirrors GoLiveWindow chrome): pre-fills the auto stem,
Enter/OK saves, Esc/Cancel keeps the auto name, strips stray .mp4, rejects invalid
filename chars. FinalizeRecordingAsync shows it before UniquePath rename. Naming logic
still covered by RecordingFileTests; UI shell is pure. verify.sh gate: 0 warnings,
244/246 (2 known).
This commit is contained in:
2026-08-29 09:24:13 -07:00
parent be529a46d7
commit 46f4696db6
5 changed files with 116 additions and 6 deletions
+3 -2
View File
@@ -70,6 +70,7 @@ docs/ytLlive-preview.jpg docs/ytLlive-preview.png(deleted)`
1. **Running-app verification** of TASK 18 (the one real gap): confirm rename-on-stop produces 1. **Running-app verification** of TASK 18 (the one real gap): confirm rename-on-stop produces
`ty-…-<len>.mp4` and dual stream+record output actually works live. Requires the creator to run the app. `ty-…-<len>.mp4` and dual stream+record output actually works live. Requires the creator to run the app.
2. Manual rename dialog (planned modal for user-chosen name) — parked, needs a trigger (toast service has 2. ~~Manual rename modal~~ **DONE** — `RenameRecordingDialog` (themed modal, pre-filled auto-stem,
no actions). OK/Enter / Cancel/Esc, invalid-char + empty = keep-auto validation) shown at finalize in
`FinalizeRecordingAsync`; reuses `YtTextBox`/`YtButton` styles.
3. Continue queue: TASK 21 media source, TASK 22 audio sync offset. 3. Continue queue: TASK 21 media source, TASK 22 audio sync offset.
+38
View File
@@ -0,0 +1,38 @@
<Window x:Class="ytLive.RenameRecordingDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Rename Recording" Height="230" Width="460"
Icon="/Assets/llamacasty-icon.png"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
ShowInTaskbar="False" Background="#1a1a2e">
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Rename Recording" FontSize="18" FontWeight="Bold"
Foreground="#e0e0e0" Margin="0,0,0,12"/>
<TextBlock Grid.Row="1" TextWrapping="Wrap" Foreground="#a0a0b0" FontSize="13"
Margin="0,0,0,16">
The recording is saved with an auto-generated name. Give it a clearer name —
leave it empty to keep the auto name.
</TextBlock>
<TextBlock Grid.Row="2" Text="File name (without .mp4)" Style="{StaticResource YtLabel}"/>
<TextBox Grid.Row="3" x:Name="NameBox" Style="{StaticResource YtTextBox}"
VerticalAlignment="Top"/>
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right"
Margin="0,20,0,0">
<Button Content="Cancel" Click="Cancel_Click"
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0"/>
<Button Content="Save" Click="Save_Click" Style="{StaticResource YtButton}" MinWidth="90"/>
</StackPanel>
</Grid>
</Window>
+63
View File
@@ -0,0 +1,63 @@
using System.IO;
using System.Windows;
using System.Windows.Input;
namespace ytLive;
/// <summary>Modal that lets the creator rename an auto-named recording before it is
/// finalized. Returns the chosen file stem (no extension) via <see cref="ResultFileName"/>,
/// or <c>null</c> on Cancel / keep-auto-name.</summary>
public partial class RenameRecordingDialog : Window
{
private readonly string _autoStem;
/// <summary>Stem (no extension) the creator chose, or <c>null</c> to keep auto-name.</summary>
public string? ResultFileName { get; private set; }
public RenameRecordingDialog(string autoStem)
{
InitializeComponent();
_autoStem = autoStem;
NameBox.Text = autoStem;
Loaded += (_, _) => { NameBox.Focus(); NameBox.SelectAll(); };
NameBox.KeyDown += OnKeyDown;
}
private void OnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter) { Save(); e.Handled = true; }
else if (e.Key == Key.Escape) { DialogResult = false; e.Handled = true; }
}
private void Cancel_Click(object sender, RoutedEventArgs e) => DialogResult = false;
private void Save_Click(object sender, RoutedEventArgs e) => Save();
private void Save()
{
var name = NameBox.Text?.Trim();
if (string.IsNullOrEmpty(name))
{
// Empty = keep the auto name.
ResultFileName = null;
DialogResult = true;
return;
}
// Strip any accidental ".mp4" so we return a clean stem.
if (name.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase))
name = Path.GetFileNameWithoutExtension(name);
// Reject characters that are illegal in Windows file names.
if (name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
{
MessageBox.Show(this, "That name contains characters not allowed in file names.",
"Invalid name", MessageBoxButton.OK, MessageBoxImage.Warning);
NameBox.Focus();
return;
}
ResultFileName = name;
DialogResult = true;
}
}
+2 -2
View File
@@ -951,7 +951,7 @@ click (volume sliders keep their manual `SetSliderValueFromClick`, harmless dupl
**Goal:** record the stream output to a local file, with or without simultaneously streaming. **Goal:** record the stream output to a local file, with or without simultaneously streaming.
### Status: ✅ Shipped `a9eb360` (2026-08-29) — code done, build 0 warnings, 244/246 tests; remaining = running-app verification + manual-rename modal ### Status: ✅ Shipped `a9eb360` (2026-08-29) — code done (incl. manual-rename modal), build 0 warnings, 244/246 tests; remaining = running-app verification
1. ✅ `EncoderOptions` extended with `StreamEnabled` / `RecordEnabled` / `RecordPath` (independent intent flags) 1. ✅ `EncoderOptions` extended with `StreamEnabled` / `RecordEnabled` / `RecordPath` (independent intent flags)
2. ✅ `FfmpegArgs.Build` reworked into per-output blocks (stream `-f flv`, record `-f mp4`) via `AddVideoTags` 2. ✅ `FfmpegArgs.Build` reworked into per-output blocks (stream `-f flv`, record `-f mp4`) via `AddVideoTags`
@@ -961,7 +961,7 @@ click (volume sliders keep their manual `SetSliderValueFromClick`, harmless dupl
6. 🟡 Branding flash carries into local recordings — same frame path as streaming (verify in the running app) 6. 🟡 Branding flash carries into local recordings — same frame path as streaming (verify in the running app)
7. ✅ Output folder `%APPDATA%\ytLlive\recordings\` default; auto-name `ty-<yyyymmdd>-<hhmm start>-0000.mp4` at start, **rename-on-stop** to `ty-…-<hh2mm2 length>.mp4` (numeric suffix on collision) 7. ✅ Output folder `%APPDATA%\ytLlive\recordings\` default; auto-name `ty-<yyyymmdd>-<hhmm start>-0000.mp4` at start, **rename-on-stop** to `ty-…-<hh2mm2 length>.mp4` (numeric suffix on collision)
Remaining: manual rename dialog for the user (toast actions unsupported — modal window was planned but parked), running-app verification of the rename + dual output. Remaining: running-app verification of the rename + dual output.
### Design decisions ### Design decisions
+10 -2
View File
@@ -3380,9 +3380,17 @@ public class MainViewModel : ViewModelBase
return; return;
} }
_recordLength = _liveElapsed; _recordLength = _liveElapsed;
var finalName = RecordingFile.BuildFinalName(_recordStartTime, _recordLength) + ".mp4"; var autoStem = RecordingFile.BuildFinalName(_recordStartTime, _recordLength);
var dir = Path.GetDirectoryName(startPath)!; var dir = Path.GetDirectoryName(startPath)!;
var finalPath = UniquePath(Path.Combine(dir, finalName));
// Manual rename modal: give the creator a chance to name the recording;
// null = keep the auto name (or the default recorded above).
string stem = autoStem;
var dialog = new RenameRecordingDialog(autoStem);
if (dialog.ShowDialog() == true && !string.IsNullOrEmpty(dialog.ResultFileName))
stem = dialog.ResultFileName;
var finalPath = UniquePath(Path.Combine(dir, stem + ".mp4"));
File.Move(startPath, finalPath); File.Move(startPath, finalPath);
AppLog.Write($"Recording saved: {finalPath}"); AppLog.Write($"Recording saved: {finalPath}");
_notifications.Info("Recording saved", _notifications.Info("Recording saved",