Sits to the right of the Built-in video pill, labelled "Test alert". It enqueues one synthetic Super Chat through the real AlertOverlayLayer, so what plays is exactly what a viewer triggers — the custom clip when one is set, the built-in clip otherwise — and it goes through the same output path (preview and encoder) as a live alert. Local only: nothing reaches YouTube and no chat row is invented.
232 lines
7.8 KiB
C#
232 lines
7.8 KiB
C#
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Windows.Input;
|
|
using ytLive.Helpers;
|
|
using ytLive.Models;
|
|
|
|
namespace ytLive.ViewModels;
|
|
|
|
public partial class MainViewModel : ViewModelBase
|
|
{
|
|
|
|
private bool _isSettingsOpen;
|
|
private bool _isBugOpen;
|
|
private bool _isFeatureOpen;
|
|
private bool _isAboutOpen;
|
|
private bool _isLicensesOpen;
|
|
private string _overlayTitle = string.Empty;
|
|
private string _bugReportText = string.Empty;
|
|
private string _bugReportEmail = string.Empty;
|
|
private string _featureRequestText = string.Empty;
|
|
private string _featureRequestEmail = string.Empty;
|
|
|
|
private const string SupportEmail = "gramps@llamachile.shop";
|
|
// The creator-hub link targets (About overlay) — public so the hub's
|
|
// integration test can assert they point at real destinations.
|
|
public const string ChannelUrl = "https://youtube.com/@llamachileshop";
|
|
public const string MastodonUrl = "https://mastodon.llamachile.tube/@gramps";
|
|
public const string PremiumUrl = "https://buy.polar.sh/polar_cl_ueMy8AyAClO0o0yo19IroZwKTbD587gzUsjEW3bFapw";
|
|
public const string CoffeeUrl = "https://buymeacoffee.com/llamachiley";
|
|
public static string AppVersionLabel => $"Version {typeof(MainViewModel).Assembly.GetName().Version?.ToString(3)}";
|
|
|
|
/// <summary>The About hub's Unlock Premium button lights up only once the
|
|
/// billing URL is set (Polar — polar.sh, pending TASK 10 implementation).</summary>
|
|
public bool IsPremiumAvailable => PremiumUrl.Length > 0;
|
|
|
|
/// <summary>Full in-app licensing/legal text, loaded on demand from the
|
|
/// shipped THIRD-PARTY-NOTICES.txt (never opens the OS viewer).</summary>
|
|
public string LicensesText { get; private set; } = string.Empty;
|
|
|
|
// Overlay panels
|
|
public bool IsSettingsOpen
|
|
{
|
|
get => _isSettingsOpen;
|
|
private set => SetPanel(ref _isSettingsOpen, value);
|
|
}
|
|
|
|
public bool IsBugOpen
|
|
{
|
|
get => _isBugOpen;
|
|
private set => SetPanel(ref _isBugOpen, value);
|
|
}
|
|
|
|
public bool IsFeatureOpen
|
|
{
|
|
get => _isFeatureOpen;
|
|
private set => SetPanel(ref _isFeatureOpen, value);
|
|
}
|
|
|
|
public bool IsAboutOpen
|
|
{
|
|
get => _isAboutOpen;
|
|
private set => SetPanel(ref _isAboutOpen, value);
|
|
}
|
|
|
|
/// <summary>About hub sub-view: the full licensing/legal texts panel
|
|
/// (in-app scrolling — no Notepad, no browser tab).</summary>
|
|
public bool IsLicensesOpen
|
|
{
|
|
get => _isLicensesOpen;
|
|
set
|
|
{
|
|
if (SetProperty(ref _isLicensesOpen, value))
|
|
OnPropertyChanged(nameof(IsHubVisible));
|
|
}
|
|
}
|
|
|
|
public bool IsAnyOverlayOpen => IsSettingsOpen || IsBugOpen || IsFeatureOpen || IsAboutOpen;
|
|
|
|
private ZoomShape _zoomShape = ZoomShape.Circle;
|
|
public ZoomShape ZoomShape
|
|
{
|
|
get => _zoomShape;
|
|
set
|
|
{
|
|
if (SetProperty(ref _zoomShape, value))
|
|
{
|
|
try { _layoutStore.UpsertSetting("ZoomShape", value.ToString()); } catch { }
|
|
}
|
|
}
|
|
}
|
|
|
|
public string OverlayTitle
|
|
{
|
|
get => _overlayTitle;
|
|
private set => SetProperty(ref _overlayTitle, value);
|
|
}
|
|
|
|
public string BugReportText
|
|
{
|
|
get => _bugReportText;
|
|
set => SetProperty(ref _bugReportText, value);
|
|
}
|
|
|
|
public string BugReportEmail
|
|
{
|
|
get => _bugReportEmail;
|
|
set => SetProperty(ref _bugReportEmail, value);
|
|
}
|
|
|
|
public string FeatureRequestText
|
|
{
|
|
get => _featureRequestText;
|
|
set => SetProperty(ref _featureRequestText, value);
|
|
}
|
|
|
|
public string FeatureRequestEmail
|
|
{
|
|
get => _featureRequestEmail;
|
|
set => SetProperty(ref _featureRequestEmail, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Properties-panel "Test alert" (creator ruling 2026-11-01): fire one synthetic Super Chat
|
|
/// through the REAL alert layer, so what plays is exactly what a viewer would trigger — the
|
|
/// custom clip if one is set, the built-in clip otherwise. Local only; nothing is sent to
|
|
/// YouTube and no chat row is invented.
|
|
/// </summary>
|
|
public RelayCommand TestAlertCommand { get; private set; } = null!;
|
|
|
|
private void TestAlert()
|
|
{
|
|
_alertLayer.Enqueue(new Models.ChatMessage
|
|
{
|
|
AuthorName = "Test viewer",
|
|
Message = "Test alert — this is what a Super Chat looks like.",
|
|
Timestamp = System.DateTime.Now,
|
|
IsSimulated = true,
|
|
IsSuperChat = true,
|
|
SuperChatAmount = 5.00,
|
|
SuperChatCurrency = "$",
|
|
SuperChatDisplayString = "$5.00",
|
|
Kind = Models.ChatEventKind.SuperChat,
|
|
});
|
|
}
|
|
|
|
public ICommand OpenSettingsCommand { get; }
|
|
public ICommand OpenBugCommand { get; }
|
|
public ICommand OpenFeatureCommand { get; }
|
|
public ICommand OpenAboutCommand { get; }
|
|
public ICommand CloseOverlayCommand { get; }
|
|
public ICommand SubmitBugCommand { get; }
|
|
public ICommand SubmitFeatureCommand { get; }
|
|
public ICommand OpenChannelCommand { get; }
|
|
public ICommand OpenMastodonCommand { get; }
|
|
public ICommand OpenCoffeeCommand { get; }
|
|
public ICommand OpenPremiumCommand { get; }
|
|
public ICommand OpenLicensesCommand { get; }
|
|
|
|
private void SetPanel(ref bool field, bool value, [System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
|
|
{
|
|
if (SetProperty(ref field, value, propertyName))
|
|
OnPropertyChanged(nameof(IsAnyOverlayOpen));
|
|
}
|
|
|
|
private void ShowOverlay(string? panel = null, string title = "")
|
|
{
|
|
IsSettingsOpen = panel == nameof(IsSettingsOpen);
|
|
IsBugOpen = panel == nameof(IsBugOpen);
|
|
IsFeatureOpen = panel == nameof(IsFeatureOpen);
|
|
IsAboutOpen = panel == nameof(IsAboutOpen);
|
|
OverlayTitle = title;
|
|
if (panel != nameof(IsAboutOpen))
|
|
IsLicensesOpen = false;
|
|
}
|
|
|
|
/// <summary>Shows the in-app licensing panel, loading the full texts from
|
|
/// the shipped THIRD-PARTY-NOTICES.txt on first open (never the OS viewer).
|
|
/// Falls back to a "not found" note if the file is missing so the panel
|
|
/// never hangs on nothing.</summary>
|
|
private void ShowLicenses()
|
|
{
|
|
if (string.IsNullOrEmpty(LicensesText))
|
|
{
|
|
try
|
|
{
|
|
var path = Path.Combine(AppContext.BaseDirectory, "THIRD-PARTY-NOTICES.txt");
|
|
LicensesText = File.Exists(path)
|
|
? File.ReadAllText(path)
|
|
: "THIRD-PARTY-NOTICES.txt not found next to the executable.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LicensesText = $"Could not read THIRD-PARTY-NOTICES.txt: {ex.Message}";
|
|
}
|
|
}
|
|
IsLicensesOpen = true;
|
|
}
|
|
|
|
private void SubmitBug()
|
|
{
|
|
var body = string.Join(Environment.NewLine,
|
|
BugReportText.Trim(),
|
|
string.Empty,
|
|
$"LlamaCasty {AppVersionLabel}",
|
|
string.IsNullOrWhiteSpace(BugReportEmail) ? string.Empty : $"Reply-to: {BugReportEmail.Trim()}");
|
|
ComposeEmail("[LlamaCasty Bug Report]", body);
|
|
ShowOverlay();
|
|
}
|
|
|
|
private void SubmitFeature()
|
|
{
|
|
var body = string.Join(Environment.NewLine,
|
|
FeatureRequestText.Trim(),
|
|
string.Empty,
|
|
$"LlamaCasty {AppVersionLabel}",
|
|
string.IsNullOrWhiteSpace(FeatureRequestEmail) ? string.Empty : $"Reply-to: {FeatureRequestEmail.Trim()}");
|
|
ComposeEmail("[LlamaCasty Feature Request]", body);
|
|
ShowOverlay();
|
|
}
|
|
|
|
private static void ComposeEmail(string subject, string body)
|
|
{
|
|
var uri = $"mailto:{SupportEmail}?subject={Uri.EscapeDataString(subject)}&body={Uri.EscapeDataString(body)}";
|
|
OpenUrl(uri);
|
|
}
|
|
|
|
private static void OpenUrl(string url)
|
|
{
|
|
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
|
|
}
|
|
}
|