diff --git a/src/PLib.Desktop/Controls/VlcVideoView.cs b/src/PLib.Desktop/Controls/VlcVideoView.cs
index 71510c8..74fa73e 100644
--- a/src/PLib.Desktop/Controls/VlcVideoView.cs
+++ b/src/PLib.Desktop/Controls/VlcVideoView.cs
@@ -1,274 +1,279 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Platform;
-using Avalonia.Threading;
-using LibVLCSharp.Shared;
-
-namespace PLib.Desktop.Controls;
-
-///
-/// A video surface backed by LibVLC.
-///
-///
-/// VLC draws straight into a native child window that Avalonia creates for us, so no frame
-/// ever crosses into managed memory — playback costs what the decoder costs and nothing more.
-/// The price is airspace: this region is a separate window on top of the Avalonia surface, so
-/// nothing can be drawn over the picture. The media page keeps its controls beside the video
-/// rather than on it for exactly that reason.
-///
-/// The property surface deliberately mirrors what a media control is expected to expose —
-/// source, position, duration, volume — so the page binds to it the same way it would to any
-/// other player.
-///
-///
-public sealed class VlcVideoView : NativeControlHost
-{
- public static readonly StyledProperty SourceProperty =
- AvaloniaProperty.Register(nameof(Source));
-
- public static readonly StyledProperty AutoPlayProperty =
- AvaloniaProperty.Register(nameof(AutoPlay), defaultValue: true);
-
- public static readonly StyledProperty PositionProperty =
- AvaloniaProperty.Register(nameof(Position));
-
- public static readonly StyledProperty DurationProperty =
- AvaloniaProperty.Register(nameof(Duration));
-
- public static readonly StyledProperty IsPlayingProperty =
- AvaloniaProperty.Register(nameof(IsPlaying));
-
- public static readonly StyledProperty IsMutedProperty =
- AvaloniaProperty.Register(nameof(IsMuted));
-
- /// Volume as a fraction; LibVLC works in percent and is converted on the way in.
- public static readonly StyledProperty VolumeProperty =
- AvaloniaProperty.Register(nameof(Volume), defaultValue: 0.8);
-
- public static readonly StyledProperty LastErrorProperty =
- AvaloniaProperty.Register(nameof(LastError));
-
- private MediaPlayer? _player;
-
- ///
- /// True once VLC has been handed the native window. Playback cannot start before that,
- /// or VLC opens a top-level window of its own.
- ///
- private bool _surfaceReady;
-
- public Uri? Source
- {
- get => GetValue(SourceProperty);
- set => SetValue(SourceProperty, value);
- }
-
- public bool AutoPlay
- {
- get => GetValue(AutoPlayProperty);
- set => SetValue(AutoPlayProperty, value);
- }
-
- public TimeSpan Position
- {
- get => GetValue(PositionProperty);
- private set => SetValue(PositionProperty, value);
- }
-
- public TimeSpan Duration
- {
- get => GetValue(DurationProperty);
- private set => SetValue(DurationProperty, value);
- }
-
- public bool IsPlaying
- {
- get => GetValue(IsPlayingProperty);
- private set => SetValue(IsPlayingProperty, value);
- }
-
- public bool IsMuted
- {
- get => GetValue(IsMutedProperty);
- set => SetValue(IsMutedProperty, value);
- }
-
- public double Volume
- {
- get => GetValue(VolumeProperty);
- set => SetValue(VolumeProperty, value);
- }
-
- public string? LastError
- {
- get => GetValue(LastErrorProperty);
- private set => SetValue(LastErrorProperty, value);
- }
-
- public void Play()
- {
- if (_player is null)
- {
- return;
- }
-
- if (_player.Media is not null)
- {
- _player.Play();
- }
- else
- {
- OpenCurrentSource();
- }
- }
-
- public void Pause() => _player?.SetPause(true);
-
- public void Stop() => _player?.Stop();
-
- public void Seek(TimeSpan position)
- {
- if (_player is { IsSeekable: true })
- {
- _player.Time = (long)position.TotalMilliseconds;
- }
- }
-
- protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
- {
- // Avalonia gives us an empty child window of the right platform kind; VLC renders
- // into it once we hand over the handle.
- var handle = base.CreateNativeControlCore(parent);
-
- _player = new MediaPlayer(VlcRuntime.Shared);
- AttachPlayerEvents(_player);
-
- if (OperatingSystem.IsWindows())
- {
- _player.Hwnd = handle.Handle;
- }
- else if (OperatingSystem.IsLinux())
- {
- _player.XWindow = (uint)handle.Handle;
- }
- else if (OperatingSystem.IsMacOS())
- {
- _player.NsObject = handle.Handle;
- }
-
- _player.Mute = IsMuted;
- _player.Volume = ToVlcVolume(Volume);
- _surfaceReady = true;
-
- if (AutoPlay)
- {
- OpenCurrentSource();
- }
-
- return handle;
- }
-
- protected override void DestroyNativeControlCore(IPlatformHandle control)
- {
- // Tear the player down before the window it draws into disappears.
- if (_player is { } player)
- {
- _player = null;
- _surfaceReady = false;
-
- DetachPlayerEvents(player);
- player.Stop();
- player.Dispose();
- }
-
- base.DestroyNativeControlCore(control);
- }
-
- protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
- {
- base.OnPropertyChanged(change);
-
- if (_player is not { } player)
- {
- return;
- }
-
- if (change.Property == SourceProperty)
- {
- OpenCurrentSource();
- }
- else if (change.Property == VolumeProperty)
- {
- player.Volume = ToVlcVolume(Volume);
- }
- else if (change.Property == IsMutedProperty)
- {
- player.Mute = IsMuted;
- }
- }
-
- private void OpenCurrentSource()
- {
- if (!_surfaceReady || _player is not { } player || Source is not { } source)
- {
- return;
- }
-
- try
- {
- LastError = null;
-
- // The media object only has to survive the call: VLC takes its own reference.
- using var media = new Media(VlcRuntime.Shared, source);
-
- if (!player.Play(media))
- {
- LastError = "VLC не смог открыть файл";
- }
- }
- catch (Exception ex)
- {
- LastError = ex.Message;
- }
- }
-
- private void AttachPlayerEvents(MediaPlayer player)
- {
- player.TimeChanged += OnTimeChanged;
- player.LengthChanged += OnLengthChanged;
- player.Playing += OnPlaying;
- player.Paused += OnStoppedPlaying;
- player.Stopped += OnStoppedPlaying;
- player.EndReached += OnStoppedPlaying;
- player.EncounteredError += OnEncounteredError;
- }
-
- private void DetachPlayerEvents(MediaPlayer player)
- {
- player.TimeChanged -= OnTimeChanged;
- player.LengthChanged -= OnLengthChanged;
- player.Playing -= OnPlaying;
- player.Paused -= OnStoppedPlaying;
- player.Stopped -= OnStoppedPlaying;
- player.EndReached -= OnStoppedPlaying;
- player.EncounteredError -= OnEncounteredError;
- }
-
- // Every VLC event arrives on one of its own threads, so nothing here may touch an
- // Avalonia property directly.
- private void OnTimeChanged(object? sender, MediaPlayerTimeChangedEventArgs e) =>
- Post(() => Position = TimeSpan.FromMilliseconds(Math.Max(0, e.Time)));
-
- private void OnLengthChanged(object? sender, MediaPlayerLengthChangedEventArgs e) =>
- Post(() => Duration = TimeSpan.FromMilliseconds(Math.Max(0, e.Length)));
-
- private void OnPlaying(object? sender, EventArgs e) => Post(() => IsPlaying = true);
-
- private void OnStoppedPlaying(object? sender, EventArgs e) => Post(() => IsPlaying = false);
-
- private void OnEncounteredError(object? sender, EventArgs e) =>
- Post(() => LastError = "VLC сообщил об ошибке воспроизведения");
-
- private static void Post(Action action) => Dispatcher.UIThread.Post(action, DispatcherPriority.Background);
-
- private static int ToVlcVolume(double volume) => (int)Math.Round(Math.Clamp(volume, 0, 1) * 100);
-}
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using LibVLCSharp.Shared;
+
+namespace PLib.Desktop.Controls;
+
+///
+/// A video surface backed by LibVLC.
+///
+///
+/// VLC draws straight into a native child window that Avalonia creates for us, so no frame
+/// ever crosses into managed memory — playback costs what the decoder costs and nothing more.
+/// The price is airspace: this region is a separate window on top of the Avalonia surface, so
+/// nothing can be drawn over the picture. The media page keeps its controls beside the video
+/// rather than on it for exactly that reason.
+///
+/// The property surface deliberately mirrors what a media control is expected to expose —
+/// source, position, duration, volume — so the page binds to it the same way it would to any
+/// other player.
+///
+///
+public sealed class VlcVideoView : NativeControlHost
+{
+ public static readonly StyledProperty SourceProperty =
+ AvaloniaProperty.Register(nameof(Source));
+
+ public static readonly StyledProperty AutoPlayProperty =
+ AvaloniaProperty.Register(nameof(AutoPlay), defaultValue: true);
+
+ public static readonly StyledProperty PositionProperty =
+ AvaloniaProperty.Register(nameof(Position));
+
+ public static readonly StyledProperty DurationProperty =
+ AvaloniaProperty.Register(nameof(Duration));
+
+ public static readonly StyledProperty IsPlayingProperty =
+ AvaloniaProperty.Register(nameof(IsPlaying));
+
+ public static readonly StyledProperty IsMutedProperty =
+ AvaloniaProperty.Register(nameof(IsMuted));
+
+ /// Volume as a fraction; LibVLC works in percent and is converted on the way in.
+ public static readonly StyledProperty VolumeProperty =
+ AvaloniaProperty.Register(nameof(Volume), defaultValue: 0.8);
+
+ public static readonly StyledProperty LastErrorProperty =
+ AvaloniaProperty.Register(nameof(LastError));
+
+ private MediaPlayer? _player;
+
+ ///
+ /// True once VLC has been handed the native window. Playback cannot start before that,
+ /// or VLC opens a top-level window of its own.
+ ///
+ private bool _surfaceReady;
+
+ public Uri? Source
+ {
+ get => GetValue(SourceProperty);
+ set => SetValue(SourceProperty, value);
+ }
+
+ public bool AutoPlay
+ {
+ get => GetValue(AutoPlayProperty);
+ set => SetValue(AutoPlayProperty, value);
+ }
+
+ public TimeSpan Position
+ {
+ get => GetValue(PositionProperty);
+ private set => SetValue(PositionProperty, value);
+ }
+
+ public TimeSpan Duration
+ {
+ get => GetValue(DurationProperty);
+ private set => SetValue(DurationProperty, value);
+ }
+
+ public bool IsPlaying
+ {
+ get => GetValue(IsPlayingProperty);
+ private set => SetValue(IsPlayingProperty, value);
+ }
+
+ public bool IsMuted
+ {
+ get => GetValue(IsMutedProperty);
+ set => SetValue(IsMutedProperty, value);
+ }
+
+ public double Volume
+ {
+ get => GetValue(VolumeProperty);
+ set => SetValue(VolumeProperty, value);
+ }
+
+ public string? LastError
+ {
+ get => GetValue(LastErrorProperty);
+ private set => SetValue(LastErrorProperty, value);
+ }
+
+ public void Play()
+ {
+ if (_player is null)
+ {
+ return;
+ }
+
+ if (_player.Media is not null)
+ {
+ _player.Play();
+ }
+ else
+ {
+ OpenCurrentSource();
+ }
+ }
+
+ public void Pause() => _player?.SetPause(true);
+
+ public void Stop() => _player?.Stop();
+
+ public void Seek(TimeSpan position)
+ {
+ if (_player is { IsSeekable: true })
+ {
+ _player.Time = (long)position.TotalMilliseconds;
+ }
+ }
+
+ protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
+ {
+ // Avalonia gives us an empty child window of the right platform kind; VLC renders
+ // into it once we hand over the handle.
+ var handle = base.CreateNativeControlCore(parent);
+
+ _player = new MediaPlayer(VlcRuntime.Shared);
+ AttachPlayerEvents(_player);
+
+ if (OperatingSystem.IsWindows())
+ {
+ _player.Hwnd = handle.Handle;
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ _player.XWindow = (uint)handle.Handle;
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ _player.NsObject = handle.Handle;
+ }
+
+ // VLC grabs mouse and keyboard on its own window by default, which swallows every
+ // gesture before Avalonia can see it. We do not need DVD menus, so hand input back.
+ _player.EnableMouseInput = false;
+ _player.EnableKeyInput = false;
+
+ _player.Mute = IsMuted;
+ _player.Volume = ToVlcVolume(Volume);
+ _surfaceReady = true;
+
+ if (AutoPlay)
+ {
+ OpenCurrentSource();
+ }
+
+ return handle;
+ }
+
+ protected override void DestroyNativeControlCore(IPlatformHandle control)
+ {
+ // Tear the player down before the window it draws into disappears.
+ if (_player is { } player)
+ {
+ _player = null;
+ _surfaceReady = false;
+
+ DetachPlayerEvents(player);
+ player.Stop();
+ player.Dispose();
+ }
+
+ base.DestroyNativeControlCore(control);
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (_player is not { } player)
+ {
+ return;
+ }
+
+ if (change.Property == SourceProperty)
+ {
+ OpenCurrentSource();
+ }
+ else if (change.Property == VolumeProperty)
+ {
+ player.Volume = ToVlcVolume(Volume);
+ }
+ else if (change.Property == IsMutedProperty)
+ {
+ player.Mute = IsMuted;
+ }
+ }
+
+ private void OpenCurrentSource()
+ {
+ if (!_surfaceReady || _player is not { } player || Source is not { } source)
+ {
+ return;
+ }
+
+ try
+ {
+ LastError = null;
+
+ // The media object only has to survive the call: VLC takes its own reference.
+ using var media = new Media(VlcRuntime.Shared, source);
+
+ if (!player.Play(media))
+ {
+ LastError = "VLC не смог открыть файл";
+ }
+ }
+ catch (Exception ex)
+ {
+ LastError = ex.Message;
+ }
+ }
+
+ private void AttachPlayerEvents(MediaPlayer player)
+ {
+ player.TimeChanged += OnTimeChanged;
+ player.LengthChanged += OnLengthChanged;
+ player.Playing += OnPlaying;
+ player.Paused += OnStoppedPlaying;
+ player.Stopped += OnStoppedPlaying;
+ player.EndReached += OnStoppedPlaying;
+ player.EncounteredError += OnEncounteredError;
+ }
+
+ private void DetachPlayerEvents(MediaPlayer player)
+ {
+ player.TimeChanged -= OnTimeChanged;
+ player.LengthChanged -= OnLengthChanged;
+ player.Playing -= OnPlaying;
+ player.Paused -= OnStoppedPlaying;
+ player.Stopped -= OnStoppedPlaying;
+ player.EndReached -= OnStoppedPlaying;
+ player.EncounteredError -= OnEncounteredError;
+ }
+
+ // Every VLC event arrives on one of its own threads, so nothing here may touch an
+ // Avalonia property directly.
+ private void OnTimeChanged(object? sender, MediaPlayerTimeChangedEventArgs e) =>
+ Post(() => Position = TimeSpan.FromMilliseconds(Math.Max(0, e.Time)));
+
+ private void OnLengthChanged(object? sender, MediaPlayerLengthChangedEventArgs e) =>
+ Post(() => Duration = TimeSpan.FromMilliseconds(Math.Max(0, e.Length)));
+
+ private void OnPlaying(object? sender, EventArgs e) => Post(() => IsPlaying = true);
+
+ private void OnStoppedPlaying(object? sender, EventArgs e) => Post(() => IsPlaying = false);
+
+ private void OnEncounteredError(object? sender, EventArgs e) =>
+ Post(() => LastError = "VLC сообщил об ошибке воспроизведения");
+
+ private static void Post(Action action) => Dispatcher.UIThread.Post(action, DispatcherPriority.Background);
+
+ private static int ToVlcVolume(double volume) => (int)Math.Round(Math.Clamp(volume, 0, 1) * 100);
+}
diff --git a/src/PLib.Desktop/Views/VideoPlayerView.axaml b/src/PLib.Desktop/Views/VideoPlayerView.axaml
index 7136940..febdaba 100644
--- a/src/PLib.Desktop/Views/VideoPlayerView.axaml
+++ b/src/PLib.Desktop/Views/VideoPlayerView.axaml
@@ -64,7 +64,7 @@
-
+
Player.IsMuted = !Player.IsMuted;
+ private void OnVideoDoubleTapped(object? sender, TappedEventArgs e)
+ {
+ if (DataContext is VideoPlayerViewModel viewModel)
+ {
+ viewModel.IsFullScreen = !viewModel.IsFullScreen;
+ e.Handled = true;
+ }
+ }
+
private void OnScrubStarted(object? sender, PointerPressedEventArgs e) => _isScrubbing = true;
private void OnScrubFinished(object? sender, PointerReleasedEventArgs e)