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:
+32
-14
@@ -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()
|
||||
|
||||
@@ -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).
|
||||
Reference in New Issue
Block a user