feat(settings): make slide-out alerts persist until dismissed
Add a persisted global App Settings switch. Off keeps the existing per-severity lifetimes (errors remain sticky); on makes every alert non-expiring and closes it when its body is clicked. The NotificationService supplies the current setting to each toast request. StickyNotificationIntegrationTests covers persistence and both request modes. 618 tests green, clean build with 0 warnings. Reference: Notification.Wpf exposes a CloseOnClick request option: https://github.com/G-Stas/Notification.Wpf
This commit is contained in:
@@ -38,6 +38,11 @@
|
||||
<ComboBoxItem Content="Bottom left" Tag="{x:Static notification:NotificationPosition.BottomLeft}"/>
|
||||
<ComboBoxItem Content="Bottom right" Tag="{x:Static notification:NotificationPosition.BottomRight}"/>
|
||||
</ComboBox>
|
||||
<CheckBox Content="Keep alerts until dismissed" Style="{StaticResource PropertyPillCheck}"
|
||||
Margin="12,2,0,2" IsChecked="{Binding StickyAlertsUntilDismissed, Mode=TwoWay}"
|
||||
ToolTip="When on, every slide-out alert stays until you click it or close it."/>
|
||||
<TextBlock Text="Off: informational alerts use their normal timeouts; errors still stay until closed."
|
||||
Style="{StaticResource PropertyHint}" Margin="10,0,0,8"/>
|
||||
|
||||
<!-- ═══ BRAND ═══ -->
|
||||
<TextBlock Text="Brand logo" Style="{StaticResource SectionHeader}" Margin="0,6,0,4"/>
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ See [`schema.md`](../schema.md) for the memory-map conventions.
|
||||
| `PreviewPane.xaml(.cs)` | Row 2 Col 1 center: 1920×1080 `OverlayCanvas`/`SelectionOverlay`/`BrandFlashLayer`/`SocialBarElement`, static TextOverlay preview image from the shared cached text raster, text drawer, outgoing-picture transition overlay, right-edge Stream Info/YPP/Socials/TEST tabs and drawers, and live controls (TRAX / game / mic meters + sliders + speakers). The list-selected draggable layer is temporarily raised in preview z-order and custom hit-testing; deselection restores the scene's unchanged order (`SelectedLayerPreviewHitTestingTests`). **TASK 57:** dragging/resizing a webcam in the vertical tier routes through `MainViewModel.MoveWebcamInVerticalTier` / `ResizeWebcamInVerticalTier` for the six-spot snap or Chat camera's two sizes. |
|
||||
| `SceneThumbnailStrip.xaml(.cs)` | Top row over the preview column: five fixed-size scene cards with scene name, hotkey, staged `PROGRAM` badge, orange staged/live border |
|
||||
| `LeftPanel.xaml(.cs)` | Row 2 Col 0 left panel: explicit EditingActive toggle on → Scene Layers+properties; off → YouTube chat, independent of stream status. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept; TextOverlay properties edit multiline content, font type/family, size, color via the shared palette, emphasis, and horizontal/vertical alignment with a static-text notice; sub-options are indented under their headings. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on). **TASK 57:** the webcam properties carry a Geometry section (X/Y/W/H) — manual placement of the camera, bound to the **live** placement (the one the current tier shows), gated on `IsWebcamSelected` (not `SelectedElement.IsWebcam` — the TASK 51 test finds that panel by binding text and would match twice); the Shape button's `Visibility` is bound to `CanToggleClipShape`, so the circle option is NOT OFFERED in the vertical Chat layout. **TASK 58:** every drop-down, check box and field in the properties panel carries the theme's style (`YtComboBox`/`YtCheckBox`/`YtTextBoxCompact`) and paints no colour of its own — the guard is `ytLive.Tests/PropertiesStyleTests.cs`. **TASK 59:** the webcam's properties are a HIERARCHY — a `Shape` title owns the mirror switch and the shape choice (`Rect` | `Circle` as two `SegmentToggle` options, Rect selected by default, the circle option hidden where `CanToggleClipShape` is false), and every titled block (Geometry, Border Color, Border Thickness, Background) owns its content one indent deeper (Strength sits under Blur). **TASK 60:** the border-colour drop-down builds its menu from `Models/BorderColorPalette.cs` — each row is the hex plus the colour's NAME (`#e94560 Crimson`, tooltip "Crimson (#e94560)"); the swatch icon is unchanged. **TASK 61:** the four geometry fields (X/Y/W/H) share one fixed width (44px — the largest value, 1920, plus a sign) and their rows set `LastChildFill="False"`, so Y/H no longer stretch to fill the row. **TASK 62:** the border-colour field shows the palette's name beside the hex (`SelectedElement.BorderColorName`, empty for a custom hex) and keeps a fixed width so the name stays next to the hex. **TASK 63:** the border colour is picked with the **app's own drop-down** (`YtComboBox`) — the `...` button and its code-built `ContextMenu` are gone; the closed box shows the swatch + the colour's NAME, the rows show swatch + hex + name (the panel's own `DataTemplate`s + `Helpers/ClosedBoxTemplateSelector.cs`), and the hex field stays for a custom colour; `IsDescendantOf` falls back to `LogicalTreeHelper.GetParent` / `TemplatedParent` when `VisualTreeHelper.GetParent` returns `null` (at `PopupRoot`) so clicking a popup row does not trigger `MainWindow`'s deselection guard |
|
||||
| `OverlayHost.xaml(.cs)` | Root-grid overlays: App Settings / Report Bug / Feature Request / About; About includes the output-quality conversion copy and an explicit llamacasty.com link, while the licensing/legal panel remains in-app. App Settings groups recording, slide-out alert placement, brand-logo, preview, transition, and staged/all-scene layout-default controls. `BackToAbout_Click` lives here |
|
||||
| `OverlayHost.xaml(.cs)` | Root-grid overlays: App Settings / Report Bug / Feature Request / About; About includes the output-quality conversion copy and an explicit llamacasty.com link, while the licensing/legal panel remains in-app. App Settings groups recording, slide-out alert placement/stickiness, brand-logo, preview, transition, and staged/all-scene layout-default controls. `BackToAbout_Click` lives here |
|
||||
| `SceneThumbnailStrip.xaml.cs` | Code-behind for the strip above |
|
||||
| `LeftPanel.xaml.cs` | Code-behind for the panel above |
|
||||
|
||||
|
||||
+7
-6
@@ -1,9 +1,9 @@
|
||||
# HANDOFF — 2026-11-11 (recovery fixes + alert placement; creator UI 2→3 next)
|
||||
# HANDOFF — 2026-11-11 (recovery fixes + alert settings; metric cues next)
|
||||
|
||||
## State
|
||||
|
||||
- `main`; `b265d42` is the latest pushed commit. Recovery fixes and alert placement are committed locally,
|
||||
verified, and not pushed yet.
|
||||
- `main`; `b265d42` is the latest pushed commit. Recovery fixes and alert-setting changes are committed
|
||||
locally, verified, and not pushed yet.
|
||||
- **Done:** TASK 35 scene-linked audio, TASK 55 disk hygiene, TASK 65 replay clips, TASK 37 defaults/current split.
|
||||
- TASK 37: SQLite keeps an immutable default snapshot beside build-stamped current rows; App Settings can
|
||||
restore the staged scene or all scenes, offline only. Webcam identity survives both restore paths.
|
||||
@@ -11,14 +11,15 @@
|
||||
- Recovery incident: private test hit step-down on 85% loss; encoder restart reused a broken audio pipe.
|
||||
Step-down now closes/recreates audio in order; recovery notifications/stops marshal to the dispatcher.
|
||||
- App Settings now controls slide-out toast position (six left/right × top/center/bottom positions).
|
||||
- Verification: **617 tests green, clean build with 0 warnings** (`FAST=1 ./scripts/verify.sh`). Creator test
|
||||
- App Settings can keep all slide-out alerts until clicked/closed; default off preserves existing timers.
|
||||
- Verification: **618 tests green, clean build with 0 warnings** (`FAST=1 ./scripts/verify.sh`). Creator test
|
||||
was stable (~5.1 Mbps, 31 fps, 0.2% drop); its log showed no step-down/reconnect event.
|
||||
- Steam remains blocked on Valve identity/tax review; no app IDs or store page.
|
||||
|
||||
## Next action
|
||||
|
||||
- Next: creator-directed **2) sticky click-to-dismiss alerts → 3) core-metric change cues**, one test/commit
|
||||
each. Then resume **40·A/C/D → Move → 9·5 → 14·16/17/18 → 66 → 67 →
|
||||
- Next: creator-directed **3) core-metric change cues**, one test/commit. Then resume
|
||||
**40·A/C/D → Move → 9·5 → 14·16/17/18 → 66 → 67 →
|
||||
68 → 69 → 70 → 36**.
|
||||
- Ask creator before TASK 68 about first-run ON-AIR default; before gold pass, settle the Release
|
||||
single-instance mutex behavior.
|
||||
|
||||
@@ -21,6 +21,12 @@ public partial class LayoutStore : IDisposable
|
||||
UpsertSetting("Notifications.Position", position.ToString());
|
||||
}
|
||||
|
||||
public bool LoadStickyAlertsUntilDismissed()
|
||||
=> GetSetting("Notifications.StickyUntilDismissed") == "1";
|
||||
|
||||
public void SaveStickyAlertsUntilDismissed(bool enabled)
|
||||
=> UpsertSetting("Notifications.StickyUntilDismissed", enabled ? "1" : "0");
|
||||
|
||||
private static bool IsSupportedNotificationPosition(NotificationPosition position)
|
||||
=> position is NotificationPosition.TopLeft or NotificationPosition.TopRight
|
||||
or NotificationPosition.CenterLeft or NotificationPosition.CenterRight
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace ytLive.Services;
|
||||
/// <summary>
|
||||
/// Toast notifications rendered inside the main window's <c>ToastArea</c>, whose
|
||||
/// position is configurable in App Settings (bottom-right by default). Info/success auto-dismiss after ~4s,
|
||||
/// warnings ~8s, errors stay until dismissed. Calls from any thread are
|
||||
/// warnings ~8s, errors stay until dismissed. App Settings can make all alerts sticky and
|
||||
/// dismissible by clicking the card. Calls from any thread are
|
||||
/// marshalled to the captured dispatcher.
|
||||
/// </summary>
|
||||
public sealed class NotificationService : INotificationService
|
||||
@@ -28,35 +29,49 @@ public sealed class NotificationService : INotificationService
|
||||
|
||||
private readonly NotificationManager _manager = new();
|
||||
private readonly Dispatcher _dispatcher;
|
||||
private readonly Func<bool> _stickyAlertsUntilDismissed;
|
||||
|
||||
public NotificationService()
|
||||
: this(Application.Current?.Dispatcher)
|
||||
: this(Application.Current?.Dispatcher, static () => false)
|
||||
{
|
||||
}
|
||||
|
||||
internal NotificationService(Dispatcher? dispatcher)
|
||||
: this(dispatcher, static () => false)
|
||||
{
|
||||
}
|
||||
|
||||
internal NotificationService(Dispatcher? dispatcher, Func<bool> stickyAlertsUntilDismissed)
|
||||
{
|
||||
_dispatcher = dispatcher ?? Dispatcher.CurrentDispatcher;
|
||||
_stickyAlertsUntilDismissed = stickyAlertsUntilDismissed;
|
||||
NotificationConstants.NotificationCornerRadius = new CornerRadius(8);
|
||||
NotificationConstants.KeepNotificationVisibleOnMouseOver = true;
|
||||
}
|
||||
|
||||
public void Show(string title, string message, AppNotificationSeverity severity)
|
||||
{
|
||||
var request = BuildRequest(title, message, severity);
|
||||
_dispatcher.Invoke(() => _manager.Show(request));
|
||||
_dispatcher.Invoke(() => _manager.Show(BuildRequestForCurrentSettings(title, message, severity)));
|
||||
}
|
||||
|
||||
internal static NotificationRequest BuildRequest(string title, string message, AppNotificationSeverity severity)
|
||||
internal NotificationRequest BuildRequestForCurrentSettings(string title, string message,
|
||||
AppNotificationSeverity severity)
|
||||
=> BuildRequest(title, message, severity, _stickyAlertsUntilDismissed());
|
||||
|
||||
internal static NotificationRequest BuildRequest(string title, string message,
|
||||
AppNotificationSeverity severity, bool stickyUntilDismissed = false)
|
||||
{
|
||||
var (backgroundHex, expirationSeconds) = Cards[severity];
|
||||
var builder = NotificationBuilder
|
||||
.Create(title, message)
|
||||
.InArea(AreaName)
|
||||
.WithBackground(NotificationColor.FromHex(backgroundHex))
|
||||
.WithForeground(NotificationColor.FromHex(ThemeBrush.Hex("Brush.OnSurface")));
|
||||
.WithForeground(NotificationColor.FromHex(ThemeBrush.Hex("Brush.OnSurface")))
|
||||
.CloseOnClick(stickyUntilDismissed);
|
||||
|
||||
if (severity == AppNotificationSeverity.Error)
|
||||
if (stickyUntilDismissed)
|
||||
builder.NeverExpires();
|
||||
else if (severity == AppNotificationSeverity.Error)
|
||||
builder.NeverExpires();
|
||||
else
|
||||
builder.ExpiresInSeconds(expirationSeconds);
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ External-facing logic: YouTube API, persistence. See
|
||||
| `Encoder/FramePump.cs` | **TASK 4 ship step 5**: the live frame producer — while live it snapshots the active scene each tick, resolves every element to its latest frame (`Func<SceneElement, VideoFrame?>` resolver), composites it into the tier's output frame, and paces frames into the encoder at the tier's FPS. All collaborators constructor-injected seams; free of WPF and the capture managers. `StartAsync` never throws (failures log + `Failed`); no RTMP URL = encoder skipped; `StopAsync` stops the encoder before awaiting the loop (backpressure deadlock); `ProcessFailed` self-stops. Since the bar bug-fix branch it takes an optional `socialBar: Func<(VideoFrame? Frame, SocialBarPosition Position)>?` seam — a pre-rendered bar strip composited last (above the flash) at the top edge or `SourceRectHeight − bar height`. **TASK 47 + scene ticker:** the `alertTicker: Func<Scene, VideoFrame?>?` seam selects the active scene's AlertBox ticker, threaded through `RenderScene`/`RenderFull` + **mixed into `BuildFullRenderSignature`**; standalone tickers are resolved as per-element dynamic sources. See [`docs/ai/frame-pipeline.md`](../docs/ai/frame-pipeline.md) → "Live frame pipeline". **2026-10-16:** owns a `CompositeTimings` collector, passes it through `RenderScene`/`RenderFull`/`GetBakedBase`/`CompositeLayers`, logs `FramePump parts (N renders/5s): …` (per-stage breakdown, slowest first) beside the stats line each 5s window, and dumps the scene shape — `FramePump scene dump:` header + one line per element (z-index, name, kind, type, visibility, rect, opacity, clip) — at pump start and on every scene change |
|
||||
| `Encoder/FfmpegLocator.cs` | The default locator: PATH probe wins, then the cache, then pull+extract (`ffmpeg.exe` **+ the `libav*.dll` family** via a staging dir, failures via `AppLog`). Pinned to BtbN `autobuild-2026-08-09-13-03` **lgpl-shared** (dynamic linking = LGPL compliance without static-relink §6 material; no GPL libx264; NVENC/QSV/AMF + libopenh264 + native AAC — see `ai.md` → Licensing). Search dirs / tools dir / downloader constructor-injected for hermetic tests. Wired into the encoder since ship step 5 |
|
||||
| `INotificationService.cs` | **Toast seam (TASK 24)**: `Show(title, message, AppNotificationSeverity)` + `AppNotificationSeverity { Info, Success, Warning, Error }` + `Info/Success/Warning/Error` extension methods — non-blocking user-facing messages; VMs/services raise through this so failures surface without MessageBox blocking the UI thread |
|
||||
| `NotificationService.cs` | The default seam impl (Notification.Wpf 11.0.0, MIT): wraps a library `NotificationManager`; routes to the in-window `NotificationArea` named `ToastArea` (position configurable in App Settings; bottom-right default); lifetimes via the pure `BuildRequest` mapping + `Cards` tint table — Info/Success ~4s, Warning ~8s auto-dismiss, Error sticky (`NeverExpires()` = `TimeSpan.MaxValue`, never null); dark card tints matching the health-banner language; all Show calls marshal to the captured Dispatcher. See [`../ai.md`](../ai.md) "Toast notifications" |
|
||||
| `NotificationService.cs` | The default seam impl (Notification.Wpf 11.0.0, MIT): wraps a library `NotificationManager`; routes to the in-window `NotificationArea` named `ToastArea` (position configurable in App Settings; bottom-right default); Info/Success ~4s, Warning ~8s by default, Error sticky; the persisted App Settings switch makes all severities sticky and body-click dismissible. Dark card tints match the health-banner language; all Show calls marshal to the captured Dispatcher. See [`../ai.md`](../ai.md) "Toast notifications" |
|
||||
| `Audio/IAudioSource.cs` | **TASK 4 ship step 4 seam**: live capture source — `Start`/`Stop`/`Started`/`SampleReady(AudioSample)`/`Failed(Exception)`, `IDisposable`. Capture now runs for the app's lifetime (started at startup) so the footer meters preview live. The app consumes this seam; tests inject hermetic fakes |
|
||||
| `Audio/AudioSample.cs` | One captured chunk: interleaved PCM float (-1..1) + sample rate + channels |
|
||||
| `Audio/WasapiLoopbackAudioSource.cs` | Desktop/game capture: NAudio `WasapiLoopbackCapture` on the default render device — automatic at unity; the game bar's meter consumes it |
|
||||
|
||||
@@ -14,6 +14,17 @@ public partial class MainViewModel
|
||||
{
|
||||
private bool _settingsDrawerOpen;
|
||||
private NotificationPosition _toastPosition = NotificationPosition.BottomRight;
|
||||
private bool _stickyAlertsUntilDismissed;
|
||||
|
||||
public bool StickyAlertsUntilDismissed
|
||||
{
|
||||
get => _stickyAlertsUntilDismissed;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _stickyAlertsUntilDismissed, value)) return;
|
||||
_layoutStore.SaveStickyAlertsUntilDismissed(value);
|
||||
}
|
||||
}
|
||||
|
||||
public NotificationPosition ToastPosition
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
|
||||
// Toasts (TASK 24): non-blocking user-facing messages. Parameterless ctor
|
||||
// news up the real service; the VM is built from XAML so no DI seam exists.
|
||||
private readonly INotificationService _notifications = new NotificationService();
|
||||
private readonly INotificationService _notifications;
|
||||
|
||||
// Live frame pipeline (TASK 4 ship step 5): composites the active scene at the
|
||||
// tier's FPS and paces frames into the encoder. The RTMP URL seam stays null
|
||||
@@ -216,6 +216,8 @@ public partial class MainViewModel : ViewModelBase
|
||||
|
||||
public MainViewModel()
|
||||
{
|
||||
_notifications = new NotificationService(System.Windows.Application.Current?.Dispatcher,
|
||||
() => StickyAlertsUntilDismissed);
|
||||
AppLog.Write("MainViewModel ctor begin");
|
||||
InitializeCurrentLaunchLogs();
|
||||
IsAppOwned = _entitlement.IsAppOwned;
|
||||
@@ -344,6 +346,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
_layoutStore = new LayoutStore(LayoutPathOverride ?? DefaultLayoutPath);
|
||||
_activeLayoutPath = _layoutStore.ActivePath;
|
||||
_toastPosition = _layoutStore.LoadNotificationPosition();
|
||||
_stickyAlertsUntilDismissed = _layoutStore.LoadStickyAlertsUntilDismissed();
|
||||
_recordFolder = _layoutStore.LoadRecordFolder();
|
||||
|
||||
// Load saved hotkey bindings (filled with defaults for any unbound actions).
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ areas' docs in their own rows keeps this index small to read.
|
||||
| `MainViewModel.Socials.cs` | Social bar (global resource, overlaid on every scene): `_socials`/`_socialBarFrame`/`_socialValidator` (`ISocialValidator`/`HttpSocialValidator`), `HasSocials`/`SocialBarVisible`/`SocialBarDotBrush`/`SocialBarGlowBrush`/`Socials`/`SocialBarTop`, `OpenSocialDialogCommand`/`OpenSocialDialog` (spawns `SocialsDialogViewModel` + window; saved copy replaces `_socials`), `NotifySocialsChanged`/`RenderSocialBarFrame` (`SocialBarRenderer` → immutable `_socialBarFrame` the frame pump reads), fediverse heal `HealFediverseSoftwareAsync` (static, test-facing) + `HealFediverseSoftwareInBackground`, bar edge toggle `SetSocialBarPosition`/`ToggleSocialBarPosition`. `BarOnBrush`/`BarOffBrush`/`CreateBrush` stay in core (shared tokens). |
|
||||
| `MainViewModel.Streaming.cs` | Go-live/record + health + quality tiers + session UI. **Stream state:** `StreamStatus`/`StreamVisibility` setters + `_streamStatus` field, `IsOffline`/`IsLive`/`IsLivePrivate`. **Go-live flow:** `StartSession` (routes by pill state) → `BeginRecordOnly` (signed-out local recording) or `BeginGoLive` (GoLive dialog → captures title/description/visibility + `BroadcastForm.CaptureGoLive` → `PrepareAndStartLiveAsync`: reusable stream → `CreateBroadcast` (stores `_currentBroadcastId`) → empty-metadata warn → chat poll start (liveChatId) → `_framePump.StartAsync()` + health poll; failures → `StreamStatus.Error`, never a crash); `StartRecordFile` (auto-named `ty-…-0000.mp4`) + `FinalizeRecordingAsync` (rename modal, `UniquePath`); `StopStream` (posts Offline, stops chat/audio pipe/pump, clears `_currentBroadcastId`; session/token deliberately NOT cleared — sign-out stays explicit). **Health:** `CurrentHealth`/`ResetHealth`/`ApplyHealth`/`OnFramePumpHealthUpdated` (marshal `FramePump.HealthUpdated` off the stderr thread), `PollHealthAsync`/`OnHealthPollTick`/`ApplyHealthIssue` (report-by-exception banner `HealthIssueBanner`/`HealthIssueBackground` via `StreamHealthReporter`). **Session timer:** `_liveTimer` (ctor-wired) `UpdateSessionTimer`/`OnLiveTimerTick`/`LiveElapsedText`/`_recDotPulse`. **Start/End UI:** mode switch `RecordPillOn`/`OnAirPillOn`, top-bar worlds (`ShowIdleWorld`/`ShowRecordAction`/`ShowStreamActions`/`ShowSignInButton`/`ShowAvatarButton`, reality line `RunningVisible`/`RunningText`/`RunningDotBrush`, Test gate `ShowTestButton`/`CanStartTest` — `RaiseTopBarModes()`), `ShowPrimaryStartButton`/`CanStartSession`/`ShowEndStreamButton`/`EndButtonText`, REC sign `RecDotBrush`/`RecTextBrush`/`RecDotOpacity`/`OnAirBrush`, live window tokens `WindowTitle`/`TopBarBackground`/`PreviewGlow*` via `UpdateLiveVisuals`; `ChooseRecordFolder`/`ResetRecordFolder`/`_recordFolder`/`RecordFolderDisplay`/`DefaultRecordFolder`. **Resolution dropdown:** `QualityOptions`/`SelectedQuality`/`ResolutionHelp`/`ApplyStreamQuality` + `OutputRect*`/`DimRects`/`IsOutputCropped`/`ResolutionBadgeText` over the 1920×1080 master; **TASK 57:** the tier change also resolves every scene's camera placement (the vertical tier brings the vertical placements in, 16:9 restores the desktop layout — see `MainViewModel.Webcam.cs`). **Pipeline bits:** `_framePump`/`_liveTimer`/`_healthPollTimer` fields, `BuildEncoderOptions`. Commands `StartStreamCommand`/`EndStreamCommand`/`ChooseRecordFolderCommand`/`ResetRecordFolderCommand`. Broadcast pull-out: `CurrentBroadcastId`/`BroadcastForm`. **TASK 57 (Unit 2):** the tier change also raises `CanToggleClipShape` (the circle option disappears in the vertical Chat layout). |
|
||||
| `MainViewModel.Overlays.cs` | Settings/bug/feature/about panels + creator-hub links: panel state `IsSettingsOpen`/`IsBugOpen`/`IsFeatureOpen`/`IsAboutOpen` (via `SetPanel` → `IsAnyOverlayOpen`), `IsLicensesOpen` (flips `IsHubVisible`), `OverlayTitle`, `ShowOverlay` (one-panel exclusivity), `ShowAbout` (returns to the About hub) + `ShowLicenses` (loads shipped `THIRD-PARTY-NOTICES.txt` into `LicensesText` in-app); bug/feature report `BugReportText`/`BugReportEmail`/`FeatureRequestText`/`FeatureRequestEmail` + `SubmitBug`/`SubmitFeature`/`ComposeEmail`/`OpenUrl`; About hub consts `ChannelUrl`/`MastodonUrl`/`CoffeeUrl` + `PremiumUrl` (`https://llamacasty.com`, opens only when explicitly clicked) + `IsPremiumAvailable` + `SupportEmail` + `AppVersionLabel`; commands `OpenSettingsCommand`/`OpenBugCommand`/`OpenFeatureCommand`/`OpenAboutCommand`/`CloseOverlayCommand`/`SubmitBugCommand`/`SubmitFeatureCommand`/`OpenChannelCommand`/`OpenMastodonCommand`/`OpenCoffeeCommand`/`OpenPremiumCommand`/`OpenLicensesCommand`. A locked quality selection opens About without changing the tier. Ctor command assignments stay in core. |
|
||||
| `MainViewModel.Settings.cs` | App Settings drawer state + global slide-out toast position (`ToastPosition`), persisted in `LayoutStore.Settings`; `ToastAreaMargin` keeps the six supported positions aligned to the window edges/safe footer gap. |
|
||||
| `MainViewModel.Settings.cs` | App Settings drawer state + global slide-out toast position (`ToastPosition`) and stickiness (`StickyAlertsUntilDismissed`), persisted in `LayoutStore.Settings`; `ToastAreaMargin` keeps the six supported positions aligned to the window edges/safe footer gap. |
|
||||
| `MainViewModel.Chat.cs` | Chat overlay + preview pipeline: `ChatMessages` (500-message cap; clear-on-fade), `OnChatMessageReceived` (marshal `_youtubeChat.MessageReceived` onto the dispatcher, switch previews mock→real, then feed **both** component layers), fade `_chatFadeTimer`/`ResetChatFadeTimer` (shortest `ChatFadeSeconds` across chat layers); live render `RenderChatBox` (called from `ResolveOutputFrame`, stays in core) + preview render `UpdateChatBoxPreview` (bitmap onto `chatBox.VideoImageSource`); mock preview `MockChatMessages`/`StartMockChatPreview`/`StopMockChatPreview`/`RunMockChatPreviewAsync`/`_mockChatCts`; `UpdateActiveChatBoxPreviews` (wires Chat* property-change subscriptions + first render); `CanAddYouTubeChat` (one-per-layout gate), `ShowChatInactiveMessage` (`!IsLive`). **Alerts (TASK 43):** same facade for the native alert box — `_alertLayer` (`AlertOverlayLayer`), `RenderAlertBox`/`UpdateAlertBoxPreview`, `UpdateActiveAlertBoxPreviews`, `CanAddAlerts` (one alert zone per layout). Ctor wiring (`MessageReceived` hookup, `new ChatBoxRenderer()`, `new AlertRenderer()`, `LoadLayout` preview start, `_youtubeChat.Dispose` + `_alertLayer.Dispose`) stays in core. |
|
||||
| `MainViewModel.Logs.cs` | Non-edit left-panel tab state and bounded current-launch log collections; refreshes the process-local AppLog snapshot while Logs is selected, pins Class 1 before Class 2 until acknowledged, then returns entries to the LIFO timeline; stops its dispatcher timer on shutdown. |
|
||||
| `GoLiveViewModel.cs` | Start Stream dialog: **account row** (saved channel shown with Change Account, or Sign in to YouTube; Start gated on `IsSignedIn`/`IsBusy`) + title/description + **locked Visibility (always "Private" — ship step 7, no dropdown)**, start/cancel requests |
|
||||
|
||||
@@ -159,7 +159,7 @@ The app is a **director's control surface**, not an editor. This replaces the OB
|
||||
**Current layout (2026-10-26 redesign commits):** `MainWindow` has a top bar and health banner, then a two-column work area with a fixed 360px left column and preview column. Its top row holds stream readouts/resolution on the left and the five-scene strip above the preview; the lower row holds the left panel and central preview. The bottom bar is now only a height-preserving spacer.
|
||||
|
||||
- **Left panel:** `EditingActive` is an explicit `EDIT` toggle, not a mode inferred from `StreamStatus`. On = scene layers + properties; off = a two-tab panel with **Live Chat** first/default and **Logs** second. Logs is a read-only view of up to 500 `AppLog` entries captured in this process; it does not load older `startup.log` contents or change file logging/rotation. Entries wrap and use the theme's type colors. Explicit Class 1/2 severity is independent of message type/source: Class 1 sorts above Class 2 in the pinned group; acknowledging unpins the entry but retains it in the chronological list. The staged scene is the preview/edit target; `LiveScene` is the program output.
|
||||
- **Settings is a right-edge drawer, not a modal (2026-11-01):** the gear rail tab opens `Controls/SettingsDrawer` in the same animated-width stack as Stream Settings / YPP / Socials / TEST (`SettingsDrawerOpen`, exclusive with the other four). Categories are section headings with their controls indented under them — Zoom holds *Zoom view shape* and *Zoom box follows*. TASK 37 adds **Restore staged scene** and **Restore all scenes** under Layout defaults; both are disabled while live or recording, and restoring a layout preserves the selected webcam. App Settings also controls slide-out toast placement (six left/right × top/center/bottom positions; bottom-right default). The app menu (Report Bug / Feature Request / About) hangs off a **right-click on the LlamaCasty wordmark**; left-click still opens About. The modal shell keeps only Bug / Feature / About, which is why it no longer needs a scroll viewer.
|
||||
- **Settings is a right-edge drawer, not a modal (2026-11-01):** the gear rail tab opens `Controls/SettingsDrawer` in the same animated-width stack as Stream Settings / YPP / Socials / TEST (`SettingsDrawerOpen`, exclusive with the other four). Categories are section headings with their controls indented under them — Zoom holds *Zoom view shape* and *Zoom box follows*. TASK 37 adds **Restore staged scene** and **Restore all scenes** under Layout defaults; both are disabled while live or recording, and restoring a layout preserves the selected webcam. App Settings controls slide-out toast placement (six left/right × top/center/bottom positions; bottom-right default) and has a sticky mode that keeps all notifications until clicked or closed. The app menu (Report Bug / Feature Request / About) hangs off a **right-click on the LlamaCasty wordmark**; left-click still opens About. The modal shell keeps only Bug / Feature / About, which is why it no longer needs a scroll viewer.
|
||||
- **Top bar:** the `EDIT` toggle sits beside the REC/ON-AIR session controls; the brand and account surfaces remain at the sides. Its background uses the idle surface color when stopped. Normal ON-AIR stays `Brush.Primary` red while connecting and turns `Brush.Positive` green only after YouTube reports the broadcast `live`; stopping restores the idle color. TEST stays gold for the entire test session. Its **right edge aligns with the layers panel below** (`TopBar` column 0 is a fixed **352** = work-area margin 8 + 360px column − the bar's own 16px padding), so the button and the panel share an edge. The **settings gear moved to the preview rail** (2026-11-01): first tab, orange (`Brush.Attention`), gear icon only — left-click opens the app menu (Settings / Bug / Feature / About), right-click goes straight to App Settings.
|
||||
- **Preview gestures (2026-11-01):** *edit mode* — left selects/drags/resizes, right opens the context menu, **Ctrl+right toggles the zoom box**. *Preview mode* — **left toggles the zoom box**, right opens the context menu; **Ctrl+right does nothing at all** (no chord while live or recording — one hand, always). ⚠️ **Both the Down and the Up event must be handled for a Ctrl+right-click**: WPF's `PopupControlService` opens a context menu on right-button-**up**, so suppressing only the down event leaves the menu appearing under the gesture (found 2026-11-01 in edit mode). Menus exist on the webcam layer and the backdrop; every other layer right-clicks through to the backdrop menu. Left-dragging with the zoom box open moves it, in both modes. **Ctrl-down closes it**: pressing Ctrl while the box is open dismisses it, so the same hand that opened it can put it away (`MainWindow_PreviewKeyDown`; the box cannot cancel itself on creation because Ctrl is already down before it exists). **Follow mode** is a Settings pill, default **off = Anchor**: with Follow on, dragging re-anchors the capture to the box centre (`ZoomFollowsBox`, persisted as the `Zoom.FollowBox` setting). **The gesture activates the box at the cursor**, not merely dismisses it: `ActivateZoom` no longer refuses when idle (it used to return unless `IsLive || IsRecording`, and `ToggleZoom` only ever dismissed — so the gesture did nothing at all outside a session until 2026-11-01). The social bar consumes its own click (toggle top ⇄ bottom), and the preview's tunnelling handler steps aside for it — otherwise the bar is unclickable in preview mode and drags the layer beneath it in edit mode.
|
||||
- **Preview undo/redo (2026-11-01):** `Ctrl+Z` / `Ctrl+Shift+Z`, window-level bindings, live only while `EditingActive` — undo with the preview showing program output would rewrite a scene nobody is looking at. `Models/PreviewHistory` snapshots element **references** plus geometry, visibility, order and membership, so move / resize / reorder / add / remove / hide all undo in one step per gesture (`PushPreviewUndo` is called once at drag or resize start, never inside the move loop). Property edits (colour, opacity, border, font) are **not** captured yet — stated in the type's doc comment so nobody assumes otherwise. The stack clears on layout load. Guard: `ytLive.Tests/PreviewHistoryTests.cs`.
|
||||
@@ -212,6 +212,8 @@ MIT — notice #10 in `THIRD-PARTY-NOTICES.txt`). Facts that cost a hunt:
|
||||
- Lifetimes: Info/Success ~4s, Warning ~8s auto-dismiss, Error sticky until dismissed (the pure mapping
|
||||
lives in `NotificationService.BuildRequest` + the `Cards` tint table: slate info `#3a3b52`, green
|
||||
success `#1d5c38`, amber warning `#b8860b`, dark-red error `#8f1f1f` — matching the health-banner language).
|
||||
- Global App Settings `StickyAlertsUntilDismissed` changes every severity to no-expiration + body-click-to-close;
|
||||
off preserves those defaults. The preference persists in the profile `Settings` table.
|
||||
- Calls marshal to the captured Dispatcher (`_dispatcher.Invoke`) — encoder/pump events fire off-thread.
|
||||
|
||||
Migrated MessageBoxes: both webcam-acquire warnings, image-read warning, sign-in unsuccessful/failed.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Notification.Core;
|
||||
using Xunit;
|
||||
using ytLive.Services;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
[Collection("RealApp")]
|
||||
public sealed class StickyNotificationIntegrationTests
|
||||
{
|
||||
private readonly RealAppHost _app;
|
||||
|
||||
public StickyNotificationIntegrationTests(RealAppHost app) => _app = app;
|
||||
|
||||
[Fact]
|
||||
public void StickyMode_Persists_AndMakesEverySeverityClickDismissible() => _app.Run(() =>
|
||||
{
|
||||
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-sticky-toast-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = tempDb;
|
||||
MainWindow? window = null;
|
||||
try
|
||||
{
|
||||
window = new MainWindow();
|
||||
window.Show();
|
||||
var vm = (MainViewModel)window.DataContext;
|
||||
Assert.False(vm.StickyAlertsUntilDismissed, "sticky notifications should default off");
|
||||
vm.StickyAlertsUntilDismissed = true;
|
||||
|
||||
window.Close();
|
||||
window = null;
|
||||
SqliteConnection.ClearAllPools();
|
||||
|
||||
window = new MainWindow();
|
||||
window.Show();
|
||||
var restored = (MainViewModel)window.DataContext;
|
||||
Assert.True(restored.StickyAlertsUntilDismissed);
|
||||
|
||||
var service = new NotificationService(System.Windows.Application.Current?.Dispatcher,
|
||||
() => restored.StickyAlertsUntilDismissed);
|
||||
var sticky = service.BuildRequestForCurrentSettings("Upload", "Recovered", AppNotificationSeverity.Info);
|
||||
Assert.Equal(TimeSpan.MaxValue, sticky.ExpirationTime);
|
||||
Assert.True(sticky.CloseOnClick);
|
||||
|
||||
restored.StickyAlertsUntilDismissed = false;
|
||||
var transient = service.BuildRequestForCurrentSettings("Upload", "Recovered", AppNotificationSeverity.Info);
|
||||
Assert.Equal(TimeSpan.FromSeconds(4), transient.ExpirationTime);
|
||||
Assert.False(transient.CloseOnClick, "disabling sticky mode restores timed, non-click-dismissable alerts");
|
||||
|
||||
var settings = SourceAssert.Text("Controls", "SettingsDrawer.xaml");
|
||||
Assert.Contains("Content=\"Keep alerts until dismissed\"", settings);
|
||||
Assert.Contains("Binding StickyAlertsUntilDismissed", settings);
|
||||
}
|
||||
finally
|
||||
{
|
||||
window?.Close();
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
SqliteConnection.ClearAllPools();
|
||||
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user