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:
2026-08-06 08:33:05 -07:00
parent 7dc8490d96
commit a74162e34e
22 changed files with 876 additions and 230 deletions
+32 -14
View File
@@ -255,30 +255,40 @@ public class MainViewModel : ViewModelBase
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 string StreamQuality
public QualityOption[] QualityOptions { get; } =
{
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
{
if (SetProperty(ref _streamQuality, value))
if (SetProperty(ref _selectedQuality, 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;
health.CurrentBitrate = bitrate;
health.FPS = fps;
health.CurrentBitrate = quality.Bitrate;
health.FPS = quality.Fps;
OnPropertyChanged(nameof(CurrentHealth));
}
@@ -331,9 +341,13 @@ public class MainViewModel : ViewModelBase
public MainViewModel()
{
AppLog.Write("MainViewModel ctor begin");
_youtubeAuth = new YouTubeAuthService(OAuthCredentials.ClientId, OAuthCredentials.ClientSecret);
_youtubeStream = new YouTubeStreamService(_youtubeAuth);
_youtubeChat = new YouTubeChatService(_youtubeAuth);
AppLog.Write("MainViewModel ctor: services created");
_selectedQuality = QualityOptions[0];
_youtubeChat.MessageReceived += OnChatMessageReceived;
@@ -369,6 +383,7 @@ public class MainViewModel : ViewModelBase
_layoutStore = new LayoutStore(DefaultLayoutPath);
_activeLayoutPath = _layoutStore.ActivePath;
LoadLayout();
AppLog.Write("MainViewModel ctor end");
}
private static string DefaultLayoutPath => Path.Combine(
@@ -381,6 +396,7 @@ public class MainViewModel : ViewModelBase
_isLoading = true;
try
{
AppLog.Write("LoadLayout begin");
var scenes = _layoutStore.Load();
Scenes.Clear();
foreach (var scene in scenes)
@@ -394,6 +410,7 @@ public class MainViewModel : ViewModelBase
AddScene("Chat", isChatScene: true);
AddScene("Ending");
}
AppLog.Write($"LoadLayout: {Scenes.Count} scenes loaded");
}
finally
{
@@ -402,6 +419,7 @@ public class MainViewModel : ViewModelBase
ActiveScene = Scenes.FirstOrDefault();
UpdateActiveBackground();
ScheduleSave();
AppLog.Write("LoadLayout end");
}
public void Shutdown()
+12
View File
@@ -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).