Resolution dropdown, list-focus preservation, unified dark theme, startup logging, and memory map
- Bottom-bar resolution dropdown (1080p60/1080p30/720p60/480p30) with tooltip on finding upload bandwidth; disabled while live; no in-app speed test - FocusPreservingListBox keeps selection/focus coherent when a selected scene/source is deleted (skip hidden scenes; leave list when empty) - Themes/Controls.xaml: single dark-theme dictionary merged once in App.xaml; custom ComboBox template fixes SelectionBoxItem rendering; GoLiveWindow and ReuseImageDialog consolidated onto shared styles - AppLog file logger + AppDomain/Dispatcher exception hooks; checkpointed MainWindow/VM/dialog constructors (caught MenuItemRole.Separator XAML crash) - Memory map: schema.md conventions, per-directory index.md, updated ai.md/ TASKS.md/README.md (OAuth creds real; token persistence still pending)
This commit is contained in:
@@ -4,6 +4,10 @@
|
|||||||
xmlns:local="clr-namespace:ytLive"
|
xmlns:local="clr-namespace:ytLive"
|
||||||
StartupUri="MainWindow.xaml">
|
StartupUri="MainWindow.xaml">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="Themes/Controls.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
||||||
|
|||||||
+34
-13
@@ -1,13 +1,34 @@
|
|||||||
using System.Configuration;
|
using System.Windows;
|
||||||
using System.Data;
|
using System.Windows.Threading;
|
||||||
using System.Windows;
|
using ytLive.Helpers;
|
||||||
|
|
||||||
namespace ytLive;
|
namespace ytLive;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interaction logic for App.xaml
|
/// Interaction logic for App.xaml
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
}
|
public App()
|
||||||
|
{
|
||||||
|
AppLog.Write("App ctor");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
// The app is a WinExe (no visible console) and is often debugged from
|
||||||
|
// WSL where it can't even run, so startup checkpoints and any unhandled
|
||||||
|
// exception are written to %APPDATA%\ytLlive\startup.log via AppLog.
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
|
||||||
|
AppLog.Write(args.ExceptionObject is Exception ex
|
||||||
|
? "AppDomain unhandled exception: " + ex
|
||||||
|
: "AppDomain unhandled exception object: " + args.ExceptionObject);
|
||||||
|
|
||||||
|
DispatcherUnhandledException += (_, args) =>
|
||||||
|
AppLog.Write(args.Exception, "Dispatcher unhandled exception");
|
||||||
|
|
||||||
|
AppLog.Write("App OnStartup begin");
|
||||||
|
base.OnStartup(e);
|
||||||
|
AppLog.Write("App OnStartup end");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+11
-60
@@ -7,58 +7,9 @@
|
|||||||
ShowInTaskbar="False" Background="#1a1a2e">
|
ShowInTaskbar="False" Background="#1a1a2e">
|
||||||
|
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
<ResourceDictionary>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||||
<Style x:Key="GoLiveButton" TargetType="Button">
|
</ResourceDictionary>
|
||||||
<Setter Property="Background" Value="#e94560"/>
|
|
||||||
<Setter Property="Foreground" Value="White"/>
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
<Setter Property="Padding" Value="16,8"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Cursor" Value="Hand"/>
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="Button">
|
|
||||||
<Border Background="{TemplateBinding Background}"
|
|
||||||
CornerRadius="4"
|
|
||||||
Padding="{TemplateBinding Padding}">
|
|
||||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
||||||
</Border>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="GoLiveButtonSecondary" TargetType="Button" BasedOn="{StaticResource GoLiveButton}">
|
|
||||||
<Setter Property="Background" Value="#16213e"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="GoLiveLabel" TargetType="TextBlock">
|
|
||||||
<Setter Property="Foreground" Value="#a0a0b0"/>
|
|
||||||
<Setter Property="FontSize" Value="12"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,0,4"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="GoLiveTextBox" TargetType="TextBox">
|
|
||||||
<Setter Property="Background" Value="#0f3460"/>
|
|
||||||
<Setter Property="Foreground" Value="#e0e0e0"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Padding" Value="8,6"/>
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
<Setter Property="CaretBrush" Value="White"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,0,12"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="GoLiveCombo" TargetType="ComboBox">
|
|
||||||
<Setter Property="Background" Value="#0f3460"/>
|
|
||||||
<Setter Property="Foreground" Value="#e0e0e0"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Padding" Value="8,6"/>
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,0,12"/>
|
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
|
||||||
</Style>
|
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
<Grid Margin="24">
|
<Grid Margin="24">
|
||||||
@@ -77,17 +28,17 @@
|
|||||||
<TextBlock Text="STREAM DETAILS" FontSize="12" FontWeight="SemiBold"
|
<TextBlock Text="STREAM DETAILS" FontSize="12" FontWeight="SemiBold"
|
||||||
Foreground="#a0a0b0" Margin="0,0,0,8"/>
|
Foreground="#a0a0b0" Margin="0,0,0,8"/>
|
||||||
|
|
||||||
<TextBlock Text="Title" Style="{StaticResource GoLiveLabel}"/>
|
<TextBlock Text="Title" Style="{StaticResource YtLabel}"/>
|
||||||
<TextBox Style="{StaticResource GoLiveTextBox}"
|
<TextBox Style="{StaticResource YtTextBox}" Margin="0,0,0,12"
|
||||||
Text="{Binding StreamTitle, UpdateSourceTrigger=PropertyChanged}"/>
|
Text="{Binding StreamTitle, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
|
||||||
<TextBlock Text="Description" Style="{StaticResource GoLiveLabel}"/>
|
<TextBlock Text="Description" Style="{StaticResource YtLabel}"/>
|
||||||
<TextBox Style="{StaticResource GoLiveTextBox}" Height="60" AcceptsReturn="True"
|
<TextBox Style="{StaticResource YtTextBox}" Height="60" AcceptsReturn="True" Margin="0,0,0,12"
|
||||||
Text="{Binding StreamDescription, UpdateSourceTrigger=PropertyChanged}"
|
Text="{Binding StreamDescription, UpdateSourceTrigger=PropertyChanged}"
|
||||||
VerticalScrollBarVisibility="Auto"/>
|
VerticalScrollBarVisibility="Auto"/>
|
||||||
|
|
||||||
<TextBlock Text="Visibility" Style="{StaticResource GoLiveLabel}"/>
|
<TextBlock Text="Visibility" Style="{StaticResource YtLabel}"/>
|
||||||
<ComboBox Style="{StaticResource GoLiveCombo}"
|
<ComboBox Style="{StaticResource YtComboBox}" Margin="0,0,0,12"
|
||||||
ItemsSource="{Binding Visibilities}"
|
ItemsSource="{Binding Visibilities}"
|
||||||
SelectedItem="{Binding Visibility}"/>
|
SelectedItem="{Binding Visibility}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -95,9 +46,9 @@
|
|||||||
<!-- ACTIONS -->
|
<!-- ACTIONS -->
|
||||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
|
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
<Button Content="Cancel" Command="{Binding CancelCommand}"
|
<Button Content="Cancel" Command="{Binding CancelCommand}"
|
||||||
Style="{StaticResource GoLiveButtonSecondary}" Margin="0,0,8,0"/>
|
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0"/>
|
||||||
<Button Content="Start Stream" Command="{Binding StartCommand}"
|
<Button Content="Start Stream" Command="{Binding StartCommand}"
|
||||||
Style="{StaticResource GoLiveButton}" MinWidth="110"/>
|
Style="{StaticResource YtButton}" MinWidth="110"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using ytLive.Helpers;
|
||||||
using ytLive.ViewModels;
|
using ytLive.ViewModels;
|
||||||
|
|
||||||
namespace ytLive;
|
namespace ytLive;
|
||||||
@@ -7,7 +8,9 @@ public partial class GoLiveWindow : Window
|
|||||||
{
|
{
|
||||||
public GoLiveWindow(GoLiveViewModel viewModel)
|
public GoLiveWindow(GoLiveViewModel viewModel)
|
||||||
{
|
{
|
||||||
|
AppLog.Write("GoLiveWindow ctor: before InitializeComponent");
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
AppLog.Write("GoLiveWindow ctor: after InitializeComponent");
|
||||||
DataContext = viewModel;
|
DataContext = viewModel;
|
||||||
viewModel.StartRequested += () => DialogResult = true;
|
viewModel.StartRequested += () => DialogResult = true;
|
||||||
viewModel.CancelRequested += () => DialogResult = false;
|
viewModel.CancelRequested += () => DialogResult = false;
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace ytLive.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal file logger used to diagnose startup/shutdown crashes on Windows,
|
||||||
|
/// where the WPF app has no visible console. Writes to
|
||||||
|
/// %APPDATA%\ytLlive\startup.log, appending synchronously so entries survive
|
||||||
|
/// a hard crash.
|
||||||
|
/// </summary>
|
||||||
|
public static class AppLog
|
||||||
|
{
|
||||||
|
private static readonly string LogPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"ytLlive",
|
||||||
|
"startup.log");
|
||||||
|
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
|
||||||
|
public static void Write(string message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(LogPath)!);
|
||||||
|
File.AppendAllText(LogPath, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {message}{Environment.NewLine}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Never let logging itself crash the app.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Write(Exception exception, string message)
|
||||||
|
{
|
||||||
|
Write($"{message}: {exception}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace ytLive.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A ListBox that keeps selection and keyboard focus coherent when the
|
||||||
|
/// selected item is deleted. Focus lands on the item now at the deleted
|
||||||
|
/// position (the previous item when the last one was removed) or leaves
|
||||||
|
/// the box entirely when it becomes empty. Drag-to-reorder is untouched.
|
||||||
|
/// </summary>
|
||||||
|
public class FocusPreservingListBox : ListBox
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Predicate deciding which items the delete-focus fallback may land on
|
||||||
|
/// (e.g. skip hidden scenes). Null means every item is eligible.
|
||||||
|
/// </summary>
|
||||||
|
public Func<object, bool>? IsSelectable { get; set; }
|
||||||
|
|
||||||
|
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnItemsChanged(e);
|
||||||
|
|
||||||
|
if (e.Action != NotifyCollectionChangedAction.Remove) return;
|
||||||
|
var hadFocus = IsKeyboardFocusWithin || IsKeyboardFocused;
|
||||||
|
if (!hadFocus) return;
|
||||||
|
if (Mouse.Captured == this) return;
|
||||||
|
|
||||||
|
if (Items.Count == 0)
|
||||||
|
{
|
||||||
|
SelectedItem = null;
|
||||||
|
MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetIndex = FindSelectableIndex(Math.Clamp(e.OldStartingIndex, 0, Items.Count - 1));
|
||||||
|
if (targetIndex < 0)
|
||||||
|
{
|
||||||
|
SelectedItem = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedIndex = targetIndex;
|
||||||
|
var item = Items[targetIndex];
|
||||||
|
|
||||||
|
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, () =>
|
||||||
|
{
|
||||||
|
ScrollIntoView(item);
|
||||||
|
if (ItemContainerGenerator.ContainerFromItem(item) is ListBoxItem container)
|
||||||
|
container.Focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private int FindSelectableIndex(int start)
|
||||||
|
{
|
||||||
|
for (var i = start; i >= 0; i--)
|
||||||
|
if (IsItemSelectable(i)) return i;
|
||||||
|
for (var i = start + 1; i < Items.Count; i++)
|
||||||
|
if (IsItemSelectable(i)) return i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsItemSelectable(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= Items.Count) return false;
|
||||||
|
var item = Items[index];
|
||||||
|
return item != null && (IsSelectable?.Invoke(item) ?? true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Helpers — index
|
||||||
|
|
||||||
|
Cross-cutting utilities. See [`schema.md`](../schema.md) for the memory-map conventions.
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `ViewModelBase.cs` | `INotifyPropertyChanged` + `SetProperty<T>()` — base for all ViewModels |
|
||||||
|
| `RelayCommand.cs` | `ICommand` implementation for all button actions |
|
||||||
|
| `AppLog.cs` | File logger to `%APPDATA%\ytLlive\startup.log`; startup checkpoints + unhandled-exception capture (the crash this map is named for) |
|
||||||
|
| `FocusPreservingListBox.cs` | `ListBox` subclass that keeps selection/focus coherent when the selected item is deleted; `IsSelectable` predicate skips hidden scenes; drag-reorder guard |
|
||||||
|
| `OAuthCredentials.cs` | Baked-in Google OAuth client ID/secret (desktop app; loopback callback) |
|
||||||
|
| `ImageCache.cs` | Image byte caching (assets live in the DB) |
|
||||||
|
| `InverseBoolToVisibilityConverter.cs` / `NotNullToVisibilityConverter.cs` | XAML value converters for visibility bindings |
|
||||||
|
|
||||||
|
Related: [`Themes/Controls.xaml`](../Themes/Controls.xaml) styles the lists this
|
||||||
|
class backs; [`Models/index.md`](../Models/index.md) and
|
||||||
|
[`ViewModels/index.md`](../ViewModels/index.md) for the objects it works on.
|
||||||
+26
-102
@@ -4,6 +4,7 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:vm="clr-namespace:ytLive.ViewModels"
|
xmlns:vm="clr-namespace:ytLive.ViewModels"
|
||||||
|
xmlns:Helpers="clr-namespace:ytLive.Helpers"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="{Binding WindowTitle}" Height="720" Width="1280"
|
Title="{Binding WindowTitle}" Height="720" Width="1280"
|
||||||
MinWidth="1180" MinHeight="600"
|
MinWidth="1180" MinHeight="600"
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
Background="#1a1a2e"
|
Background="#1a1a2e"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
Closing="MainWindow_Closing"
|
Closing="MainWindow_Closing"
|
||||||
|
Loaded="MainWindow_Loaded"
|
||||||
PreviewMouseLeftButtonDown="Window_PreviewMouseLeftButtonDown">
|
PreviewMouseLeftButtonDown="Window_PreviewMouseLeftButtonDown">
|
||||||
|
|
||||||
<Window.DataContext>
|
<Window.DataContext>
|
||||||
@@ -22,7 +24,8 @@
|
|||||||
</Window.TaskbarItemInfo>
|
</Window.TaskbarItemInfo>
|
||||||
|
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
<ResourceDictionary>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||||
<Helpers:InverseBoolToVisibilityConverter x:Key="InverseBoolToVis"
|
<Helpers:InverseBoolToVisibilityConverter x:Key="InverseBoolToVis"
|
||||||
xmlns:Helpers="clr-namespace:ytLive.Helpers"/>
|
xmlns:Helpers="clr-namespace:ytLive.Helpers"/>
|
||||||
<Helpers:NotNullToVisibilityConverter x:Key="NotNullToVis"
|
<Helpers:NotNullToVisibilityConverter x:Key="NotNullToVis"
|
||||||
@@ -64,55 +67,6 @@
|
|||||||
</DrawingImage.Drawing>
|
</DrawingImage.Drawing>
|
||||||
</DrawingImage>
|
</DrawingImage>
|
||||||
|
|
||||||
<!-- Base button style -->
|
|
||||||
<Style x:Key="YtButton" TargetType="Button">
|
|
||||||
<Setter Property="Background" Value="#e94560"/>
|
|
||||||
<Setter Property="Foreground" Value="White"/>
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
<Setter Property="Padding" Value="16,8"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Cursor" Value="Hand"/>
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="Button">
|
|
||||||
<Border Background="{TemplateBinding Background}"
|
|
||||||
CornerRadius="4"
|
|
||||||
Padding="{TemplateBinding Padding}">
|
|
||||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
||||||
</Border>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="YtButtonSecondary" TargetType="Button" BasedOn="{StaticResource YtButton}">
|
|
||||||
<Setter Property="Background" Value="#16213e"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="IconButton" TargetType="Button">
|
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Padding" Value="4"/>
|
|
||||||
<Setter Property="Cursor" Value="Hand"/>
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="Button">
|
|
||||||
<Border Background="{TemplateBinding Background}"
|
|
||||||
CornerRadius="3"
|
|
||||||
Padding="{TemplateBinding Padding}">
|
|
||||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
||||||
</Border>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
<Style.Triggers>
|
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
|
||||||
<Setter Property="Background" Value="#26ffffff"/>
|
|
||||||
</Trigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- Eye icon: open when visible, slashed when hidden -->
|
<!-- Eye icon: open when visible, slashed when hidden -->
|
||||||
<Style x:Key="EyeIconStyle" TargetType="Path">
|
<Style x:Key="EyeIconStyle" TargetType="Path">
|
||||||
<Setter Property="Data" Value="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
|
<Setter Property="Data" Value="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
|
||||||
@@ -131,36 +85,7 @@
|
|||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
<Style x:Key="YtTextBox" TargetType="TextBox">
|
|
||||||
<Setter Property="Background" Value="#0f3460"/>
|
|
||||||
<Setter Property="Foreground" Value="#e0e0e0"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Padding" Value="8,6"/>
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
<Setter Property="CaretBrush" Value="White"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="YtComboBox" TargetType="ComboBox">
|
|
||||||
<Setter Property="Background" Value="#0f3460"/>
|
|
||||||
<Setter Property="Foreground" Value="#e0e0e0"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Padding" Value="8,6"/>
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="YtLabel" TargetType="TextBlock">
|
|
||||||
<Setter Property="Foreground" Value="#a0a0b0"/>
|
|
||||||
<Setter Property="FontSize" Value="12"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="SectionHeader" TargetType="TextBlock">
|
|
||||||
<Setter Property="Foreground" Value="#e0e0e0"/>
|
|
||||||
<Setter Property="FontSize" Value="14"/>
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
|
||||||
</Style>
|
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
@@ -244,7 +169,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<ListBox x:Name="SceneList" Grid.Row="1" Background="Transparent" BorderThickness="0"
|
<Helpers:FocusPreservingListBox x:Name="SceneList" Grid.Row="1" Background="Transparent" BorderThickness="0"
|
||||||
ItemsSource="{Binding Scenes}"
|
ItemsSource="{Binding Scenes}"
|
||||||
SelectedItem="{Binding ActiveScene}"
|
SelectedItem="{Binding ActiveScene}"
|
||||||
SelectionChanged="SceneList_SelectionChanged"
|
SelectionChanged="SceneList_SelectionChanged"
|
||||||
@@ -252,8 +177,8 @@
|
|||||||
PreviewMouseMove="List_PreviewMouseMove"
|
PreviewMouseMove="List_PreviewMouseMove"
|
||||||
PreviewMouseLeftButtonUp="List_PreviewMouseLeftButtonUp"
|
PreviewMouseLeftButtonUp="List_PreviewMouseLeftButtonUp"
|
||||||
Foreground="#e0e0e0" FontSize="13">
|
Foreground="#e0e0e0" FontSize="13">
|
||||||
<ListBox.ItemContainerStyle>
|
<Helpers:FocusPreservingListBox.ItemContainerStyle>
|
||||||
<Style TargetType="ListBoxItem">
|
<Style TargetType="ListBoxItem" BasedOn="{StaticResource YtListBoxItem}">
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<DataTrigger Binding="{Binding IsHidden}" Value="True">
|
<DataTrigger Binding="{Binding IsHidden}" Value="True">
|
||||||
@@ -261,7 +186,7 @@
|
|||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
</ListBox.ItemContainerStyle>
|
</Helpers:FocusPreservingListBox.ItemContainerStyle>
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Grid Margin="0,2">
|
<Grid Margin="0,2">
|
||||||
@@ -326,7 +251,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</Helpers:FocusPreservingListBox>
|
||||||
|
|
||||||
<!-- Sources (for active scene) -->
|
<!-- Sources (for active scene) -->
|
||||||
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,12,0,0">
|
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,12,0,0">
|
||||||
@@ -347,18 +272,18 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<ListBox x:Name="SourceList" Grid.Row="3" Background="Transparent" BorderThickness="0"
|
<Helpers:FocusPreservingListBox x:Name="SourceList" Grid.Row="3" Background="Transparent" BorderThickness="0"
|
||||||
ItemsSource="{Binding ActiveScene.Sources}"
|
ItemsSource="{Binding ActiveScene.Sources}"
|
||||||
SelectedItem="{Binding SelectedSource, Mode=TwoWay}"
|
SelectedItem="{Binding SelectedSource, Mode=TwoWay}"
|
||||||
PreviewMouseLeftButtonDown="List_PreviewMouseLeftButtonDown"
|
PreviewMouseLeftButtonDown="List_PreviewMouseLeftButtonDown"
|
||||||
PreviewMouseMove="List_PreviewMouseMove"
|
PreviewMouseMove="List_PreviewMouseMove"
|
||||||
PreviewMouseLeftButtonUp="List_PreviewMouseLeftButtonUp"
|
PreviewMouseLeftButtonUp="List_PreviewMouseLeftButtonUp"
|
||||||
Foreground="#e0e0e0" FontSize="13">
|
Foreground="#e0e0e0" FontSize="13">
|
||||||
<ListBox.ItemContainerStyle>
|
<Helpers:FocusPreservingListBox.ItemContainerStyle>
|
||||||
<Style TargetType="ListBoxItem">
|
<Style TargetType="ListBoxItem" BasedOn="{StaticResource YtListBoxItem}">
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||||
</Style>
|
</Style>
|
||||||
</ListBox.ItemContainerStyle>
|
</Helpers:FocusPreservingListBox.ItemContainerStyle>
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Grid Margin="0,2">
|
<Grid Margin="0,2">
|
||||||
@@ -379,7 +304,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</Helpers:FocusPreservingListBox>
|
||||||
|
|
||||||
<TextBlock Grid.Row="3" Text="No sources yet" Foreground="#555" FontSize="12"
|
<TextBlock Grid.Row="3" Text="No sources yet" Foreground="#555" FontSize="12"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
@@ -515,6 +440,7 @@
|
|||||||
|
|
||||||
<ListBox Grid.Row="1" Background="Transparent" BorderThickness="0"
|
<ListBox Grid.Row="1" Background="Transparent" BorderThickness="0"
|
||||||
ItemsSource="{Binding ChatMessages}"
|
ItemsSource="{Binding ChatMessages}"
|
||||||
|
ItemContainerStyle="{StaticResource YtListBoxItem}"
|
||||||
Visibility="{Binding ShowChatInactiveMessage, Converter={StaticResource InverseBoolToVis}}"
|
Visibility="{Binding ShowChatInactiveMessage, Converter={StaticResource InverseBoolToVis}}"
|
||||||
Foreground="#e0e0e0" FontSize="12">
|
Foreground="#e0e0e0" FontSize="12">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
@@ -567,7 +493,14 @@
|
|||||||
Foreground="#e94560" Margin="20,0,0,0"/>
|
Foreground="#e94560" Margin="20,0,0,0"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Button Grid.Column="1" ToolTip="Menu" Style="{StaticResource IconButton}" Click="GearButton_Click">
|
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<ComboBox Style="{StaticResource YtComboBox}" MinWidth="150"
|
||||||
|
ItemsSource="{Binding QualityOptions}"
|
||||||
|
SelectedItem="{Binding SelectedQuality}"
|
||||||
|
DisplayMemberPath="Label"
|
||||||
|
ToolTip="{Binding ResolutionHelp}"
|
||||||
|
IsEnabled="{Binding IsOffline}"/>
|
||||||
|
<Button ToolTip="Menu" Style="{StaticResource IconButton}" Click="GearButton_Click">
|
||||||
<Button.ContextMenu>
|
<Button.ContextMenu>
|
||||||
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
||||||
<MenuItem Header="Save Layout" Command="{Binding SaveLayoutCommand}"/>
|
<MenuItem Header="Save Layout" Command="{Binding SaveLayoutCommand}"/>
|
||||||
@@ -582,7 +515,8 @@
|
|||||||
</Button.ContextMenu>
|
</Button.ContextMenu>
|
||||||
<Path Data="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"
|
<Path Data="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"
|
||||||
Fill="#d0d0d0" Width="16" Height="16" Stretch="Uniform"/>
|
Fill="#d0d0d0" Width="16" Height="16" Stretch="Uniform"/>
|
||||||
</Button>
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -638,16 +572,6 @@
|
|||||||
|
|
||||||
<TextBlock Text="These prefill the Start Stream dialog. Scene and stream settings are stored on the stream itself."
|
<TextBlock Text="These prefill the Start Stream dialog. Scene and stream settings are stored on the stream itself."
|
||||||
Style="{StaticResource YtLabel}" Margin="0,16,0,0" TextWrapping="Wrap"/>
|
Style="{StaticResource YtLabel}" Margin="0,16,0,0" TextWrapping="Wrap"/>
|
||||||
|
|
||||||
<TextBlock Text="STREAM SETTINGS" Style="{StaticResource SectionHeader}" Margin="0,20,0,8"/>
|
|
||||||
|
|
||||||
<TextBlock Text="Stream Quality" Style="{StaticResource YtLabel}" Margin="0,0,0,4"/>
|
|
||||||
<ComboBox Style="{StaticResource YtComboBox}" HorizontalAlignment="Left" MinWidth="140"
|
|
||||||
ItemsSource="{Binding StreamQualities}"
|
|
||||||
SelectedItem="{Binding StreamQuality}"/>
|
|
||||||
|
|
||||||
<TextBlock Text="Switching quality updates the stream metrics in the status bar. 1080p60 is the standard."
|
|
||||||
Style="{StaticResource YtLabel}" Margin="0,16,0,0" TextWrapping="Wrap"/>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Report Bug -->
|
<!-- Report Bug -->
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Windows;
|
|||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
using ytLive.Helpers;
|
||||||
using ytLive.Models;
|
using ytLive.Models;
|
||||||
using ytLive.ViewModels;
|
using ytLive.ViewModels;
|
||||||
|
|
||||||
@@ -15,11 +16,21 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
|
AppLog.Write("MainWindow ctor: before InitializeComponent");
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
AppLog.Write("MainWindow ctor: after InitializeComponent");
|
||||||
_viewModel = (MainViewModel)DataContext;
|
_viewModel = (MainViewModel)DataContext;
|
||||||
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||||
|
// When a focused scene is deleted, never land focus on a hidden scene.
|
||||||
|
SceneList.IsSelectable = item => item is Scene { IsHidden: false };
|
||||||
UpdateTaskbarOverlay();
|
UpdateTaskbarOverlay();
|
||||||
UpdateSelectionOverlay();
|
UpdateSelectionOverlay();
|
||||||
|
AppLog.Write("MainWindow ctor: end");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
AppLog.Write("MainWindow loaded");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace ytLive.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A resolution tier offered by the bottom-bar dropdown. Bitrate is Mbps.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record QualityOption(string Display, int Fps, double Bitrate)
|
||||||
|
{
|
||||||
|
/// <summary>Self-documenting label, e.g. "1080p60 (8Mbps)".</summary>
|
||||||
|
public string Label => $"{Display} ({Bitrate:0.#}Mbps)";
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Models — index
|
||||||
|
|
||||||
|
Plain data types. No logic beyond what a property can carry. See
|
||||||
|
[`schema.md`](../schema.md) for the memory-map conventions.
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `Scene.cs` | A scene: `Name`, `IsHidden`, `IsChatScene`, `IsEditing`, `Sources` collection |
|
||||||
|
| `Source.cs` | A source: `SourceType` enum (Image/Webcam/Screen/Background/Text), `X`/`Y`/`Width`/`Height`, `Opacity`, `IsEnabled`, `ImageSource`, asset identity |
|
||||||
|
| `QualityOption.cs` | Resolution tier record `(Display, Fps, Bitrate)`; `Label` formats as `1080p60 (8Mbps)` — drives the bottom-bar dropdown |
|
||||||
|
| `StreamConfig.cs` | `StreamConfig` (note: `TargetBitrate`/`Resolution` defaults are stale — not yet wired to the dropdown), `StreamHealth` (bitrate/FPS/dropped/duration), `StreamStatus` enum |
|
||||||
|
| `YouTube.cs` | `YouTubeChannel` and `ChatMessage` (chat feed) |
|
||||||
|
|
||||||
|
Related: [`ViewModels/index.md`](../ViewModels/index.md) consume these;
|
||||||
|
[`Services/LayoutStore.cs`](../Services/LayoutStore.cs) persists `Scene`/`Source`.
|
||||||
@@ -41,8 +41,14 @@ dotnet run
|
|||||||
|
|
||||||
| Path | Role |
|
| Path | Role |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `Models/` | Scene, Source, StreamConfig, StreamHealth, YouTube channel/chat |
|
| `Models/` | Scene, Source, QualityOption, StreamConfig, StreamHealth, YouTube channel/chat |
|
||||||
| `ViewModels/` | MainViewModel — scenes, stream controls, chat |
|
| `ViewModels/` | MainViewModel — scenes, stream controls, chat; GoLiveViewModel, ReuseImageViewModel |
|
||||||
| `Services/` | YouTubeAuthService (OAuth2), YouTubeStreamService (broadcast/health), YouTubeChatService (chat polling) |
|
| `Services/` | YouTubeAuthService (OAuth2), YouTubeStreamService (broadcast/health), YouTubeChatService (chat polling), LayoutStore (SQLite) |
|
||||||
| `Helpers/` | ViewModelBase, RelayCommand |
|
| `Helpers/` | ViewModelBase, RelayCommand, ImageCache, AppLog, FocusPreservingListBox, OAuthCredentials |
|
||||||
|
| `Themes/` | `Controls.xaml` — the dark-theme control styles, merged once in `App.xaml` |
|
||||||
| `MainWindow.xaml` | Dark-theme main UI: scene/source panel, preview, chat, status bar |
|
| `MainWindow.xaml` | Dark-theme main UI: scene/source panel, preview, chat, status bar |
|
||||||
|
|
||||||
|
## Docs (memory map)
|
||||||
|
|
||||||
|
`schema.md` defines the conventions; `ai.md` is the AI/session guide;
|
||||||
|
`TASKS.md` is the task queue. Each code directory carries an `index.md`.
|
||||||
|
|||||||
+5
-27
@@ -7,30 +7,7 @@
|
|||||||
ShowInTaskbar="False" Background="#1a1a2e">
|
ShowInTaskbar="False" Background="#1a1a2e">
|
||||||
|
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<Style x:Key="GoLiveButton" TargetType="Button">
|
<ResourceDictionary>
|
||||||
<Setter Property="Background" Value="#e94560"/>
|
|
||||||
<Setter Property="Foreground" Value="White"/>
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
<Setter Property="Padding" Value="16,8"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Cursor" Value="Hand"/>
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="Button">
|
|
||||||
<Border Background="{TemplateBinding Background}"
|
|
||||||
CornerRadius="4"
|
|
||||||
Padding="{TemplateBinding Padding}">
|
|
||||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
||||||
</Border>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="GoLiveButtonSecondary" TargetType="Button" BasedOn="{StaticResource GoLiveButton}">
|
|
||||||
<Setter Property="Background" Value="#16213e"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="CandidateItem" TargetType="ListBoxItem">
|
<Style x:Key="CandidateItem" TargetType="ListBoxItem">
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||||
@@ -53,6 +30,7 @@
|
|||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
<Grid Margin="24">
|
<Grid Margin="24">
|
||||||
@@ -95,11 +73,11 @@
|
|||||||
|
|
||||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
||||||
<Button Content="New Image…" Command="{Binding NewImageCommand}"
|
<Button Content="New Image…" Command="{Binding NewImageCommand}"
|
||||||
Style="{StaticResource GoLiveButtonSecondary}" Margin="0,0,8,0"/>
|
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0"/>
|
||||||
<Button Content="Cancel" Command="{Binding CancelCommand}"
|
<Button Content="Cancel" Command="{Binding CancelCommand}"
|
||||||
Style="{StaticResource GoLiveButtonSecondary}" Margin="0,0,8,0"/>
|
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0"/>
|
||||||
<Button Content="Use Selected" Command="{Binding UseSelectedCommand}"
|
<Button Content="Use Selected" Command="{Binding UseSelectedCommand}"
|
||||||
Style="{StaticResource GoLiveButton}" MinWidth="110"/>
|
Style="{StaticResource YtButton}" MinWidth="110"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using ytLive.Helpers;
|
||||||
using ytLive.ViewModels;
|
using ytLive.ViewModels;
|
||||||
|
|
||||||
namespace ytLive;
|
namespace ytLive;
|
||||||
@@ -10,7 +11,9 @@ public partial class ReuseImageDialog : Window
|
|||||||
|
|
||||||
public ReuseImageDialog(ReuseImageViewModel viewModel)
|
public ReuseImageDialog(ReuseImageViewModel viewModel)
|
||||||
{
|
{
|
||||||
|
AppLog.Write("ReuseImageDialog ctor: before InitializeComponent");
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
AppLog.Write("ReuseImageDialog ctor: after InitializeComponent");
|
||||||
DataContext = viewModel;
|
DataContext = viewModel;
|
||||||
viewModel.ReuseRequested += () =>
|
viewModel.ReuseRequested += () =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Services — index
|
||||||
|
|
||||||
|
External-facing logic: YouTube API, persistence. See
|
||||||
|
[`schema.md`](../schema.md) for the memory-map conventions.
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `YouTubeAuthService.cs` | OAuth2 via Google: loopback callback (`http://localhost:8765/oauth2/callback`), token exchange, refresh, channel fetch. **Complete — but no token persistence yet** (DPAPI planned) |
|
||||||
|
| `YouTubeStreamService.cs` | Broadcast/stream management via the v3 API (`enableAutoStart/Stop`). **Not yet switched to the `variable` reusable stream** |
|
||||||
|
| `YouTubeChatService.cs` | Polls `liveChat/messages`, raises `MessageReceived`; `IDisposable` |
|
||||||
|
| `LayoutStore.cs` | SQLite persistence (`Microsoft.Data.Sqlite`) at `%APPDATA%\ytLlive\ytLlive.db`; assets stored as BLOBs keyed by SHA-256 content hash; save/open layout files |
|
||||||
|
|
||||||
|
Related: constructed in [`ViewModels/MainViewModel.cs`](../ViewModels/MainViewModel.cs)
|
||||||
|
(no DI container yet). Models in [`Models/index.md`](../Models/index.md).
|
||||||
|
OAuth credentials live in [`Helpers/OAuthCredentials.cs`](../Helpers/OAuthCredentials.cs).
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
# ytLlive — Task List
|
# ytLlive — Task List
|
||||||
|
|
||||||
|
> Task queue and authoritative research. Memory-map conventions: [`schema.md`](schema.md);
|
||||||
|
> architecture/decisions: [`ai.md`](ai.md). Update statuses here whenever a task moves.
|
||||||
|
|
||||||
## YouTube Live API — research facts (authoritative, v3 build)
|
## YouTube Live API — research facts (authoritative, v3 build)
|
||||||
|
|
||||||
Lifecycle: `created → ready → [testing] → live → complete` (transitional `liveStarting` / `testStarting`).
|
Lifecycle: `created → ready → [testing] → live → complete` (transitional `liveStarting` / `testStarting`).
|
||||||
@@ -71,9 +74,9 @@ Lifecycle: `created → ready → [testing] → live → complete` (transitional
|
|||||||
|
|
||||||
### Status: 🔶 UI flow done — auth wiring pending
|
### Status: 🔶 UI flow done — auth wiring pending
|
||||||
- ✅ Two-state Start/End Stream button, go-live dialog (account + title/description/visibility), red top bar, pulsing LIVE badge + elapsed timer, preview glow, taskbar red dot
|
- ✅ Two-state Start/End Stream button, go-live dialog (account + title/description/visibility), red top bar, pulsing LIVE badge + elapsed timer, preview glow, taskbar red dot
|
||||||
- ⬜ Real OAuth2 with the baked-in Google credentials (desktop client type; loopback callback, no redirect registration)
|
- ✅ Real OAuth2 wiring — baked-in Google credentials (desktop client; loopback callback) + `YouTubeAuthService` complete: browser launch, `HttpListener` callback, token exchange, refresh, channel fetch
|
||||||
- ⬜ Token persistence via Windows DPAPI, reload on startup
|
- ⬜ Token persistence via Windows DPAPI, reload on startup
|
||||||
- ⬜ Account sign-in/change wired to YouTubeAuthService (currently simulated in GoLiveViewModel)
|
- ⬜ Account sign-in/change surfaced in the GoLive dialog (GoLiveViewModel still simulates it)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,458 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<!-- Base button style -->
|
||||||
|
<Style x:Key="YtButton" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="#e94560"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Padding" Value="16,8"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="4"
|
||||||
|
Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<Border x:Name="Glow" Background="White" CornerRadius="4" Opacity="0"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Glow" Property="Opacity" Value="0.08"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Glow" Property="Opacity" Value="0.16"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.45"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="YtButtonSecondary" TargetType="Button" BasedOn="{StaticResource YtButton}">
|
||||||
|
<Setter Property="Background" Value="#16213e"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="IconButton" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Padding" Value="4"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="3"
|
||||||
|
Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="#26ffffff"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="YtTextBox" TargetType="TextBox">
|
||||||
|
<Setter Property="Background" Value="#0f3460"/>
|
||||||
|
<Setter Property="Foreground" Value="#e0e0e0"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#2a3a5e"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="8,6"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="CaretBrush" Value="White"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="TextBox">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="4" Padding="{TemplateBinding Padding}">
|
||||||
|
<ScrollViewer x:Name="PART_ContentHost" Margin="0"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="#3d5076"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsKeyboardFocusWithin" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="#e94560"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="Bd" Property="Opacity" Value="0.45"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="YtComboBox" TargetType="ComboBox">
|
||||||
|
<Setter Property="Foreground" Value="#e0e0e0"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="Background" Value="#0f3460"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#2a3a5e"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="8,6"/>
|
||||||
|
<Setter Property="MinHeight" Value="30"/>
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||||
|
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||||
|
<Setter Property="ScrollViewer.CanContentScroll" Value="True"/>
|
||||||
|
<Setter Property="ItemContainerStyle">
|
||||||
|
<Setter.Value>
|
||||||
|
<Style TargetType="ComboBoxItem">
|
||||||
|
<Setter Property="Foreground" Value="#e0e0e0"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="Padding" Value="10,7"/>
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ComboBoxItem">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="3" Margin="0,1" Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsHighlighted" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="#26ffffff"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="#1a3a66"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ComboBox">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="Chrome" Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="4"/>
|
||||||
|
<ContentPresenter x:Name="ContentSite" IsHitTestVisible="False"
|
||||||
|
Margin="{TemplateBinding Padding}"
|
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Content="{TemplateBinding SelectionBoxItem}"
|
||||||
|
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
||||||
|
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"/>
|
||||||
|
<ToggleButton x:Name="ToggleButton" Focusable="False" ClickMode="Press"
|
||||||
|
Background="Transparent" BorderThickness="0"
|
||||||
|
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
|
||||||
|
<ToggleButton.Template>
|
||||||
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
|
<Grid Background="Transparent">
|
||||||
|
<Path x:Name="Chevron" Data="M0,0 L4,4 L8,0" Stroke="#8a93a8"
|
||||||
|
StrokeThickness="1.5" HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Chevron" Property="Stroke" Value="#c8d0e0"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.45"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</ToggleButton.Template>
|
||||||
|
</ToggleButton>
|
||||||
|
<Popup x:Name="PART_Popup" Placement="Bottom" AllowsTransparency="True"
|
||||||
|
PopupAnimation="Fade" StaysOpen="False"
|
||||||
|
IsOpen="{TemplateBinding IsDropDownOpen}">
|
||||||
|
<Border Background="#16213e" BorderBrush="#2a3a5e" BorderThickness="1"
|
||||||
|
CornerRadius="5" Padding="2" Margin="0,4,0,0"
|
||||||
|
SnapsToDevicePixels="True">
|
||||||
|
<ScrollViewer MaxHeight="{TemplateBinding MaxDropDownHeight}"
|
||||||
|
ScrollViewer.CanContentScroll="True">
|
||||||
|
<ItemsPresenter/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</Popup>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Chrome" Property="Background" Value="#163a6b"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsDropDownOpen" Value="True">
|
||||||
|
<Setter TargetName="Chrome" Property="Background" Value="#163a6b"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="Chrome" Property="Opacity" Value="0.45"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Thin dark scrollbar -->
|
||||||
|
<Style x:Key="YtScrollBarPageButton" TargetType="RepeatButton">
|
||||||
|
<Setter Property="Focusable" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RepeatButton">
|
||||||
|
<Grid Background="Transparent"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="YtScrollBarThumb" TargetType="Thumb">
|
||||||
|
<Setter Property="IsTabStop" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Thumb">
|
||||||
|
<Border x:Name="ThumbBd" Background="#2c3c5e" CornerRadius="4" Margin="2"/>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="ThumbBd" Property="Background" Value="#3d5076"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsDragging" Value="True">
|
||||||
|
<Setter TargetName="ThumbBd" Property="Background" Value="#4a5f8a"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="ScrollBar">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Width" Value="10"/>
|
||||||
|
<Setter Property="MinWidth" Value="10"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ScrollBar">
|
||||||
|
<Grid Background="Transparent">
|
||||||
|
<Track x:Name="PART_Track" IsDirectionReversed="True">
|
||||||
|
<Track.DecreaseRepeatButton>
|
||||||
|
<RepeatButton Command="{x:Static ScrollBar.PageUpCommand}"
|
||||||
|
Style="{StaticResource YtScrollBarPageButton}"/>
|
||||||
|
</Track.DecreaseRepeatButton>
|
||||||
|
<Track.IncreaseRepeatButton>
|
||||||
|
<RepeatButton Command="{x:Static ScrollBar.PageDownCommand}"
|
||||||
|
Style="{StaticResource YtScrollBarPageButton}"/>
|
||||||
|
</Track.IncreaseRepeatButton>
|
||||||
|
<Track.Thumb>
|
||||||
|
<Thumb Style="{StaticResource YtScrollBarThumb}"/>
|
||||||
|
</Track.Thumb>
|
||||||
|
</Track>
|
||||||
|
</Grid>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="Orientation" Value="Horizontal">
|
||||||
|
<Setter Property="Width" Value="Auto"/>
|
||||||
|
<Setter Property="Height" Value="10"/>
|
||||||
|
<Setter Property="MinHeight" Value="10"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Slider -->
|
||||||
|
<Style x:Key="YtSliderTrackButton" TargetType="RepeatButton">
|
||||||
|
<Setter Property="Focusable" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RepeatButton">
|
||||||
|
<Border Background="{Binding Tag, RelativeSource={RelativeSource TemplatedParent}}"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="YtSliderThumb" TargetType="Thumb">
|
||||||
|
<Setter Property="IsTabStop" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Thumb">
|
||||||
|
<Ellipse x:Name="Knob" Width="16" Height="16" Fill="#e94560"
|
||||||
|
Stroke="#ff8fa3" StrokeThickness="1"/>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Knob" Property="Fill" Value="#ff5b78"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsDragging" Value="True">
|
||||||
|
<Setter TargetName="Knob" Property="Fill" Value="#ff8fa3"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="Slider">
|
||||||
|
<Setter Property="Height" Value="20"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Slider">
|
||||||
|
<Grid VerticalAlignment="Center">
|
||||||
|
<Border Height="4" CornerRadius="2" Background="#2a3a5e"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Track x:Name="PART_Track" Height="18">
|
||||||
|
<Track.DecreaseRepeatButton>
|
||||||
|
<RepeatButton Command="{x:Static Slider.DecreaseLarge}"
|
||||||
|
Style="{StaticResource YtSliderTrackButton}" Tag="#e94560"/>
|
||||||
|
</Track.DecreaseRepeatButton>
|
||||||
|
<Track.IncreaseRepeatButton>
|
||||||
|
<RepeatButton Command="{x:Static Slider.IncreaseLarge}"
|
||||||
|
Style="{StaticResource YtSliderTrackButton}" Tag="Transparent"/>
|
||||||
|
</Track.IncreaseRepeatButton>
|
||||||
|
<Track.Thumb>
|
||||||
|
<Thumb Style="{StaticResource YtSliderThumb}"/>
|
||||||
|
</Track.Thumb>
|
||||||
|
</Track>
|
||||||
|
</Grid>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Dark tooltip -->
|
||||||
|
<Style TargetType="ToolTip">
|
||||||
|
<Setter Property="Background" Value="#1a1a2e"/>
|
||||||
|
<Setter Property="Foreground" Value="#e0e0e0"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#2a3a5e"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="8,6"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ToolTip">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="4" Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Setter Property="ContentTemplate">
|
||||||
|
<Setter.Value>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding}" TextWrapping="Wrap" MaxWidth="320"
|
||||||
|
Foreground="#e0e0e0" FontSize="12"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Dark context menu -->
|
||||||
|
<Style TargetType="ContextMenu">
|
||||||
|
<Setter Property="Background" Value="#16213e"/>
|
||||||
|
<Setter Property="Foreground" Value="#e0e0e0"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#2a3a5e"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="3"/>
|
||||||
|
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ContextMenu">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="5" Padding="{TemplateBinding Padding}">
|
||||||
|
<ItemsPresenter/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="MenuItem">
|
||||||
|
<Setter Property="Foreground" Value="#e0e0e0"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Padding" Value="10,6"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="MenuItem">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="3"
|
||||||
|
Padding="{TemplateBinding Padding}" Margin="0,1">
|
||||||
|
<ContentPresenter ContentSource="Header" VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Left"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsHighlighted" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="#26ffffff"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.45"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Menu separators render as standalone Separator controls -->
|
||||||
|
<Style TargetType="Separator">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Separator">
|
||||||
|
<Border Height="1" Background="#2a3a5e" Margin="8,4"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Dark list item (used by all list boxes) -->
|
||||||
|
<Style x:Key="YtListBoxItem" TargetType="ListBoxItem">
|
||||||
|
<Setter Property="Foreground" Value="#e0e0e0"/>
|
||||||
|
<Setter Property="Padding" Value="4,2"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="3" Padding="{TemplateBinding Padding}" Margin="0,1">
|
||||||
|
<ContentPresenter VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="#22ffffff"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="#1a3a66"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="YtLabel" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="#a0a0b0"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SectionHeader" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="#e0e0e0"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Themes — index
|
||||||
|
|
||||||
|
The single source of the dark UI theme. See [`schema.md`](../schema.md) for the
|
||||||
|
memory-map conventions.
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `Controls.xaml` | All custom control styles/templates: `YtButton`, `YtButtonSecondary`, `IconButton`, `YtTextBox`, `YtComboBox`, scrollbar/slider/tooltip/context-menu/menu-item/separator/list-item (implicit where app-wide), `YtListBoxItem`, `YtLabel`, `SectionHeader` |
|
||||||
|
|
||||||
|
How it's wired:
|
||||||
|
- Merged once in [`App.xaml`](../App.xaml) — app-wide. New windows get the theme
|
||||||
|
automatically; **do not re-merge per-window**.
|
||||||
|
- Palette: window `#1a1a2e`, fields `#0f3460`, borders `#2a3a5e`, popups
|
||||||
|
`#16213e`, accent `#e94560`, hover `#26ffffff`, text `#e0e0e0`.
|
||||||
|
|
||||||
|
Gotchas learned the hard way:
|
||||||
|
- `MenuItemRole` has **no `Separator` value** — separators are standalone
|
||||||
|
`Separator` controls with their own implicit style.
|
||||||
|
- A custom `ComboBox` template must put the `ContentPresenter` (`SelectionBoxItem`)
|
||||||
|
**inside the ComboBox template, not the ToggleButton template** — the latter
|
||||||
|
resolves against the wrong TemplatedParent and renders nothing.
|
||||||
|
|
||||||
|
Related: [`Helpers/index.md`](../Helpers/index.md) (custom controls the theme
|
||||||
|
styles), the XAML files at repo root that consume these styles.
|
||||||
+32
-14
@@ -255,30 +255,40 @@ public class MainViewModel : ViewModelBase
|
|||||||
set => SetProperty(ref _defaultStreamVisibility, value);
|
set => SetProperty(ref _defaultStreamVisibility, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string[] StreamQualities { get; } = { "1080p60", "1080p30", "720p60" };
|
// ─── Resolution quality dropdown (bottom bar) ───
|
||||||
|
// Tiers offered to the creator before going live. First = default. The
|
||||||
|
// dropdown is disabled while live because YouTube stream resolution is
|
||||||
|
// immutable after stream creation (see TASKS.md compliance notes).
|
||||||
|
|
||||||
private string _streamQuality = "1080p60";
|
public QualityOption[] QualityOptions { get; } =
|
||||||
public string StreamQuality
|
|
||||||
{
|
{
|
||||||
get => _streamQuality;
|
new("1080p60", 60, 8.0),
|
||||||
|
new("1080p30", 30, 8.0),
|
||||||
|
new("720p60", 60, 6.0),
|
||||||
|
new("480p30", 30, 2.5),
|
||||||
|
};
|
||||||
|
|
||||||
|
public string ResolutionHelp { get; } =
|
||||||
|
"Pick the highest resolution your upload bandwidth can comfortably sustain. " +
|
||||||
|
"Find your upload speed on your ISP plan or with a speed test (e.g. fast.com), " +
|
||||||
|
"then choose a tier your upload comfortably exceeds. 1080p60 is the default.";
|
||||||
|
|
||||||
|
private QualityOption _selectedQuality;
|
||||||
|
public QualityOption SelectedQuality
|
||||||
|
{
|
||||||
|
get => _selectedQuality;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (SetProperty(ref _streamQuality, value))
|
if (SetProperty(ref _selectedQuality, value))
|
||||||
ApplyStreamQuality(value);
|
ApplyStreamQuality(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyStreamQuality(string quality)
|
private void ApplyStreamQuality(QualityOption quality)
|
||||||
{
|
{
|
||||||
var (bitrate, fps) = quality switch
|
|
||||||
{
|
|
||||||
"1080p30" => (8.0, 30.0),
|
|
||||||
"720p60" => (6.0, 60.0),
|
|
||||||
_ => (8.0, 60.0),
|
|
||||||
};
|
|
||||||
var health = CurrentHealth;
|
var health = CurrentHealth;
|
||||||
health.CurrentBitrate = bitrate;
|
health.CurrentBitrate = quality.Bitrate;
|
||||||
health.FPS = fps;
|
health.FPS = quality.Fps;
|
||||||
OnPropertyChanged(nameof(CurrentHealth));
|
OnPropertyChanged(nameof(CurrentHealth));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,9 +341,13 @@ public class MainViewModel : ViewModelBase
|
|||||||
|
|
||||||
public MainViewModel()
|
public MainViewModel()
|
||||||
{
|
{
|
||||||
|
AppLog.Write("MainViewModel ctor begin");
|
||||||
_youtubeAuth = new YouTubeAuthService(OAuthCredentials.ClientId, OAuthCredentials.ClientSecret);
|
_youtubeAuth = new YouTubeAuthService(OAuthCredentials.ClientId, OAuthCredentials.ClientSecret);
|
||||||
_youtubeStream = new YouTubeStreamService(_youtubeAuth);
|
_youtubeStream = new YouTubeStreamService(_youtubeAuth);
|
||||||
_youtubeChat = new YouTubeChatService(_youtubeAuth);
|
_youtubeChat = new YouTubeChatService(_youtubeAuth);
|
||||||
|
AppLog.Write("MainViewModel ctor: services created");
|
||||||
|
|
||||||
|
_selectedQuality = QualityOptions[0];
|
||||||
|
|
||||||
_youtubeChat.MessageReceived += OnChatMessageReceived;
|
_youtubeChat.MessageReceived += OnChatMessageReceived;
|
||||||
|
|
||||||
@@ -369,6 +383,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
_layoutStore = new LayoutStore(DefaultLayoutPath);
|
_layoutStore = new LayoutStore(DefaultLayoutPath);
|
||||||
_activeLayoutPath = _layoutStore.ActivePath;
|
_activeLayoutPath = _layoutStore.ActivePath;
|
||||||
LoadLayout();
|
LoadLayout();
|
||||||
|
AppLog.Write("MainViewModel ctor end");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string DefaultLayoutPath => Path.Combine(
|
private static string DefaultLayoutPath => Path.Combine(
|
||||||
@@ -381,6 +396,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
AppLog.Write("LoadLayout begin");
|
||||||
var scenes = _layoutStore.Load();
|
var scenes = _layoutStore.Load();
|
||||||
Scenes.Clear();
|
Scenes.Clear();
|
||||||
foreach (var scene in scenes)
|
foreach (var scene in scenes)
|
||||||
@@ -394,6 +410,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
AddScene("Chat", isChatScene: true);
|
AddScene("Chat", isChatScene: true);
|
||||||
AddScene("Ending");
|
AddScene("Ending");
|
||||||
}
|
}
|
||||||
|
AppLog.Write($"LoadLayout: {Scenes.Count} scenes loaded");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -402,6 +419,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
ActiveScene = Scenes.FirstOrDefault();
|
ActiveScene = Scenes.FirstOrDefault();
|
||||||
UpdateActiveBackground();
|
UpdateActiveBackground();
|
||||||
ScheduleSave();
|
ScheduleSave();
|
||||||
|
AppLog.Write("LoadLayout end");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Shutdown()
|
public void Shutdown()
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# ViewModels — index
|
||||||
|
|
||||||
|
MVVM layer. See [`schema.md`](../schema.md) for the memory-map conventions.
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `MainViewModel.cs` | The app brain: scenes/sources collections + commands, stream state (`IsLive`/`IsOffline`/`IsConnected`), chat feed, overlays, layout save/open, **resolution dropdown** (`QualityOptions`, `SelectedQuality`, `ResolutionHelp`, `ApplyStreamQuality`) |
|
||||||
|
| `GoLiveViewModel.cs` | Start Stream dialog: title/description/visibility, start/cancel requests |
|
||||||
|
| `ReuseImageViewModel.cs` | Add Image dialog: candidate list (`ReuseImageCandidate`), reuse/new/cancel |
|
||||||
|
|
||||||
|
Related: [`Models/index.md`](../Models/index.md) for the data; [`Services/index.md`](../Services/index.md)
|
||||||
|
for what the ViewModels drive; base class in [`Helpers/ViewModelBase.cs`](../Helpers/ViewModelBase.cs).
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
# ytLlive — AI Guide
|
# ytLlive — AI Guide
|
||||||
|
|
||||||
|
> Memory map entry point. Conventions live in [`schema.md`](schema.md); task
|
||||||
|
> status and YouTube API research in [`TASKS.md`](TASKS.md); directory maps in
|
||||||
|
> each folder's `index.md`. Reading order: this file → `TASKS.md` → `<dir>/index.md` → source.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -19,10 +23,11 @@ C# / WPF (.NET 8) following MVVM:
|
|||||||
|
|
||||||
| Path | Role |
|
| Path | Role |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `Models/` | Plain data types — Scene, Source, StreamConfig, StreamHealth, YouTubeChannel, ChatMessage |
|
| `Models/` | Plain data types — Scene, Source, QualityOption, StreamConfig, StreamHealth, YouTubeChannel, ChatMessage |
|
||||||
| `ViewModels/` | MainViewModel — exposes collections + commands for the UI |
|
| `ViewModels/` | MainViewModel — exposes collections + commands for the UI; GoLiveViewModel, ReuseImageViewModel |
|
||||||
| `Services/` | YouTube OAuth2, stream/broadcast management, live chat polling, LayoutStore (SQLite) |
|
| `Services/` | YouTube OAuth2, stream/broadcast management, live chat polling, LayoutStore (SQLite) |
|
||||||
| `Helpers/` | ViewModelBase (INotifyPropertyChanged), RelayCommand, ImageCache |
|
| `Helpers/` | ViewModelBase (INotifyPropertyChanged), RelayCommand, ImageCache, AppLog (file logger), FocusPreservingListBox, OAuthCredentials, visibility converters |
|
||||||
|
| `Themes/` | `Controls.xaml` — the single dark-theme source, merged once in `App.xaml` (see `Themes/index.md`) |
|
||||||
| `MainWindow.xaml` | Dark theme; layout: top bar (controls), center (preview), left (scenes/sources), right (chat), bottom (health) |
|
| `MainWindow.xaml` | Dark theme; layout: top bar (controls), center (preview), left (scenes/sources), right (chat), bottom (health) |
|
||||||
|
|
||||||
### Key patterns
|
### Key patterns
|
||||||
@@ -32,16 +37,18 @@ C# / WPF (.NET 8) following MVVM:
|
|||||||
- ViewModels are constructed in XAML (`<vm:MainViewModel/>` as DataContext)
|
- ViewModels are constructed in XAML (`<vm:MainViewModel/>` as DataContext)
|
||||||
- Services are currently instantiated in MainViewModel's constructor — no DI container yet
|
- Services are currently instantiated in MainViewModel's constructor — no DI container yet
|
||||||
- Layout persists to SQLite (`Microsoft.Data.Sqlite`); scenes/sources/asset bytes stored in the DB, asset identity is a SHA-256 content hash (1:M reuse, no file paths — assets are always available)
|
- Layout persists to SQLite (`Microsoft.Data.Sqlite`); scenes/sources/asset bytes stored in the DB, asset identity is a SHA-256 content hash (1:M reuse, no file paths — assets are always available)
|
||||||
|
- Theming: all custom styles live in `Themes/Controls.xaml`, merged in `App.xaml` — never duplicate styles per-window (dialog duplicates were consolidated into this dictionary)
|
||||||
|
- Resolution tiers (bottom bar): 1080p60@8 → 1080p30@8 → 720p60@6 → 480p30@2.5 Mbps; default = first. A **tooltip** explains finding upload bandwidth — an in-app speed test was deliberately dropped (unreliable)
|
||||||
|
- Crash diagnosis: `AppLog` writes startup checkpoints to `%APPDATA%\ytLlive\startup.log`; `App.xaml.cs` logs `DispatcherUnhandledException`/`AppDomain.UnhandledException`. When WPF won't run from WSL, this log is how you find the failure (it caught the `MenuItemRole.Separator` XAML crash and the ComboBox SelectionBoxItem bug)
|
||||||
|
|
||||||
### Current limitations / TODOs
|
### Current limitations / TODOs
|
||||||
|
|
||||||
- `OAuthCredentials.ClientId` / `ClientSecret` in `Helpers/OAuthCredentials.cs` are empty — the app
|
- `Helpers/OAuthCredentials.cs` now contains the real ClientId/ClientSecret — auth service is implemented, but **tokens still don't persist** (Windows DPAPI planned; account UI in the GoLive dialog is simulated)
|
||||||
owner fills them in once (developer task, baked into the binary; creators never configure anything)
|
|
||||||
- `GoLiveViewModel.SignIn`/`ChangeAccount` removed — Connect (OAuth) is the only entry to streaming
|
- `GoLiveViewModel.SignIn`/`ChangeAccount` removed — Connect (OAuth) is the only entry to streaming
|
||||||
- No token persistence yet (Windows DPAPI planned) — scene/source/asset layout *does* persist (SQLite)
|
- Scene/source/asset layout *does* persist (SQLite); token persistence does not (yet)
|
||||||
- `YouTubeStreamService` uses hardcoded `1080p`/`60fps` and per-broadcast streams — must switch to the v3 `variable` reusable stream
|
- `YouTubeStreamService` uses hardcoded `1080p`/`60fps` and per-broadcast streams — must switch to the v3 `variable` reusable stream
|
||||||
- No capture/encoding/RTMP yet
|
- No capture/encoding/RTMP yet
|
||||||
- Stream config (title/description/visibility/quality) still in-memory
|
- `StreamConfig` defaults (`TargetBitrate=6000`, `Resolution="1920x1080"`) are stale — the live dropdown drives `StreamHealth.CurrentBitrate`/`FPS` instead
|
||||||
|
|
||||||
## Design Principle
|
## Design Principle
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# ytLlive — Memory Map (schema)
|
||||||
|
|
||||||
|
This file defines how the repo stores durable, associative memory: context as
|
||||||
|
plain markdown files, versioned in git. It is the map of the map — the schema
|
||||||
|
that every other memory file follows. Read `ai.md` first; this file explains
|
||||||
|
*why* and *how*.
|
||||||
|
|
||||||
|
## Why markdown files, not a database
|
||||||
|
|
||||||
|
- Grep-able, diffable, portable, human-editable — no tooling lock-in.
|
||||||
|
- Git gives versioning and history for free; every stale fact is a `git log` away.
|
||||||
|
- Files form a graph: each file is a node, each relative link an edge. The map
|
||||||
|
stays associative (like memory) rather than a rigid tree.
|
||||||
|
|
||||||
|
## File inventory
|
||||||
|
|
||||||
|
| File | Role | Entry point? |
|
||||||
|
|------|------|--------------|
|
||||||
|
| `README.md` | Human-facing intro: what the app is, how to run it, roadmap | no |
|
||||||
|
| `ai.md` | **AI guide + session handoff** — architecture, patterns, decisions, the cognitive map home | **yes — start here** |
|
||||||
|
| `TASKS.md` | Task queue + authoritative YouTube API research facts + task statuses | yes — for status |
|
||||||
|
| `schema.md` | This file: the conventions below | when in doubt |
|
||||||
|
| `<dir>/index.md` | Per-directory map (progressive disclosure): what lives there + links | when diving into code |
|
||||||
|
| `Views/` | Reserved for Views; currently empty | — |
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
1. **Semantic filenames.** A file's name says what it is (`YouTubeAuthService.cs`,
|
||||||
|
`FocusPreservingListBox.cs`). No codes, no abbreviations. Naming beats search.
|
||||||
|
2. **One `index.md` per code directory.** The index is a Map of Content (MOC):
|
||||||
|
it lists every file in that directory with a one-line purpose and links out to
|
||||||
|
related directories. Details live in the files, never duplicated in the index.
|
||||||
|
3. **Links over hierarchy.** Prefer relative links between related files over
|
||||||
|
deep nesting. Relationships that matter should be visible from `ai.md` or an
|
||||||
|
`index.md`; a note that is unreachable from `ai.md` might as well not exist.
|
||||||
|
4. **Stale facts get FIXED, not appended.** A wrong map is worse than no map.
|
||||||
|
When the code changes, update the memory file in the same change — no
|
||||||
|
"TODO: update later" notes.
|
||||||
|
5. **Progressive disclosure.** Don't cram detail into `ai.md`. `ai.md` names
|
||||||
|
concepts and points at files; the indexes and source are the detail. Each
|
||||||
|
layer adds resolution only when you drill in.
|
||||||
|
|
||||||
|
## Reading order
|
||||||
|
|
||||||
|
1. `ai.md` — what this app is, how it's built, current state, open decisions.
|
||||||
|
2. `TASKS.md` — what's done, what's next, and the YouTube API constraints.
|
||||||
|
3. `<dir>/index.md` — to understand a directory before reading its files.
|
||||||
|
4. The source files themselves — always the authority over any memory file.
|
||||||
|
|
||||||
|
## Integration rule
|
||||||
|
|
||||||
|
Every feature change ships with its memory update: `ai.md` for architecture /
|
||||||
|
patterns, `TASKS.md` for status, index files when the layout changes. That is
|
||||||
|
what keeps the map accurate enough to trust next session.
|
||||||
Reference in New Issue
Block a user