127 lines
4.4 KiB
C#
127 lines
4.4 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Transport controls for the media page.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Playback state lives on <see cref="GpuMediaPlayer"/> 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.
|
|
/// </remarks>
|
|
public sealed partial class VideoPlayerView : UserControl
|
|
{
|
|
/// <summary>True while the user is dragging the seek bar, so playback must not fight them.</summary>
|
|
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;
|
|
}
|