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 @@