From 535a50192c6f6f92dcc99258839aee5a1969aedd Mon Sep 17 00:00:00 2001 From: gramps Date: Sun, 30 Aug 2026 08:24:22 -0700 Subject: [PATCH] =?UTF-8?q?refactor:=20split=20Trax=20out=20of=20MainViewM?= =?UTF-8?q?odel=20=E2=80=94=20Commit=207/12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MainViewModel.Trax.cs: music-player state (_musicPlayer/_music/_musicPlaying, TraxIdleBrush), TRAX UI (IsMusicPlaying/TraxError/TraxDotBrush/TraxToolTip), interaction (TraxLeftClick/TraxRightClick/PickTraxTrack), playback (LoadTrax/PlayTrax/PauseTrax/OnTraxPlaybackEnded/RefreshTraxUi), local loopback ducking (UpdateTraxLocalGain). Core keeps the ctor event hookup, LoadLayout/Shutdown/SaveLayoutNow call sites, and Scenes partial's hotkey dispatch. Verify: 0 warnings, 246 passed, 2 known failures only. --- ViewModels/MainViewModel.Trax.cs | 132 +++++++++++++++++++++++++++++++ ViewModels/MainViewModel.cs | 121 ---------------------------- ViewModels/index.md | 12 ++- 3 files changed, 141 insertions(+), 124 deletions(-) create mode 100644 ViewModels/MainViewModel.Trax.cs diff --git a/ViewModels/MainViewModel.Trax.cs b/ViewModels/MainViewModel.Trax.cs new file mode 100644 index 0000000..6afb6aa --- /dev/null +++ b/ViewModels/MainViewModel.Trax.cs @@ -0,0 +1,132 @@ +using System.IO; +using System.Windows.Media; +using Microsoft.Win32; +using ytLive.Helpers; +using ytLive.Models; +using ytLive.Services.Audio; + +namespace ytLive.ViewModels; + +public partial class MainViewModel +{ + // TRAX (TASK 9): free background music at a fixed quiet volume, playing to + // the default device so it rides the loopback into the stream. The dot is + // red (no track) / yellow (loaded, stopped) / green (playing). + private readonly MusicPlayer _musicPlayer = new(); + private Music? _music; + private bool _musicPlaying; + + private static readonly SolidColorBrush TraxIdleBrush = CreateBrush("#555"); + /// True while the TRAX background track is playing. + public bool IsMusicPlaying + { + get => _musicPlaying; + private set + { + if (SetProperty(ref _musicPlaying, value)) + { + OnPropertyChanged(nameof(TraxDotBrush)); + OnPropertyChanged(nameof(TraxToolTip)); + } + } + } + + private string? _traxError; + + /// Error message from the last TRAX load/playback failure; null = no error. + public string? TraxError + { + get => _traxError; + private set => SetProperty(ref _traxError, value); + } + + /// TRAX status dot: green = playing, gray = idle or no track. + public SolidColorBrush TraxDotBrush => _musicPlayer.IsPlaying + ? BarOnBrush + : TraxIdleBrush; + + public string TraxToolTip => _musicPlayer.TrackName != null + ? $"TRAX: {_musicPlayer.TrackName} — left-click {(IsMusicPlaying ? "pauses" : "plays")}, right-click chooses a track" + : "No track — left-click or right-click to choose background music"; + + /// Left-click on TRAX: no track → pick one; otherwise toggle play/pause. + public void TraxLeftClick() + { + if (_music == null || string.IsNullOrWhiteSpace(_music.TrackPath)) + PickTraxTrack(); + else if (IsMusicPlaying) + PauseTrax(); + else + PlayTrax(); + } + + /// Right-click on TRAX: always open the track picker (never the OS viewer). + public void TraxRightClick() => PickTraxTrack(); + + private void PickTraxTrack() + { + var dialog = new OpenFileDialog + { + Title = "Choose background music", + Filter = "Music (*.mp3;*.wav;*.m4a;*.aac;*.flac;*.ogg)|*.mp3;*.wav;*.m4a;*.aac;*.flac;*.ogg|All files (*.*)|*.*", + }; + if (dialog.ShowDialog() != true) + return; + + _music = new Music { TrackPath = dialog.FileName, IsEnabled = true }; + LoadTrax(play: true); + } + + private void LoadTrax(bool play) + { + if (_music == null || string.IsNullOrWhiteSpace(_music.TrackPath)) + return; + + try + { + _musicPlayer.Load(_music.TrackPath); + UpdateTraxLocalGain(); + _music.IsEnabled = true; + TraxError = null; + if (play) + _musicPlayer.Play(); + } + catch (Exception ex) + { + AppLog.Write($"TRAX: failed to load '{_music.TrackPath}': {ex.Message}"); + TraxError = $"Failed to load: {Path.GetFileName(_music.TrackPath)} — {ex.Message}"; + } + RefreshTraxUi(); + ScheduleSave(); + } + + private void PlayTrax() => _musicPlayer.Play(); + + private void PauseTrax() => _musicPlayer.Pause(); + + private void OnTraxPlaybackEnded(Exception? ex) + { + if (ex != null) + { + AppLog.Write($"TRAX: playback error: {ex.Message}"); + TraxError = $"Playback error: {ex.Message}"; + } + RefreshTraxUi(); + } + + private void RefreshTraxUi() + { + IsMusicPlaying = _musicPlayer.IsPlaying; + OnPropertyChanged(nameof(TraxDotBrush)); + OnPropertyChanged(nameof(TraxToolTip)); + } + + /// Mirrors the desktop/game audio bar's volume and mute onto the + /// music so the creator hears the controls work in the headphones (the + /// track rides the loopback channel, so it must duck the same way locally). + /// Called whenever the game volume/mute change and on TRAX load. + private void UpdateTraxLocalGain() + { + _musicPlayer.LocalGain = GameMuted ? 0f : 1f; + } +} diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 9f716eb..34ffa95 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -31,12 +31,6 @@ public partial class MainViewModel : ViewModelBase private SceneElement? _selectedElement; - // TRAX (TASK 9): free background music at a fixed quiet volume, playing to - // the default device so it rides the loopback into the stream. The dot is - // red (no track) / yellow (loaded, stopped) / green (playing). - private readonly MusicPlayer _musicPlayer = new(); - private Music? _music; - private bool _musicPlaying; private StreamStatus _streamStatus = StreamStatus.Offline; private StreamHealth _currentHealth = new(); private string _streamTitle = string.Empty; @@ -234,121 +228,6 @@ public partial class MainViewModel : ViewModelBase } } - private static readonly SolidColorBrush TraxIdleBrush = CreateBrush("#555"); - /// True while the TRAX background track is playing. - public bool IsMusicPlaying - { - get => _musicPlaying; - private set - { - if (SetProperty(ref _musicPlaying, value)) - { - OnPropertyChanged(nameof(TraxDotBrush)); - OnPropertyChanged(nameof(TraxToolTip)); - } - } - } - - private string? _traxError; - - /// Error message from the last TRAX load/playback failure; null = no error. - public string? TraxError - { - get => _traxError; - private set => SetProperty(ref _traxError, value); - } - - /// TRAX status dot: green = playing, gray = idle or no track. - public SolidColorBrush TraxDotBrush => _musicPlayer.IsPlaying - ? BarOnBrush - : TraxIdleBrush; - - public string TraxToolTip => _musicPlayer.TrackName != null - ? $"TRAX: {_musicPlayer.TrackName} — left-click {(IsMusicPlaying ? "pauses" : "plays")}, right-click chooses a track" - : "No track — left-click or right-click to choose background music"; - - /// Left-click on TRAX: no track → pick one; otherwise toggle play/pause. - public void TraxLeftClick() - { - if (_music == null || string.IsNullOrWhiteSpace(_music.TrackPath)) - PickTraxTrack(); - else if (IsMusicPlaying) - PauseTrax(); - else - PlayTrax(); - } - - /// Right-click on TRAX: always open the track picker (never the OS viewer). - public void TraxRightClick() => PickTraxTrack(); - - private void PickTraxTrack() - { - var dialog = new OpenFileDialog - { - Title = "Choose background music", - Filter = "Music (*.mp3;*.wav;*.m4a;*.aac;*.flac;*.ogg)|*.mp3;*.wav;*.m4a;*.aac;*.flac;*.ogg|All files (*.*)|*.*", - }; - if (dialog.ShowDialog() != true) - return; - - _music = new Music { TrackPath = dialog.FileName, IsEnabled = true }; - LoadTrax(play: true); - } - - private void LoadTrax(bool play) - { - if (_music == null || string.IsNullOrWhiteSpace(_music.TrackPath)) - return; - - try - { - _musicPlayer.Load(_music.TrackPath); - UpdateTraxLocalGain(); - _music.IsEnabled = true; - TraxError = null; - if (play) - _musicPlayer.Play(); - } - catch (Exception ex) - { - AppLog.Write($"TRAX: failed to load '{_music.TrackPath}': {ex.Message}"); - TraxError = $"Failed to load: {Path.GetFileName(_music.TrackPath)} — {ex.Message}"; - } - RefreshTraxUi(); - ScheduleSave(); - } - - private void PlayTrax() => _musicPlayer.Play(); - - private void PauseTrax() => _musicPlayer.Pause(); - - private void OnTraxPlaybackEnded(Exception? ex) - { - if (ex != null) - { - AppLog.Write($"TRAX: playback error: {ex.Message}"); - TraxError = $"Playback error: {ex.Message}"; - } - RefreshTraxUi(); - } - - private void RefreshTraxUi() - { - IsMusicPlaying = _musicPlayer.IsPlaying; - OnPropertyChanged(nameof(TraxDotBrush)); - OnPropertyChanged(nameof(TraxToolTip)); - } - - /// Mirrors the desktop/game audio bar's volume and mute onto the - /// music so the creator hears the controls work in the headphones (the - /// track rides the loopback channel, so it must duck the same way locally). - /// Called whenever the game volume/mute change and on TRAX load. - private void UpdateTraxLocalGain() - { - _musicPlayer.LocalGain = GameMuted ? 0f : 1f; - } - - public bool IsOffline => StreamStatus == StreamStatus.Offline; public bool IsLive => StreamStatus == StreamStatus.Streaming; public bool IsLivePrivate => IsLive && string.Equals(StreamVisibility, "Private", StringComparison.OrdinalIgnoreCase); diff --git a/ViewModels/index.md b/ViewModels/index.md index 9ee6889..ca393e8 100644 --- a/ViewModels/index.md +++ b/ViewModels/index.md @@ -68,14 +68,20 @@ Progress (Commit N/12 · tag): `OnLoopbackLevelChanged`/`OnMicConnected`/`OnMicFailed`, `StartMicCaptureAsync`, audio fields + `ToggleMicMuteCommand`/`ToggleGameMuteCommand`/`OpenMicPickerCommand`). TRAX stays in core (next commit). +- **Commit 7/12 · `refactor-commit-7`: Trax landed** → `MainViewModel.Trax.cs` + (`_musicPlayer`/`_music`/`_musicPlaying` + `TraxIdleBrush`, `IsMusicPlaying`/ + `TraxError`/`TraxDotBrush`/`TraxToolTip`, `TraxLeftClick`/`TraxRightClick`/ + `PickTraxTrack`/`LoadTrax`/`PlayTrax`/`PauseTrax`/`OnTraxPlaybackEnded`/ + `RefreshTraxUi`/`UpdateTraxLocalGain`). Core keeps the ctor hookup + (`_musicPlayer.PlaybackEnded += ...`), the `LoadLayout`/`Shutdown`/ + `SaveLayoutNow` call sites, and the Scenes partial's hotkey dispatch into `TraxLeftClick`. - Core file keeps: fields, ctor, `Shutdown`/`SaveLayoutNow`/`ScheduleSave`/`LoadLayout`, `SetPanel`/`ShowOverlay`/`ShowLicenses`, `CreateBrush`, `OpenUrl`/`ComposeEmail`, `SubmitBug`/`SubmitFeature`, window-geometry pass-through. - Partials: `Account` ✔ (Commit 1), `License` ✔ (Commit 2), `Scenes` ✔ (Commit 3), `Background` ✔ (Commit 4), - `Webcam` ✔ (Commit 5), `Audio` ✔ (Commit 6), - `Trax` (music player), `Socials` (social bar + dialog), - `Streaming` (go-live/record + health + quality tiers + media source pipeline), + `Webcam` ✔ (Commit 5), `Audio` ✔ (Commit 6), `Trax` ✔ (Commit 7), + `Socials` (social bar + dialog), `Streaming` (go-live/record + health + quality tiers + media source pipeline), `Chat` (YouTube chat + mock preview + chatbox render), `Overlays` (settings/bug/feature/about panels + license entry).