a9d5f29084
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.
168 lines
5.8 KiB
C#
168 lines
5.8 KiB
C#
using System;
|
|
using System.ComponentModel;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using ytLive.Helpers;
|
|
using ytLive.ViewModels;
|
|
|
|
namespace ytLive;
|
|
|
|
public partial class MainWindow : Window
|
|
{
|
|
private readonly MainViewModel _viewModel;
|
|
private Controls.TopBar _topBar = null!;
|
|
private Controls.PreviewPane _previewPane = null!;
|
|
private Controls.LeftPanel _leftPanel = null!;
|
|
|
|
public MainWindow()
|
|
{
|
|
AppLog.Write("MainWindow ctor: before InitializeComponent");
|
|
InitializeComponent();
|
|
AppLog.Write("MainWindow ctor: after InitializeComponent");
|
|
_viewModel = (MainViewModel)DataContext;
|
|
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
|
_topBar = (Controls.TopBar)FindName("TopBar")!;
|
|
_previewPane = (Controls.PreviewPane)FindName("PreviewPane")!;
|
|
_leftPanel = (Controls.LeftPanel)FindName("LeftPanel")!;
|
|
UpdateTaskbarOverlay();
|
|
UpdateSelectionOverlay();
|
|
_topBar.RefreshAvatar();
|
|
RestoreWindowGeometry();
|
|
AppLog.Write("MainWindow ctor: end");
|
|
}
|
|
|
|
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
|
{
|
|
AppLog.Write("MainWindow loaded");
|
|
// Web sources need the window HWND as the composition controller's parent,
|
|
// which does not exist in the ctor — InitWebView2 runs here (Loaded), after
|
|
// SourceInitialized. Nothing renders from web frames before this.
|
|
_viewModel.InitWebView2();
|
|
}
|
|
|
|
// ─── Window geometry persistence (launch size/position) ───
|
|
// Saved on close (RestoreBounds so a maximized window remembers its
|
|
// un-maximize rect); restored in the ctor before the window is shown.
|
|
// Values clamp to the minimums and to the primary work area — a position
|
|
// saved on a since-disconnected secondary monitor falls back on-screen.
|
|
|
|
private void RestoreWindowGeometry()
|
|
{
|
|
try
|
|
{
|
|
var saved = _viewModel.RestoreWindowState();
|
|
if (saved == null) return;
|
|
var (left, top, width, height, state) = saved.Value;
|
|
|
|
var work = SystemParameters.WorkArea;
|
|
width = Math.Clamp(width, MinWidth, work.Width);
|
|
height = Math.Clamp(height, MinHeight, work.Height);
|
|
left = Math.Clamp(left, work.Left - width + 120, work.Right - 120);
|
|
top = Math.Clamp(top, work.Top, work.Bottom - 80);
|
|
|
|
Left = left;
|
|
Top = top;
|
|
Width = width;
|
|
Height = height;
|
|
if (state == "Maximized") WindowState = WindowState.Maximized;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLog.Write($"Window geometry restore failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void SaveWindowGeometry()
|
|
{
|
|
try
|
|
{
|
|
var bounds = RestoreBounds;
|
|
var state = WindowState == WindowState.Maximized ? "Maximized" : "Normal";
|
|
_viewModel.PersistWindowState(bounds.Left, bounds.Top, bounds.Width, bounds.Height, state);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLog.Write($"Window geometry save failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private ytLive.Services.GlobalHotkeyManager? _hotkeys;
|
|
|
|
protected override void OnSourceInitialized(EventArgs e)
|
|
{
|
|
base.OnSourceInitialized(e);
|
|
_hotkeys = new ytLive.Services.GlobalHotkeyManager();
|
|
_hotkeys.Activated += id => _viewModel.HandleHotkey(id);
|
|
_hotkeys.Attach(this, _viewModel.CurrentHotkeyBindings);
|
|
_viewModel.HotkeyBindingsChanged = bindings => _hotkeys.Reattach(this, bindings);
|
|
}
|
|
|
|
private void Window_ContextMenuOpening(object sender, ContextMenuEventArgs e)
|
|
{
|
|
if (!_viewModel.IsEditMode)
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName == nameof(MainViewModel.IsLive))
|
|
UpdateTaskbarOverlay();
|
|
else if (e.PropertyName == nameof(MainViewModel.SelectedElement))
|
|
UpdateSelectionOverlay();
|
|
else if (e.PropertyName == nameof(MainViewModel.AccountAvatarUrl)
|
|
|| e.PropertyName == nameof(MainViewModel.IsConnected))
|
|
_topBar.RefreshAvatar();
|
|
}
|
|
|
|
private void MainWindow_Closing(object? sender, CancelEventArgs e)
|
|
{
|
|
SaveWindowGeometry();
|
|
_viewModel.Shutdown();
|
|
}
|
|
|
|
private void MainWindow_Activated(object? sender, EventArgs e)
|
|
{
|
|
_viewModel.RefreshBackgroundAutoCapture();
|
|
}
|
|
|
|
private void MainWindow_Deactivated(object? sender, EventArgs e)
|
|
{
|
|
}
|
|
|
|
private void UpdateTaskbarOverlay()
|
|
{
|
|
TaskbarInfo.Overlay = _viewModel.IsLive
|
|
? (ImageSource)FindResource("LiveOverlay")
|
|
: null;
|
|
}
|
|
|
|
private void Window_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (e.OriginalSource is not DependencyObject original) return;
|
|
var form = _viewModel.BroadcastForm;
|
|
var ypp = _viewModel.Ypp;
|
|
if (!_previewPane.IsClickInsideDrawer(original))
|
|
{
|
|
if (form.IsDrawerOpen) form.CloseDrawerCommand.Execute(null);
|
|
if (ypp.IsDrawerOpen) ypp.CloseDrawerCommand.Execute(null);
|
|
}
|
|
if (_previewPane.IsClickInsidePreview(original)) return;
|
|
if (_leftPanel.IsClickInside(original)) return;
|
|
_viewModel.SelectedElement = null;
|
|
}
|
|
|
|
private void UpdateSelectionOverlay()
|
|
{
|
|
if (!_viewModel.IsEditMode)
|
|
{
|
|
_previewPane.UpdateSelectionOverlay(false);
|
|
return;
|
|
}
|
|
var selected = _viewModel.SelectedElement is { } s && Controls.PreviewPane.IsDraggableElement(s);
|
|
_previewPane.UpdateSelectionOverlay(selected);
|
|
_leftPanel.OnSelectionChanged(_viewModel);
|
|
}
|
|
}
|