Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
470103f
feat: add fullscreen video support and fix pre-existing errors
Dawnflare Mar 29, 2026
ab7107b
chore: add .agent folder to .gitignore
Dawnflare Mar 29, 2026
e4214eb
docs: clarify Extensions folder path
Dawnflare Mar 29, 2026
12ee7f5
fix: replace missing FontIcoFont fullscreen icon with Segoe MDL2 icon
Dawnflare Mar 29, 2026
81045a6
docs: user-provided path updates and filenames list
Dawnflare Mar 29, 2026
581fa03
feat: pause and play video via spacebar and single click in fullscreen
Dawnflare Mar 29, 2026
763cccb
docs: add fork purpose section to main README
Dawnflare Mar 29, 2026
ae6b441
Delete documentation/705fdbca-e1fc-4004-b839-1d040b8b4429 filenames.txt
Dawnflare Mar 29, 2026
e476fe9
feat: add transport controls to fullscreen video and fix mute sync
Dawnflare Mar 29, 2026
6927d88
Merge branch 'master' of github.com:Dawnflare/PlayniteExtensionsColle…
Dawnflare Mar 29, 2026
c5c054b
style: animate exit button opacity to match transport controls
Dawnflare Mar 29, 2026
b219209
fix: briefly play and pause video to render frame when starting fulls…
Dawnflare Mar 30, 2026
49964b4
fix: revert bad play/pause toggle in MediaOpened; use Pause() explici…
Dawnflare Mar 30, 2026
5d0dcd3
fix: prevent stream reset to 0 by reapplying position immediately aft…
Dawnflare Mar 30, 2026
8072e5f
docs: store proposed pull request string
Dawnflare Mar 30, 2026
83b57a2
chore: remove PR wording, build output, and fork README
Dawnflare Mar 30, 2026
9aa9a65
docs: comprehensively detail new controls, WPF bugfixes, and opacity …
Dawnflare Mar 30, 2026
2ce9bf1
style: rename logger to _logger to comply with author's camelCase pri…
Dawnflare Mar 30, 2026
2ebc286
fix(video): prevent controls from stealing spacebar focus (Focusable=…
Dawnflare Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
75 changes: 75 additions & 0 deletions documentation/PRD-FullScreenVideos.md
Original file line number Diff line number Diff line change
@@ -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.
70 changes: 70 additions & 0 deletions documentation/Readme-FullScreenVideo.md
Original file line number Diff line number Diff line change
@@ -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.
166 changes: 166 additions & 0 deletions source/Generic/ExtraMetadataLoader/Controls/FullscreenVideoWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<Window x:Class="EmlFullscreen.FullscreenVideoWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
WindowStyle="None"
WindowState="Maximized"
AllowsTransparency="False"
Topmost="True"
Background="Black"
KeyDown="Window_KeyDown"
MouseDoubleClick="Window_MouseDoubleClick"
MouseLeftButtonDown="Window_MouseLeftButtonDown">
<Grid>
<MediaElement Name="fsPlayer"
LoadedBehavior="Manual"
ScrubbingEnabled="True"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MediaOpened="FsPlayer_MediaOpened"
MediaEnded="FsPlayer_MediaEnded" />

<!-- Exit fullscreen button overlay -->
<Border HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="20"
Padding="14,6"
Background="#88000000"
CornerRadius="6"
Cursor="Hand"
MouseLeftButtonDown="ExitButton_MouseLeftButtonDown">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Opacity" Value="0.15" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="0.9" Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="0.15" Duration="0:0:0.4" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="✕" FontSize="20" Foreground="White" FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>

<!-- Transport controls bar at the bottom -->
<Border Name="ControlBar"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Margin="40,0,40,30"
Padding="12,8"
Background="#88000000"
CornerRadius="6"
Cursor="Arrow"
MouseLeftButtonDown="ControlBar_MouseLeftButtonDown">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Opacity" Value="0.15" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="0.9" Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="0.15" Duration="0:0:0.4" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>

<DockPanel LastChildFill="True">
<!-- Play/Pause button -->
<Button Name="PlayPauseButton"
DockPanel.Dock="Left"
Click="PlayPauseButton_Click"
Focusable="False"
Foreground="White"
Background="Transparent"
BorderThickness="0"
Padding="4"
Margin="0,0,10,0"
Cursor="Hand"
VerticalAlignment="Center">
<TextBlock Name="PlayPauseIcon"
FontFamily="Segoe MDL2 Assets"
Text="&#xE769;"
FontSize="20" />
</Button>

<!-- Time display -->
<TextBlock Name="TimeDisplay"
DockPanel.Dock="Left"
Foreground="White"
FontSize="13"
VerticalAlignment="Center"
Margin="0,0,12,0"
Text="00:00 / 00:00" />

<!-- Volume controls (right-docked) -->
<Slider Name="VolumeSlider"
Focusable="False"
DockPanel.Dock="Right"
Width="90"
Minimum="0" Maximum="1"
IsSnapToTickEnabled="True"
TickFrequency="0.025"
VerticalAlignment="Center"
Margin="4,0,0,0"
ValueChanged="VolumeSlider_ValueChanged" />

<!-- Mute button -->
<Button Name="MuteButton"
DockPanel.Dock="Right"
Click="MuteButton_Click"
Focusable="False"
Foreground="White"
Background="Transparent"
BorderThickness="0"
Padding="4"
Margin="10,0,0,0"
Cursor="Hand"
VerticalAlignment="Center">
<TextBlock Name="MuteIcon"
FontFamily="Segoe MDL2 Assets"
Text="&#xE767;"
FontSize="18" />
</Button>

<!-- Timeline slider (fills remaining space) -->
<Slider Name="TimelineSlider"
Focusable="False"
Minimum="0"
IsMoveToPointEnabled="True"
VerticalAlignment="Center"
Margin="0,0,10,0"
Thumb.DragStarted="TimelineSlider_DragStarted"
Thumb.DragCompleted="TimelineSlider_DragCompleted"
PreviewMouseUp="TimelineSlider_PreviewMouseUp" />
</DockPanel>
</Border>
</Grid>
</Window>
Loading