using System; using System.Collections.Generic; using System.Threading.Tasks; using global::Avalonia; using global::Avalonia.Controls; using global::Avalonia.Controls.ApplicationLifetimes; using global::Avalonia.Controls.Primitives; using global::Avalonia.Data; using global::Avalonia.Input; using global::Avalonia.Interactivity; using global::Avalonia.Media; using global::Avalonia.Media.Imaging; using global::Avalonia.Threading; using LANCommander.Launcher.Helpers; using LANCommander.Launcher.ViewModels.Components; namespace LANCommander.Launcher.Views; public partial class LightboxOverlay : UserControl { public event EventHandler? Closed; /// /// Raised when a video overlay closes. The long is the playback position in ms. /// The int is the item index that was playing. /// public event EventHandler<(int Index, long TimeMs)>? VideoClosed; private IReadOnlyList _items = Array.Empty(); private int _currentIndex; private bool _closing; // Video state private VideoFrameRenderer? _renderer; private VideoCanvas? _canvas; private DispatcherTimer? _timer; private bool _isUpdatingSlider; private bool _isMuted; private long _videoStartTimeMs; private EventHandler? _endReachedHandler; public LightboxOverlay() { InitializeComponent(); SeekSlider.AddHandler(RangeBase.ValueChangedEvent, SeekSlider_ValueChanged); } /// /// Opens the lightbox with the given items, starting at the specified index. /// /// All items available for navigation. /// Index of the item to show first. /// For video items, the starting playback position. public void Show(IReadOnlyList items, int startIndex = 0, long videoStartTimeMs = 0) { _items = items; _currentIndex = Math.Clamp(startIndex, 0, Math.Max(0, items.Count - 1)); _videoStartTimeMs = videoStartTimeMs; ShowCurrentItem(); Focus(); } /// Shows the lightbox as an overlay on the main window. public static LightboxOverlay ShowOverlay(IReadOnlyList items, int startIndex = 0, long videoStartTimeMs = 0) { var overlay = new LightboxOverlay { HorizontalAlignment = global::Avalonia.Layout.HorizontalAlignment.Stretch, VerticalAlignment = global::Avalonia.Layout.VerticalAlignment.Stretch, }; var mainWindow = (global::Avalonia.Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow; var layer = OverlayLayer.GetOverlayLayer(mainWindow); if (layer is null) return overlay; overlay.Bind(global::Avalonia.Layout.Layoutable.WidthProperty, new Binding("Bounds.Width") { Source = layer }); overlay.Bind(global::Avalonia.Layout.Layoutable.HeightProperty, new Binding("Bounds.Height") { Source = layer }); layer.Children.Add(overlay); overlay.Show(items, startIndex, videoStartTimeMs); return overlay; } // ── Navigation ─────────────────────────────────────────────────────── private void Navigate(int delta) { if (_items.Count <= 1) return; StopVideo(); _videoStartTimeMs = 0; _currentIndex = (_currentIndex + delta + _items.Count) % _items.Count; ShowCurrentItem(); } private void ShowCurrentItem() { if (_items.Count == 0) return; var item = _items[_currentIndex]; // Hide all content ImageBorder.IsVisible = false; VideoBorder.IsVisible = false; PdfBorder.IsVisible = false; TransportControls.IsVisible = false; LoadingIndicator.IsVisible = false; // Update navigation PrevButton.IsVisible = _items.Count > 1; NextButton.IsVisible = _items.Count > 1; ItemCounter.Text = _items.Count > 1 ? $"{_currentIndex + 1} / {_items.Count}" : string.Empty; switch (item.Type) { case LightboxItemType.Image: ShowImage(item); break; case LightboxItemType.Video: ShowVideo(item); break; case LightboxItemType.Pdf: ShowPdf(item); break; } } // ── Image ──────────────────────────────────────────────────────────── private void ShowImage(LightboxItem item) { if (item.ImageSource != null) { LightboxImage.Source = item.ImageSource; } else if (!string.IsNullOrEmpty(item.Path)) { try { LightboxImage.Source = new Bitmap(item.Path); } catch { LightboxImage.Source = null; } } ImageBorder.IsVisible = true; } // ── Video ──────────────────────────────────────────────────────────── private void ShowVideo(LightboxItem item) { LoadingIndicator.IsVisible = true; VideoBorder.IsVisible = true; TransportControls.IsVisible = true; try { _renderer = new VideoFrameRenderer(maxWidth: 1920, maxHeight: 1080); } catch { // LibVLC may not be available (native libraries missing in deployment). // Fall back to showing the image if one is available, otherwise leave blank. LoadingIndicator.IsVisible = false; VideoBorder.IsVisible = false; TransportControls.IsVisible = false; if (item.ImageSource != null || !string.IsNullOrEmpty(item.Path)) ShowImage(item); return; } _canvas = new VideoCanvas(); VideoBorder.Child = _canvas; _renderer.BitmapReady += () => { _canvas.Bitmap = _renderer?.Bitmap; _canvas.InvalidateVisual(); }; _renderer.FrameReady += () => { if (LoadingIndicator.IsVisible) LoadingIndicator.IsVisible = false; _canvas?.InvalidateVisual(); }; _endReachedHandler = (_, _) => Dispatcher.UIThread.Post(() => PlayPauseIcon.Value = "Play"); if (_renderer.Player != null) _renderer.Player.EndReached += _endReachedHandler; _renderer.Play(item.Path, muted: false, loop: false, startTimeMs: _videoStartTimeMs); PlayPauseIcon.Value = "Pause"; _isMuted = false; VolumeIcon.Value = "SpeakerHigh"; _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) }; _timer.Tick += UpdateTransportControls; _timer.Start(); } private void StopVideo() { if (_timer != null) { _timer.Tick -= UpdateTransportControls; _timer.Stop(); _timer = null; } if (_renderer != null) { if (_endReachedHandler != null && _renderer.Player != null) _renderer.Player.EndReached -= _endReachedHandler; _endReachedHandler = null; _renderer.FrameReady = null; _renderer.BitmapReady = null; var renderer = _renderer; _renderer = null; _ = Task.Run(() => renderer.Dispose()); } _canvas = null; VideoBorder.Child = null; } private void UpdateTransportControls(object? sender, EventArgs e) { if (_renderer?.Player is not { } player) return; _isUpdatingSlider = true; SeekSlider.Value = player.Position; _isUpdatingSlider = false; var current = TimeSpan.FromMilliseconds(Math.Max(0, player.Time)); var total = TimeSpan.FromMilliseconds(Math.Max(0, player.Length)); TimeLabel.Text = $"{current:m\\:ss} / {total:m\\:ss}"; } private void PlayPause_Click(object? sender, RoutedEventArgs e) => TogglePlayPause(); private void TogglePlayPause() { if (_renderer?.Player is not { } player) return; if (player.IsPlaying) { player.Pause(); PlayPauseIcon.Value = "Play"; } else { player.Play(); PlayPauseIcon.Value = "Pause"; } } private void SeekSlider_ValueChanged(object? sender, RangeBaseValueChangedEventArgs e) { if (_isUpdatingSlider || _renderer?.Player == null) return; _renderer.Player.Position = (float)SeekSlider.Value; } private void Volume_Click(object? sender, RoutedEventArgs e) { if (_renderer?.Player == null) return; _isMuted = !_isMuted; _renderer.Player.Volume = _isMuted ? 0 : 100; VolumeIcon.Value = _isMuted ? "SpeakerSlash" : "SpeakerHigh"; } // ── PDF ────────────────────────────────────────────────────────────── private void ShowPdf(LightboxItem item) { PdfViewer.Source = item.Path; PdfBorder.IsVisible = true; } // ── Close / keyboard ───────────────────────────────────────────────── private void Close_Click(object? sender, RoutedEventArgs e) => CloseOverlay(); private void Backdrop_PointerPressed(object? sender, PointerPressedEventArgs e) => CloseOverlay(); private void Content_PointerPressed(object? sender, PointerPressedEventArgs e) => e.Handled = true; private void Prev_Click(object? sender, RoutedEventArgs e) => Navigate(-1); private void Next_Click(object? sender, RoutedEventArgs e) => Navigate(1); private void OnKeyDown(object? sender, KeyEventArgs e) { switch (e.Key) { case Key.Escape: CloseOverlay(); e.Handled = true; break; case Key.Space: if (_items.Count > 0 && _items[_currentIndex].Type == LightboxItemType.Video) TogglePlayPause(); e.Handled = true; break; case Key.Left: Navigate(-1); e.Handled = true; break; case Key.Right: Navigate(1); e.Handled = true; break; } } private void CloseOverlay() { if (_closing) return; _closing = true; var videoTimeMs = _renderer?.Player?.Time ?? 0; var videoIndex = _currentIndex; var wasVideo = _items.Count > 0 && _items[_currentIndex].Type == LightboxItemType.Video; StopVideo(); // Clean up PDF PdfViewer.Source = null; var layer = OverlayLayer.GetOverlayLayer(this); if (wasVideo) VideoClosed?.Invoke(this, (videoIndex, videoTimeMs)); Closed?.Invoke(this, EventArgs.Empty); layer?.Children.Remove(this); } // ── VideoCanvas ────────────────────────────────────────────────────── private sealed class VideoCanvas : Control { public WriteableBitmap? Bitmap { get; set; } protected override Size MeasureOverride(Size availableSize) => availableSize; public override void Render(DrawingContext context) { if (Bitmap is not { } bmp) return; var srcW = (double)bmp.PixelSize.Width; var srcH = (double)bmp.PixelSize.Height; var dstW = Bounds.Width; var dstH = Bounds.Height; if (srcW <= 0 || srcH <= 0 || dstW <= 0 || dstH <= 0) return; var scale = Math.Min(dstW / srcW, dstH / srcH); var w = srcW * scale; var h = srcH * scale; context.DrawImage( bmp, new Rect(0, 0, srcW, srcH), new Rect((dstW - w) / 2, (dstH - h) / 2, w, h)); } } }