refactor: split Socials out of MainViewModel — Commit 8/12
MainViewModel.Socials.cs: social-bar state (_socials/_socialBarFrame/ _socialValidator), bar props (HasSocials/SocialBarVisible/SocialBarDotBrush/ SocialBarGlowBrush/Socials/SocialBarTop), OpenSocialDialog + NotifySocialsChanged + RenderSocialBarFrame (SocialBarRenderer), fediverse heal (HealFediverseSoftwareAsync/HealFediverseSoftwareInBackground), SetSocialBarPosition/ToggleSocialBarPosition, OpenSocialDialogCommand. BarOnBrush/BarOffBrush/CreateBrush stay in core (shared tokens used by Audio/Trax partials); core keeps ctor command assignment, LoadLayout, SaveLayoutNow, and the compositor socialBar seam. Docs: index.md restructured — each landed partial (Account..Trax) now owns its own table row, the core row shrinks to only what remains in MainViewModel.cs, and the split tracker is a compact code<->commit map. Verify: 0 warnings, 246 passed, 2 known failures only.
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using ytLive;
|
||||
using ytLive.Helpers;
|
||||
using ytLive.Models;
|
||||
using ytLive.Services;
|
||||
using ytLive.Services.Compositor;
|
||||
|
||||
namespace ytLive.ViewModels;
|
||||
|
||||
public partial class MainViewModel
|
||||
{
|
||||
private SocialsConfig? _socials;
|
||||
private VideoFrame? _socialBarFrame;
|
||||
private readonly ISocialValidator _socialValidator = new HttpSocialValidator();
|
||||
|
||||
public ICommand OpenSocialDialogCommand { get; }
|
||||
|
||||
// ─── Social bar (global resource, shown on every scene) ───
|
||||
|
||||
/// <summary>True when the global socials config has at least one validated entry.</summary>
|
||||
public bool HasSocials => _socials != null && _socials.Entries.Count > 0;
|
||||
|
||||
/// <summary>The bar is on the stream when it's enabled and has entries.</summary>
|
||||
public bool SocialBarVisible => (_socials?.BarEnabled ?? false) && HasSocials;
|
||||
|
||||
/// <summary>Footer indicator dot: green when the bar is on the stream, red otherwise.</summary>
|
||||
public SolidColorBrush SocialBarDotBrush => SocialBarVisible ? BarOnBrush : BarOffBrush;
|
||||
|
||||
/// <summary>Green glow on the bar while it's on the stream.</summary>
|
||||
public SolidColorBrush SocialBarGlowBrush => BarOnBrush;
|
||||
|
||||
/// <summary>The global socials config (entries + bar settings), or null.</summary>
|
||||
public SocialsConfig? Socials => _socials;
|
||||
|
||||
/// <summary>Canvas.Top for the social bar: 0 = top, 1040 = bottom (40px from the 1080 edge).</summary>
|
||||
public double SocialBarTop => _socials?.BarPosition == SocialBarPosition.Top ? 0 : 1040;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a missing nodeinfo software name (mastodon, peertube, ...) for every
|
||||
/// fediverse entry that doesn't have one, so the bar shows the instance's real
|
||||
/// logo instead of the generic fediverse glyph. Best-effort: a failed resolution
|
||||
/// leaves the glyph untouched. Returns handle → software for what was resolved —
|
||||
/// the caller applies + persists (the app hops to the dispatcher; a test applies
|
||||
/// directly).
|
||||
/// </summary>
|
||||
public static async Task<Dictionary<string, string>> HealFediverseSoftwareAsync(
|
||||
SocialsConfig socials,
|
||||
ISocialValidator validator,
|
||||
Action<string>? log = null)
|
||||
{
|
||||
var resolved = new Dictionary<string, string>();
|
||||
if (socials == null || validator == null) return resolved;
|
||||
foreach (var entry in socials.Entries)
|
||||
{
|
||||
if (entry.Service != SocialService.Fediverse
|
||||
|| !string.IsNullOrWhiteSpace(entry.FediverseSoftware))
|
||||
continue;
|
||||
if (!SocialServiceIcons.TryParseFediverse(entry.Handle, out _, out var domain))
|
||||
continue;
|
||||
var software = await validator.ResolveFediverseSoftwareAsync(
|
||||
domain, System.Threading.CancellationToken.None);
|
||||
if (string.IsNullOrWhiteSpace(software)) continue;
|
||||
resolved[entry.Handle] = software;
|
||||
log?.Invoke($"Socials heal: {entry.Handle} → {software}");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/// <summary>Runs the heal off the UI thread and applies + saves any result.</summary>
|
||||
private void HealFediverseSoftwareInBackground()
|
||||
{
|
||||
var socials = _socials;
|
||||
if (socials == null) return;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var resolved = await HealFediverseSoftwareAsync(
|
||||
socials, _socialValidator, m => AppLog.Write(m));
|
||||
if (resolved.Count == 0) return;
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var current = _socials;
|
||||
if (current == null) return;
|
||||
var applied = false;
|
||||
foreach (var entry in current.Entries)
|
||||
{
|
||||
if (!resolved.TryGetValue(entry.Handle, out var software)) continue;
|
||||
if (entry.FediverseSoftware == software) continue;
|
||||
entry.FediverseSoftware = software;
|
||||
applied = true;
|
||||
}
|
||||
if (applied) NotifySocialsChanged(); // re-renders the bar + saves
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLog.Write($"Socials heal failed: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-rasterizes the social bar strip the output compositor overlays. UI thread
|
||||
/// only (WPF rendering); the resulting frame is immutable, so the frame pump may
|
||||
/// read it from its own thread. Null when the bar is off or empty.
|
||||
/// </summary>
|
||||
private void RenderSocialBarFrame()
|
||||
{
|
||||
_socialBarFrame = _socials != null && _socials.BarEnabled
|
||||
? SocialBarRenderer.Render(_socials.Entries)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the Social Media Site Promotion dialog (6 slots, sign-in gate,
|
||||
/// validation). On save the working copy replaces <see cref="_socials"/>.
|
||||
/// </summary>
|
||||
private void OpenSocialDialog()
|
||||
{
|
||||
var dialog = new SocialsDialogViewModel(
|
||||
_socialValidator,
|
||||
SignInAsync,
|
||||
SignOutYouTubeAsync,
|
||||
_youtubeAuth.CurrentChannel,
|
||||
_socials);
|
||||
var window = new ytLive.SocialsDialog(dialog) { Owner = Application.Current.MainWindow };
|
||||
if (window.ShowDialog() == true)
|
||||
{
|
||||
_socials = dialog.BuildSocialsConfig();
|
||||
NotifySocialsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifySocialsChanged()
|
||||
{
|
||||
RenderSocialBarFrame();
|
||||
OnPropertyChanged(nameof(HasSocials));
|
||||
OnPropertyChanged(nameof(SocialBarVisible));
|
||||
OnPropertyChanged(nameof(SocialBarDotBrush));
|
||||
ScheduleSave();
|
||||
}
|
||||
|
||||
/// <summary>Sets the bar's edge; called by the preview click-toggle.</summary>
|
||||
public void SetSocialBarPosition(SocialBarPosition position)
|
||||
{
|
||||
if (_socials == null || _socials.BarPosition == position) return;
|
||||
_socials.BarPosition = position;
|
||||
OnPropertyChanged(nameof(SocialBarTop));
|
||||
ScheduleSave();
|
||||
}
|
||||
|
||||
/// <summary>Preview click flips the bar top ⇄ bottom (KISS — no drag math).</summary>
|
||||
public void ToggleSocialBarPosition()
|
||||
=> SetSocialBarPosition(_socials?.BarPosition == SocialBarPosition.Top
|
||||
? SocialBarPosition.Bottom
|
||||
: SocialBarPosition.Top);
|
||||
}
|
||||
@@ -83,9 +83,6 @@ public partial class MainViewModel : ViewModelBase
|
||||
// news up the real service; the VM is built from XAML so no DI seam exists.
|
||||
private readonly INotificationService _notifications = new NotificationService();
|
||||
|
||||
private SocialsConfig? _socials;
|
||||
private VideoFrame? _socialBarFrame;
|
||||
|
||||
// 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
|
||||
// until the live-stream create flow (TASK 5) supplies the reusable stream URL.
|
||||
@@ -169,33 +166,11 @@ public partial class MainViewModel : ViewModelBase
|
||||
=> StagedScene != null
|
||||
&& !Scenes.Any(s => s.Elements.OfType<Source>().Any(e => e.Type == SourceType.ChatBox));
|
||||
|
||||
// ─── Social bar (global resource, shown on every scene) ───
|
||||
|
||||
private static readonly SolidColorBrush BarOnBrush = CreateBrush("#2ecc71");
|
||||
private static readonly SolidColorBrush BarOffBrush = CreateBrush("#e94560");
|
||||
private static SolidColorBrush CreateBrush(string hex)
|
||||
=> (SolidColorBrush)new BrushConverter().ConvertFromString(hex)!;
|
||||
|
||||
/// <summary>True when the global socials config has at least one validated entry.</summary>
|
||||
public bool HasSocials => _socials != null && _socials.Entries.Count > 0;
|
||||
|
||||
/// <summary>The bar is on the stream when it's enabled and has entries.</summary>
|
||||
public bool SocialBarVisible => (_socials?.BarEnabled ?? false) && HasSocials;
|
||||
|
||||
/// <summary>Footer indicator dot: green when the bar is on the stream, red otherwise.</summary>
|
||||
public SolidColorBrush SocialBarDotBrush => SocialBarVisible ? BarOnBrush : BarOffBrush;
|
||||
|
||||
/// <summary>Green glow on the bar while it's on the stream.</summary>
|
||||
public SolidColorBrush SocialBarGlowBrush => BarOnBrush;
|
||||
|
||||
/// <summary>The global socials config (entries + bar settings), or null.</summary>
|
||||
public SocialsConfig? Socials => _socials;
|
||||
|
||||
/// <summary>Canvas.Top for the social bar: 0 = top, 1040 = bottom (40px from the 1080 edge).</summary>
|
||||
public double SocialBarTop => _socials?.BarPosition == SocialBarPosition.Top ? 0 : 1040;
|
||||
|
||||
private readonly ISocialValidator _socialValidator = new HttpSocialValidator();
|
||||
|
||||
/// <summary>True when the Elements panel should show the "No configurable elements" hint.</summary>
|
||||
public bool IsElementsPanelEmpty => SelectedElement is { } el
|
||||
&& !el.IsDraggable && !el.IsWebcam
|
||||
@@ -586,7 +561,6 @@ public partial class MainViewModel : ViewModelBase
|
||||
public ICommand RemoveSourceCommand { get; }
|
||||
public ICommand EditElementCommand { get; }
|
||||
public ICommand ToggleElementVisibilityCommand { get; }
|
||||
public ICommand OpenSocialDialogCommand { get; }
|
||||
public ICommand StartStreamCommand { get; }
|
||||
public ICommand EndStreamCommand { get; }
|
||||
public ICommand ChooseRecordFolderCommand { get; }
|
||||
@@ -1913,127 +1887,4 @@ public partial class MainViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(RecDotOpacity));
|
||||
}
|
||||
|
||||
// ─── Social bar ───
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a missing nodeinfo software name (mastodon, peertube, ...) for every
|
||||
/// fediverse entry that doesn't have one, so the bar shows the instance's real
|
||||
/// logo instead of the generic fediverse glyph. Best-effort: a failed resolution
|
||||
/// leaves the glyph untouched. Returns handle → software for what was resolved —
|
||||
/// the caller applies + persists (the app hops to the dispatcher; a test applies
|
||||
/// directly).
|
||||
/// </summary>
|
||||
public static async Task<Dictionary<string, string>> HealFediverseSoftwareAsync(
|
||||
SocialsConfig socials,
|
||||
ISocialValidator validator,
|
||||
Action<string>? log = null)
|
||||
{
|
||||
var resolved = new Dictionary<string, string>();
|
||||
if (socials == null || validator == null) return resolved;
|
||||
foreach (var entry in socials.Entries)
|
||||
{
|
||||
if (entry.Service != SocialService.Fediverse
|
||||
|| !string.IsNullOrWhiteSpace(entry.FediverseSoftware))
|
||||
continue;
|
||||
if (!SocialServiceIcons.TryParseFediverse(entry.Handle, out _, out var domain))
|
||||
continue;
|
||||
var software = await validator.ResolveFediverseSoftwareAsync(
|
||||
domain, System.Threading.CancellationToken.None);
|
||||
if (string.IsNullOrWhiteSpace(software)) continue;
|
||||
resolved[entry.Handle] = software;
|
||||
log?.Invoke($"Socials heal: {entry.Handle} → {software}");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/// <summary>Runs the heal off the UI thread and applies + saves any result.</summary>
|
||||
private void HealFediverseSoftwareInBackground()
|
||||
{
|
||||
var socials = _socials;
|
||||
if (socials == null) return;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var resolved = await HealFediverseSoftwareAsync(
|
||||
socials, _socialValidator, m => AppLog.Write(m));
|
||||
if (resolved.Count == 0) return;
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var current = _socials;
|
||||
if (current == null) return;
|
||||
var applied = false;
|
||||
foreach (var entry in current.Entries)
|
||||
{
|
||||
if (!resolved.TryGetValue(entry.Handle, out var software)) continue;
|
||||
if (entry.FediverseSoftware == software) continue;
|
||||
entry.FediverseSoftware = software;
|
||||
applied = true;
|
||||
}
|
||||
if (applied) NotifySocialsChanged(); // re-renders the bar + saves
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLog.Write($"Socials heal failed: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-rasterizes the social bar strip the output compositor overlays. UI thread
|
||||
/// only (WPF rendering); the resulting frame is immutable, so the frame pump may
|
||||
/// read it from its own thread. Null when the bar is off or empty.
|
||||
/// </summary>
|
||||
private void RenderSocialBarFrame()
|
||||
{
|
||||
_socialBarFrame = _socials != null && _socials.BarEnabled
|
||||
? SocialBarRenderer.Render(_socials.Entries)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the Social Media Site Promotion dialog (6 slots, sign-in gate,
|
||||
/// validation). On save the working copy replaces <see cref="_socials"/>.
|
||||
/// </summary>
|
||||
private void OpenSocialDialog()
|
||||
{
|
||||
var dialog = new SocialsDialogViewModel(
|
||||
_socialValidator,
|
||||
SignInAsync,
|
||||
SignOutYouTubeAsync,
|
||||
_youtubeAuth.CurrentChannel,
|
||||
_socials);
|
||||
var window = new ytLive.SocialsDialog(dialog) { Owner = Application.Current.MainWindow };
|
||||
if (window.ShowDialog() == true)
|
||||
{
|
||||
_socials = dialog.BuildSocialsConfig();
|
||||
NotifySocialsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifySocialsChanged()
|
||||
{
|
||||
RenderSocialBarFrame();
|
||||
OnPropertyChanged(nameof(HasSocials));
|
||||
OnPropertyChanged(nameof(SocialBarVisible));
|
||||
OnPropertyChanged(nameof(SocialBarDotBrush));
|
||||
ScheduleSave();
|
||||
}
|
||||
|
||||
/// <summary>Sets the bar's edge; called by the preview click-toggle.</summary>
|
||||
public void SetSocialBarPosition(SocialBarPosition position)
|
||||
{
|
||||
if (_socials == null || _socials.BarPosition == position) return;
|
||||
_socials.BarPosition = position;
|
||||
OnPropertyChanged(nameof(SocialBarTop));
|
||||
ScheduleSave();
|
||||
}
|
||||
|
||||
/// <summary>Preview click flips the bar top ⇄ bottom (KISS — no drag math).</summary>
|
||||
public void ToggleSocialBarPosition()
|
||||
=> SetSocialBarPosition(_socials?.BarPosition == SocialBarPosition.Top
|
||||
? SocialBarPosition.Bottom
|
||||
: SocialBarPosition.Top);
|
||||
|
||||
}
|
||||
|
||||
+27
-63
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user