using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Win32;
using ytLive.Helpers;
using ytLive.Services;
namespace ytLive.ViewModels;
public partial class MainViewModel : ViewModelBase
{
private string? _recordFolder;
public string RecordFolderDisplay => _recordFolder ?? DefaultRecordFolder();
private string? _activeRecordPath;
private DateTime _recordStartTime;
private TimeSpan _recordLength;
private string? StartRecordFile()
{
try
{
var folder = _recordFolder ?? DefaultRecordFolder();
Directory.CreateDirectory(folder);
var name = RecordingFile.BuildStartName(DateTime.Now) + ".mp4";
var path = Path.Combine(folder, name);
_activeRecordPath = path;
return path;
}
catch (Exception ex)
{
AppLog.Write(ex, "Recording: failed to prepare output path");
return null;
}
}
private async Task FinalizeRecordingAsync()
{
var startPath = _activeRecordPath;
_activeRecordPath = null;
if (string.IsNullOrEmpty(startPath)) return;
try
{
while (_framePump.IsRunning) await Task.Delay(50);
if (!File.Exists(startPath))
{
AppLog.Write($"Recording: output not found for rename: {startPath}");
return;
}
_recordLength = _liveElapsed;
var autoStem = RecordingFile.BuildFinalName(_recordStartTime, _recordLength);
var dir = Path.GetDirectoryName(startPath)!;
// Manual rename modal: give the creator a chance to name the recording.
// CREATOR RULING 2026-09-26: Cancel means DISCARD, not "save under the
// default name". This used to fall through and File.Move anyway, so a
// cancelled dialog silently kept the recording — Cancel was a lie.
var dialog = new RenameRecordingDialog(autoStem);
var creatorSaved = dialog.ShowDialog() == true;
var result = CompleteRecordingSave(
startPath, dir, autoStem, dialog.ResultFileName, creatorSaved);
switch (result.Outcome)
{
case RecordingSaveOutcome.Saved:
AppLog.Write($"Recording saved: {result.FinalPath}");
_notifications.Info("Recording saved",
Path.GetFileName(result.FinalPath!) +
" — you can rename it in your videos folder at any time.");
break;
case RecordingSaveOutcome.Discarded:
AppLog.Write($"Recording discarded by creator: {startPath}");
_notifications.Info("Recording discarded",
"The recording was deleted and nothing was saved.");
break;
case RecordingSaveOutcome.DiscardFailed:
// A "discarded" recording still sitting on disk is worse than no
// report at all — name the file and the folder so it can be found.
AppLog.Write($"Recording discard failed: {startPath}");
_notifications.Error("Recording discarded?",
$"Could not delete {Path.GetFileName(startPath)} — it is still in {dir}.");
break;
}
}
catch (Exception ex)
{
AppLog.Write(ex, "Recording: rename-on-stop failed");
_notifications.Error("Recording saved",
$"The recording was written but couldn't be renamed: {Path.GetFileName(startPath)}");
}
}
/// What happened to the footage once the save dialog was answered, plus
/// where it landed (FinalPath is set only for ).
internal readonly record struct RecordingSaveResult(RecordingSaveOutcome Outcome, string? FinalPath);
/// What happened to the footage once the save dialog was answered.
internal enum RecordingSaveOutcome
{
Saved,
Discarded,
DiscardFailed,
}
/// The commit-or-discard decision behind the recording save dialog, split out
/// so it can be tested against real files without a frame pump (CREATOR RULING
/// 2026-09-26). is the dialog's DialogResult:
/// false — Cancel, Escape, or the X — DELETES the footage; only an explicit
/// Save (including Enter on the pre-filled default) moves it to its final name.
internal static RecordingSaveResult CompleteRecordingSave(
string startPath, string dir, string autoStem, string? chosenStem, bool creatorSaved)
{
if (!creatorSaved)
{
if (!File.Exists(startPath)) return new RecordingSaveResult(RecordingSaveOutcome.Discarded, null);
try
{
File.Delete(startPath);
return new RecordingSaveResult(RecordingSaveOutcome.Discarded, null);
}
catch (Exception)
{
return new RecordingSaveResult(RecordingSaveOutcome.DiscardFailed, startPath);
}
}
// A blank name still means "keep the auto name" (the dialog says so), but
// only on an explicit Save. Enter with the pre-filled default lands here too.
var stem = string.IsNullOrWhiteSpace(chosenStem) ? autoStem : chosenStem!;
var finalPath = UniquePath(Path.Combine(dir, stem + ".mp4"));
File.Move(startPath, finalPath);
return new RecordingSaveResult(RecordingSaveOutcome.Saved, finalPath);
}
private static string UniquePath(string path)
{
if (!File.Exists(path)) return path;
var dir = Path.GetDirectoryName(path)!;
var name = Path.GetFileNameWithoutExtension(path);
var ext = Path.GetExtension(path);
for (var i = 2; ; i++)
{
var candidate = Path.Combine(dir, $"{name}-{i}{ext}");
if (!File.Exists(candidate)) return candidate;
}
}
private void ChooseRecordFolder()
{
var dialog = new Microsoft.Win32.OpenFolderDialog
{
Title = "Choose a folder for local recordings",
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);
}
internal static string DefaultRecordFolder()
{
var downloads = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads");
return Directory.Exists(downloads)
? downloads
: Environment.GetFolderPath(Environment.SpecialFolder.MyVideos);
}
}