feat(65): F10 saves a clip; the gear drawer owns the three settings
Slice 4. 607 green, 0 warnings. Clips remain **off by default** and, while off, the ring is never constructed — so no reader thread and no encoder output exist until someone asks for the feature. - **F10** (`HotkeyId.SaveClip`, default F10, remappable in the hotkey dialog) snapshots the ring, writes the file, prunes to the retention count and toasts the name. Empty ring → "nothing to clip yet" instead of a zero-byte file; clips off → says so and points at Settings. - **Settings**: Enable, length (10 or 15, default 10), keep (3/4/5, default 3). Persisted in the layout store and **coerced on load** — a bad stored value falls back to a legal one, so the ring can never be sized for a length the UI cannot show. - The ring is sized from length × tier bitrate × 1.25 and rebuilt if either changes; it is created at session start and dropped at stop, so memory is only spent while a show is running. - The drawer states the boundary in the creator's terms: "A capture, not an editor: for trimming, captions or a library, use a video editor." Still to come: the replay layer itself (play once then hide in Live, loop the kept clips elsewhere).
This commit is contained in:
@@ -74,3 +74,52 @@ public sealed class ClipLibraryTests : IDisposable
|
||||
Assert.True(tenAt8 < 15L * 1024 * 1024); // and still modest
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TASK 65 slice 4 — the three settings behind the gear drawer. They persist, and the loader coerces
|
||||
/// anything unexpected back into the allowed set: a bad value must never leave the user with a clip
|
||||
/// length the ring cannot serve.
|
||||
/// </summary>
|
||||
public sealed class ClipSettingsTests : IDisposable
|
||||
{
|
||||
private readonly string _db = Path.Combine(Path.GetTempPath(), $"ytLlive-clipsettings-{Guid.NewGuid():N}.db");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
try { File.Delete(_db); } catch { /* best effort */ }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingsPersistAndCoerce()
|
||||
{
|
||||
using (var store = new LayoutStore(_db))
|
||||
{
|
||||
Assert.False(store.LoadClipsEnabled()); // opt-in
|
||||
Assert.Equal(10, store.LoadClipSeconds()); // default 10
|
||||
Assert.Equal(3, store.LoadClipKeep()); // default 3
|
||||
|
||||
store.SaveClipsEnabled(true);
|
||||
store.SaveClipSeconds(15);
|
||||
store.SaveClipKeep(5);
|
||||
}
|
||||
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
using (var store = new LayoutStore(_db))
|
||||
{
|
||||
Assert.True(store.LoadClipsEnabled());
|
||||
Assert.Equal(15, store.LoadClipSeconds());
|
||||
Assert.Equal(5, store.LoadClipKeep());
|
||||
|
||||
store.SaveClipSeconds(7); // not a choice we offer
|
||||
store.SaveClipKeep(9); // past the ceiling
|
||||
}
|
||||
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
using (var store = new LayoutStore(_db))
|
||||
{
|
||||
Assert.Equal(10, store.LoadClipSeconds());
|
||||
Assert.Equal(3, store.LoadClipKeep());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user