From df61a2f03e7438f74a52bd9613500ce4955e52d5 Mon Sep 17 00:00:00 2001 From: 59xa <59xa@proton.me> Date: Sun, 22 Feb 2026 16:44:18 -0600 Subject: [PATCH 01/11] Update dependencies --- DynamicWin/DynamicWin.csproj | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/DynamicWin/DynamicWin.csproj b/DynamicWin/DynamicWin.csproj index 21210b7..49f52fe 100644 --- a/DynamicWin/DynamicWin.csproj +++ b/DynamicWin/DynamicWin.csproj @@ -41,14 +41,15 @@ - + - - - + + + + From 29a9eefefc84fed1947ca9db3a1f115227d8ffb9 Mon Sep 17 00:00:00 2001 From: 59xa <59xa@proton.me> Date: Sun, 1 Mar 2026 01:05:00 -0600 Subject: [PATCH 02/11] Mark canary build --- DynamicWin/Main/App.xaml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DynamicWin/Main/App.xaml.cs b/DynamicWin/Main/App.xaml.cs index 8c6b4fd..f72dfb1 100644 --- a/DynamicWin/Main/App.xaml.cs +++ b/DynamicWin/Main/App.xaml.cs @@ -20,8 +20,8 @@ public partial class DynamicWinMain : System.Windows.Application public static MMDevice? defaultDevice; public static MMDevice? defaultMicrophone; - public static string Version => "v1.6.1r"; - public static Channel ReleaseStream => Channel.Release; + public static string Version => "v1.6.2b1"; + public static Channel ReleaseStream => Channel.Canary; public static Architecture ProcessArchitecture => RuntimeInformation.ProcessArchitecture; [STAThread] From ab1a3726a7cb7f7c2f00d11c99e056f64ab4ad87 Mon Sep 17 00:00:00 2001 From: 59xa <59xa@proton.me> Date: Sun, 1 Mar 2026 01:21:11 -0600 Subject: [PATCH 03/11] Add setting for ReduceWorkingArea --- DynamicWin/Main/Settings.cs | 6 ++++++ DynamicWin/UI/Menu/Menus/SettingsMenu.cs | 22 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/DynamicWin/Main/Settings.cs b/DynamicWin/Main/Settings.cs index e9a1f5f..3e18d95 100644 --- a/DynamicWin/Main/Settings.cs +++ b/DynamicWin/Main/Settings.cs @@ -28,6 +28,7 @@ public class Settings private static int activeScreenIndex; private static int releaseStream; private static bool allowAutomaticUpdates = true; + private static bool reduceWorkingArea; public static IslandObject.IslandMode IslandMode { get => islandMode; set => islandMode = value; } public static bool AllowBlur { get => allowBlur; set => allowBlur = value; } @@ -42,6 +43,7 @@ public class Settings public static int ScreenIndex { get => activeScreenIndex; set => activeScreenIndex = value; } public static int ReleaseStream { get => releaseStream; set => releaseStream = value; } public static bool AllowAutomaticUpdates { get => allowAutomaticUpdates; set => allowAutomaticUpdates = value; } + public static bool ReduceWorkingArea { get => reduceWorkingArea; set => reduceWorkingArea = value; } public static List smallWidgetsLeft; public static List smallWidgetsRight; @@ -77,6 +79,8 @@ public static void InitializeSettings() AllowAutomaticUpdates = SaveManager.Contains("settings.AllowAutomaticUpdates") ? (bool)SaveManager.Get("settings.AllowAutomaticUpdates") : true; + ReduceWorkingArea = SaveManager.Contains("settings.ReduceWorkingArea") ? (bool)SaveManager.Get("settings.ReduceWorkingArea") : true; + Theme = (int)((Int64)SaveManager.Get("settings.theme")); ScreenIndex = (int)((Int64)SaveManager.Get("settings.screenindex")); ReleaseStream = SaveManager.Contains("settings.ReleaseStream") ? (int)((Int64)SaveManager.Get("settings.ReleaseStream")) : 0; @@ -137,6 +141,7 @@ public static void InitializeSettings() // default automatic updates enabled AllowAutomaticUpdates = true; + ReduceWorkingArea = true; Theme = 0; @@ -186,6 +191,7 @@ public static void Save() SaveManager.Add("settings.ReleaseStream", ReleaseStream); SaveManager.Add("settings.AllowAutomaticUpdates", AllowAutomaticUpdates); + SaveManager.Add("settings.ReduceWorkingArea", ReduceWorkingArea); SaveManager.Add("settings.theme", Theme); SaveManager.Add("settings.screenindex", ScreenIndex); diff --git a/DynamicWin/UI/Menu/Menus/SettingsMenu.cs b/DynamicWin/UI/Menu/Menus/SettingsMenu.cs index 5f5c297..0434f17 100644 --- a/DynamicWin/UI/Menu/Menus/SettingsMenu.cs +++ b/DynamicWin/UI/Menu/Menus/SettingsMenu.cs @@ -52,6 +52,7 @@ void SaveAndBack() Settings.ToggleHomeMenuShadow = toggleHomeMenuShadow.IsChecked; Settings.RunOnStartup = runOnStartup.IsChecked; Settings.AllowAutomaticUpdates = allowAutomaticUpdates.IsChecked; + Settings.ReduceWorkingArea = reduceWorkingArea.IsChecked; // Save the selected default big menu mode if (bigMenuModeSelector != null) @@ -88,12 +89,13 @@ void SaveAndBack() DWCheckbox antiAliasing; DWCheckbox runOnStartup; DWCheckbox allowAutomaticUpdates; + DWCheckbox reduceWorkingArea; DWCheckbox toggleIslandShadow; DWCheckbox toggleHomeMenuShadow; DWCheckbox toggleHighRefreshRate; DWCheckbox limitRefreshRateWhenIdle; - DWText refreshRateDisclaimer1, refreshRateDisclaimer2, limitRefreshRateDisclaimer1, limitRefreshRateDisclaimer2; + DWText refreshRateDisclaimer1, refreshRateDisclaimer2, limitRefreshRateDisclaimer1, limitRefreshRateDisclaimer2, workingAreaDisclaimer1, workingAreaDisclaimer2; UIObject bottomMask; @@ -132,6 +134,24 @@ public override List InitializeMenu(IslandObject island) objects.Add(islandMode); } + workingAreaDisclaimer1 = new DWText(island, "Renders the interface to its minimum height and width possible, also improves performance.", new Vec2(25, 0), UIAlignment.TopLeft); + workingAreaDisclaimer1.Font = Res.SFProRegular; + workingAreaDisclaimer1.TextSize = 12; + workingAreaDisclaimer1.Anchor.X = 0; + + workingAreaDisclaimer2 = new DWText(island, "Enabling this setting may prevent the interface from being placed correctly at the top.", new Vec2(25, 0), UIAlignment.TopLeft); + workingAreaDisclaimer2.Font = Res.SFProRegular; + workingAreaDisclaimer2.TextSize = 12; + workingAreaDisclaimer2.Anchor.X = 0; + + objects.Add(workingAreaDisclaimer1); + objects.Add(workingAreaDisclaimer2); + + reduceWorkingArea = new DWCheckbox(island, "Reduce working area", new Vec2(25, 0), new Vec2(25, 25), () => { }, UIAlignment.TopLeft); + reduceWorkingArea.IsChecked = Settings.ReduceWorkingArea; + reduceWorkingArea.Anchor.X = 0; + objects.Add(reduceWorkingArea); + allowBlur = new DWCheckbox(island, "Toggle blur", new Vec2(25, 0), new Vec2(25, 25), () => { }, UIAlignment.TopLeft); allowBlur.IsChecked = Settings.AllowBlur; allowBlur.Anchor.X = 0; From 285c7b324023cd59979b6be8a22666e0c2e38739 Mon Sep 17 00:00:00 2001 From: 59xa <59xa@proton.me> Date: Sun, 1 Mar 2026 01:51:20 -0600 Subject: [PATCH 04/11] Implement working area reduction logic --- DynamicWin/Main/Settings.cs | 31 +++++++++++++++++++++++- DynamicWin/Utils/WindowPositionHelper.cs | 22 +++++++++++++---- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/DynamicWin/Main/Settings.cs b/DynamicWin/Main/Settings.cs index 3e18d95..73db682 100644 --- a/DynamicWin/Main/Settings.cs +++ b/DynamicWin/Main/Settings.cs @@ -43,7 +43,36 @@ public class Settings public static int ScreenIndex { get => activeScreenIndex; set => activeScreenIndex = value; } public static int ReleaseStream { get => releaseStream; set => releaseStream = value; } public static bool AllowAutomaticUpdates { get => allowAutomaticUpdates; set => allowAutomaticUpdates = value; } - public static bool ReduceWorkingArea { get => reduceWorkingArea; set => reduceWorkingArea = value; } + public static bool ReduceWorkingArea + { + get => reduceWorkingArea; + set + { + reduceWorkingArea = value; + + try + { + if (Application.Current != null) + { + Application.Current.Dispatcher.BeginInvoke(new Action(() => + { + try + { + if (MainForm.Instance != null) + WindowPositionHelper.CenterWindowOnMonitor(MainForm.Instance, ScreenIndex); + } + catch { } + })); + } + else + { + if (MainForm.Instance != null) + WindowPositionHelper.CenterWindowOnMonitor(MainForm.Instance, ScreenIndex); + } + } + catch { } + } + } public static List smallWidgetsLeft; public static List smallWidgetsRight; diff --git a/DynamicWin/Utils/WindowPositionHelper.cs b/DynamicWin/Utils/WindowPositionHelper.cs index 0ea8235..f05819c 100644 --- a/DynamicWin/Utils/WindowPositionHelper.cs +++ b/DynamicWin/Utils/WindowPositionHelper.cs @@ -1,3 +1,4 @@ +using DynamicWin.Main; using System; using System.Windows; using System.Windows.Forms; @@ -15,6 +16,8 @@ public static void CenterWindowOnMonitor(Window window, int monitorIndex) var screen = screens[clampedIndex]; var bounds = screen.Bounds; + double windowWidth = window is { ActualWidth: > 0 } ? window.ActualWidth : window.Width; + // Get DPI scaling for the target monitor double dpiX = 96.0, dpiY = 96.0; var source = PresentationSource.FromVisual(window); @@ -35,12 +38,21 @@ public static void CenterWindowOnMonitor(Window window, int monitorIndex) double scaleX = dpiX / 96.0; double scaleY = dpiY / 96.0; - // Aggressively place window at the very top and full width of the physical screen (ignoring taskbar) var screenBounds = screen.Bounds; - window.Left = screenBounds.Left / scaleX; - window.Top = screenBounds.Top / scaleY; - window.Width = screenBounds.Width / scaleX; - window.Height = screenBounds.Height / scaleY; + double targetLeft = (bounds.Left + (bounds.Width - windowWidth * scaleX) / 2.0) / scaleX; + double targetTop = screenBounds.Top / scaleY; + + const double epsilon = 1.0; + + if (double.IsNaN(window.Left) || Math.Abs(window.Left - targetLeft) > epsilon) + window.Left = targetLeft; + + if (double.IsNaN(window.Top) || Math.Abs(window.Top - targetTop) > epsilon) + window.Top = targetTop; + + double desiredHeight = !Settings.ReduceWorkingArea ? screenBounds.Height / scaleY : 500.0; + if (double.IsNaN(window.Height) || Math.Abs(window.Height - desiredHeight) > epsilon) + window.Height = desiredHeight; } } } From 5fd28d0a3415b959f509fe6c51b0c29c80b0ba5f Mon Sep 17 00:00:00 2001 From: 59xa <59xa@proton.me> Date: Sun, 1 Mar 2026 13:18:31 -0600 Subject: [PATCH 05/11] Implement logic for toggling media display when idle --- .../UI/Widgets/Small/MediaThumbnailWidget.cs | 58 +++++++- .../UI/Widgets/Small/SmallVisualiserWidget.cs | 93 +++++++++++- DynamicWin/Utils/MediaThumbnailService.cs | 140 +++++++++++++++++- 3 files changed, 281 insertions(+), 10 deletions(-) diff --git a/DynamicWin/UI/Widgets/Small/MediaThumbnailWidget.cs b/DynamicWin/UI/Widgets/Small/MediaThumbnailWidget.cs index afdd02e..c567929 100644 --- a/DynamicWin/UI/Widgets/Small/MediaThumbnailWidget.cs +++ b/DynamicWin/UI/Widgets/Small/MediaThumbnailWidget.cs @@ -1,4 +1,4 @@ -using DynamicWin.Utils; +using DynamicWin.Utils; using DynamicWin.UI.UIElements; using SkiaSharp; using System; @@ -52,6 +52,9 @@ public class MediaThumbnailWidget : SmallWidgetBase public MediaThumbnailWidget(UIObject? parent, Vec2 position, UIAlignment alignment = UIAlignment.TopCenter) : base(parent, position, alignment) { + // Ensure shared setting loaded + try { RegisterSmallVisualiserWidgetSettings.SharedMediaSettings.Load(); } catch { } + MediaThumbnailService.Instance.ThumbnailChanged += OnThumbnailChanged; // Try to initialise from service canonical bitmap (fast path) @@ -70,7 +73,23 @@ public MediaThumbnailWidget(UIObject? parent, Vec2 position, UIAlignment alignme thumbnailBitmap = bmp; try { currentBitmapFingerprint = BitmapUtils.GetBitmapFingerprint(bmp); } catch { currentBitmapFingerprint = null; } hasMedia = true; - collapseProgress = 1f; + + // Respect shared hide-when-idle setting: if enabled and media has been paused for longer than 30 seconds, start collapsed + bool hideWhenIdle = false; + try { hideWhenIdle = RegisterSmallVisualiserWidgetSettings.SharedMediaSettings.HideMediaWhenIdle; } catch { hideWhenIdle = false; } + if (hideWhenIdle) + { + try + { + bool pausedLong = MediaThumbnailService.Instance.IsPausedLongerThan(TimeSpan.FromSeconds(30)); + collapseProgress = pausedLong ? 0f : 1f; + } + catch { collapseProgress = 1f; } + } + else + { + collapseProgress = 1f; + } } } catch { } @@ -88,10 +107,41 @@ private void OnThumbnailChanged(object? sender, MediaChangedEventArgs e) // Adopt metadata immediately and ensure widget expanded when media exists lock (mediaLock) { + // If shared setting requests hiding media when idle, only expand when playback active + bool hideWhenIdle = false; + try { hideWhenIdle = RegisterSmallVisualiserWidgetSettings.SharedMediaSettings.HideMediaWhenIdle; } catch { hideWhenIdle = false; } + if (e.Media != null) { hasMedia = true; - BeginInvokeUI(() => StartCollapseOrExpand(true)); + + // If hideWhenIdle is enabled, determine expand state based on playback status and pause duration + if (hideWhenIdle) + { + try + { + var status = DynamicWin.Utils.MediaThumbnailService.Instance?.LastPlaybackStatus; + bool playing = status.HasValue && status.Value == Windows.Media.Control.GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing; + + // If not playing, check if paused longer than 30 seconds + bool shouldExpand = playing; + if (!playing) + { + bool pausedLong = MediaThumbnailService.Instance.IsPausedLongerThan(TimeSpan.FromSeconds(30)); + shouldExpand = !pausedLong; + } + + BeginInvokeUI(() => StartCollapseOrExpand(shouldExpand)); + } + catch + { + BeginInvokeUI(() => StartCollapseOrExpand(true)); + } + } + else + { + BeginInvokeUI(() => StartCollapseOrExpand(true)); + } } else { @@ -250,7 +300,7 @@ public override void Update(float deltaTime) bool isPaused = false; try { - var status = DynamicWin.Utils.MediaThumbnailService.Instance?.LastPlaybackStatus; + var status = MediaThumbnailService.Instance?.LastPlaybackStatus; if (status.HasValue) { isPaused = status.Value != Windows.Media.Control.GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing; diff --git a/DynamicWin/UI/Widgets/Small/SmallVisualiserWidget.cs b/DynamicWin/UI/Widgets/Small/SmallVisualiserWidget.cs index 4005543..5b996f3 100644 --- a/DynamicWin/UI/Widgets/Small/SmallVisualiserWidget.cs +++ b/DynamicWin/UI/Widgets/Small/SmallVisualiserWidget.cs @@ -39,6 +39,34 @@ class RegisterSmallVisualiserWidgetSettings : IRegisterableSetting public static SmallVisualiserSave saveData; + // Shared setting between visualiser and media thumbnail widgets + public static class SharedMediaSettings + { + public const string SettingKey = "Setting.HideMediaWhenIdle"; + public static bool HideMediaWhenIdle = false; + + public static void Load() + { + try + { + if (SaveManager.Contains(SettingKey)) + { + HideMediaWhenIdle = (bool)SaveManager.Get(SettingKey); + } + else + { + HideMediaWhenIdle = false; + } + } + catch { HideMediaWhenIdle = false; } + } + + public static void Save() + { + try { SaveManager.Add(SettingKey, HideMediaWhenIdle); } catch { } + } + } + public struct SmallVisualiserSave { public bool displayDotWhenIdle; @@ -47,7 +75,7 @@ public struct SmallVisualiserSave } /// - /// Loads the visualizer settings from persistent storage or initializes them with default values if no saved + /// Loads the visualiser settings from persistent storage or initialises them with default values if no saved /// settings are found. /// /// This method attempts to retrieve previously saved settings using the current setting @@ -68,6 +96,9 @@ public void LoadSettings() useThumbnailBackground = true }; } + + // Load shared setting + SharedMediaSettings.Load(); } /// @@ -79,6 +110,9 @@ public void LoadSettings() public void SaveSettings() { SaveManager.Add(SettingID, JsonConvert.SerializeObject(saveData)); + + // Persist shared setting + SharedMediaSettings.Save(); } /// @@ -96,7 +130,9 @@ public List SettingsObjects() var displayDotWhenIdle = new DWCheckbox(null, "Display visualiser dots when idle", new Vec2(25, 0), new Vec2(25, 25), null, UIAlignment.TopLeft); var enableColourTransition = new DWCheckbox(null, "Enable visualiser colour transitioning", new Vec2(25, 0), new Vec2(25, 25), null, UIAlignment.TopLeft); var useThumbnailBackground = new DWCheckbox(null, "Use media thumbnail as background", new Vec2(25, 0), new Vec2(25, 25), null, UIAlignment.TopLeft); - var thumbnailDisclaimer = new DWText(null, "By enabling this option, colour transitioning will be bypassed.", new Vec2(25, 0), UIAlignment.TopLeft); + var thumbnailDisclaimer = new DWText(null, "Colour transition and media thumbnail options are mutually exclusive.", new Vec2(25, 0), UIAlignment.TopLeft); + var hideMediaWhenIdle = new DWCheckbox(null, "Hide media thumbnail when idle (shared)", new Vec2(25, 0), new Vec2(25, 25), null, UIAlignment.TopLeft); + var hideMediaDisclaimer = new DWText(null, "Hides media thumbnail and visualiser after 30 seconds when paused.", new Vec2(25, 0), UIAlignment.TopLeft); displayDotWhenIdle.clickCallback += () => { @@ -106,26 +142,51 @@ public List SettingsObjects() enableColourTransition.clickCallback += () => { saveData.enableColourTransition = enableColourTransition.IsChecked; + + if (enableColourTransition.IsChecked) + { + // If enabling colour transition, disable thumbnail background + saveData.useThumbnailBackground = false; + useThumbnailBackground.IsChecked = false; + } }; useThumbnailBackground.clickCallback += () => { saveData.useThumbnailBackground = useThumbnailBackground.IsChecked; + + if (useThumbnailBackground.IsChecked) + { + // If enabling thumbnail background, disable colour transition + saveData.enableColourTransition = false; + enableColourTransition.IsChecked = false; + } + }; + + hideMediaWhenIdle.clickCallback += () => + { + SharedMediaSettings.HideMediaWhenIdle = hideMediaWhenIdle.IsChecked; + // Save immediately so other widget instances can read it + SharedMediaSettings.Save(); }; displayDotWhenIdle.IsChecked = saveData.displayDotWhenIdle; enableColourTransition.IsChecked = saveData.enableColourTransition; useThumbnailBackground.IsChecked = saveData.useThumbnailBackground; + hideMediaWhenIdle.IsChecked = SharedMediaSettings.HideMediaWhenIdle; displayDotWhenIdle.Anchor.X = 0; enableColourTransition.Anchor.X = 0; thumbnailDisclaimer.Anchor.X = 0; useThumbnailBackground.Anchor.X = 0; + hideMediaWhenIdle.Anchor.X = 0; objects.Add(displayDotWhenIdle); - objects.Add(enableColourTransition); objects.Add(thumbnailDisclaimer); + objects.Add(enableColourTransition); objects.Add(useThumbnailBackground); + objects.Add(hideMediaDisclaimer); + objects.Add(hideMediaWhenIdle); return objects; } @@ -185,11 +246,33 @@ private async void OnServiceThumbnailChanged(object? sender, MediaChangedEventAr try { var tl = await MediaInfo.FetchCurrentTimelineAsync().ConfigureAwait(false); - bool shouldExpand = tl != null && tl.PlaybackStatus != GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed; hasMedia = true; - targetExpanded = shouldExpand; + // Evaluate shared setting: if HideMediaWhenIdle is enabled and media has been paused for >= 1 minute, collapse + bool hideWhenIdle = RegisterSmallVisualiserWidgetSettings.SharedMediaSettings.HideMediaWhenIdle; + bool shouldExpand; + + if (hideWhenIdle) + { + // If timeline is present and playing, expand; otherwise if paused longer than threshold, collapse + if (tl != null && tl.PlaybackStatus == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing) + { + shouldExpand = true; + } + else + { + // If paused longer than threshold -> collapse + bool pausedLong = MediaThumbnailService.Instance.IsPausedLongerThan(TimeSpan.FromSeconds(30)); + shouldExpand = !pausedLong; + } + } + else + { + shouldExpand = tl != null && tl.PlaybackStatus != GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed; + } + + targetExpanded = shouldExpand; BeginInvokeUI(() => StartCollapseOrExpand(shouldExpand)); } catch diff --git a/DynamicWin/Utils/MediaThumbnailService.cs b/DynamicWin/Utils/MediaThumbnailService.cs index 5ccc70e..5d42efa 100644 --- a/DynamicWin/Utils/MediaThumbnailService.cs +++ b/DynamicWin/Utils/MediaThumbnailService.cs @@ -115,6 +115,25 @@ public event EventHandler? ThumbnailChanged // Add playback status tracking for widgets private GlobalSystemMediaTransportControlsSessionPlaybackStatus? _lastPlaybackStatus = null; public GlobalSystemMediaTransportControlsSessionPlaybackStatus? LastPlaybackStatus => _lastPlaybackStatus; + // Track when playback transitioned from playing to a non-playing state + private DateTime? lastPlaybackNotPlayingAt = null; + // Whether we've observed playback status at least once since service started + private bool playbackStateInitialized = false; + // Track if we've already notified about being paused for longer than 30 seconds + private bool hasNotifiedPausedLongThreshold = false; + + /// + /// Returns true if media has been in a non-playing state for at least the provided duration. + /// + public bool IsPausedLongerThan(TimeSpan duration) + { + if (lastPlaybackNotPlayingAt == null) return false; + try + { + return (DateTime.UtcNow - lastPlaybackNotPlayingAt.Value) >= duration; + } + catch { return false; } + } private MediaThumbnailService() { } @@ -230,7 +249,54 @@ private async Task FetchAndUpdateAsync(bool forceRefreshThumbnail = false) playbackStatus = timeline.PlaybackStatus; } catch { } - _lastPlaybackStatus = playbackStatus; + // Update last playback status and record when it became non-playing + var previousPlaybackStatus = _lastPlaybackStatus; + try + { + _lastPlaybackStatus = playbackStatus; + + // If this is the first observed playback state since service start, treat a non-playing + // state as having been not-playing for longer than the idle threshold so widgets that + // should hide on startup will collapse immediately. Subsequent non-playing transitions + // use the normal timestamping behaviour (mark time when transition from playing occurs). + if (!playbackStateInitialized) + { + playbackStateInitialized = true; + if (playbackStatus.HasValue && playbackStatus.Value == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing) + { + lastPlaybackNotPlayingAt = null; + } + else + { + // Mark as having been not-playing for a while so startup widgets can hide immediately + lastPlaybackNotPlayingAt = DateTime.UtcNow.AddSeconds(-31); + } + } + else + { + if (playbackStatus.HasValue && playbackStatus.Value == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing) + { + lastPlaybackNotPlayingAt = null; + hasNotifiedPausedLongThreshold = false; + } + else + { + if (previousPlaybackStatus.HasValue && previousPlaybackStatus.Value == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing) + { + // Just transitioned from playing to not-playing + lastPlaybackNotPlayingAt = DateTime.UtcNow; + hasNotifiedPausedLongThreshold = false; + } + else if (!previousPlaybackStatus.HasValue && playbackStatus.HasValue && playbackStatus.Value != GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing) + { + // Started already not-playing (fallback) + lastPlaybackNotPlayingAt = DateTime.UtcNow; + hasNotifiedPausedLongThreshold = false; + } + } + } + } + catch { _lastPlaybackStatus = playbackStatus; } // If there is no media, clear all cached metadata and thumbnail, and notify subscribers immediately if (media == null) @@ -278,6 +344,51 @@ private async Task FetchAndUpdateAsync(bool forceRefreshThumbnail = false) if (!metadataChanged && lastBytes != null && !forceRefreshThumbnail) { + // Before returning, check if playback status change or pause threshold crossing must be notified + try + { + if (playbackStatus != null && lastMedia != null) + { + bool statusChanged = !Equals(previousPlaybackStatus, playbackStatus); + bool isPausedLong = IsPausedLongerThan(TimeSpan.FromSeconds(30)); + bool shouldNotify = false; + + // Check if playback status changed + if (statusChanged) + { + shouldNotify = true; + } + // Check if 30-second pause threshold was crossed + else if (isPausedLong && !hasNotifiedPausedLongThreshold) + { + // Just crossed into "paused long" territory + hasNotifiedPausedLongThreshold = true; + shouldNotify = true; + } + else if (!isPausedLong && hasNotifiedPausedLongThreshold) + { + // Transitioned back to "not paused long" (resumed playing) + hasNotifiedPausedLongThreshold = false; + shouldNotify = true; + } + + if (shouldNotify) + { + _thumbnailChanged?.Invoke(this, new MediaChangedEventArgs( + new Media { Title = lastMedia.Title, Artist = lastMedia.Artist }, + lastBytes + )); + + List> snap3; + lock (listLock) { snap3 = new List>(legacyListeners); } + foreach (var l in snap3) + { + try { l(lastMedia); } catch { } + } + } + } + } + catch { } return; } @@ -328,6 +439,33 @@ private async Task FetchAndUpdateAsync(bool forceRefreshThumbnail = false) ThumbnailData = lastBytes }; + // If playback status changed during this fetch, notify subscribers so widgets can re-evaluate + try + { + if (playbackStatus != null && lastMedia != null) + { + // Check if playback status changed + bool statusChanged = !Equals(previousPlaybackStatus, playbackStatus); + + // If playback status changed, notify subscribers + if (statusChanged) + { + _thumbnailChanged?.Invoke(this, new MediaChangedEventArgs( + new Media { Title = lastMedia.Title, Artist = lastMedia.Artist }, + lastBytes + )); + + List> snap2; + lock (listLock) { snap2 = new List>(legacyListeners); } + foreach (var l in snap2) + { + try { l(lastMedia); } catch { } + } + } + } + } + catch { } + bool bytesChanged = false; if (newFingerprint.HasValue || prevFingerprint.HasValue) { From 7314018c7c9b71d9aa605c7b447a8899376de654 Mon Sep 17 00:00:00 2001 From: 59xa <59xa@proton.me> Date: Sun, 1 Mar 2026 13:35:28 -0600 Subject: [PATCH 06/11] Increase thumbnail scale to 0.8f when paused --- DynamicWin/UI/UIElements/Custom/MediaPlayer.cs | 4 ++-- DynamicWin/UI/Widgets/Small/MediaThumbnailWidget.cs | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/DynamicWin/UI/UIElements/Custom/MediaPlayer.cs b/DynamicWin/UI/UIElements/Custom/MediaPlayer.cs index 2d0aeb8..0a80d74 100644 --- a/DynamicWin/UI/UIElements/Custom/MediaPlayer.cs +++ b/DynamicWin/UI/UIElements/Custom/MediaPlayer.cs @@ -1360,7 +1360,7 @@ public override void Draw(SKCanvas canvas) int save = canvas.Save(); // Shrink and dim thumbnail if paused, animated - float thumbScale = 0.6f + 0.4f * thumbnailAnim; // 0.6 (paused) to 1.0 (playing) + float thumbScale = 0.8f + 0.2f * thumbnailAnim; // 0.8 (paused) to 1.0 (playing) float dimAlpha = (1f - thumbnailAnim) * 120f; // 0 (playing) to 120 (paused) float centerX = thumbRect.MidX; float centerY = thumbRect.MidY; @@ -1547,7 +1547,7 @@ public override void Draw(SKCanvas canvas) paint.IsAntialias = Settings.AntiAliasing; paint.Color = GetColor(timelineTextColor).Value(); paint.TextSize = timelineTextSize; - paint.Typeface = Resources.Res.SFProRegular; + paint.Typeface = Res.SFProRegular; float timelineTextY = barY + timelineHeight + timelineTextSize - 10f; float leftX = barX - timelineSidePadding + 4f; diff --git a/DynamicWin/UI/Widgets/Small/MediaThumbnailWidget.cs b/DynamicWin/UI/Widgets/Small/MediaThumbnailWidget.cs index c567929..ed43dd7 100644 --- a/DynamicWin/UI/Widgets/Small/MediaThumbnailWidget.cs +++ b/DynamicWin/UI/Widgets/Small/MediaThumbnailWidget.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Diagnostics; using DynamicWin.Resources; +using DynamicWin.Main; namespace DynamicWin.UI.Widgets.Small { @@ -368,7 +369,7 @@ public override void Draw(SKCanvas canvas) float flipScale = animator.GetFlipScale(); bool doFlip = animator.IsFlipping; - float thumbScale = 0.6f + 0.4f * thumbnailAnim; + float thumbScale = 0.8f + 0.2f * thumbnailAnim; float dimAlpha = (1f - thumbnailAnim) * 120f; float centerX = thumbRect.MidX; float centerY = thumbRect.MidY; @@ -383,9 +384,9 @@ public override void Draw(SKCanvas canvas) var localRect = SKRect.Create(-thumbRect.Width / 2f, -thumbRect.Height / 2f, thumbRect.Width, thumbRect.Height); var localPath = BuildSuperellipsePath(localRect, 7f, 1f); canvas.Save(); - canvas.ClipPath(localPath, antialias: true); + canvas.ClipPath(localPath, antialias: Settings.AntiAliasing); var paint = GetPaint(); - paint.IsAntialias = true; + paint.IsAntialias = Settings.AntiAliasing; paint.ImageFilter = animator.BlurAmount > 0f ? SKImageFilter.CreateBlur(animator.BlurAmount, animator.BlurAmount) : null; if (bmp != null) { @@ -411,9 +412,9 @@ public override void Draw(SKCanvas canvas) canvas.Translate(centerX, centerY); canvas.Scale(thumbScale, thumbScale); canvas.Translate(-centerX, -centerY); - canvas.ClipPath(path, antialias: true); + canvas.ClipPath(path, antialias: Settings.AntiAliasing); var paint = GetPaint(); - paint.IsAntialias = true; + paint.IsAntialias = Settings.AntiAliasing; paint.ImageFilter = animator.BlurAmount > 0f ? SKImageFilter.CreateBlur(animator.BlurAmount, animator.BlurAmount) : null; if (bmp != null) { From 5a2bffbeca19b15d8c0d7ee95080039c50ea4aef Mon Sep 17 00:00:00 2001 From: 59xa <59xa@proton.me> Date: Sun, 1 Mar 2026 14:33:12 -0600 Subject: [PATCH 07/11] Downgrade SkiaSharp dependency as it caused large file sizes --- DynamicWin/DynamicWin.csproj | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/DynamicWin/DynamicWin.csproj b/DynamicWin/DynamicWin.csproj index 49f52fe..546109e 100644 --- a/DynamicWin/DynamicWin.csproj +++ b/DynamicWin/DynamicWin.csproj @@ -46,9 +46,8 @@ - - - + + From 68a2be337309d75425c0ed714c45ee8fb481dc29 Mon Sep 17 00:00:00 2001 From: 59xa <59xa@proton.me> Date: Tue, 14 Apr 2026 16:22:10 -0600 Subject: [PATCH 08/11] Update visualiser to react closer to native iOS --- DynamicWin/Utils/AudioVisualiser.cs | 58 +++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/DynamicWin/Utils/AudioVisualiser.cs b/DynamicWin/Utils/AudioVisualiser.cs index a20a01f..af217d8 100644 --- a/DynamicWin/Utils/AudioVisualiser.cs +++ b/DynamicWin/Utils/AudioVisualiser.cs @@ -39,7 +39,7 @@ public class AudioVisualiser : UIObject private float[] targetHeights; // Re-use per-frame to avoid allocations - private float[] bandBalance = new float[] { 1f, 0.75f, 1.15f, 1.10f, 1.25f, 1.35f }; + private float[] bandBalance = new float[] { 1f, 1f, 1.15f, 1.10f, 1.25f, 1.35f }; private WasapiLoopbackCapture capture; private readonly object fftLock = new object(); @@ -96,7 +96,7 @@ public class AudioVisualiser : UIObject private bool enableDotWhenLow = true; public bool EnableDotWhenLow { get => enableDotWhenLow; set => enableDotWhenLow = value; } public float BlurAmount { get; set; } = 0f; - public float BarSpacing { get; set; } = 1f; + public float BarSpacing { get; set; } = 1.5f; // Initialise class public AudioVisualiser(UIObject? parent, Vec2 position, Vec2 size, UIAlignment alignment = UIAlignment.TopRight, Col Primary = null, Col Secondary = null) : base(parent, position, size, alignment) @@ -189,7 +189,7 @@ private void InitBarBinMapping(float sampleRate) } double minFreq = 20.0; // 20Hz - human hearing start - double maxFreq = 12000.0; // 12kHz - frequency end + double maxFreq = 6000.0; // 12kHz - frequency end // Use Logarithmic scale for natural frequency distribution double logMin = Math.Log10(minFreq); @@ -329,7 +329,7 @@ public override void Update(float deltaTime) // Bar 0: Sub (1.2x) -> Strongest // Bar 1: Bass (0.8x) -> Dipped to let bar 0 lead // Bar 5: Highs (2.5x) -> Extreme boost for hi-hat visibility - float[] barWeights = { 1.0f, 0.85f, 1.15f, 1.5f, 2.0f, 2.5f }; + float[] barWeights = { 1.0f, 0.9f, 1.15f, 1.5f, 2.0f, 2.5f }; for (int i = 0; i < barCount; i++) { @@ -577,22 +577,48 @@ public override void Draw(SKCanvas canvas) float rawHeight = barHeight[i] * visualBoost; float dynamicHeight = rawHeight * height * 0.8f; - float bH = dynamicHeight; + float activity = Math.Clamp(barHeight[i], 0f, 1f); - // Handle dot clamping - if (EnableDotWhenLow) - { - // The dot should be a perfect circle/square, so its height equals its width - // Clamp the height so it never gets smaller than the dot - bH = Math.Max(dotHeight, dynamicHeight); - } + float center = (barCount - 1) / 2f; + float distFromCenter = MathF.Abs(i - center) / center; + float centerBoost = 1f - distFromCenter; + + float midEmphasis = 1f - MathF.Abs(activity - 0.5f) * 2f; + midEmphasis = MathF.Pow(midEmphasis, 1.5f); + + float minScale = 0.7f; + float baseScale = activity; + + float breathStrength = 0.15f + (0.15f * centerBoost); + + float time = (float)Stopwatch.GetTimestamp() / Stopwatch.Frequency; + + float phaseOffset = i * 0.6f; + + float lag = distFromCenter * 0.08f; + float delayedActivity = Math.Clamp(activity - lag, 0f, 1f); + + float scale = minScale + (1f - minScale) * delayedActivity; + scale += midEmphasis * breathStrength; + + float pulse = MathF.Sin(time * 4f + phaseOffset) * (0.03f + 0.02f * centerBoost) * midEmphasis; + scale += pulse; + + scale = Math.Clamp(scale, minScale, 1.1f); + + float scaledWidth = barWidth2 * scale; + float scaledDotHeight = dotHeight * scale; + + float bH = EnableDotWhenLow + ? Math.Max(scaledDotHeight, dynamicHeight) + : dynamicHeight; - // Positioning - float x = Position.X + i * (barWidth2 + spacing2); + float xBase = Position.X + i * (barWidth2 + spacing2); + float x = xBase + (barWidth2 - scaledWidth) / 2f; float barTopY = centerY - bH / 2; - var rect = SKRect.Create(x, barTopY, barWidth2, bH); - var roundRect = new SKRoundRect(rect, barWidth2 / 2, barWidth2 / 2); + var rect = SKRect.Create(x, barTopY, scaledWidth, bH); + var roundRect = new SKRoundRect(rect, scaledWidth / 2, scaledWidth / 2); // Color logic: dot must stay at the "Secondary" color until it starts growing // We calculate a 'colorActivity' based on how much it has grown past the dot From a59d5e364d3ac0155fec15c8526abc43e7fde392 Mon Sep 17 00:00:00 2001 From: 59xa <59xa@proton.me> Date: Tue, 14 Apr 2026 16:24:29 -0600 Subject: [PATCH 09/11] Bump --- DynamicWin/Main/App.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DynamicWin/Main/App.xaml.cs b/DynamicWin/Main/App.xaml.cs index f72dfb1..25c46fd 100644 --- a/DynamicWin/Main/App.xaml.cs +++ b/DynamicWin/Main/App.xaml.cs @@ -20,7 +20,7 @@ public partial class DynamicWinMain : System.Windows.Application public static MMDevice? defaultDevice; public static MMDevice? defaultMicrophone; - public static string Version => "v1.6.2b1"; + public static string Version => "v1.6.2b2"; public static Channel ReleaseStream => Channel.Canary; public static Architecture ProcessArchitecture => RuntimeInformation.ProcessArchitecture; From c4a035fbad285fafee962341b60422cf64d284f7 Mon Sep 17 00:00:00 2001 From: 59xa <59xa@proton.me> Date: Tue, 14 Apr 2026 17:31:07 -0600 Subject: [PATCH 10/11] Change misleading text from "Reduce working area" to "Keep interface always topmost" --- DynamicWin/Main/Settings.cs | 8 ++++---- DynamicWin/UI/Menu/Menus/SettingsMenu.cs | 13 +++++++------ DynamicWin/Utils/WindowPositionHelper.cs | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/DynamicWin/Main/Settings.cs b/DynamicWin/Main/Settings.cs index 73db682..cc0d231 100644 --- a/DynamicWin/Main/Settings.cs +++ b/DynamicWin/Main/Settings.cs @@ -43,7 +43,7 @@ public class Settings public static int ScreenIndex { get => activeScreenIndex; set => activeScreenIndex = value; } public static int ReleaseStream { get => releaseStream; set => releaseStream = value; } public static bool AllowAutomaticUpdates { get => allowAutomaticUpdates; set => allowAutomaticUpdates = value; } - public static bool ReduceWorkingArea + public static bool AlwaysTopmost { get => reduceWorkingArea; set @@ -108,7 +108,7 @@ public static void InitializeSettings() AllowAutomaticUpdates = SaveManager.Contains("settings.AllowAutomaticUpdates") ? (bool)SaveManager.Get("settings.AllowAutomaticUpdates") : true; - ReduceWorkingArea = SaveManager.Contains("settings.ReduceWorkingArea") ? (bool)SaveManager.Get("settings.ReduceWorkingArea") : true; + AlwaysTopmost = SaveManager.Contains("settings.ReduceWorkingArea") ? (bool)SaveManager.Get("settings.ReduceWorkingArea") : true; Theme = (int)((Int64)SaveManager.Get("settings.theme")); ScreenIndex = (int)((Int64)SaveManager.Get("settings.screenindex")); @@ -170,7 +170,7 @@ public static void InitializeSettings() // default automatic updates enabled AllowAutomaticUpdates = true; - ReduceWorkingArea = true; + AlwaysTopmost = true; Theme = 0; @@ -220,7 +220,7 @@ public static void Save() SaveManager.Add("settings.ReleaseStream", ReleaseStream); SaveManager.Add("settings.AllowAutomaticUpdates", AllowAutomaticUpdates); - SaveManager.Add("settings.ReduceWorkingArea", ReduceWorkingArea); + SaveManager.Add("settings.ReduceWorkingArea", AlwaysTopmost); SaveManager.Add("settings.theme", Theme); SaveManager.Add("settings.screenindex", ScreenIndex); diff --git a/DynamicWin/UI/Menu/Menus/SettingsMenu.cs b/DynamicWin/UI/Menu/Menus/SettingsMenu.cs index 0434f17..1801c0a 100644 --- a/DynamicWin/UI/Menu/Menus/SettingsMenu.cs +++ b/DynamicWin/UI/Menu/Menus/SettingsMenu.cs @@ -17,6 +17,7 @@ using System.Text; using System.Threading.Tasks; using System.Windows.Controls; +using System.Windows.Forms; using System.Windows.Input; using System.Windows.Navigation; using System.Xml.Linq; @@ -52,7 +53,7 @@ void SaveAndBack() Settings.ToggleHomeMenuShadow = toggleHomeMenuShadow.IsChecked; Settings.RunOnStartup = runOnStartup.IsChecked; Settings.AllowAutomaticUpdates = allowAutomaticUpdates.IsChecked; - Settings.ReduceWorkingArea = reduceWorkingArea.IsChecked; + Settings.AlwaysTopmost = alwaysTopmost.IsChecked; // Save the selected default big menu mode if (bigMenuModeSelector != null) @@ -89,7 +90,7 @@ void SaveAndBack() DWCheckbox antiAliasing; DWCheckbox runOnStartup; DWCheckbox allowAutomaticUpdates; - DWCheckbox reduceWorkingArea; + DWCheckbox alwaysTopmost; DWCheckbox toggleIslandShadow; DWCheckbox toggleHomeMenuShadow; DWCheckbox toggleHighRefreshRate; @@ -147,10 +148,10 @@ public override List InitializeMenu(IslandObject island) objects.Add(workingAreaDisclaimer1); objects.Add(workingAreaDisclaimer2); - reduceWorkingArea = new DWCheckbox(island, "Reduce working area", new Vec2(25, 0), new Vec2(25, 25), () => { }, UIAlignment.TopLeft); - reduceWorkingArea.IsChecked = Settings.ReduceWorkingArea; - reduceWorkingArea.Anchor.X = 0; - objects.Add(reduceWorkingArea); + alwaysTopmost = new DWCheckbox(island, $"Keep interface always topmost", new Vec2(25, 0), new Vec2(25, 25), () => { }, UIAlignment.TopLeft); + alwaysTopmost.IsChecked = Settings.AlwaysTopmost; + alwaysTopmost.Anchor.X = 0; + objects.Add(alwaysTopmost); allowBlur = new DWCheckbox(island, "Toggle blur", new Vec2(25, 0), new Vec2(25, 25), () => { }, UIAlignment.TopLeft); allowBlur.IsChecked = Settings.AllowBlur; diff --git a/DynamicWin/Utils/WindowPositionHelper.cs b/DynamicWin/Utils/WindowPositionHelper.cs index f05819c..0b10db6 100644 --- a/DynamicWin/Utils/WindowPositionHelper.cs +++ b/DynamicWin/Utils/WindowPositionHelper.cs @@ -50,7 +50,7 @@ public static void CenterWindowOnMonitor(Window window, int monitorIndex) if (double.IsNaN(window.Top) || Math.Abs(window.Top - targetTop) > epsilon) window.Top = targetTop; - double desiredHeight = !Settings.ReduceWorkingArea ? screenBounds.Height / scaleY : 500.0; + double desiredHeight = !Settings.AlwaysTopmost ? screenBounds.Height / scaleY : 500.0; if (double.IsNaN(window.Height) || Math.Abs(window.Height - desiredHeight) > epsilon) window.Height = desiredHeight; } From 8c049bb09ed215e620afb633806044e58d9a4bfd Mon Sep 17 00:00:00 2001 From: 59xa <59xa@proton.me> Date: Tue, 14 Apr 2026 17:31:41 -0600 Subject: [PATCH 11/11] Bump --- DynamicWin/Main/App.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DynamicWin/Main/App.xaml.cs b/DynamicWin/Main/App.xaml.cs index 25c46fd..463c46e 100644 --- a/DynamicWin/Main/App.xaml.cs +++ b/DynamicWin/Main/App.xaml.cs @@ -20,7 +20,7 @@ public partial class DynamicWinMain : System.Windows.Application public static MMDevice? defaultDevice; public static MMDevice? defaultMicrophone; - public static string Version => "v1.6.2b2"; + public static string Version => "v1.6.2b3"; public static Channel ReleaseStream => Channel.Canary; public static Architecture ProcessArchitecture => RuntimeInformation.ProcessArchitecture;