Gear cleanup + Default Location for Recordings + single Start + socials hover glow

KISS: removed Gear-menu Save/SaveAs/Open Layout (redundant with auto-save; multi-layout = OBS).
App Settings: first real setting — Default Location for Recordings (Browse + Use Downloads reset,
fallback: configured → Downloads → MyVideos). Single Start button (pills = intent, button = trigger).
Socials button moved to preview bottom row, left of TRAX; hover pulses the SocialBarElement
DropShadowEffect (sine wave BlurRadius 24→48, Opacity 0.7→1.0 at 30ms). TextBox OneWay binding
fix for get-only RecordFolderDisplay. ONE fallback test. Build 0 warnings, 247 tests (2 known).
This commit is contained in:
2026-08-29 10:30:40 -07:00
parent eba6af5b9b
commit 6031a5682d
6 changed files with 159 additions and 120 deletions
+28 -54
View File
@@ -84,6 +84,10 @@ public class MainViewModel : ViewModelBase
private bool _onAirPillOn;
private bool _isRecording;
private string? _recordFolder;
/// <summary>Resolved record folder — the configured choice, or Downloads (0th case).</summary>
public string RecordFolderDisplay => _recordFolder ?? DefaultRecordFolder();
private string? _activeRecordPath;
private DateTime _recordStartTime;
private TimeSpan _recordLength;
@@ -519,8 +523,6 @@ public class MainViewModel : ViewModelBase
OnPropertyChanged(nameof(RecDotOpacity));
OnPropertyChanged(nameof(OnAirBrush));
OnPropertyChanged(nameof(IsLivePrivate));
OnPropertyChanged(nameof(IsStreamingStart));
OnPropertyChanged(nameof(PrimaryButtonText));
OnPropertyChanged(nameof(ShowPrimaryStartButton));
OnPropertyChanged(nameof(ShowLogInButton));
OnPropertyChanged(nameof(ShowEndStreamButton));
@@ -545,8 +547,6 @@ public class MainViewModel : ViewModelBase
if (!value) OnAirPillOn = false;
OnPropertyChanged(nameof(CanToggleOnAir));
OnPropertyChanged(nameof(ShowPrimaryStartButton));
OnPropertyChanged(nameof(PrimaryButtonText));
OnPropertyChanged(nameof(IsStreamingStart));
OnPropertyChanged(nameof(AccountStatusLightToolTip));
}
}
@@ -1004,7 +1004,6 @@ public class MainViewModel : ViewModelBase
set
{
if (!SetProperty(ref _recordPillOn, value)) return;
OnPropertyChanged(nameof(PrimaryButtonText));
OnPropertyChanged(nameof(ShowPrimaryStartButton));
OnPropertyChanged(nameof(CanStartSession));
}
@@ -1017,7 +1016,6 @@ public class MainViewModel : ViewModelBase
{
if (value && !CanToggleOnAir) return; // needs a sign-in
if (!SetProperty(ref _onAirPillOn, value)) return;
OnPropertyChanged(nameof(PrimaryButtonText));
OnPropertyChanged(nameof(ShowPrimaryStartButton));
OnPropertyChanged(nameof(CanStartSession));
}
@@ -1045,12 +1043,6 @@ public class MainViewModel : ViewModelBase
}
}
/// <summary>"Start Streaming" unless ONLY the record pill is on → "Start Recording".</summary>
public string PrimaryButtonText => IsStreamingStart ? "Start Streaming" : "Start Recording";
/// <summary>Streaming (not record-only) — the ON-AIR pill is on OR we're already live.</summary>
public bool IsStreamingStart => OnAirPillOn || IsLive;
/// <summary>The primary button shows when there's something to start, but never
/// while a session is running — once recording/live the ONLY control is the End
/// button.</summary>
@@ -1613,6 +1605,7 @@ public class MainViewModel : ViewModelBase
public ICommand StartStreamCommand { get; }
public ICommand EndStreamCommand { get; }
public ICommand ChooseRecordFolderCommand { get; }
public ICommand ResetRecordFolderCommand { get; }
public ICommand OpenSettingsCommand { get; }
public ICommand OpenBugCommand { get; }
public ICommand OpenFeatureCommand { get; }
@@ -1625,9 +1618,6 @@ public class MainViewModel : ViewModelBase
public ICommand OpenCoffeeCommand { get; }
public ICommand OpenPremiumCommand { get; }
public ICommand OpenLicensesCommand { get; }
public ICommand SaveLayoutCommand { get; }
public ICommand SaveLayoutAsCommand { get; }
public ICommand OpenLayoutCommand { get; }
public ICommand ActivateLicenseCommand { get; }
public ICommand OpenLicenseEntryCommand { get; }
public ICommand OpenHotkeyConfigCommand { get; }
@@ -1702,9 +1692,7 @@ public class MainViewModel : ViewModelBase
StartStreamCommand = new RelayCommand(_ => StartSession());
EndStreamCommand = new RelayCommand(_ => StopStream(), _ => IsLive || IsRecording);
ChooseRecordFolderCommand = new RelayCommand(_ => ChooseRecordFolder());
SaveLayoutCommand = new RelayCommand(_ => SaveLayoutNow());
SaveLayoutAsCommand = new RelayCommand(_ => SaveLayoutAs());
OpenLayoutCommand = new RelayCommand(_ => OpenLayoutFile());
ResetRecordFolderCommand = new RelayCommand(_ => ResetRecordFolder());
ActivateLicenseCommand = new RelayCommand(_ => _ = ActivateLicenseAsync(), _ => !IsLicenseValidating);
OpenLicenseEntryCommand = new RelayCommand(_ => IsLicenseEntryOpen = !IsLicenseEntryOpen);
OpenHotkeyConfigCommand = new RelayCommand(_ => OpenHotkeyConfig());
@@ -2439,38 +2427,6 @@ public class MainViewModel : ViewModelBase
}
}
private void SaveLayoutAs()
{
var dialog = new SaveFileDialog
{
Title = "Save Layout As",
Filter = "LlamaCasty layout (*.yll)|*.yll|All files (*.*)|*.*",
DefaultExt = ".yll",
FileName = "my-layout.yll",
};
if (dialog.ShowDialog() != true) return;
_layoutStore.Dispose();
_layoutStore = new LayoutStore(dialog.FileName);
_activeLayoutPath = dialog.FileName;
SaveLayoutNow();
}
private void OpenLayoutFile()
{
var dialog = new OpenFileDialog
{
Title = "Open Layout",
Filter = "LlamaCasty layout (*.yll;*.db)|*.yll;*.db|All files (*.*)|*.*",
};
if (dialog.ShowDialog() != true) return;
_layoutStore.Dispose();
_layoutStore = new LayoutStore(dialog.FileName);
_activeLayoutPath = dialog.FileName;
LoadLayout();
}
// ─── Auto-save wiring ───
private void OnScenesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
@@ -3195,8 +3151,7 @@ public class MainViewModel : ViewModelBase
{
try
{
var folder = _recordFolder ?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ytLlive", "recordings");
var folder = _recordFolder ?? DefaultRecordFolder();
Directory.CreateDirectory(folder);
var name = RecordingFile.BuildStartName(DateTime.Now) + ".mp4";
var path = Path.Combine(folder, name);
@@ -3422,17 +3377,36 @@ public class MainViewModel : ViewModelBase
var dialog = new Microsoft.Win32.OpenFolderDialog
{
Title = "Choose a folder for local recordings",
InitialDirectory = _recordFolder ??
Environment.GetFolderPath(Environment.SpecialFolder.MyVideos),
InitialDirectory = _recordFolder ?? DefaultRecordFolder(),
};
if (dialog.ShowDialog() == true)
{
_recordFolder = dialog.FolderName;
_layoutStore.SaveRecordFolder(_recordFolder);
OnPropertyChanged(nameof(RecordFolderDisplay));
_notifications.Info("Record folder set", _recordFolder);
}
}
private void ResetRecordFolder()
{
_recordFolder = null;
_layoutStore.SaveRecordFolder(null);
OnPropertyChanged(nameof(RecordFolderDisplay));
_notifications.Info("Record folder reset", RecordFolderDisplay);
}
/// <summary>Falls back to the user's Downloads folder when no preference is set;
/// returns MyVideos if Downloads doesn't exist.</summary>
internal static string DefaultRecordFolder()
{
var downloads = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads");
return Directory.Exists(downloads)
? downloads
: Environment.GetFolderPath(Environment.SpecialFolder.MyVideos);
}
private void OnMicLevelChanged(float level)
{
// NAudio raises on its capture thread; marshal to the UI thread so the