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 ObservableCollection 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() : value.Split(' ', StringSplitOptions.RemoveEmptyEntries); OnPropertyChanged(); } } public int? Rotate { get => Job.Rotate; set { Job.Rotate = value; OnPropertyChanged(); } } 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); 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); Preview = new PreviewData(frame, [], null); OnPropertyChanged(nameof(Preview)); } 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() { var step = 10.0; // seconds if (DurationSeconds <= 0) return; PositionSeconds = Math.Min(DurationSeconds, PositionSeconds + step); // trigger seek in your playback pipeline here } private void StepBackward() { var step = 10.0; // seconds if (DurationSeconds <= 0) return; PositionSeconds = Math.Max(0, PositionSeconds - 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); } }