Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f1e770c
Fix #1019: non-square album art cropped in media flyout
frans15-2006 Sep 5, 2026
1615500
Fix #1029: second launch at boot pops settings on every startup
frans15-2006 Sep 5, 2026
c7aea56
Fix #1084: pause-others fires on metadata spam (inescapable exclusive…
frans15-2006 Sep 5, 2026
3ffa660
Fix #1085: ghost widget + dead taskbar on monitor off/projection change
frans15-2006 Sep 5, 2026
fd40acf
Fix #1087: mixer names/icons missing for elevated apps
frans15-2006 Sep 5, 2026
64f7221
Fix #1107: visualizer dead on Dolby Atmos/multi-channel/24-bit audio
frans15-2006 Sep 5, 2026
5a0a5a4
Fix #1078: native volume OSD returns after Explorer restart
frans15-2006 Sep 5, 2026
bfef8a5
Fix #1075: abnormal exit on volume popup (throw vectors + OSD hunt)
frans15-2006 Sep 5, 2026
4e4e356
Fix #984: media flyout never appears (off-screen stranding + silent g…
frans15-2006 Sep 5, 2026
eaf26fc
Fix #1015: hook stalls breaking Alt+Tab, dismiss-animation snap
frans15-2006 Sep 5, 2026
3faaa00
Fix #1096: stale scroll animation blanks/blinks widget on song change
frans15-2006 Sep 5, 2026
04340db
Fix #1094: MSIX parse/launch failures on GitHub builds
frans15-2006 Sep 5, 2026
c3a632a
Fix #1071: recover visualizer + volume mixer after S3 sleep/resume
frans15-2006 Sep 5, 2026
d6650d4
Fix #1071: recover visualizer + volume mixer after S3 sleep/resume
Sep 6, 2026
3b17d7d
Fix non-square album art in media flyout (#1019)
frans15-2006 Sep 5, 2026
f7da5e6
Group volume mixer entries per app instead of per audio session (#973)
frans15-2006 Sep 5, 2026
10850c0
Ignore unresolved indirect string resources as mixer session names (#…
frans15-2006 Sep 5, 2026
717b917
Recover volume flyout when the audio endpoint is stale or missing (#1…
frans15-2006 Sep 5, 2026
581ba45
Keep flyouts clear of an auto-hidden taskbar (#987, #1039)
frans15-2006 Sep 5, 2026
ffe706a
Only let the active media session auto-pause the others (#1084)
frans15-2006 Sep 5, 2026
610fd9b
Fix taskbar widget and next-up flyout going blank on looped tracks (#…
frans15-2006 Sep 5, 2026
99d053e
Recreate the taskbar widget window after an Explorer restart (#1065, …
frans15-2006 Sep 5, 2026
b72c78f
Recover the taskbar visualizer when loopback capture goes silent (#1071)
frans15-2006 Sep 5, 2026
90059cd
Rebuild taskbar widget placement after unlock and power resume (#983)
frans15-2006 Sep 5, 2026
6094966
Don't throw when the monitor list is momentarily empty (#1085)
frans15-2006 Sep 5, 2026
3a44498
Hide the native volume OSD at startup, with retries (#966, #1078)
frans15-2006 Sep 5, 2026
c10c80f
Stop volume flyout failures from crashing the app (#1075)
frans15-2006 Sep 5, 2026
daf4ab7
Fix #920: only show volume flyout for volume media keys
frans15-2006 Sep 5, 2026
44a575e
Fix #746: stop seekbar extrapolation while media is paused
frans15-2006 Sep 5, 2026
adf900e
Fix #153: reset seekbar position when the track changes
frans15-2006 Sep 5, 2026
444cde2
Fix #659: keep media deduplication scoped to each session
frans15-2006 Sep 5, 2026
c5e69d9
Fix #608: fall back to the process name for empty media titles
frans15-2006 Sep 5, 2026
785dbc5
Fix #529: keep RTL taskbar widgets in the visible host coordinates
frans15-2006 Sep 5, 2026
a3c64df
Fix #843: restart the visualizer on audio device topology changes
frans15-2006 Sep 5, 2026
5b6e4d1
Fix #817: restart the visualizer after spatial audio changes
frans15-2006 Sep 5, 2026
d52641a
Fix #849: refresh widget placement during taskbar auto-hide
frans15-2006 Sep 5, 2026
01720cc
Fix #802: remove unused flyout width for disabled controls
frans15-2006 Sep 5, 2026
b56e0f9
Fix #1084: whitespace formatting in VolumeMixerViewModel
Sep 6, 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
26 changes: 26 additions & 0 deletions .github/build-files/FluentFlyout_Installer.bat
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,32 @@ ECHO Please follow the prompts in the blue window.
ECHO ===================================================
ECHO.

:: Prerequisite: the GitHub build is framework-dependent and needs the
:: .NET Desktop Runtime 10 (x64). Without it the package installs fine but
:: the app silently fails to start (no window, no Task Manager entry).
SET "DotNetFxKey=HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App"
reg query "%DotNetFxKey%" 2>NUL | findstr /R "10\." >NUL
IF %ERRORLEVEL% NEQ 0 (
ECHO ---------------------------------------------------
ECHO .NET 10 Desktop Runtime ^(x64^) was NOT detected.
ECHO FluentFlyout will NOT start without it.
ECHO Download it here:
ECHO https://aka.ms/dotnet/10.0/windowsdesktop-runtime-win-x64.exe
ECHO ---------------------------------------------------
ECHO.
SET /P OpenDotNet="Open the download page now? (Y/N): "
IF /I "%OpenDotNet%"=="Y" (
START "" "https://aka.ms/dotnet/10.0/windowsdesktop-runtime-win-x64.exe"
ECHO.
ECHO Install the runtime, then run this installer again.
PAUSE
EXIT /B 1
) ELSE (
ECHO Continuing without the runtime - the app may fail to start.
ECHO.
)
)

cd /d "%~dp0SystemFiles"

:: Run the VS generated script
Expand Down
7 changes: 7 additions & 0 deletions .github/build-files/README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ Since you've chosen to install via the GitHub method, there are two ways to inst
  this is the manual method. Follow the installation instructions below:
https://github.com/unchihugo/FluentFlyout?tab=readme-ov-file#using-msixbundle-installer

PREREQUISITE (both methods):
The GitHub build needs the .NET 10 Desktop Runtime (x64) installed, otherwise
the package installs but the app silently fails to start (no window, not even
in Task Manager). Get it here:
https://aka.ms/dotnet/10.0/windowsdesktop-runtime-win-x64.exe
(The FluentFlyout_Installer.bat checks this for you automatically.)

-----------------------------------------------------------------------------------------------------

Note: GitHub releases provide the full feature set without restrictions. The only trade-off is
Expand Down
1 change: 0 additions & 1 deletion FluentFlyoutMSIX/FluentFlyoutMSIX.wapproj
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundlePlatforms>x64</AppxBundlePlatforms>
<AppInstallerUri>C:\Users\hu8go\Desktop</AppInstallerUri>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
<PackageCertificateThumbprint>25D3EFB3B470444AB88088492FCDA18F99BAF513</PackageCertificateThumbprint>
Expand Down
6 changes: 3 additions & 3 deletions FluentFlyoutMSIX/Package.appxmanifest
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>

<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
Expand All @@ -20,8 +20,8 @@
</Properties>

<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14393.0" MaxVersionTested="10.0.14393.0" />
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>

<Resources>
Expand Down
32 changes: 27 additions & 5 deletions FluentFlyoutWPF/Classes/AudioDeviceMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class AudioDeviceMonitor : IDisposable
private AudioDeviceNotificationClient? _notificationClient;

public event EventHandler<DefaultDeviceChangedEventArgs>? DefaultDeviceChanged;
public event EventHandler? DeviceTopologyChanged;
public event EventHandler? DevicePropertyChanged;

public static AudioDeviceMonitor Instance
{
Expand Down Expand Up @@ -44,6 +46,8 @@ private void Initialize()
_deviceEnumerator = new MMDeviceEnumerator();
_notificationClient = new AudioDeviceNotificationClient();
_notificationClient.DefaultDeviceChanged += OnDefaultDeviceChanged;
_notificationClient.DeviceTopologyChanged += OnDeviceTopologyChanged;
_notificationClient.DevicePropertyChanged += OnDevicePropertyChanged;
_deviceEnumerator.RegisterEndpointNotificationCallback(_notificationClient);

Logger.Info("Audio device monitoring initialized");
Expand All @@ -64,6 +68,17 @@ private void OnDefaultDeviceChanged(object? sender, DefaultDeviceChangedEventArg
DefaultDeviceChanged?.Invoke(this, e);
}

private void OnDeviceTopologyChanged(object? sender, EventArgs e)
{
Logger.Info("Audio device topology changed");
DeviceTopologyChanged?.Invoke(this, e);
}

private void OnDevicePropertyChanged(object? sender, EventArgs e)
{
DevicePropertyChanged?.Invoke(this, e);
}

public MMDevice? GetDefaultRenderDevice()
{
try
Expand Down Expand Up @@ -97,7 +112,12 @@ private void OnDefaultDeviceChanged(object? sender, DefaultDeviceChangedEventArg

public void Dispose()
{
_notificationClient?.DefaultDeviceChanged -= OnDefaultDeviceChanged;
if (_notificationClient != null)
{
_notificationClient.DefaultDeviceChanged -= OnDefaultDeviceChanged;
_notificationClient.DeviceTopologyChanged -= OnDeviceTopologyChanged;
_notificationClient.DevicePropertyChanged -= OnDevicePropertyChanged;
}

if (_deviceEnumerator != null && _notificationClient != null)
{
Expand Down Expand Up @@ -137,16 +157,18 @@ public void Dispose()
public class AudioDeviceNotificationClient : IMMNotificationClient
{
public event EventHandler<DefaultDeviceChangedEventArgs>? DefaultDeviceChanged;
public event EventHandler? DeviceTopologyChanged;
public event EventHandler? DevicePropertyChanged;

public void OnDefaultDeviceChanged(DataFlow flow, Role role, string defaultDeviceId)
{
DefaultDeviceChanged?.Invoke(this, new DefaultDeviceChangedEventArgs(flow, role, defaultDeviceId));
}

public void OnDeviceStateChanged(string deviceId, DeviceState newState) { }
public void OnDeviceAdded(string pwstrDeviceId) { }
public void OnDeviceRemoved(string deviceId) { }
public void OnPropertyValueChanged(string pwstrDeviceId, PropertyKey key) { }
public void OnDeviceStateChanged(string deviceId, DeviceState newState) => DeviceTopologyChanged?.Invoke(this, EventArgs.Empty);
public void OnDeviceAdded(string pwstrDeviceId) => DeviceTopologyChanged?.Invoke(this, EventArgs.Empty);
public void OnDeviceRemoved(string deviceId) => DeviceTopologyChanged?.Invoke(this, EventArgs.Empty);
public void OnPropertyValueChanged(string pwstrDeviceId, PropertyKey key) => DevicePropertyChanged?.Invoke(this, EventArgs.Empty);
}

public class DefaultDeviceChangedEventArgs(DataFlow dataFlow, Role role, string deviceId) : EventArgs
Expand Down
22 changes: 22 additions & 0 deletions FluentFlyoutWPF/Classes/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ public static partial class NativeMethods
internal const int APPCOMMAND_MEDIA_PLAY_PAUSE = 14;
internal const int FAPPCOMMAND_KEY = 0x0000;

// Process Access Rights
internal const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;

// AppBar (taskbar) messages and states
internal const uint ABM_GETSTATE = 0x00000004;
internal const uint ABM_GETTASKBARPOS = 0x00000005;
internal const int ABS_AUTOHIDE = 0x0000001;

#endregion

#region Enums
Expand Down Expand Up @@ -197,6 +205,17 @@ internal struct DISPLAY_DEVICE
public string DeviceKey;
}

[StructLayout(LayoutKind.Sequential)]
internal struct APPBARDATA
{
public int cbSize;
public IntPtr hWnd;
public uint uCallbackMessage;
public uint uEdge;
public RECT rc;
public int lParam;
}

[StructLayout(LayoutKind.Sequential)]
internal struct AccentPolicy
{
Expand Down Expand Up @@ -386,5 +405,8 @@ internal struct WindowCompositionAttributeData
[LibraryImport("shell32.dll")]
internal static partial int SHQueryUserNotificationState(out QUERY_USER_NOTIFICATION_STATE pquns);

[DllImport("shell32.dll", CharSet = CharSet.Auto)]
internal static extern IntPtr SHAppBarMessage(uint dwMessage, ref APPBARDATA pData);

#endregion
}
62 changes: 59 additions & 3 deletions FluentFlyoutWPF/Classes/Utils/MediaPlayerData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ public static class MediaPlayerData
{
private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger();

[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, uint processId);

[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
private static extern bool QueryFullProcessImageName(IntPtr hProcess, uint dwFlags, System.Text.StringBuilder lpExeName, ref int lpdwSize);

[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);

private class CachedMediaPlayerInfo
{
public required string Title { get; set; }
Expand Down Expand Up @@ -92,7 +102,9 @@ public static (string, ImageSource?) GetAndCacheMediaPlayerData(string mediaPlay
// fall back to MainWindowTitle if the description is empty
string title = !string.IsNullOrWhiteSpace(mainModule.FileVersionInfo.FileDescription)
? mainModule.FileVersionInfo.FileDescription
: p.MainWindowTitle;
: !string.IsNullOrWhiteSpace(p.MainWindowTitle)
? p.MainWindowTitle
: p.ProcessName;

return new { Title = title, Path = path, ProcessId = p.Id, IsExactMatch = isExactMatch };
}
Expand Down Expand Up @@ -256,6 +268,51 @@ private static bool IsBrowser(string processName) =>
new[] { "chrome", "msedge" }
.Contains(processName, StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Resolves the executable path of a process, including elevated/admin
/// processes that deny <see cref="Process.MainModule"/> to a non-elevated
/// caller. <c>PROCESS_QUERY_LIMITED_INFORMATION</c> plus
/// <c>QueryFullProcessImageName</c> is granted for such processes, and file
/// metadata/icon reads work on the path without opening the process.
/// Returns null only when the process is gone or fully inaccessible.
/// </summary>
public static string? TryGetProcessPath(int processId)
{
IntPtr handle = IntPtr.Zero;
try
{
handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, (uint)processId);
if (handle != IntPtr.Zero)
{
var builder = new System.Text.StringBuilder(1024);
int size = builder.Capacity;
if (QueryFullProcessImageName(handle, 0, builder, ref size) && size > 0)
return builder.ToString(0, size);
}
}
catch
{
// fall through to the MainModule attempt below
}
finally
{
if (handle != IntPtr.Zero)
{
try { CloseHandle(handle); } catch { }
}
}

try
{
using var process = Process.GetProcessById(processId);
return process.MainModule?.FileName;
}
catch
{
return null;
}
}

/// <summary>
/// Extracts the associated icon for a given process ID. Returns null if the process is inaccessible.
/// </summary>
Expand All @@ -274,8 +331,7 @@ private static bool IsBrowser(string processName) =>
}
}

var process = Process.GetProcessById(processId);
var path = process.MainModule?.FileName;
var path = TryGetProcessPath(processId);
if (path == null) return null;

// store in cache for future lookups
Expand Down
10 changes: 10 additions & 0 deletions FluentFlyoutWPF/Classes/Utils/MonitorUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ public struct MonitorInfo
public static MonitorInfo GetSelectedMonitor(int index = 0)
{
var monitors = GetMonitors();

// Math.Clamp throws when min > max, which is exactly what happens with an
// empty monitor list: turning off a secondary display or switching the
// projection mode makes EnumDisplayMonitors briefly return nothing, and
// the resulting ArgumentException propagated out of every flyout
// placement and taskbar position tick, leaving the widget stuck and the
// taskbar unresponsive (#1085).
if (monitors.Count == 0)
return default;

return monitors[Math.Clamp(index, 0, monitors.Count - 1)];
}

Expand Down
Loading