diff --git a/.gitignore b/.gitignore
index b89e12b910..f2e478e512 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,3 +38,7 @@ packages/
# Ignore everything else in release folders
**/bin/[Rr]elease*/
/.stfolder/
+
+# Agent and temporary build files
+.agent/
+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..17cabfaf4c
--- /dev/null
+++ b/documentation/Readme-FullScreenVideo.md
@@ -0,0 +1,70 @@
+# 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
+
+- **Multiple Triggers**:
+ - 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.
+
+## π οΈ 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
+
+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 > About Playnite > User data directory`.
+ Navigate to the `Extensions` folder (**not** `ExtensionsData`).
+ Look for a folder named `ExtraMetadataLoader` or `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 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.
diff --git a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml
new file mode 100644
index 0000000000..82264f5997
--- /dev/null
+++ b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs
new file mode 100644
index 0000000000..7fd49c68d0
--- /dev/null
+++ b/source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml.cs
@@ -0,0 +1,346 @@
+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 with transport controls.
+ /// 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;
+ private bool _isDragging;
+ private bool _isMuted;
+ private double _volumeBeforeMute;
+ private readonly DispatcherTimer _timer;
+
+ ///
+ /// The playback position at the time the window was closed.
+ ///
+ public TimeSpan ExitPosition { get; private set; }
+
+ ///
+ /// Whether the video was actively playing when the window was closed.
+ ///
+ 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.
+ ///
+ /// 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.
+ /// Whether the player should start muted.
+ public FullscreenVideoWindow(Uri source, TimeSpan startPosition, double volume, bool startPlaying, bool shouldLoop, bool isMuted)
+ {
+ InitializeComponent();
+
+ _startPosition = startPosition;
+ _startPlaying = startPlaying;
+ _shouldLoop = shouldLoop;
+ _hasAppliedStartPosition = false;
+ _isDragging = false;
+ _isMuted = isMuted;
+ _volumeBeforeMute = 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
+ {
+ fsPlayer.Source = source;
+
+ if (_startPlaying)
+ {
+ fsPlayer.Play();
+ WasPlaying = true;
+ PlayPauseIcon.Text = "\uE769"; // Pause icon
+ _timer.Start();
+ }
+ 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
+ }
+ }
+ catch (Exception ex)
+ {
+ _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.
+ 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)
+ {
+ if (_shouldLoop)
+ {
+ fsPlayer.Position = TimeSpan.Zero;
+ fsPlayer.Play();
+ }
+ 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
+ {
+ 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();
+ }
+ }
+
+ 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)
+ {
+ CloseFullscreen();
+ }
+ else if (e.Key == Key.Space)
+ {
+ TogglePlayPause();
+ e.Handled = true;
+ }
+ else if (e.Key == Key.M)
+ {
+ ToggleMute();
+ e.Handled = true;
+ }
+ }
+
+ private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ClickCount == 1)
+ {
+ TogglePlayPause();
+ }
+ }
+
+ 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_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ e.Handled = true;
+ CloseFullscreen();
+ }
+
+ private void CloseFullscreen()
+ {
+ _timer.Stop();
+
+ try
+ {
+ ExitPosition = fsPlayer.Position;
+ // Capture the actual volume (not muted value)
+ ExitVolume = _volumeBeforeMute;
+ ExitMuted = _isMuted;
+ fsPlayer.Stop();
+ }
+ catch (Exception ex)
+ {
+ _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 b/source/Generic/ExtraMetadataLoader/Controls/VideoPlayerControl.xaml
index 786ba9ec6e..a7a29f3c4a 100644
--- a/source/Generic/ExtraMetadataLoader/Controls/VideoPlayerControl.xaml
+++ b/source/Generic/ExtraMetadataLoader/Controls/VideoPlayerControl.xaml
@@ -1,4 +1,4 @@
-ο»Ώ
-
+