Social bar (Branch B): global resource with per-scene presence — Models/Socials.cs (19 services, icons, canonical URLs), Services/SocialValidator.cs (HTTP lookup validation seam), LayoutStore schema v7 (Socials + SocialEntry tables + Scene.HasSocialBar), MainViewModel (socials collection, AddSocial/RemoveSocial/ToggleSceneSocialBar commands, freemium cap YT+1, premium seam), MainWindow.xaml (footer Social button + [+/-] toggle on meter line, preview bar rendering top/bottom + LCR), 4 integration tests (roundtrip, empty-not-persisted, canonical URLs, icons), 85 tests passing, 0 warnings
This commit is contained in:
@@ -12,6 +12,7 @@ public class Scene : INotifyPropertyChanged
|
||||
private bool _isEditing;
|
||||
private bool _isHidden;
|
||||
private bool _hasBackdrop;
|
||||
private bool _hasSocialBar;
|
||||
|
||||
public string Name
|
||||
{
|
||||
@@ -41,6 +42,16 @@ public class Scene : INotifyPropertyChanged
|
||||
set => Set(ref _hasBackdrop, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the social bar plays on this scene. Toggled by the footer [+]/[-]
|
||||
/// control — the socials themselves are a global resource (see SocialsConfig).
|
||||
/// </summary>
|
||||
public bool HasSocialBar
|
||||
{
|
||||
get => _hasSocialBar;
|
||||
set => Set(ref _hasSocialBar, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The scene's rendered elements in z-order (back to front): multi-instance
|
||||
/// Sources plus this scene's webcam usage (<see cref="WebcamConfig"/>), if any.
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace ytLive.Models;
|
||||
|
||||
public enum SocialService
|
||||
{
|
||||
YouTube, Twitch, X, Instagram, TikTok, Facebook, Discord, Kick,
|
||||
Threads, Bluesky, GitHub, LinkedIn, Pinterest, Snapchat, Reddit,
|
||||
WhatsApp, Telegram, Link, Website
|
||||
}
|
||||
|
||||
public enum SocialBarPosition { Top, Bottom }
|
||||
public enum SocialBarJustify { Left, Center, Right }
|
||||
|
||||
public sealed class SocialEntry : INotifyPropertyChanged
|
||||
{
|
||||
public SocialService Service { get; init; }
|
||||
public string Handle { get; init; } = string.Empty;
|
||||
public string ProfileUrl { get; init; } = string.Empty;
|
||||
|
||||
public string ServiceName => Service.ToString();
|
||||
public string Initials => SocialServiceIcons.InitialsFor(Service);
|
||||
public string AccentColor => SocialServiceIcons.ColorFor(Service);
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
private void Raise([CallerMemberName] string? name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The app-wide social bar config — a global resource (like the webcam) whose
|
||||
/// per-scene presence is toggled by <see cref="Scene.HasSocialBar"/>. The creator
|
||||
/// provides handles/URLs; the app validates each by looking up the social page
|
||||
/// before adding it. Freemium: YT + 1 other. Premium: as many as fit one line.
|
||||
/// </summary>
|
||||
public sealed class SocialsConfig : INotifyPropertyChanged
|
||||
{
|
||||
public ObservableCollection<SocialEntry> Entries { get; } = new();
|
||||
|
||||
private SocialBarPosition _barPosition = SocialBarPosition.Bottom;
|
||||
public SocialBarPosition BarPosition
|
||||
{
|
||||
get => _barPosition;
|
||||
set => Set(ref _barPosition, value);
|
||||
}
|
||||
|
||||
private SocialBarJustify _barJustify = SocialBarJustify.Center;
|
||||
public SocialBarJustify BarJustify
|
||||
{
|
||||
get => _barJustify;
|
||||
set => Set(ref _barJustify, value);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
private bool Set<T>(ref T field, T value, [CallerMemberName] string? name = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SocialServiceIcons
|
||||
{
|
||||
public static string InitialsFor(SocialService service) => service switch
|
||||
{
|
||||
SocialService.YouTube => "YT",
|
||||
SocialService.Twitch => "TW",
|
||||
SocialService.X => "X",
|
||||
SocialService.Instagram => "IG",
|
||||
SocialService.TikTok => "TT",
|
||||
SocialService.Facebook => "FB",
|
||||
SocialService.Discord => "DC",
|
||||
SocialService.Kick => "K",
|
||||
SocialService.Threads => "TH",
|
||||
SocialService.Bluesky => "BS",
|
||||
SocialService.GitHub => "GH",
|
||||
SocialService.LinkedIn => "LI",
|
||||
SocialService.Pinterest => "PN",
|
||||
SocialService.Snapchat => "SC",
|
||||
SocialService.Reddit => "RD",
|
||||
SocialService.WhatsApp => "WA",
|
||||
SocialService.Telegram => "TG",
|
||||
SocialService.Link => "L",
|
||||
_ => "★",
|
||||
};
|
||||
|
||||
public static string ColorFor(SocialService service) => service switch
|
||||
{
|
||||
SocialService.YouTube => "#FF0000",
|
||||
SocialService.Twitch => "#9146FF",
|
||||
SocialService.X => "#000000",
|
||||
SocialService.Instagram => "#E4405F",
|
||||
SocialService.TikTok => "#010101",
|
||||
SocialService.Facebook => "#1877F2",
|
||||
SocialService.Discord => "#5865F2",
|
||||
SocialService.Kick => "#53FC18",
|
||||
SocialService.Threads => "#010101",
|
||||
SocialService.Bluesky => "#0085FF",
|
||||
SocialService.GitHub => "#181717",
|
||||
SocialService.LinkedIn => "#0A66C2",
|
||||
SocialService.Pinterest => "#BD081C",
|
||||
SocialService.Snapchat => "#FFFC00",
|
||||
SocialService.Reddit => "#FF4500",
|
||||
SocialService.WhatsApp => "#25D366",
|
||||
SocialService.Telegram => "#26A5E4",
|
||||
_ => "#e94560",
|
||||
};
|
||||
|
||||
public static string CanonicalUrlFor(SocialService service, string handle)
|
||||
{
|
||||
var h = handle.Trim().TrimStart('@');
|
||||
return service switch
|
||||
{
|
||||
SocialService.YouTube => $"https://www.youtube.com/@{h}",
|
||||
SocialService.Twitch => $"https://www.twitch.tv/{h}",
|
||||
SocialService.X => $"https://x.com/{h}",
|
||||
SocialService.Instagram => $"https://www.instagram.com/{h}",
|
||||
SocialService.TikTok => $"https://www.tiktok.com/@{h}",
|
||||
SocialService.Facebook => $"https://www.facebook.com/{h}",
|
||||
SocialService.Discord => $"https://discord.gg/{h}",
|
||||
SocialService.Kick => $"https://kick.com/{h}",
|
||||
SocialService.Threads => $"https://www.threads.net/@{h}",
|
||||
SocialService.Bluesky => $"https://bsky.app/profile/{h}",
|
||||
SocialService.GitHub => $"https://github.com/{h}",
|
||||
SocialService.LinkedIn => $"https://www.linkedin.com/in/{h}",
|
||||
SocialService.Pinterest => $"https://www.pinterest.com/{h}",
|
||||
SocialService.Snapchat => $"https://www.snapchat.com/add/{h}",
|
||||
SocialService.Reddit => $"https://www.reddit.com/user/{h}",
|
||||
SocialService.WhatsApp => $"https://wa.me/{h}",
|
||||
SocialService.Telegram => $"https://t.me/{h}",
|
||||
SocialService.Link or SocialService.Website => h.StartsWith("http") ? h : $"https://{h}",
|
||||
_ => $"https://{h}",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ Plain data types. No logic beyond what a property can carry. See
|
||||
| `QualityOption.cs` | Resolution tier record `(Display, Fps, Bitrate, Width, Height)`; `Label` formats as `1080p60 (8Mbps)` — drives the bottom-bar dropdown and the output-rect math |
|
||||
| `StreamConfig.cs` | `StreamConfig` (note: `TargetBitrate`/`Resolution` defaults are stale — not yet wired to the dropdown), `StreamHealth` (bitrate/FPS/dropped/duration), `StreamStatus` enum |
|
||||
| `YouTube.cs` | `YouTubeChannel` and `ChatMessage` (chat feed) |
|
||||
| `Socials.cs` | **Social bar** (global resource, per-scene presence): `SocialService` enum (YouTube/Twitch/X/Instagram/TikTok/Facebook/Discord/Kick/Threads/Bluesky/GitHub/LinkedIn/Pinterest/Snapchat/Reddit/WhatsApp/Telegram/Link/Website), `SocialEntry` (Service/Handle/ProfileUrl + Initials/AccentColor), `SocialsConfig` (Entries + BarPosition Top/Bottom + BarJustify LCR), `SocialServiceIcons` (canonical URL builder + initials/color per service) |
|
||||
|
||||
Related: [`ViewModels/index.md`](../ViewModels/index.md) consume these;
|
||||
[`Services/LayoutStore.cs`](../Services/LayoutStore.cs) persists `Scene`/`Source`.
|
||||
|
||||
Reference in New Issue
Block a user