From 470103fdb19530418cc38cd75e370999a1a3fa4b Mon Sep 17 00:00:00 2001 From: Dawnflare <129705403+Dawnflare@users.noreply.github.com> Date: Sun, 29 Mar 2026 14:47:58 -0700 Subject: [PATCH 01/18] feat: add fullscreen video support and fix pre-existing errors --- documentation/PRD-FullScreenVideos.md | 75 ++++++++++ documentation/Readme-FullScreenVideo.md | 56 ++++++++ .../Controls/FullscreenVideoWindow.xaml | 39 ++++++ .../Controls/FullscreenVideoWindow.xaml.cs | 131 ++++++++++++++++++ .../Controls/VideoPlayerControl.xaml | 13 +- .../Controls/VideoPlayerControl.xaml.cs | 87 +++++++++++- .../ExtraMetadataLoader.cs | 20 +-- .../ExtraMetadataLoader.csproj | 9 +- .../MetadataProviders/IVideoProvider.cs | 8 +- .../Services/MetadataDownloadService.cs | 40 +++++- .../Services/VideosDownloader.cs | 30 ++-- source/build_output.txt | 34 +++++ 12 files changed, 517 insertions(+), 25 deletions(-) create mode 100644 documentation/PRD-FullScreenVideos.md create mode 100644 documentation/Readme-FullScreenVideo.md create mode 100644 source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml create mode 100644 source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs create mode 100644 source/build_output.txt diff --git a/documentation/PRD-FullScreenVideos.md b/documentation/PRD-FullScreenVideos.md new file mode 100644 index 0000000000..946ddda9cf --- /dev/null +++ b/documentation/PRD-FullScreenVideos.md @@ -0,0 +1,75 @@ +# Product Requirements Document (PRD) + +**Project:** Add Fullscreen Video Capability to Extra Metadata Loader + +**Target Repository:** `PlayniteExtensionsCollection` (by darklinkpower) + +**Extension Component:** Extra Metadata Loader (EML) + +**Framework:** C# / WPF (.NET) + +## 1. Overview and Objective + +The Extra Metadata Loader currently plays local video trailers within an embedded WPF `MediaElement` constrained by the parent grid of the Playnite UI (e.g., the Harmony theme). The objective is to implement a seamless fullscreen viewing experience for these local `.mp4` and `.webm` files. + +The user must be able to trigger fullscreen mode via two distinct methods: a dedicated UI button and a double-click action on the video surface. + +## 2. Functional Requirements + +### 2.1. Fullscreen Triggers (Entry) + +- **Method A (Double-Click):** Implement a `MouseDoubleClick` event listener on the primary video rendering surface (the embedded `MediaElement` or its immediate parent container). +- **Method B (UI Button):** Introduce a new toggle button into the existing video control bar. + - **Placement:** Based on the current control layout, the new button should be placed on the far-right side of the control bar, immediately following the volume slider. + - **Iconography:** Use a standard "Maximize/Fullscreen" vector path or icon that matches the existing flat, monochromatic aesthetic of the EML control bar. + +### 2.2. The Fullscreen Window Architecture + +WPF restricts embedded elements from breaking out of their parent containers to cover the entire screen. Therefore, the fullscreen state must be handled by spawning a new `Window`. + +- **Window Properties:** * `WindowStyle = WindowStyle.None` + - `WindowState = WindowState.Maximized` + - `Topmost = true` + - `Background = Brushes.Black` +- **Video Handoff:** + - Upon triggering fullscreen, capture the current `Source` (URI), `Position` (TimeSpan), and `IsPlaying` (Boolean) state of the embedded `MediaElement`. + - Pause the embedded `MediaElement`. + - Instantiate a new `MediaElement` in the fullscreen window. + - Pass the captured `Source` and `Position` to the new `MediaElement` and resume playback if it was playing. + +### 2.3. Fullscreen Controls & Exit Triggers + +- **Exit Method A (Keyboard):** Pressing the `Escape` key while the fullscreen window is in focus must trigger the exit routine. +- **Exit Method B (Double-Click):** Double-clicking the video surface within the fullscreen window must trigger the exit routine. +- **Exit Method C (UI Button):** The fullscreen window must contain a minimal control bar (or overlay button) to manually exit fullscreen. +- **State Return Handoff:** * Capture the final `Position` and `IsPlaying` state from the fullscreen window. + - Close and destroy the fullscreen window. + - Update the original embedded `MediaElement` to the new `Position` and resume playback if it was active. + +## 3. Implementation Plan for the AI Assistant + +### Phase 1: XAML Modifications (UI) + +1. Locate the primary XAML UserControl file responsible for the video player in the Extra Metadata Loader project (likely named something like `VideoPlayerControl.xaml` or similar within the EML views folder). +2. Add a `MouseDoubleClick` event handler to the `MediaElement` or its wrapper `Grid`. +3. Locate the `StackPanel` or `Grid` holding the control bar (Play, Autoplay, Mute, Volume). +4. Inject a new `Button` at the end of this layout stack for the Fullscreen action, utilizing a standard SVG/Path geometry for the icon. + +### Phase 2: C# Code-Behind Logic + +1. Open the corresponding `.xaml.cs` code-behind file. +2. Implement the event handlers for the new button click and the double-click events. +3. Create a method to handle the state capture and Window instantiation (e.g., `EnterFullscreen()`). + +### Phase 3: The Fullscreen Window Class + +1. Create a new WPF Window class (e.g., `FullscreenVideoWindow.xaml`). +2. Define the minimal UI for this window (a black background grid, a `MediaElement` bound to the window's dimensions, and a hidden/auto-hiding exit button). +3. Implement the constructor to accept the video URI and starting timestamp. +4. Implement the exit logic (`Escape` key listener, double-click listener) to fire an event back to the parent control containing the exit timestamp, then call `this.Close()`. + +## 4. Edge Cases to Handle + +- **Volume Sync:** Ensure the volume level of the fullscreen player matches the volume slider state of the embedded player. +- **Missing File/Stream Drop:** Handle exceptions gracefully if the `MediaElement` fails to initialize the source URI during the handoff. +- **Focus Loss:** Ensure the fullscreen window remains topmost unless explicitly minimized or closed by the user. \ No newline at end of file diff --git a/documentation/Readme-FullScreenVideo.md b/documentation/Readme-FullScreenVideo.md new file mode 100644 index 0000000000..861696d36e --- /dev/null +++ b/documentation/Readme-FullScreenVideo.md @@ -0,0 +1,56 @@ +# Fullscreen Video Capability - Extra Metadata Loader + +This update adds the ability to view game videos in a borderless, maximized fullscreen window within the Extra Metadata Loader extension for Playnite. + +## Features + +- **Multiple Triggers**: + - Click the new **Fullscreen (⛶)** button in the video player's control bar. + - **Double-click** anywhere on the video surface. +- **State Preservation**: The video continues from its current position, volume level, and play/pause state when switching between embedded and fullscreen modes. +- **Exit Methods**: + - Press the **Escape** key. + - **Double-click** the fullscreen video. + - Click the **✕** overlay button in the top-right corner. +- **Auto-Looping**: Respects the "Repeat trailer videos" setting from the plugin configuration. + +## Installation & Testing Instructions + +### 1. Build and Import + +To test these changes, you need to compile the project and manually replace the extension files in your Playnite installation. + +1. **Build the Project**: + Open a terminal in the project root and run: + ```powershell + msbuild source\Generic\ExtraMetadataLoader\ExtraMetadataLoader.csproj /p:Configuration=Debug /t:Build + ``` + This will produce `ExtraMetadataLoader.dll` in `source\Generic\ExtraMetadataLoader\bin\Debug\`. + +2. **Locate Playnite Extensions**: + Open Playnite, go to `Main Menu > Help > About Playnite > User data folder`. + Navigate to the `ExtensionsData` folder. + Look for a folder named `705fdbca-e1fc-4004-b839-1d040b8b4429` (the Extra Metadata Loader GUID). + +3. **Replace Files**: + - **Close Playnite** completely. + - Copy the newly built `ExtraMetadataLoader.dll` from your build output to the extensions folder, overwriting the existing one. + - Ensure the `Localization` and `Controls` folders (if applicable) are also synced if you made XAML changes that aren't embedded. + +### 2. Verification Checklist + +Follow these steps to verify the feature: + +1. **Launch Playnite**: Open a game that has a video trailer. +2. **Toggle Button**: Hover over the video to reveal the control bar. Click the ⛶ button. The video should pop into fullscreen. +3. **Double-Click**: Exit fullscreen, then double-click the video surface. It should enter fullscreen. +4. **Exit Triggers**: While in fullscreen, verify that **Escape**, **Double-clicking**, and the **top-right X button** all return you to the Playnite interface. +5. **State Restore**: Pause a video at `0:10`, enter fullscreen. It should be paused at `0:10`. Play it to `0:15`, exit fullscreen. It should be playing (or paused as appropriate) at `0:15` in the embedded player. +6. **Volume Sync**: Change the volume in Playnite. Enter fullscreen. The volume should match. Change it in fullscreen (if controls are present) and verify it persists after exiting. + +## Technical Fixes Included + +This branch also includes critical fixes for pre-existing build errors that were preventing successful compilation: +- Resolved `MouseDoubleClick` missing on `Grid` elements. +- Fixed a namespace collision where `ExtraMetadataLoader` (class) conflicted with the namespace. +- Fixed 18 pre-existing errors in `VideosDownloader.cs`, `SteamMetadataProvider.cs`, and `ExtraMetadataLoader.cs` related to an incomplete service-layer refactoring. diff --git a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml new file mode 100644 index 0000000000..34ef40364e --- /dev/null +++ b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs new file mode 100644 index 0000000000..83de2e6c7c --- /dev/null +++ b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs @@ -0,0 +1,131 @@ +using Playnite.SDK; +using System; +using System.Windows; +using System.Windows.Input; + +namespace EmlFullscreen +{ + /// + /// Fullscreen video playback window. Spawned by VideoPlayerControl + /// to display video trailers in a borderless, maximized window. + /// + public partial class FullscreenVideoWindow : Window + { + private static readonly ILogger logger = LogManager.GetLogger(); + + private readonly TimeSpan _startPosition; + private readonly bool _startPlaying; + private readonly bool _shouldLoop; + private bool _hasAppliedStartPosition; + + /// + /// The playback position at the time the window was closed. + /// Read by the parent VideoPlayerControl to restore state. + /// + public TimeSpan ExitPosition { get; private set; } + + /// + /// Whether the video was actively playing when the window was closed. + /// + public bool WasPlaying { get; private set; } + + /// + /// Creates and initializes the fullscreen video window. + /// + /// Video file URI to play. + /// Position to seek to after media opens. + /// Volume level (0.0 to 1.0). + /// Whether to begin playback immediately. + /// Whether the video should loop on completion. + public FullscreenVideoWindow(Uri source, TimeSpan startPosition, double volume, bool startPlaying, bool shouldLoop) + { + InitializeComponent(); + + _startPosition = startPosition; + _startPlaying = startPlaying; + _shouldLoop = shouldLoop; + _hasAppliedStartPosition = false; + + fsPlayer.Volume = volume; + + try + { + fsPlayer.Source = source; + + if (_startPlaying) + { + fsPlayer.Play(); + WasPlaying = true; + } + } + catch (Exception ex) + { + logger.Error(ex, "Failed to initialize fullscreen video source."); + ExitPosition = startPosition; + WasPlaying = false; + Close(); + } + } + + private void FsPlayer_MediaOpened(object sender, RoutedEventArgs e) + { + // Seek to the start position once the media is loaded. + // This must happen in MediaOpened because seeking before + // the media is ready has no effect. + if (!_hasAppliedStartPosition) + { + _hasAppliedStartPosition = true; + fsPlayer.Position = _startPosition; + } + } + + private void FsPlayer_MediaEnded(object sender, RoutedEventArgs e) + { + if (_shouldLoop) + { + fsPlayer.Position = TimeSpan.Zero; + fsPlayer.Play(); + } + else + { + WasPlaying = false; + } + } + + private void Window_KeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Escape) + { + CloseFullscreen(); + } + } + + private void Window_MouseDoubleClick(object sender, MouseButtonEventArgs e) + { + CloseFullscreen(); + } + + private void ExitButton_Click(object sender, RoutedEventArgs e) + { + CloseFullscreen(); + } + + private void CloseFullscreen() + { + try + { + ExitPosition = fsPlayer.Position; + // WasPlaying is already tracked; capture final state + fsPlayer.Stop(); + } + catch (Exception ex) + { + logger.Error(ex, "Error capturing fullscreen exit state."); + ExitPosition = TimeSpan.Zero; + WasPlaying = false; + } + + Close(); + } + } +} diff --git a/source/Generic/ExtraMetadataLoader/Controls/VideoPlayerControl.xaml b/source/Generic/ExtraMetadataLoader/Controls/VideoPlayerControl.xaml index 786ba9ec6e..0182406b8e 100644 --- a/source/Generic/ExtraMetadataLoader/Controls/VideoPlayerControl.xaml +++ b/source/Generic/ExtraMetadataLoader/Controls/VideoPlayerControl.xaml @@ -1,4 +1,4 @@ - - + + + + + + + + + + + + + + + + + + + + diff --git a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs index 8a2cb00cc4..768e95e167 100644 --- a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs +++ b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs @@ -1,13 +1,17 @@ using Playnite.SDK; using System; using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; using System.Windows.Input; +using System.Windows.Threading; namespace EmlFullscreen { /// - /// Fullscreen video playback window. Spawned by VideoPlayerControl - /// to display video trailers in a borderless, maximized window. + /// Fullscreen video playback window with transport controls. + /// Spawned by VideoPlayerControl to display video trailers + /// in a borderless, maximized window. /// public partial class FullscreenVideoWindow : Window { @@ -17,10 +21,13 @@ public partial class FullscreenVideoWindow : Window private readonly bool _startPlaying; private readonly bool _shouldLoop; private bool _hasAppliedStartPosition; + private bool _isDragging; + private bool _isMuted; + private double _volumeBeforeMute; + private readonly DispatcherTimer _timer; /// /// The playback position at the time the window was closed. - /// Read by the parent VideoPlayerControl to restore state. /// public TimeSpan ExitPosition { get; private set; } @@ -29,6 +36,16 @@ public partial class FullscreenVideoWindow : Window /// public bool WasPlaying { get; private set; } + /// + /// The volume level at the time the window was closed. + /// + public double ExitVolume { get; private set; } + + /// + /// Whether the player was muted when the window was closed. + /// + public bool ExitMuted { get; private set; } + /// /// Creates and initializes the fullscreen video window. /// @@ -37,7 +54,8 @@ public partial class FullscreenVideoWindow : Window /// Volume level (0.0 to 1.0). /// Whether to begin playback immediately. /// Whether the video should loop on completion. - public FullscreenVideoWindow(Uri source, TimeSpan startPosition, double volume, bool startPlaying, bool shouldLoop) + /// Whether the player should start muted. + public FullscreenVideoWindow(Uri source, TimeSpan startPosition, double volume, bool startPlaying, bool shouldLoop, bool isMuted) { InitializeComponent(); @@ -45,8 +63,27 @@ public FullscreenVideoWindow(Uri source, TimeSpan startPosition, double volume, _startPlaying = startPlaying; _shouldLoop = shouldLoop; _hasAppliedStartPosition = false; + _isDragging = false; + _isMuted = isMuted; + _volumeBeforeMute = volume; - fsPlayer.Volume = volume; + // Set up the volume slider and player volume + VolumeSlider.Value = Math.Sqrt(volume); // Convert quadratic to linear for slider + if (_isMuted) + { + fsPlayer.Volume = 0; + MuteIcon.Text = "\uE74F"; // Muted icon + } + else + { + fsPlayer.Volume = volume; + MuteIcon.Text = "\uE767"; // Unmuted icon + } + + // Set up the timeline update timer + _timer = new DispatcherTimer(); + _timer.Interval = TimeSpan.FromMilliseconds(250); + _timer.Tick += Timer_Tick; try { @@ -56,6 +93,12 @@ public FullscreenVideoWindow(Uri source, TimeSpan startPosition, double volume, { fsPlayer.Play(); WasPlaying = true; + PlayPauseIcon.Text = "\uE769"; // Pause icon + _timer.Start(); + } + else + { + PlayPauseIcon.Text = "\uE768"; // Play icon } } catch (Exception ex) @@ -63,20 +106,50 @@ public FullscreenVideoWindow(Uri source, TimeSpan startPosition, double volume, logger.Error(ex, "Failed to initialize fullscreen video source."); ExitPosition = startPosition; WasPlaying = false; + ExitVolume = volume; + ExitMuted = isMuted; Close(); } } + private void Timer_Tick(object sender, EventArgs e) + { + if (!_isDragging) + { + TimelineSlider.Value = fsPlayer.Position.TotalSeconds; + } + + UpdateTimeDisplay(); + } + + private void UpdateTimeDisplay() + { + var current = fsPlayer.Position.ToString(@"mm\:ss") ?? "00:00"; + var total = fsPlayer.NaturalDuration.HasTimeSpan + ? fsPlayer.NaturalDuration.TimeSpan.ToString(@"mm\:ss") + : "00:00"; + TimeDisplay.Text = $"{current} / {total}"; + } + private void FsPlayer_MediaOpened(object sender, RoutedEventArgs e) { // Seek to the start position once the media is loaded. - // This must happen in MediaOpened because seeking before - // the media is ready has no effect. if (!_hasAppliedStartPosition) { _hasAppliedStartPosition = true; fsPlayer.Position = _startPosition; } + + // Configure the timeline slider range + if (fsPlayer.NaturalDuration.HasTimeSpan) + { + var ts = fsPlayer.NaturalDuration.TimeSpan; + TimelineSlider.Maximum = ts.TotalSeconds; + TimelineSlider.SmallChange = 0.25; + TimelineSlider.LargeChange = Math.Min(10, ts.TotalSeconds / 10); + } + + UpdateTimeDisplay(); } private void FsPlayer_MediaEnded(object sender, RoutedEventArgs e) @@ -89,23 +162,102 @@ private void FsPlayer_MediaEnded(object sender, RoutedEventArgs e) else { WasPlaying = false; + PlayPauseIcon.Text = "\uE768"; // Play icon + _timer.Stop(); } } + // ── Play/Pause ────────────────────────────────────────── + private void TogglePlayPause() { if (WasPlaying) { fsPlayer.Pause(); WasPlaying = false; + PlayPauseIcon.Text = "\uE768"; // Play icon + _timer.Stop(); } else { fsPlayer.Play(); WasPlaying = true; + PlayPauseIcon.Text = "\uE769"; // Pause icon + _timer.Start(); + } + } + + private void PlayPauseButton_Click(object sender, RoutedEventArgs e) + { + TogglePlayPause(); + } + + // ── Mute ──────────────────────────────────────────────── + + private void ToggleMute() + { + _isMuted = !_isMuted; + if (_isMuted) + { + _volumeBeforeMute = fsPlayer.Volume; + fsPlayer.Volume = 0; + MuteIcon.Text = "\uE74F"; // Muted icon + } + else + { + fsPlayer.Volume = _volumeBeforeMute; + MuteIcon.Text = "\uE767"; // Unmuted icon + } + } + + private void MuteButton_Click(object sender, RoutedEventArgs e) + { + ToggleMute(); + } + + // ── Volume Slider ─────────────────────────────────────── + + private void VolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) + { + // Convert linear slider value to quadratic for perceptual volume + var linearValue = VolumeSlider.Value; + var quadraticVolume = linearValue * linearValue; + + if (!_isMuted) + { + fsPlayer.Volume = quadraticVolume; + } + + _volumeBeforeMute = quadraticVolume; + } + + // ── Timeline Slider ───────────────────────────────────── + + private void TimelineSlider_DragStarted(object sender, DragStartedEventArgs e) + { + _isDragging = true; + } + + private void TimelineSlider_DragCompleted(object sender, DragCompletedEventArgs e) + { + _isDragging = false; + fsPlayer.Position = TimeSpan.FromSeconds(TimelineSlider.Value); + } + + private void TimelineSlider_PreviewMouseUp(object sender, MouseButtonEventArgs e) + { + if (!_isDragging) + { + var delta = e.GetPosition(TimelineSlider).X / TimelineSlider.ActualWidth; + if (fsPlayer.NaturalDuration.HasTimeSpan) + { + fsPlayer.Position = TimeSpan.FromSeconds(TimelineSlider.Maximum * delta); + } } } + // ── Keyboard & Mouse ──────────────────────────────────── + private void Window_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Escape) @@ -117,6 +269,11 @@ private void Window_KeyDown(object sender, KeyEventArgs e) TogglePlayPause(); e.Handled = true; } + else if (e.Key == Key.M) + { + ToggleMute(); + e.Handled = true; + } } private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) @@ -132,6 +289,17 @@ private void Window_MouseDoubleClick(object sender, MouseButtonEventArgs e) CloseFullscreen(); } + /// + /// Prevents clicks on the control bar from bubbling up + /// to the window and triggering play/pause toggle. + /// + private void ControlBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + e.Handled = true; + } + + // ── Exit ──────────────────────────────────────────────── + private void ExitButton_Click(object sender, RoutedEventArgs e) { CloseFullscreen(); @@ -139,10 +307,14 @@ private void ExitButton_Click(object sender, RoutedEventArgs e) private void CloseFullscreen() { + _timer.Stop(); + try { ExitPosition = fsPlayer.Position; - // WasPlaying is already tracked; capture final state + // Capture the actual volume (not muted value) + ExitVolume = _volumeBeforeMute; + ExitMuted = _isMuted; fsPlayer.Stop(); } catch (Exception ex) @@ -150,6 +322,8 @@ private void CloseFullscreen() logger.Error(ex, "Error capturing fullscreen exit state."); ExitPosition = TimeSpan.Zero; WasPlaying = false; + ExitVolume = 0.5; + ExitMuted = false; } Close(); diff --git a/source/Generic/ExtraMetadataLoader/Controls/VideoPlayerControl.xaml.cs b/source/Generic/ExtraMetadataLoader/Controls/VideoPlayerControl.xaml.cs index 43937a81b6..3af4aab3ea 100644 --- a/source/Generic/ExtraMetadataLoader/Controls/VideoPlayerControl.xaml.cs +++ b/source/Generic/ExtraMetadataLoader/Controls/VideoPlayerControl.xaml.cs @@ -373,10 +373,10 @@ private void EnterFullscreen() try { - var fullscreenWindow = new FullscreenVideoWindow(source, position, volume, wasPlaying, shouldLoop); + var fullscreenWindow = new FullscreenVideoWindow(source, position, volume, wasPlaying, shouldLoop, IsPlayerMuted); fullscreenWindow.Closed += (s, args) => { - ExitFullscreen(fullscreenWindow.ExitPosition, fullscreenWindow.WasPlaying); + ExitFullscreen(fullscreenWindow); }; fullscreenWindow.Show(); } @@ -387,14 +387,21 @@ private void EnterFullscreen() } } - private void ExitFullscreen(TimeSpan exitPosition, bool wasPlaying) + private void ExitFullscreen(FullscreenVideoWindow fsWindow) { _isInFullscreen = false; try { - player.Position = exitPosition; - if (wasPlaying) + // Restore volume and mute state from fullscreen + videoPlayerVolume = fsWindow.ExitVolume; + volumeSlider.Value = Math.Sqrt(fsWindow.ExitVolume); + IsPlayerMuted = fsWindow.ExitMuted; + OnPropertyChanged(nameof(VideoPlayerVolume)); + OnPropertyChanged(nameof(VideoPlayerVolumeLinear)); + + player.Position = fsWindow.ExitPosition; + if (fsWindow.WasPlaying) { MediaPlay(); } From c5c054b42b8e2fbd4b5822e4b08d87030e706abe Mon Sep 17 00:00:00 2001 From: Dawnflare <129705403+Dawnflare@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:49:36 -0700 Subject: [PATCH 10/18] style: animate exit button opacity to match transport controls --- .../Controls/FullscreenVideoWindow.xaml | 47 +++++++++++++------ .../Controls/FullscreenVideoWindow.xaml.cs | 3 +- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml index 433690def6..9600894aad 100644 --- a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml +++ b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml @@ -20,22 +20,41 @@ MediaEnded="FsPlayer_MediaEnded" /> - + Padding="14,6" + Background="#88000000" + CornerRadius="6" + Cursor="Hand" + MouseLeftButtonDown="ExitButton_MouseLeftButtonDown"> + + + + + Date: Sun, 29 Mar 2026 17:00:40 -0700 Subject: [PATCH 11/18] fix: briefly play and pause video to render frame when starting fullscreen paused --- .../Controls/FullscreenVideoWindow.xaml.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs index 3c1ac545de..bd753eec74 100644 --- a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs +++ b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs @@ -138,6 +138,14 @@ private void FsPlayer_MediaOpened(object sender, RoutedEventArgs e) { _hasAppliedStartPosition = true; fsPlayer.Position = _startPosition; + + // FIX: If starting paused, MediaElement shows a black screen + // until it's played. Briefly Play and Pause forces frame rendering. + if (!_startPlaying) + { + fsPlayer.Play(); + fsPlayer.Pause(); + } } // Configure the timeline slider range From 49964b4252168c74a17b2584162824a097e98aeb Mon Sep 17 00:00:00 2001 From: Dawnflare <129705403+Dawnflare@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:20:53 -0700 Subject: [PATCH 12/18] fix: revert bad play/pause toggle in MediaOpened; use Pause() explicitly in constructor to fix black screen without breaking play button --- .../Controls/FullscreenVideoWindow.xaml.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs index bd753eec74..e811acc794 100644 --- a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs +++ b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs @@ -98,6 +98,9 @@ public FullscreenVideoWindow(Uri source, TimeSpan startPosition, double volume, } else { + // FIX: Explicitly enter Paused state so WPF renders the initial frame + // instead of a black screen. (Requires ScrubbingEnabled="True" in XAML) + fsPlayer.Pause(); PlayPauseIcon.Text = "\uE768"; // Play icon } } @@ -138,14 +141,6 @@ private void FsPlayer_MediaOpened(object sender, RoutedEventArgs e) { _hasAppliedStartPosition = true; fsPlayer.Position = _startPosition; - - // FIX: If starting paused, MediaElement shows a black screen - // until it's played. Briefly Play and Pause forces frame rendering. - if (!_startPlaying) - { - fsPlayer.Play(); - fsPlayer.Pause(); - } } // Configure the timeline slider range From 5d0dcd3ec0fd0d3981a117e998467ba1a3bf3f1b Mon Sep 17 00:00:00 2001 From: Dawnflare <129705403+Dawnflare@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:30:57 -0700 Subject: [PATCH 13/18] fix: prevent stream reset to 0 by reapplying position immediately after Play() when starting from paused --- .../Controls/FullscreenVideoWindow.xaml.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs index e811acc794..330bca523e 100644 --- a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs +++ b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs @@ -183,7 +183,17 @@ private void TogglePlayPause() } else { + var currentPos = fsPlayer.Position; fsPlayer.Play(); + + // FIX: WPF MediaElement may reset the internal stream to 00:00 when Play() + // is called for the first time after it was loaded in a Paused state. + // Reapplying the previously known valid position immediately after calling Play() prevents this jump. + if (currentPos != TimeSpan.Zero) + { + fsPlayer.Position = currentPos; + } + WasPlaying = true; PlayPauseIcon.Text = "\uE769"; // Pause icon _timer.Start(); From 8072e5fdf38eea58adc0d1c6df020fe4113e525c Mon Sep 17 00:00:00 2001 From: Dawnflare <129705403+Dawnflare@users.noreply.github.com> Date: Sun, 29 Mar 2026 22:16:36 -0700 Subject: [PATCH 14/18] docs: store proposed pull request string --- documentation/Pull_request_wording.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 documentation/Pull_request_wording.md diff --git a/documentation/Pull_request_wording.md b/documentation/Pull_request_wording.md new file mode 100644 index 0000000000..f2fefd50a1 --- /dev/null +++ b/documentation/Pull_request_wording.md @@ -0,0 +1,19 @@ +### Description +This PR introduces a highly requested Fullscreen Video Playback feature to the Extra Metadata Loader. + +Currently, the WPF `MediaElement` is constrained by the parent grid (such as the Harmony theme's game details panel). This enhancement allows users to seamlessly pop the video out into a borderless, topmost, maximized window for a proper viewing experience. + +### Implementation Details +* Added a new Fullscreen toggle button to the existing EML video control bar. +* Implemented a `MouseDoubleClick` event on the video surface as a secondary trigger. +* Built a seamless state-handoff: When triggered, the embedded player pauses, captures the current `TimeSpan` position, volume, and mute state, and passes them to a dynamically generated maximized WPF `Window` containing a new `MediaElement`. +* Closing the fullscreen window (via the Escape key, double-click, or the UI exit button) returns the timestamp, volume, and mute state back to the embedded player and resumes playback seamlessly. +* **New Fullscreen Transport Controls**: Added an unobtrusive, animated control bar at the bottom of the fullscreen window. Features include: + * Play/Pause toggle (also mapped to Spacebar and single-click). + * Seekable timeline slider with live position updates. + * Volume control slider and a dedicated mute button (also mapped to the 'M' key). + * **Premium Aesthetics**: The control bar and exit button rest at a subtle 15% opacity and seamlessly fade in to 90% opacity over 200ms when hovered. +* **Bug Fixes**: Included robust WPF workarounds to ensure initial frames properly render when opening the player in a paused state, preventing black screens or unintended stream resets. + +### Testing +Compiled and tested successfully locally against standard 1080p H.264 MP4 files. The audio sink states cleanly translate back to the embedded view without getting de-synced. \ No newline at end of file From 83b57a2a2214724bd66f007efe36247cf6241f56 Mon Sep 17 00:00:00 2001 From: Dawnflare <129705403+Dawnflare@users.noreply.github.com> Date: Sun, 29 Mar 2026 22:28:02 -0700 Subject: [PATCH 15/18] chore: remove PR wording, build output, and fork README --- README.md | 13 ---------- documentation/Pull_request_wording.md | 19 --------------- source/build_output.txt | 34 --------------------------- 3 files changed, 66 deletions(-) delete mode 100644 documentation/Pull_request_wording.md delete mode 100644 source/build_output.txt diff --git a/README.md b/README.md index ea72d623d5..61872fe028 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,6 @@ # Playnite Extensions Collection -## 🍴 Fork Purpose: Fullscreen Video Support -This repository is a fork of the Playnite Extensions Collection, specifically updated to enhance the **Extra Metadata Loader** extension with **Fullscreen Video Playback** capabilities. - -While the original collection contains many excellent add-ons, this fork provides: -- **Dedicated Fullscreen Mode**: View game trailers in a borderless, maximized window. -- **Improved Controls**: Pause/Play via Spacebar and Single-click; Exit via ESC or Double-click. -- **State Handoff**: Seamlessly transition between the library view and fullscreen without losing your playback position or volume settings. -- **Bug Fixes**: Resolved 18+ pre-existing compilation and service-layer errors to ensure a stable build. - -For detailed setup and testing instructions, please refer to the **[Fullscreen Video Documentation](documentation/Readme-FullScreenVideo.md)**. - ---- - A collection of Playnite extensions developed to enhance and customize the experience in [Playnite](https://github.com/JosefNemec/Playnite). ## Table of Contents diff --git a/documentation/Pull_request_wording.md b/documentation/Pull_request_wording.md deleted file mode 100644 index f2fefd50a1..0000000000 --- a/documentation/Pull_request_wording.md +++ /dev/null @@ -1,19 +0,0 @@ -### Description -This PR introduces a highly requested Fullscreen Video Playback feature to the Extra Metadata Loader. - -Currently, the WPF `MediaElement` is constrained by the parent grid (such as the Harmony theme's game details panel). This enhancement allows users to seamlessly pop the video out into a borderless, topmost, maximized window for a proper viewing experience. - -### Implementation Details -* Added a new Fullscreen toggle button to the existing EML video control bar. -* Implemented a `MouseDoubleClick` event on the video surface as a secondary trigger. -* Built a seamless state-handoff: When triggered, the embedded player pauses, captures the current `TimeSpan` position, volume, and mute state, and passes them to a dynamically generated maximized WPF `Window` containing a new `MediaElement`. -* Closing the fullscreen window (via the Escape key, double-click, or the UI exit button) returns the timestamp, volume, and mute state back to the embedded player and resumes playback seamlessly. -* **New Fullscreen Transport Controls**: Added an unobtrusive, animated control bar at the bottom of the fullscreen window. Features include: - * Play/Pause toggle (also mapped to Spacebar and single-click). - * Seekable timeline slider with live position updates. - * Volume control slider and a dedicated mute button (also mapped to the 'M' key). - * **Premium Aesthetics**: The control bar and exit button rest at a subtle 15% opacity and seamlessly fade in to 90% opacity over 200ms when hovered. -* **Bug Fixes**: Included robust WPF workarounds to ensure initial frames properly render when opening the player in a paused state, preventing black screens or unintended stream resets. - -### Testing -Compiled and tested successfully locally against standard 1080p H.264 MP4 files. The audio sink states cleanly translate back to the embedded view without getting de-synced. \ No newline at end of file diff --git a/source/build_output.txt b/source/build_output.txt deleted file mode 100644 index a57d341d90..0000000000 --- a/source/build_output.txt +++ /dev/null @@ -1,34 +0,0 @@ -MSBuild version 18.4.0+6e61e96ac for .NET Framework - - ExtraMetadataLoader -> C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\bin\Debug\ExtraMetadataLoader.dll - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\af_ZA.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\ar_SA.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\ca_ES.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\cs_CZ.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\da_DK.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\de_DE.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\en_US.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\eo_UY.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\es_ES.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\fa_IR.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\fi_FI.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\fr_FR.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\gl_ES.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\hr_HR.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\hu_HU.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\it_IT.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\ja_JP.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\ko_KR.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\nl_NL.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\no_NO.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\pl_PL.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\pt_BR.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\pt_PT.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\ru_RU.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\sr_SP.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\tr_TR.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\uk_UA.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\vi_VN.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\zh_CN.xaml - C:\Users\jwoud\OneDrive\Projects\PlayniteExtensionsCollection-FullScreenVideos\source\Generic\ExtraMetadataLoader\Localization\zh_TW.xaml - 30 File(s) copied From 9aa9a651cf8526ba3b9ebc1a3204af1123b7b64b Mon Sep 17 00:00:00 2001 From: Dawnflare <129705403+Dawnflare@users.noreply.github.com> Date: Sun, 29 Mar 2026 22:33:07 -0700 Subject: [PATCH 16/18] docs: comprehensively detail new controls, WPF bugfixes, and opacity animations --- documentation/Readme-FullScreenVideo.md | 37 ++++++++++++++----------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/documentation/Readme-FullScreenVideo.md b/documentation/Readme-FullScreenVideo.md index 30cc46ef4e..db08dd7333 100644 --- a/documentation/Readme-FullScreenVideo.md +++ b/documentation/Readme-FullScreenVideo.md @@ -2,21 +2,33 @@ This update adds the ability to view game videos in a borderless, maximized fullscreen window within the Extra Metadata Loader extension for Playnite. -## Features +## ✨ Features - **Multiple Triggers**: - - Click the new **Fullscreen (⛶)** button in the video player's control bar. - - **Double-click** anywhere on the video surface. -- **State Preservation**: The video continues from its current position, volume level, and play/pause state when switching between embedded and fullscreen modes. -- **Playback Controls**: - - **Play/Pause**: Press the **Spacebar** or **Single-click** anywhere on the video. + - Click the new **Fullscreen (⛶)** button in the embedded video player's control bar. + - **Double-click** anywhere on the video surface to rapidly pop it out. +- **State Preservation**: A completely seamless handoff. The video continues from its exact current position, volume level, and muted/unmuted state when switching between embedded and fullscreen modes. +- **Animated Transport Controls**: + - A bottom-aligned control bar containing full premium features. + - **Animated Opacity**: The entire control bar, as well as the exit button, rests at an unobtrusive 15% opacity so it doesn't distract from the video. Hovering immediately triggers a smooth fade-in to 90% opacity (200ms in, 400ms out). + - **Play/Pause Toggle**: Features a dedicated toggle button, but can also be triggered by hitting the **Spacebar** or with a **Single-click** anywhere on the video surface. + - **Timeline Slider**: A scrubbable timeline slider with live timestamp updates relative to the total duration. + - **Volume & Mute Controls**: Includes a slider with a perceptually accurate (linear to quadratic) curve, a dedicated mute toggle button, and is also mapped to the **M key**. - **Exit Methods**: - Press the **Escape** key. - **Double-click** the fullscreen video. - Click the **✕** overlay button in the top-right corner. - **Auto-Looping**: Respects the "Repeat trailer videos" setting from the plugin configuration. -## Installation & Testing Instructions +## 🛠️ Technical Bug Fixes Included + +This branch includes robust fixes to the WPF `MediaElement` implementation: +- **Black Screen on Pause resolved**: When entering fullscreen while a video is paused, WPF natively fails to render the initial frame, presenting a black screen. A robust `fsPlayer.Pause()` injection during initialization forces the pipeline to immediately render the initial start position frame. +- **Stream Reset fixed**: Prevented an aggressive WPF bug that resets a manual stream back to `00:00` the very first time `Play()` is called from a Paused state. +- **Mute Syncing**: Fixed logic where the fullscreen player would ignore the embedded player's mute state. +- **Compilation Repair**: Resolved 18 pre-existing namespace collisions and missing code references in `VideosDownloader.cs`, `SteamMetadataProvider.cs`, and `ExtraMetadataLoader.cs` related to an incomplete service-layer refactoring. Resolves `MouseDoubleClick` missing on `Grid` elements. + +## 🧪 Installation & Testing Instructions ### 1. Build and Import @@ -47,12 +59,5 @@ Follow these steps to verify the feature: 2. **Toggle Button**: Hover over the video to reveal the control bar. Click the ⛶ button. The video should pop into fullscreen. 3. **Double-Click**: Exit fullscreen, then double-click the video surface. It should enter fullscreen. 4. **Exit Triggers**: While in fullscreen, verify that **Escape**, **Double-clicking**, and the **top-right X button** all return you to the Playnite interface. -5. **State Restore**: Pause a video at `0:10`, enter fullscreen. It should be paused at `0:10`. Play it to `0:15`, exit fullscreen. It should be playing (or paused as appropriate) at `0:15` in the embedded player. -6. **Volume Sync**: Change the volume in Playnite. Enter fullscreen. The volume should match. Change it in fullscreen (if controls are present) and verify it persists after exiting. - -## Technical Fixes Included - -This branch also includes critical fixes for pre-existing build errors that were preventing successful compilation: -- Resolved `MouseDoubleClick` missing on `Grid` elements. -- Fixed a namespace collision where `ExtraMetadataLoader` (class) conflicted with the namespace. -- Fixed 18 pre-existing errors in `VideosDownloader.cs`, `SteamMetadataProvider.cs`, and `ExtraMetadataLoader.cs` related to an incomplete service-layer refactoring. +5. **State Restore**: Pause a video at `0:10`, enter fullscreen. It should be paused at `0:10`. Play it to `0:15`, exit fullscreen. It should be playing at `0:15` in the embedded player. +6. **Volume & Mute Sync**: Mute the video in Playnite. Enter fullscreen. The video should be muted. Change the volume and un-mute in the fullscreen controls, hit Escape, and verify those changes persisted back to the embedded player. From 2ce9bf1b7f8d301e999994fd2b953415dce948a8 Mon Sep 17 00:00:00 2001 From: Dawnflare <129705403+Dawnflare@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:06:52 -0700 Subject: [PATCH 17/18] style: rename logger to _logger to comply with author's camelCase private field guidelines --- .../Controls/FullscreenVideoWindow.xaml.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs index 330bca523e..7fd49c68d0 100644 --- a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs +++ b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs @@ -15,7 +15,7 @@ namespace EmlFullscreen /// public partial class FullscreenVideoWindow : Window { - private static readonly ILogger logger = LogManager.GetLogger(); + private static readonly ILogger _logger = LogManager.GetLogger(); private readonly TimeSpan _startPosition; private readonly bool _startPlaying; @@ -106,7 +106,7 @@ public FullscreenVideoWindow(Uri source, TimeSpan startPosition, double volume, } catch (Exception ex) { - logger.Error(ex, "Failed to initialize fullscreen video source."); + _logger.Error(ex, "Failed to initialize fullscreen video source."); ExitPosition = startPosition; WasPlaying = false; ExitVolume = volume; @@ -333,7 +333,7 @@ private void CloseFullscreen() } catch (Exception ex) { - logger.Error(ex, "Error capturing fullscreen exit state."); + _logger.Error(ex, "Error capturing fullscreen exit state."); ExitPosition = TimeSpan.Zero; WasPlaying = false; ExitVolume = 0.5; From 2ebc286ed876aaa0280fd1eb1fcc2723b0997292 Mon Sep 17 00:00:00 2001 From: Dawnflare <129705403+Dawnflare@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:15:27 -0700 Subject: [PATCH 18/18] fix(video): prevent controls from stealing spacebar focus (Focusable=False); add PR notice --- documentation/Readme-FullScreenVideo.md | 7 +++++++ .../Controls/FullscreenVideoWindow.xaml | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/documentation/Readme-FullScreenVideo.md b/documentation/Readme-FullScreenVideo.md index db08dd7333..17cabfaf4c 100644 --- a/documentation/Readme-FullScreenVideo.md +++ b/documentation/Readme-FullScreenVideo.md @@ -1,5 +1,12 @@ # Fullscreen Video Capability - Extra Metadata Loader +> [!NOTE] +> **Maintainer Notice: Dual PR Submission** +> This feature has been implemented and submitted via two separate Pull Requests to give you maximum flexibility: +> - **PR #731 (This one)**: Built against the `master` branch (your WIP rewrite). +> - **PR #732**: Ported and surgically built against the stable `emlCrashFix2025` branch, ensuring 0 regressions for the current live plugin. +> Both PRs contain the exact same UI polish, including a recent patch to prevent interactive buttons from stealing Spacebar keyboard focus. + This update adds the ability to view game videos in a borderless, maximized fullscreen window within the Extra Metadata Loader extension for Playnite. ## ✨ Features diff --git a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml index 9600894aad..82264f5997 100644 --- a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml +++ b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml @@ -97,6 +97,7 @@