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) ─── /// True when the global socials config has at least one validated entry. public bool HasSocials => _socials != null && _socials.Entries.Count > 0; /// The bar is on the stream when it's enabled and has entries. public bool SocialBarVisible => (_socials?.BarEnabled ?? false) && HasSocials; /// Footer indicator dot: green when the bar is on the stream, red otherwise. public SolidColorBrush SocialBarDotBrush => SocialBarVisible ? BarOnBrush : BarOffBrush; /// Green glow on the bar while it's on the stream. public SolidColorBrush SocialBarGlowBrush => BarOnBrush; /// The global socials config (entries + bar settings), or null. public SocialsConfig? Socials => _socials; /// Canvas.Top for the social bar: 0 = top, 1040 = bottom (40px from the 1080 edge). public double SocialBarTop => _socials?.BarPosition == SocialBarPosition.Top ? 0 : 1040; /// /// 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). /// public static async Task> HealFediverseSoftwareAsync( SocialsConfig socials, ISocialValidator validator, Action? log = null) { var resolved = new Dictionary(); 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; } /// Runs the heal off the UI thread and applies + saves any result. 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}"); } }); } /// /// 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. /// private void RenderSocialBarFrame() { _socialBarFrame = _socials != null && _socials.BarEnabled ? SocialBarRenderer.Render(_socials.Entries) : null; } /// /// Opens the Social Media Site Promotion dialog (6 slots, sign-in gate, /// validation). On save the working copy replaces . /// 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(); } /// Sets the bar's edge; called by the preview click-toggle. public void SetSocialBarPosition(SocialBarPosition position) { if (_socials == null || _socials.BarPosition == position) return; _socials.BarPosition = position; OnPropertyChanged(nameof(SocialBarTop)); ScheduleSave(); } /// Preview click flips the bar top ⇄ bottom (KISS — no drag math). public void ToggleSocialBarPosition() => SetSocialBarPosition(_socials?.BarPosition == SocialBarPosition.Top ? SocialBarPosition.Bottom : SocialBarPosition.Top); }