using System; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.ProfileEditor; using Avalonia.Threading; using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.Playback; public class PlaybackViewModel : ActivatableViewModelBase { private readonly IProfileEditorService _profileEditorService; private readonly ISettingsService _settingsService; private ObservableAsPropertyHelper? _currentTime; private ObservableAsPropertyHelper? _formattedCurrentTime; private ObservableAsPropertyHelper? _keyBindingsEnabled; private DateTime _lastUpdate; private ObservableAsPropertyHelper? _playing; private RenderProfileElement? _profileElement; private bool _repeating; private bool _repeatSegment; private bool _repeatTimeline; public PlaybackViewModel(IProfileEditorService profileEditorService, ISettingsService settingsService) { _profileEditorService = profileEditorService; _settingsService = settingsService; if (_settingsService.GetSetting("ProfileEditor.RepeatTimeline", true).Value) { _repeating = true; _repeatTimeline = true; } else if (_settingsService.GetSetting("ProfileEditor.RepeatSegment", false).Value) { _repeating = true; _repeatSegment = true; } else { _repeating = false; _repeatTimeline = true; } this.WhenActivated(d => { _profileEditorService.ProfileElement.Subscribe(e => _profileElement = e).DisposeWith(d); _currentTime = _profileEditorService.Time.ToProperty(this, vm => vm.CurrentTime).DisposeWith(d); _formattedCurrentTime = _profileEditorService.Time.Select(t => $"{Math.Floor(t.TotalSeconds):00}.{t.Milliseconds:000}").ToProperty(this, vm => vm.FormattedCurrentTime).DisposeWith(d); _playing = _profileEditorService.Playing.ToProperty(this, vm => vm.Playing).DisposeWith(d); _keyBindingsEnabled = Shared.UI.KeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); _lastUpdate = DateTime.MinValue; DispatcherTimer updateTimer = new(TimeSpan.FromMilliseconds(60.0 / 1000), DispatcherPriority.Render, Update); updateTimer.Start(); Disposable.Create(() => { updateTimer.Stop(); _settingsService.GetSetting("ProfileEditor.RepeatTimeline", true).Value = _repeating && _repeatTimeline; _settingsService.GetSetting("ProfileEditor.RepeatSegment", false).Value = _repeating && _repeatSegment; }).DisposeWith(d); }); PlayFromStart = ReactiveCommand.Create(ExecutePlayFromStart, this.WhenAnyValue(vm => vm.KeyBindingsEnabled)); TogglePlay = ReactiveCommand.Create(ExecuteTogglePlay, this.WhenAnyValue(vm => vm.KeyBindingsEnabled)); GoToStart = ReactiveCommand.Create(ExecuteGoToStart); GoToEnd = ReactiveCommand.Create(ExecuteGoToEnd); GoToPreviousFrame = ReactiveCommand.Create(ExecuteGoToPreviousFrame); GoToNextFrame = ReactiveCommand.Create(ExecuteGoToNextFrame); CycleRepeating = ReactiveCommand.Create(ExecuteCycleRepeating); } public TimeSpan CurrentTime => _currentTime?.Value ?? TimeSpan.Zero; public string? FormattedCurrentTime => _formattedCurrentTime?.Value; public bool Playing => _playing?.Value ?? false; public bool KeyBindingsEnabled => _keyBindingsEnabled?.Value ?? false; public bool Repeating { get => _repeating; set => RaiseAndSetIfChanged(ref _repeating, value); } public bool RepeatTimeline { get => _repeatTimeline; set => RaiseAndSetIfChanged(ref _repeatTimeline, value); } public bool RepeatSegment { get => _repeatSegment; set => RaiseAndSetIfChanged(ref _repeatSegment, value); } public ReactiveCommand PlayFromStart { get; } public ReactiveCommand TogglePlay { get; } public ReactiveCommand GoToStart { get; } public ReactiveCommand GoToEnd { get; } public ReactiveCommand GoToPreviousFrame { get; } public ReactiveCommand GoToNextFrame { get; } public ReactiveCommand CycleRepeating { get; } private void ExecutePlayFromStart() { ExecuteGoToStart(); if (!Playing) _profileEditorService.Play(); } private void ExecuteTogglePlay() { if (!Playing) _profileEditorService.Play(); else _profileEditorService.Pause(); } private void ExecuteGoToStart() { _profileEditorService.ChangeTime(TimeSpan.Zero); } private void ExecuteGoToEnd() { if (_profileElement == null) return; _profileEditorService.ChangeTime(_profileElement.Timeline.EndSegmentEndPosition); } private void ExecuteGoToPreviousFrame() { if (_profileElement == null) return; double frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 30).Value; double newTime = Math.Max(0, Math.Round((CurrentTime.TotalMilliseconds - frameTime) / frameTime) * frameTime); _profileEditorService.ChangeTime(TimeSpan.FromMilliseconds(newTime)); } private void ExecuteGoToNextFrame() { if (_profileElement == null) return; double frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 30).Value; double newTime = Math.Round((CurrentTime.TotalMilliseconds + frameTime) / frameTime) * frameTime; newTime = Math.Min(newTime, _profileElement.Timeline.EndSegmentEndPosition.TotalMilliseconds); _profileEditorService.ChangeTime(TimeSpan.FromMilliseconds(newTime)); } private void ExecuteCycleRepeating() { if (!Repeating) { RepeatTimeline = true; RepeatSegment = false; Repeating = true; } else if (RepeatTimeline) { RepeatTimeline = false; RepeatSegment = true; this.RaisePropertyChanged(nameof(Repeating)); } else if (RepeatSegment) { RepeatTimeline = true; RepeatSegment = false; Repeating = false; } } private TimeSpan GetCurrentSegmentStart() { if (_profileElement == null) return TimeSpan.Zero; if (CurrentTime < _profileElement.Timeline.StartSegmentEndPosition) return TimeSpan.Zero; if (CurrentTime < _profileElement.Timeline.MainSegmentEndPosition) return _profileElement.Timeline.MainSegmentStartPosition; if (CurrentTime < _profileElement.Timeline.EndSegmentEndPosition) return _profileElement.Timeline.EndSegmentStartPosition; return TimeSpan.Zero; } private TimeSpan GetCurrentSegmentEnd() { if (_profileElement == null) return TimeSpan.Zero; if (CurrentTime < _profileElement.Timeline.StartSegmentEndPosition) return _profileElement.Timeline.StartSegmentEndPosition; if (CurrentTime < _profileElement.Timeline.MainSegmentEndPosition) return _profileElement.Timeline.MainSegmentEndPosition; if (CurrentTime < _profileElement.Timeline.EndSegmentEndPosition) return _profileElement.Timeline.EndSegmentEndPosition; return TimeSpan.Zero; } private void Update(object? sender, EventArgs e) { try { if (!Playing) return; if (_lastUpdate == DateTime.MinValue) _lastUpdate = DateTime.Now; TimeSpan newTime = CurrentTime.Add(DateTime.Now - _lastUpdate); if (_profileElement != null) { if (Repeating && RepeatTimeline) { if (newTime > _profileElement.Timeline.Length) newTime = TimeSpan.Zero; } else if (Repeating && RepeatSegment) { if (newTime > GetCurrentSegmentEnd()) newTime = GetCurrentSegmentStart(); } else if (newTime > _profileElement.Timeline.Length) { newTime = _profileElement.Timeline.Length; _profileEditorService.Pause(); } } _profileEditorService.ChangeTime(newTime); } finally { _lastUpdate = DateTime.Now; } } }