using System.Windows.Media;
using ytLive.Helpers;
namespace ytLive.Models;
public enum SourceType
{
DisplayCapture,
WindowCapture,
Background,
Image,
TextOverlay,
Countdown,
WebSource,
ChatBox,
/// The session scoreboard strip: full-width, state not events.
Scoreboard,
MediaSource,
/// The designated alert zone: the six monetization events play their
/// unique celebration animations here (OBS alert-box equivalent, native).
AlertBox,
/// A privacy blur: blurs the frame beneath it inside a rect or ellipse. Live scene
/// only, always below the brand logo, several allowed (strongest wins on overlap).
Blur,
/// A scene-local feed of recent alert events.
Ticker
}
/// Distinguishes how a media source presents its content (TASK 21).
public enum MediaSourceType
{
Video,
Audio
}
/// Playback state of a media source (TASK 21).
public enum MediaPlaybackState
{
Stopped,
Playing,
Paused
}
public enum TextLayerAlignment
{
Left,
Center,
Right
}
public enum ClipShape
{
Traditional,
Round
}
/// How an alert box presents its announcement strip (TASK 47). The strip is
/// the full-width pill naming the event ("Viewer — Super Chat · $12.34").
public enum AlertDisplayMethod
{
/// Marquee: the pill scrolls across the top edge, paced so the text is
/// read times per alert.
TickerScroll,
/// Pulse: the static pill blinks on and off for the whole alert.
Flash,
/// Static: the pill sits centred and still for the whole alert.
Solid
}
///
/// A multi-instance scene object: image/background/text (screen/window later).
/// The webcam is NOT a Source — it's a singleton resource whose per-scene usage
/// is a . See the singleton-vs-multi-instance
/// discriminator in ai.md.
///
public class Source : SceneElement
{
private readonly Ticker _ticker = new();
public Source()
{
_ticker.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(Ticker.IsEnabled)) Raise(nameof(AlertShowTicker));
if (e.PropertyName == nameof(Ticker.DisplayMethod)) Raise(nameof(AlertDisplayMethod));
};
}
public Ticker Ticker => _ticker;
private SourceType _type;
public SourceType Type
{
get => _type;
set
{
if (Set(ref _type, value))
{
IsBackground = value == SourceType.Background;
Raise(nameof(IsLiveCapture));
Raise(nameof(DisplaySource));
Raise(nameof(IsWebSource));
Raise(nameof(IsTextOverlay));
Raise(nameof(IsScoreboard));
Raise(nameof(IsTicker));
}
}
}
/// Blur is draggable like a layer: it has no pixels of its own, but the creator places
/// and sizes it in the preview, so it needs the same selection box, grab and resize handle
/// (creator ruling 2026-11-01).
public override bool IsDraggable => Type is SourceType.Image or SourceType.WebSource or SourceType.ChatBox
or SourceType.TextOverlay or SourceType.AlertBox or SourceType.Blur or SourceType.Ticker;
/// Live captures, chat, web sources and media are dynamic; static art is fixed.
public override ElementKind Kind => Type is SourceType.DisplayCapture or SourceType.WindowCapture
or SourceType.ChatBox or SourceType.WebSource or SourceType.MediaSource or SourceType.AlertBox
or SourceType.Blur or SourceType.Ticker
? ElementKind.Dynamic : ElementKind.Static;
public override bool IsWebSource => Type == SourceType.WebSource;
public override bool IsTextOverlay => Type == SourceType.TextOverlay;
public override bool IsScoreboard => Type == SourceType.Scoreboard;
public bool IsTicker => Type == SourceType.Ticker;
/// The permanent brand logo layer (one per scene, directly above the background).
/// Locked against removal; its image comes from the App Settings brand logo.
public bool IsBrandLogo { get; set; }
/// The permanent scene background (live desktop/game capture). Never removable/reorderable.
private bool _isBackground;
public override bool IsBackground { get => _isBackground; set => Set(ref _isBackground, value); }
private bool _showDesktop;
public bool ShowDesktop { get => _showDesktop; set => Set(ref _showDesktop, value); }
private bool _isEnabled = true;
public bool IsEnabled { get => _isEnabled; set => Set(ref _isEnabled, value); }
// Display/Window capture
private int? _monitorIndex;
public int? MonitorIndex { get => _monitorIndex; set => Set(ref _monitorIndex, value); }
private IntPtr? _windowHandle;
public IntPtr? WindowHandle { get => _windowHandle; set => Set(ref _windowHandle, value); }
///
/// Identifies what this live-capture source points at: "monitor:<index>" or
/// "window:<hwnd>". Persisted so a re-designated background survives a reload.
///
private string? _captureKey;
public string? CaptureKey { get => _captureKey; set => Set(ref _captureKey, value); }
public override bool IsLiveCapture => Type is SourceType.DisplayCapture or SourceType.WindowCapture;
// Image (asset stored in the layout database)
private string? _assetId;
private ImageSource? _imageSource;
public string? AssetId
{
get => _assetId;
set
{
if (Set(ref _assetId, value))
{
_imageSource = ImageCache.Get(value ?? string.Empty);
Raise(nameof(ImageSource));
Raise(nameof(DisplaySource));
}
}
}
public ImageSource? ImageSource => _imageSource;
/// Types whose frame is produced at runtime rather than stored as an asset. The
/// scoreboard joined this list 2026-10-30 — without it the strip was rendered, published to
/// VideoImageSource, and never displayed (the same shape as the invisible-alert defect).
private bool IsFrameSource =>
Type is SourceType.ChatBox or SourceType.WebSource or SourceType.MediaSource
or SourceType.AlertBox or SourceType.Scoreboard or SourceType.Ticker;
public override ImageSource? DisplaySource => IsLiveCapture
? (VideoImageSource ?? _imageSource)
: (IsFrameSource ? VideoImageSource : _imageSource);
public override bool IsImageSource => Type == SourceType.Image;
// Countdown config
private int _timerMinutes = 15;
// Privacy blur (Live scene only): 0–1, the same scale and default as the webcam's background
// blur so "half strength" means the same blur in both places.
private double _blurAmount = 0.5;
public double BlurAmount
{
get => _blurAmount;
set => Set(ref _blurAmount, Math.Clamp(value, 0, 1));
}
public bool IsBlur => Type == SourceType.Blur;
/// True for a round blur (circle) — the shape rides the existing clip surface, so the
/// box/circle choice is the same switch every other layer uses rather than a second concept.
public bool IsRoundBlur => IsBlur && ClipShape == ClipShape.Round;
public int TimerMinutes
{
get => _timerMinutes;
set => Set(ref _timerMinutes, Math.Clamp(value, 1, 60));
}
// Web source config
private string? _webUri;
public string? WebUri { get => _webUri; set => Set(ref _webUri, value); }
// Media source config (TASK 21: video/audio file playback)
private MediaSourceType _mediaType = MediaSourceType.Video;
public MediaSourceType MediaType
{
get => _mediaType;
set => Set(ref _mediaType, value);
}
private string? _mediaPath;
public string? MediaPath
{
get => _mediaPath;
set
{
if (Set(ref _mediaPath, value))
Raise(nameof(IsMediaSource));
}
}
private bool _mediaIsLooping;
public bool MediaIsLooping
{
get => _mediaIsLooping;
set => Set(ref _mediaIsLooping, value);
}
private double _mediaVolume = 1.0;
public double MediaVolume
{
get => _mediaVolume;
set => Set(ref _mediaVolume, Math.Clamp(value, 0.0, 1.0));
}
private MediaPlaybackState _mediaPlaybackState = MediaPlaybackState.Stopped;
public MediaPlaybackState MediaPlaybackState
{
get => _mediaPlaybackState;
set => Set(ref _mediaPlaybackState, value);
}
public override bool IsMediaSource => Type == SourceType.MediaSource;
// Alert video config (TASK 47): the shipped default clip lives in the Asset
// table (seeded like image assets); the creator's replacement is a FILE PATH
// reference only — never into the DB (creator ruling, 2026-09-26). Runtime
// order: replacement path exists → DB default → six celebration animations.
private string? _alertVideoAssetId;
public string? AlertVideoAssetId
{
get => _alertVideoAssetId;
set => Set(ref _alertVideoAssetId, value);
}
private string? _alertVideoPath;
public string? AlertVideoPath
{
get => _alertVideoPath;
set => Set(ref _alertVideoPath, value);
}
private bool _alertUseDefaultVideo = true;
public bool AlertUseDefaultVideo
{
get => _alertUseDefaultVideo;
set => Set(ref _alertUseDefaultVideo, value);
}
public bool AlertShowTicker
{
get => _ticker.IsEnabled;
set => _ticker.IsEnabled = value;
}
private double _alertVideoVolume = 1.0;
public double AlertVideoVolume
{
get => _alertVideoVolume;
set => Set(ref _alertVideoVolume, Math.Clamp(value, 0.0, 1.0));
}
// Sound byte that plays with the alert clip (creator ruling 2026-11-01). Referenced from disk
// like the clip; the video always plays once to completion, the sound may loop under it.
private string? _alertAudioPath;
public string? AlertAudioPath
{
get => _alertAudioPath;
set => Set(ref _alertAudioPath, value);
}
private bool _alertAudioLoop;
public bool AlertAudioLoop
{
get => _alertAudioLoop;
set => Set(ref _alertAudioLoop, value);
}
public string AlertAudioFileDisplay =>
string.IsNullOrWhiteSpace(_alertAudioPath) ? "No sound byte" : System.IO.Path.GetFileName(_alertAudioPath);
/// True once a file is chosen — gates the loop switch and the clear button.
public bool HasAlertAudio => !string.IsNullOrWhiteSpace(_alertAudioPath);
public AlertDisplayMethod AlertDisplayMethod
{
get => _ticker.DisplayMethod;
set => _ticker.DisplayMethod = value;
}
public override bool IsAlertBox => Type == SourceType.AlertBox;
// Chat box config (TASK 3.18)
private double _chatFontSize = 16;
public double ChatFontSize
{
get => _chatFontSize;
set => Set(ref _chatFontSize, Math.Clamp(value, 8, 72));
}
private string _chatFontColor = "#FFFFFF";
public string ChatFontColor { get => _chatFontColor; set => Set(ref _chatFontColor, value); }
private string _chatBackgroundColor = "#80000000";
public string ChatBackgroundColor
{
get => _chatBackgroundColor;
set
{
if (Set(ref _chatBackgroundColor, value))
Raise(nameof(ChatBackgroundOpacity));
}
}
/// Alpha channel of ChatBackgroundColor as 0.0–1.0. Slider writes back the full hex string.
public double ChatBackgroundOpacity
{
get
{
var hex = _chatBackgroundColor.Trim().TrimStart('#');
return hex.Length == 8
? byte.Parse(hex.AsSpan(0, 2), System.Globalization.NumberStyles.HexNumber) / 255.0
: 1.0;
}
set
{
var alpha = (byte)Math.Clamp(value * 255, 0, 255);
var hex = _chatBackgroundColor.Trim().TrimStart('#');
if (hex.Length == 8) hex = hex[2..];
if (hex.Length == 6) ChatBackgroundColor = $"#{alpha:X2}{hex}";
}
}
private int _chatMaxMessages = 5;
public int ChatMaxMessages
{
get => _chatMaxMessages;
set => Set(ref _chatMaxMessages, Math.Clamp(value, 1, 20));
}
private bool _chatShowTimestamps = true;
public bool ChatShowTimestamps { get => _chatShowTimestamps; set => Set(ref _chatShowTimestamps, value); }
private bool _chatShowBadges = true;
public bool ChatShowBadges { get => _chatShowBadges; set => Set(ref _chatShowBadges, value); }
private string _chatMemberColor = "#e94560";
public string ChatMemberColor { get => _chatMemberColor; set => Set(ref _chatMemberColor, value); }
private string _chatSuperChatColor = "#eab308";
public string ChatSuperChatColor { get => _chatSuperChatColor; set => Set(ref _chatSuperChatColor, value); }
private string _chatFontFamily = "Segoe UI";
public string ChatFontFamily { get => _chatFontFamily; set => Set(ref _chatFontFamily, value); }
private int _chatFadeSeconds;
public int ChatFadeSeconds
{
get => _chatFadeSeconds;
set => Set(ref _chatFadeSeconds, Math.Clamp(value, 0, 60));
}
public override bool IsChatBox => Type == SourceType.ChatBox;
private string _textContent = "Text";
public string TextContent
{
get => _textContent;
set => Set(ref _textContent, value ?? string.Empty);
}
private string _textFontFamily = "Segoe UI";
public string TextFontFamily
{
get => _textFontFamily;
set => Set(ref _textFontFamily, string.IsNullOrWhiteSpace(value) ? "Segoe UI" : value.Trim());
}
private double _textFontSize = 48;
public double TextFontSize
{
get => _textFontSize;
set => Set(ref _textFontSize, double.IsFinite(value) ? Math.Clamp(value, 8, 300) : 48);
}
private string _textColor = "#FFFFFF";
public string TextColor
{
get => _textColor;
set => Set(ref _textColor, IsValidTextColor(value) ? value.ToUpperInvariant() : "#FFFFFF");
}
private bool _textBold;
public bool TextBold { get => _textBold; set => Set(ref _textBold, value); }
private bool _textItalic;
public bool TextItalic { get => _textItalic; set => Set(ref _textItalic, value); }
private bool _textUnderline;
public bool TextUnderline { get => _textUnderline; set => Set(ref _textUnderline, value); }
private TextLayerAlignment _textAlignment;
public TextLayerAlignment TextAlignment
{
get => _textAlignment;
set => Set(ref _textAlignment, Enum.IsDefined(value) ? value : TextLayerAlignment.Left);
}
private static bool IsValidTextColor(string? value)
{
if (value is not { Length: 7 or 9 } || value[0] != '#') return false;
for (var i = 1; i < value.Length; i++)
if (!Uri.IsHexDigit(value[i])) return false;
return true;
}
public override bool IsCountdown => Type == SourceType.Countdown;
private bool _chatPreviewEnabled = true;
public bool ChatPreviewEnabled
{
get => _chatPreviewEnabled;
set => Set(ref _chatPreviewEnabled, value);
}
}