using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using MediaPlayer.Controls; using PLib.Desktop.ViewModels; namespace PLib.Desktop.Views; /// /// Transport controls for the media page. /// /// /// Playback state lives on itself — position, duration and /// whether it is playing are all its own properties, and seeking is a method call. Mirroring /// that into the view model would buy nothing but a second copy to keep in sync, so this /// code-behind wires the buttons straight to the control. Everything the page knows about /// the video — title, path, the commands around it — stays in the view model. /// public sealed partial class VideoPlayerView : UserControl { /// True while the user is dragging the seek bar, so playback must not fight them. private bool _isScrubbing; public VideoPlayerView() { InitializeComponent(); PlayPauseButton.Click += OnPlayPause; MuteButton.Click += OnToggleMute; // Tunnelled: the Slider's own handlers mark these as handled on the way back up. Seek.AddHandler(PointerPressedEvent, OnScrubStarted, RoutingStrategies.Tunnel); Seek.AddHandler(PointerReleasedEvent, OnScrubFinished, RoutingStrategies.Tunnel); VolumeSlider.PropertyChanged += OnVolumeChanged; Player.GetObservable(GpuMediaPlayer.PositionProperty).Subscribe(OnPositionChanged); Player.GetObservable(GpuMediaPlayer.DurationProperty).Subscribe(OnDurationChanged); Player.GetObservable(GpuMediaPlayer.IsPlayingProperty).Subscribe(OnIsPlayingChanged); Player.GetObservable(GpuMediaPlayer.LastErrorProperty).Subscribe(OnErrorChanged); } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); // Leaving the page has to stop the decoder; nothing else will. Player.Stop(); } private void OnPlayPause(object? sender, RoutedEventArgs e) { if (Player.IsPlaying) { Player.Pause(); } else { Player.Play(); } } private void OnToggleMute(object? sender, RoutedEventArgs e) => Player.IsMuted = !Player.IsMuted; private void OnScrubStarted(object? sender, PointerPressedEventArgs e) => _isScrubbing = true; private void OnScrubFinished(object? sender, PointerReleasedEventArgs e) { _isScrubbing = false; Player.Seek(TimeSpan.FromSeconds(Seek.Value)); } private void OnVolumeChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property != RangeBase.ValueProperty) { return; } Player.Volume = VolumeSlider.Value; MuteIcon.Kind = VolumeIconFor(VolumeSlider.Value, Player.IsMuted); } private void OnPositionChanged(TimeSpan position) { PositionText.Text = DisplayText.Duration(position); if (!_isScrubbing) { Seek.Value = position.TotalSeconds; } } private void OnDurationChanged(TimeSpan duration) { DurationText.Text = DisplayText.Duration(duration); // A zero maximum would pin the thumb to the left and swallow every seek. Seek.Maximum = duration > TimeSpan.Zero ? duration.TotalSeconds : 1; Seek.IsEnabled = duration > TimeSpan.Zero; } private void OnIsPlayingChanged(bool isPlaying) => PlayPauseIcon.Kind = isPlaying ? Material.Icons.MaterialIconKind.Pause : Material.Icons.MaterialIconKind.Play; private void OnErrorChanged(string? error) { ErrorText.Text = string.IsNullOrWhiteSpace(error) ? null : $"Не удалось воспроизвести файл: {error}"; ErrorText.IsVisible = !string.IsNullOrWhiteSpace(error); } private static Material.Icons.MaterialIconKind VolumeIconFor(double volume, bool isMuted) => isMuted || volume <= 0.001 ? Material.Icons.MaterialIconKind.VolumeOff : volume < 0.5 ? Material.Icons.MaterialIconKind.VolumeMedium : Material.Icons.MaterialIconKind.VolumeHigh; }