Files
LlamaCasty/ViewModels/MainViewModel.Ypp.cs
T
gramps a1d861608f feat(ypp): the three eligibility bars, with dated thresholds (TASK 39·2)
YouTube moves the goalposts, so the app stops modelling them as its own truth and shows progress against
a threshold table that carries the date it was read.

- `Models/YppProgress.cs` — pure: three metrics, per-criterion met, and `MeetsPublishedRule`, which
  encodes the real shape of the rule (subscribers AND either view route). The drawer says "x of 3 criteria
  met — YouTube reviews eligibility" and spells out the either/or, so "2 of 3" can't be misread.
- `Services/YouTubeAnalyticsService.cs` — the two numbers only Analytics publishes: long-form watch hours
  (trailing 12 months, `creatorContentType==video`) and Shorts-feed views (90 days, `==shorts`). No rows
  means NO DATA, not zero.
- **Thresholds are data with a publication date** (`YppThresholds.AsOfUtc`, shown in the drawer) and a
  test fails once that reading is more than 180 days old. The 2027-02-01 doubling (4,000h → 8,000h,
  10M → 20M Shorts views) resolves from the date, not from an edit.
- **No Analytics scope → no invented numbers:** the drawer explains the missing consent instead of drawing
  two empty bars as zero. Money stays behind 3·20's "Show monetary metrics" toggle.
- Fan-funding tier (500 subs) demoted to a single muted line, since the three bars are the ad-revenue path.

Tests: `YppProgressTests` — either/or eligibility, subscribers alone insufficient, Shorts as a qualifying
route, bars clamp at 100%, the doubling date, the staleness guard, Analytics parsing (no rows = no data),
and compact value formatting. FAST gate: 567 passed, 0 failed, 0 warnings.
2026-11-05 16:41:09 -08:00

70 lines
2.9 KiB
C#

using ytLive.Services;
namespace ytLive.ViewModels;
public partial class MainViewModel
{
/// <summary>Test seam (mirrors CameraEnumeratorOverride): tests inject a fake
/// stats service before constructing the VM so refresh never hits the network.</summary>
internal static Func<ChannelStatsService>? ChannelStatsFactoryOverride { get; set; }
/// <summary>The YPP pull-out backing VM — the drawer below Stream Settings
/// on the Live screen's right edge (TASK 39).</summary>
public Services.YppTrackerViewModel Ypp { get; private set; } = null!;
/// <summary>The Test Stream pull-out backing VM — the TEST drawer at the
/// bottom of the right rail (TASK 41): mock chat input (real insert →
/// poll round-trip) + locally-simulated member / SuperChat / subscriber rows.</summary>
public Services.TestSessionViewModel TestSession { get; private set; } = null!;
private void InitYpp()
{
Ypp = new YppTrackerViewModel(
() => _layoutStore,
() => ChannelStatsFactoryOverride?.Invoke() ?? new ChannelStatsService(_youtubeAuth),
() => IsConnected,
() => _youtubeAuth.CurrentChannel?.DisplayName,
rewardEvents: () => new SuperChatEventService(_youtubeAuth),
channelId: () => _youtubeAuth.CurrentChannel?.ChannelId,
sessionStartedAtUtc: () => _sessionStartedAtUtc,
watchMetrics: () => new YouTubeAnalyticsService(_youtubeAuth));
TestSession = new TestSessionViewModel(
_youtubeStream,
_youtubeChat,
onEndStream: StopStream);
// One drawer at a time: the three pull-outs share the preview's right edge,
// so opening one collapses the other two (all directions).
BroadcastForm.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(LiveBroadcastFormViewModel.IsDrawerOpen) && BroadcastForm.IsDrawerOpen)
{
Ypp.IsDrawerOpen = false;
TestSession.IsDrawerOpen = false;
CloseSocialsDrawer();
CloseSettingsDrawerIfOpen();
}
};
Ypp.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(YppTrackerViewModel.IsDrawerOpen) && Ypp.IsDrawerOpen)
{
BroadcastForm.IsDrawerOpen = false;
TestSession.IsDrawerOpen = false;
CloseSocialsDrawer();
CloseSettingsDrawerIfOpen();
}
};
TestSession.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(TestSessionViewModel.IsDrawerOpen) && TestSession.IsDrawerOpen)
{
BroadcastForm.IsDrawerOpen = false;
Ypp.IsDrawerOpen = false;
CloseSocialsDrawer();
CloseSettingsDrawerIfOpen();
}
};
}
}