feat(ypp): YPP journey tracker slice 1 — the YPP pull-out below Stream Settings (TASK 39, Good Dog ONE test)

Current-OAuth-scope data, no re-consent: subscriber bars for both tiers (500 fan-funding /
1,000 ad-revenue; the 2027-02-01 doubling is versioned date-aware data in YppThresholds),
3-uploads/90d, live standing from channels.list auditDetails, self-reported 2FA/AdSense
checkboxes + deep links (Google 2-Step / youtube.com/earn), and the "what counts" education.
Every refresh appends a YppSnapshot (SQLite v10) so the analytics slice has history from day one.

- ChannelStatsService (channels.list statistics,auditDetails,contentDetails + playlistItems.list,
  virtual CaptureCurrentAsync = test seam); YppTrackerViewModel mirrors LiveBroadcastFormViewModel.
- MainViewModel.Ypp.cs: Ypp property, ChannelStatsFactoryOverride seam, one-open-at-a-time drawer
  exclusivity (both directions); account sign-in/out/restore hooks in Account.cs.
- PreviewPane.xaml: TextPullOutHost is now a drawer bank [broadcast][ypp][stacked white tabs];
  outside-click collapse covers both drawers. MainWindow.xaml.cs updated.
- Wait - watchers: the whole feature already reviewed against YouTube's own docs (YPP Earn tab +
  API availability) before building — see TASKS/task-39-ypp-journey-tracker.md.
- ONE integration test: YppPullOutTests (snapshot capture/persist + checklist round-trip +
  drawer exclusivity). 313/313 pass, 0 warnings.
- Memory in same commit: TASKS.md row, ai.md Journey-tracker bullet (roadmap→reality), HANDOFF.md.

Slice 2 (Analytics yt-analytics.readonly re-consent → watch-hours/Shorts + velocity/ETA +
sparkline) is queued behind this, not merged.
This commit is contained in:
2026-09-22 08:07:54 -07:00
parent 4e0915a36e
commit a9d5f29084
16 changed files with 1103 additions and 117 deletions
+3
View File
@@ -73,6 +73,7 @@ public partial class MainViewModel
IsConnected = true;
SyncConnectedAccount();
Ypp.OnAccountChanged();
}
catch (Exception ex)
{
@@ -103,6 +104,7 @@ public partial class MainViewModel
IsConnected = true;
SyncConnectedAccount();
Ypp.OnAccountChanged();
return channel;
}
catch (Exception ex)
@@ -120,6 +122,7 @@ public partial class MainViewModel
IsConnected = false;
SyncConnectedAccount();
NotifySocialsChanged();
Ypp.OnAccountChanged();
AppLog.Write("Socials: signed out of YouTube");
await Task.CompletedTask;
}
+36
View File
@@ -0,0 +1,36 @@
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!;
private void InitYpp()
{
Ypp = new YppTrackerViewModel(
() => _layoutStore,
() => ChannelStatsFactoryOverride?.Invoke() ?? new ChannelStatsService(_youtubeAuth),
() => IsConnected,
() => _youtubeAuth.CurrentChannel?.DisplayName);
// One drawer at a time: the two pull-outs share the preview's right edge,
// so opening one collapses the other (both directions).
BroadcastForm.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(LiveBroadcastFormViewModel.IsDrawerOpen) && BroadcastForm.IsDrawerOpen)
Ypp.IsDrawerOpen = false;
};
Ypp.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(YppTrackerViewModel.IsDrawerOpen) && Ypp.IsDrawerOpen)
BroadcastForm.IsDrawerOpen = false;
};
}
}
+1
View File
@@ -242,6 +242,7 @@ public partial class MainViewModel : ViewModelBase
() => _youtubeStream,
() => CurrentBroadcastId,
() => IsLive);
InitYpp();
// Restore the last picked mic so the mixer's first capture attaches to
// the same, already-vetted device (green dot) or reports it missing