splitter/Splitter-UI/ViewModels/JobViewModel.cs

246 lines
7.2 KiB
C#

using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Splitter_UI.ViewModels;
public partial class JobViewModel : ObservableObject
{
public SingleJob Job { get; }
public VideoInfo? Probe { get; set; }
[ObservableProperty]
private PreviewData? _preview = new(null, [], null);
public ProgressInfo? Progress { get; set; }
[ObservableProperty]
private Bitmap? _thumbnail;
[ObservableProperty]
private string _suggestedAction = "";
// This updates continuously
[ObservableProperty]
private double _sliderLiveValue;
// This updates only on release
[ObservableProperty]
private double _positionSeconds;
public double DurationSeconds => Probe?.Duration ?? 0;
public IRelayCommand StepForwardCommand { get; }
public IRelayCommand StepBackwardCommand { get; }
private readonly IThumbnailService _thumbnails;
private readonly IFileProbeService _fileProbe;
private readonly DispatcherTimer _debounceTimer;
public string FileName => Path.GetFileName(Job.InputFile);
public string TextDesc => Probe != null
? $"{Probe.Width}x{Probe.Height}, {TimeSpan.FromSeconds(Probe.Duration).ToString(@"hh\:mm\:ss")}), FPS: {Probe.Fps:F2}, Bitrate: {Probe.Bitrate/1024/1024:F2} MB/s"
: "";
public override string ToString() => $"{FileName} - {TextDesc}";
public ObservableCollection<ParameterEntry> ParametersList { get; }
= new();
public string CropText
{
get => Job.Crop is { } c ? $"{c.width},{c.height}" : "";
set
{
if (string.IsNullOrWhiteSpace(value))
{
Job.Crop = null;
}
else
{
var parts = value.Split(',');
if (parts.Length == 2 &&
int.TryParse(parts[0], out var w) &&
int.TryParse(parts[1], out var h))
Job.Crop = (w, h);
}
OnPropertyChanged();
}
}
public string GravitateText
{
get => Job.GravitateTo is { } p ? $"{p.X:F3},{p.Y:F3}" : "";
set
{
if (string.IsNullOrWhiteSpace(value))
{
Job.GravitateTo = null;
}
else
{
var parts = value.Split(',');
if (parts.Length == 2 &&
float.TryParse(parts[0], out var x) &&
float.TryParse(parts[1], out var y))
Job.GravitateTo = new Point2f(x, y);
}
OnPropertyChanged();
}
}
public string PassthroughText
{
get => string.Join(' ', Job.Passthrough);
set
{
Job.Passthrough = string.IsNullOrWhiteSpace(value)
? Array.Empty<string>()
: value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
OnPropertyChanged();
}
}
public int? Rotate
{
get => Job.Rotate;
set
{
Job.Rotate = value;
OnPropertyChanged();
Task.Run(CreatePreview);
}
}
public JobViewModel(SingleJob job, IThumbnailService thumbnails, IFileProbeService fileProbe)
{
Job = job;
_thumbnails = thumbnails;
_fileProbe = fileProbe;
ParametersList.Add(new ParameterEntry("DropoutToleranceFrames", ""));
ParametersList.Add(new ParameterEntry("EmaFactor", ""));
ParametersList.Add(new ParameterEntry("CameraEasing", ""));
ParametersList.Add(new ParameterEntry("LostFreezeFrames", ""));
ParametersList.Add(new ParameterEntry("RotationDetectorSampleCount", ""));
ParametersList.Add(new ParameterEntry("RotationDetectorSampleLength", ""));
ParametersList.Add(new ParameterEntry("RotationDetectorFrameWidth", ""));
ParametersList.Add(new ParameterEntry("RotationDetectorFrameHeight", ""));
foreach (var entry in ParametersList)
{
entry.PropertyChanged += OnParameterChanged;
}
ParametersList.CollectionChanged += OnParametersCollectionChanged;
StepForwardCommand = new RelayCommand(StepForward);
StepBackwardCommand = new RelayCommand(StepBackward);
_debounceTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_debounceTimer.Tick += DebounceTimerTick;
_ = Task.Run( LoadThumbnailAsync );
}
private async Task LoadThumbnailAsync()
{
Probe = await _fileProbe.ProbeAsync(Job);
Thumbnail = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe, rotateDegree: Job.Rotate);
SuggestedAction = Probe.Rotation == 0 ? "crop" : "rotate";
await CreatePreview();
}
private async Task CreatePreview()
{
if ( Probe == null)
return;
try
{
var frame = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe, TimeSpan.FromSeconds(PositionSeconds), Probe.Width, Probe.Height, Job.Rotate);
Preview = new PreviewData(frame, [], null);
}
catch (Exception ex)
{
}
}
private void OnParameterChanged(object? sender, PropertyChangedEventArgs e)
{
if (sender is ParameterEntry p && e.PropertyName == nameof(ParameterEntry.Value))
{
Job.Parameters[p.Key] = p.Value;
}
}
private void OnParametersCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (ParameterEntry p in e.NewItems)
{
Job.Parameters[p.Key] = p.Value;
p.PropertyChanged += OnParameterChanged;
}
}
if (e.OldItems != null)
{
foreach (ParameterEntry p in e.OldItems)
{
Job.Parameters.Remove(p.Key);
p.PropertyChanged -= OnParameterChanged;
}
}
}
private void StepForward()
{
if (DurationSeconds <= 0)
return;
var step = DurationSeconds * 0.1; // 10% of total duration
SliderLiveValue = Math.Min(DurationSeconds, SliderLiveValue + step);
// trigger seek in your playback pipeline here
}
private void StepBackward()
{
if (DurationSeconds <= 0)
return;
var step = DurationSeconds * 0.1; // 10% of total duration
SliderLiveValue = Math.Max(0, SliderLiveValue - step);
// trigger seek in your playback pipeline here
}
partial void OnSliderLiveValueChanged(double value)
{
// Restart debounce timer on every slider update
_debounceTimer.Stop();
_debounceTimer.Start();
}
private void DebounceTimerTick(object? sender, EventArgs e)
{
_debounceTimer.Stop();
// Commit the final value
PositionSeconds = SliderLiveValue;
}
partial void OnPositionSecondsChanged(double value)
{
Task.Run(CreatePreview);
}
}