feat(alerts): add scene-local recent ticker
Model the AlertBox and standalone ticker with shared settings; scene tickers render the in-memory active and queued events without saving history. Keep AlertBox content active-event-only. Ref: https://obsproject.com/kb/obs-studio-overview
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using System.Linq;
|
||||
using ytLive.Models;
|
||||
using ytLive.Services;
|
||||
|
||||
namespace ytLive.ViewModels;
|
||||
@@ -44,8 +46,15 @@ public partial class MainViewModel
|
||||
/// Reuses one <see cref="System.Windows.Media.Imaging.WriteableBitmap"/> and
|
||||
/// overwrites its back buffer — a fresh bitmap every tick would churn the
|
||||
/// render cache for no reason.</summary>
|
||||
internal void PublishAlertTickerPreview(VideoFrame? frame)
|
||||
internal void PublishAlertTickerPreview(VideoFrame? incomingFrame)
|
||||
{
|
||||
var frame = incomingFrame;
|
||||
var previewScene = IsEditMode ? StagedScene : LiveScene ?? StagedScene;
|
||||
var alertBox = previewScene?.Elements.OfType<Source>()
|
||||
.FirstOrDefault(source => source.Type == SourceType.AlertBox && source.IsVisible);
|
||||
if (_alertLayer.IsPlaying)
|
||||
frame = _alertLayer.AlertBoxTickerFrame(alertBox);
|
||||
|
||||
if (frame == null)
|
||||
{
|
||||
if (_alertTickerBitmap != null)
|
||||
|
||||
@@ -44,6 +44,11 @@ public partial class MainViewModel : ViewModelBase
|
||||
=> StagedScene != null
|
||||
&& !Scenes.Any(s => s.Elements.OfType<Source>().Any(e => e.Type == SourceType.AlertBox));
|
||||
|
||||
/// <summary>A recent-alert ticker is an independent layer, one per scene.</summary>
|
||||
public bool CanAddTicker
|
||||
=> StagedScene != null
|
||||
&& !StagedScene.Elements.OfType<Source>().Any(e => e.Type == SourceType.Ticker);
|
||||
|
||||
/// <summary>Privacy blur is a Live-scene tool only (creator ruling 2026-11-01) — every other
|
||||
/// scene's art is authored, and there is no desktop there to hide.</summary>
|
||||
public bool CanAddBlur
|
||||
|
||||
@@ -55,6 +55,7 @@ public partial class MainViewModel
|
||||
OnPropertyChanged(nameof(CanChangeBackground));
|
||||
OnPropertyChanged(nameof(CanAddWebcam));
|
||||
OnPropertyChanged(nameof(CanAddYouTubeChat));
|
||||
OnPropertyChanged(nameof(CanAddTicker));
|
||||
OnPropertyChanged(nameof(CanShowWebcamInStagedScene));
|
||||
OnPropertyChanged(nameof(WebcamAddToolTip));
|
||||
OnPropertyChanged(nameof(IsCountdownAvailable));
|
||||
@@ -313,6 +314,7 @@ public partial class MainViewModel
|
||||
element.PropertyChanged -= OnElementPropertyChanged;
|
||||
OnPropertyChanged(nameof(CanAddWebcam));
|
||||
OnPropertyChanged(nameof(CanAddYouTubeChat));
|
||||
OnPropertyChanged(nameof(CanAddTicker));
|
||||
ScheduleSave();
|
||||
}
|
||||
|
||||
@@ -356,4 +358,4 @@ public partial class MainViewModel
|
||||
Scenes.Add(scene);
|
||||
StagedScene = scene;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ public partial class MainViewModel
|
||||
SourceType.WebSource => "Web Resource",
|
||||
SourceType.ChatBox => "YouTube Chat",
|
||||
SourceType.AlertBox => "Stream Alerts",
|
||||
SourceType.Ticker => "Ticker",
|
||||
SourceType.Scoreboard => "Scoreboard",
|
||||
_ => "Layer",
|
||||
};
|
||||
@@ -45,11 +46,12 @@ public partial class MainViewModel
|
||||
// The (+) menu only adds regular layers. The Background is a locked
|
||||
// per-screen layer managed by EnsureBackground/NormalizeBackgrounds —
|
||||
// never user-created, never deleted. YouTube Chat and Stream Alerts
|
||||
// are one-per-layout.
|
||||
// are one-per-layout; a ticker is one-per-scene.
|
||||
if (sourceType is SourceType.DisplayCapture or SourceType.WindowCapture or SourceType.Background)
|
||||
return;
|
||||
if ((sourceType == SourceType.ChatBox && !CanAddYouTubeChat)
|
||||
|| (sourceType == SourceType.AlertBox && !CanAddAlerts))
|
||||
|| (sourceType == SourceType.AlertBox && !CanAddAlerts)
|
||||
|| (sourceType == SourceType.Ticker && !CanAddTicker))
|
||||
return;
|
||||
|
||||
// Web resources use "Web Resource-N" naming (0-indexed per scene)
|
||||
@@ -73,11 +75,13 @@ public partial class MainViewModel
|
||||
: sourceType == SourceType.ChatBox ? 500
|
||||
: sourceType == SourceType.AlertBox ? 680
|
||||
: sourceType == SourceType.Scoreboard ? 1920
|
||||
: sourceType == SourceType.Ticker ? 1920
|
||||
: sourceType == SourceType.Blur ? 240 : 0,
|
||||
Height = sourceType == SourceType.WebSource ? 480
|
||||
: sourceType == SourceType.ChatBox ? 648
|
||||
: sourceType == SourceType.AlertBox ? 200
|
||||
: sourceType == SourceType.Scoreboard ? 56
|
||||
: sourceType == SourceType.Ticker ? 48
|
||||
: sourceType == SourceType.Blur ? 160 : 0,
|
||||
};
|
||||
|
||||
|
||||
@@ -405,7 +405,8 @@ public partial class MainViewModel : ViewModelBase
|
||||
encoderFactory: () => new FfmpegEncoder(new FfmpegLocator()),
|
||||
log: message => AppLog.Write(message),
|
||||
socialBar: () => (_socialBarFrame, _socials?.BarPosition ?? SocialBarPosition.Bottom),
|
||||
alertTicker: () => _alertLayer.AlertTickerFrame,
|
||||
alertTicker: scene => _alertLayer.AlertBoxTickerFrame(
|
||||
scene.Elements.OfType<Source>().FirstOrDefault(source => source.Type == SourceType.AlertBox && source.IsVisible)),
|
||||
brandFlash: BrandFlashFrame,
|
||||
transition: _transition,
|
||||
sceneGraph: _sceneGraph);
|
||||
@@ -639,6 +640,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
WebcamSceneConfig webcam => _cameraManager.GetLatestFrame(DeviceKeyForWebcam(webcam.WebcamId), EffectKeyFor(webcam)),
|
||||
Source { IsLiveCapture: true, CaptureKey: not null } live => _screenCaptureManager.GetLatestFrame(live.CaptureKey),
|
||||
Source { Type: SourceType.ChatBox } chatBox => RenderChatBox(chatBox),
|
||||
Source { Type: SourceType.Ticker } ticker => _alertLayer.RenderSceneTicker(ticker),
|
||||
Source { Type: SourceType.Scoreboard } => _scoreboardPresenter?.Frame,
|
||||
Source { Type: SourceType.AlertBox } alertBox => RenderAlertBox(alertBox),
|
||||
Source { Type: SourceType.WebSource } web => _webView2Manager?.GetLatestFrame(web.Id),
|
||||
|
||||
@@ -28,6 +28,9 @@ areas' docs in their own rows keeps this index small to read.
|
||||
| `MicPickerViewModel.cs` | Select Microphone dialog (voice source): async mic list (loading / has / none states), `UseRequested(MicrophoneDeviceInfo)`/`CancelRequested` |
|
||||
| `SocialsDialogViewModel.cs` | Social Media Site Promotion dialog (six fixed slots, `SocialSlotViewModel`): row 0 = YouTube sign-in gate (sign-in provider fills the channel, delete = sign-out); rows 1-5 validated entries / empty / freemium locks 2-5 (`isPremium` seam); `DetectService` → `ISocialValidator` on confirm (input snaps to text + real logo on success, red do-not + stays editable on failure); `CanSave` blocked while any slot edits/validates/errors; `BarEnabled` switch; commit via `CommittedEntries` + `BuildSocialsConfig()` (YouTube first) + `SaveRequested`/`CancelRequested`. WPF-free — the window owns the dialogs (delete confirm, sign-in gate) and calls `StartEdit`/`ConfirmEdit`/`DeleteSlotAsync`. |
|
||||
|
||||
| `MainViewModel.Sources.cs` | Source creation/removal; `AddSource` creates the standalone ticker layer with one-per-scene gating and its 1920×48 default geometry. |
|
||||
| `MainViewModel.AlertTicker.cs` | AlertBox ticker preview routing: selects the preview scene's AlertBox settings and publishes the positioned box-sized frame to PreviewPane. |
|
||||
|
||||
Related: [`Models/index.md`](../Models/index.md) for the data; [`Services/index.md`](../Services/index.md)
|
||||
for what the ViewModels drive; base class in [`Helpers/ViewModelBase.cs`](../Helpers/ViewModelBase.cs).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user