diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84649bb..903c302 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ name: build env: - DWAppVersion: v1.6.0r + DWAppVersion: v1.6.1r permissions: contents: write @@ -99,7 +99,7 @@ jobs: /p:SelfContained=false ` /p:PublishDir=${{ github.workspace }}\publish\Release-${{ matrix.platform }}\ - - name: Publish Updater + - name: Publish updater run: msbuild Updater/Updater.csproj ` /t:Publish ` /p:Configuration=Release ` diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..18b49b1 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,38 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: Validate .NET build + +on: + push: + branches: [ "release" ] + pull_request: + branches: [ "release" ] + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Restore dependencies + run: dotnet restore + + - name: List files + run: dir -Recurse *.sln + + - name: Build + run: msbuild DynamicWin.sln /p:Configuration=Debug + + - name: Test + run: dotnet test --no-build --verbosity normal + diff --git a/DynamicWin.sln b/DynamicWin.sln index 576dbc0..58001ac 100644 --- a/DynamicWin.sln +++ b/DynamicWin.sln @@ -10,17 +10,27 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|ARM64 = Debug|ARM64 Release|Any CPU = Release|Any CPU + Release|ARM64 = Release|ARM64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9102D609-0350-46E8-B6E7-DA7BF5318151}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9102D609-0350-46E8-B6E7-DA7BF5318151}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9102D609-0350-46E8-B6E7-DA7BF5318151}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {9102D609-0350-46E8-B6E7-DA7BF5318151}.Debug|ARM64.Build.0 = Debug|ARM64 {9102D609-0350-46E8-B6E7-DA7BF5318151}.Release|Any CPU.ActiveCfg = Release|Any CPU {9102D609-0350-46E8-B6E7-DA7BF5318151}.Release|Any CPU.Build.0 = Release|Any CPU + {9102D609-0350-46E8-B6E7-DA7BF5318151}.Release|ARM64.ActiveCfg = Release|ARM64 + {9102D609-0350-46E8-B6E7-DA7BF5318151}.Release|ARM64.Build.0 = Release|ARM64 {63BF87E8-9057-4BB2-91CE-BE1CAB32AFC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {63BF87E8-9057-4BB2-91CE-BE1CAB32AFC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63BF87E8-9057-4BB2-91CE-BE1CAB32AFC8}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {63BF87E8-9057-4BB2-91CE-BE1CAB32AFC8}.Debug|ARM64.Build.0 = Debug|ARM64 {63BF87E8-9057-4BB2-91CE-BE1CAB32AFC8}.Release|Any CPU.ActiveCfg = Release|Any CPU {63BF87E8-9057-4BB2-91CE-BE1CAB32AFC8}.Release|Any CPU.Build.0 = Release|Any CPU + {63BF87E8-9057-4BB2-91CE-BE1CAB32AFC8}.Release|ARM64.ActiveCfg = Release|ARM64 + {63BF87E8-9057-4BB2-91CE-BE1CAB32AFC8}.Release|ARM64.Build.0 = Release|ARM64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DynamicWin/DynamicWin.csproj b/DynamicWin/DynamicWin.csproj index 91cd266..21210b7 100644 --- a/DynamicWin/DynamicWin.csproj +++ b/DynamicWin/DynamicWin.csproj @@ -10,6 +10,7 @@ true app.ico $(Authors) + AnyCPU;ARM64 diff --git a/DynamicWin/Main/App.xaml.cs b/DynamicWin/Main/App.xaml.cs index e4bfc47..8c6b4fd 100644 --- a/DynamicWin/Main/App.xaml.cs +++ b/DynamicWin/Main/App.xaml.cs @@ -17,11 +17,12 @@ namespace DynamicWin { public partial class DynamicWinMain : System.Windows.Application { - public static MMDevice defaultDevice; - public static MMDevice defaultMicrophone; + public static MMDevice? defaultDevice; + public static MMDevice? defaultMicrophone; - public static string Version => "v1.6.0r"; + public static string Version => "v1.6.1r"; public static Channel ReleaseStream => Channel.Release; + public static Architecture ProcessArchitecture => RuntimeInformation.ProcessArchitecture; [STAThread] public static void Main() @@ -49,9 +50,9 @@ public static void UpdateStartup() } } - Mutex mutex; - private System.Timers.Timer topmostTimer; // use System.Timers.Timer to avoid creating Win32 dispatcher timers - private MainForm mainForm; + private Mutex? mutex; + private System.Timers.Timer? topmostTimer; // use System.Timers.Timer to avoid creating Win32 dispatcher timers + private MainForm? mainForm; protected override void OnStartup(StartupEventArgs e) { diff --git a/DynamicWin/Main/MainForm.xaml.cs b/DynamicWin/Main/MainForm.xaml.cs index 875f9ad..6b4ae5f 100644 --- a/DynamicWin/Main/MainForm.xaml.cs +++ b/DynamicWin/Main/MainForm.xaml.cs @@ -71,6 +71,7 @@ public MainForm() InitializeComponent(); _trayIcon = new Forms.NotifyIcon(); + _trayIcon.MouseUp += TrayIcon_MouseUp; // Initialise mouse tracking _lastMouseMoveTime = DateTime.UtcNow; @@ -130,7 +131,6 @@ public MainForm() _trayIcon.ContextMenuStrip.Opening += (s, e) => { this.Topmost = false; - Activate(); }; _trayIcon.ContextMenuStrip.Closing += (s, e) => @@ -401,5 +401,39 @@ internal void DisposeTrayIcon() { _trayIcon.Dispose(); } + + private void TrayIcon_MouseUp(object? sender, Forms.MouseEventArgs e) + { + if (e.Button == Forms.MouseButtons.Right) + { + var screen = Forms.Screen.FromPoint(Forms.Control.MousePosition); + + var iconX = Forms.Control.MousePosition.X; + var iconY = Forms.Control.MousePosition.Y; + + float dpiScale = 1.0f; + using (var g = System.Drawing.Graphics.FromHwnd(IntPtr.Zero)) + { + dpiScale = g.DpiX / 96.0f; + } + int iconWidth = (int)(16 * dpiScale); + + int menuWidth = 200; + if (_trayIcon.ContextMenuStrip != null && _trayIcon.ContextMenuStrip.Items.Count > 0) + { + _trayIcon.ContextMenuStrip.Show(0, -1000); + menuWidth = _trayIcon.ContextMenuStrip.Width; + _trayIcon.ContextMenuStrip.Hide(); + } + + int menuX = iconX - (menuWidth / 2) + (iconWidth / 2); + int menuY = iconY; + + if (menuX < screen.WorkingArea.Left) menuX = screen.WorkingArea.Left; + if (menuX + menuWidth > screen.WorkingArea.Right) menuX = screen.WorkingArea.Right - menuWidth; + + _trayIcon.ContextMenuStrip?.Show(menuX, menuY); + } + } } } \ No newline at end of file diff --git a/DynamicWin/Main/RendererMain.cs b/DynamicWin/Main/RendererMain.cs index 5e799bd..afeb500 100644 --- a/DynamicWin/Main/RendererMain.cs +++ b/DynamicWin/Main/RendererMain.cs @@ -19,7 +19,7 @@ namespace DynamicWin.Main { public class RendererMain : SKElement { - private IslandObject islandObject; + private readonly IslandObject islandObject; public IslandObject MainIsland => islandObject; private List objects => MenuManager.Instance.ActiveMenu.UiObjects; @@ -31,8 +31,8 @@ public class RendererMain : SKElement public static Vec2 ScreenDimensions => new Vec2(MainForm.Instance.Width, MainForm.Instance.Height); public static Vec2 CursorPosition => new Vec2(Mouse.GetPosition(MainForm.Instance).X, Mouse.GetPosition(MainForm.Instance).Y); - private static RendererMain instance; - public static RendererMain Instance => instance; + private static RendererMain? instance; + public static RendererMain? Instance => instance; // Guard so the startup updater sequence runs only once per application lifetime private static bool startupUpdaterSequenceStarted = false; @@ -42,8 +42,8 @@ public class RendererMain : SKElement public float blurOverride = 0f; public float alphaOverride = 1f; - public Action onUpdate; - public Action onDraw; + public Action? onUpdate; + public Action? onDraw; private Stopwatch? updateStopwatch; private int initialScreenBrightness = 0; diff --git a/DynamicWin/Main/Settings.cs b/DynamicWin/Main/Settings.cs index 7fd2e3c..e9a1f5f 100644 --- a/DynamicWin/Main/Settings.cs +++ b/DynamicWin/Main/Settings.cs @@ -48,11 +48,19 @@ public class Settings public static List smallWidgetsMiddle; public static List bigWidgets; + private static HomeMenu.BigMenuMode defaultBigMenuMode = HomeMenu.BigMenuMode.Widgets; + public static HomeMenu.BigMenuMode DefaultBigMenuMode + { + get => defaultBigMenuMode; + set => defaultBigMenuMode = value; + } + public static void InitializeSettings() { try { + if (SaveManager.Contains("settings")) { IslandMode = ((Int64)SaveManager.Get("settings.islandmode") == 0) ? IslandObject.IslandMode.Island : IslandObject.IslandMode.Notch; @@ -73,6 +81,19 @@ public static void InitializeSettings() ScreenIndex = (int)((Int64)SaveManager.Get("settings.screenindex")); ReleaseStream = SaveManager.Contains("settings.ReleaseStream") ? (int)((Int64)SaveManager.Get("settings.ReleaseStream")) : 0; + if (SaveManager.Contains("settings.DefaultBigMenuMode")) + { + int mode = (int)(Int64)SaveManager.Get("settings.DefaultBigMenuMode"); + if (Enum.IsDefined(typeof(HomeMenu.BigMenuMode), mode)) + DefaultBigMenuMode = (HomeMenu.BigMenuMode)mode; + else + DefaultBigMenuMode = HomeMenu.BigMenuMode.Widgets; + } + else + { + DefaultBigMenuMode = HomeMenu.BigMenuMode.Widgets; + } + Settings.smallWidgetsLeft = new List(); Settings.smallWidgetsRight = new List(); Settings.smallWidgetsMiddle = new List(); @@ -174,6 +195,8 @@ public static void Save() SaveManager.Add("settings.smallwidgetsmiddle", smallWidgetsMiddle); SaveManager.Add("settings.bigwidgets", bigWidgets); + SaveManager.Add("settings.DefaultBigMenuMode", (int)DefaultBigMenuMode); + SaveManager.SaveAll(); } } diff --git a/DynamicWin/UI/Menu/Menus/HomeMenu.cs b/DynamicWin/UI/Menu/Menus/HomeMenu.cs index afa09a6..7317a2d 100644 --- a/DynamicWin/UI/Menu/Menus/HomeMenu.cs +++ b/DynamicWin/UI/Menu/Menus/HomeMenu.cs @@ -58,9 +58,27 @@ public override Vec2 IslandSize() public override Vec2 IslandSizeBig() { - Vec2 size = new Vec2(275, 145); - + // Use different sizing logic depending on the current big menu mode + if (currentBigMenuMode == BigMenuMode.Tray) + { + // Tray mode + return new Vec2(350, 250); + } + else if (currentBigMenuMode == BigMenuMode.Media) + { + // Media mode + float desiredMediaWidth = 440f; + float desiredMediaHeight = 110f; + float horizontalPadding = 60f; + float topContainerHeight = 30f; + float width = desiredMediaWidth + horizontalPadding; + float height = desiredMediaHeight + bCD + topContainerHeight + topSpacing; + return new Vec2(width, height); + } + else // Widgets mode (default) { + Vec2 size = new Vec2(275, 145); + float sizeTogetherBiggest = 0f; float sizeTogether = 0f; @@ -75,10 +93,8 @@ public override Vec2 IslandSizeBig() } size.X = (float)Math.Max(size.X, sizeTogetherBiggest); - } - { - float sizeTogetherBiggest = 0f; + sizeTogetherBiggest = 0f; for (int i = 0; i < bigWidgets.Count; i++) { @@ -92,34 +108,9 @@ public override Vec2 IslandSizeBig() // Set the container height to the total height of all rows size.Y = Math.Max(size.Y, sizeTogetherBiggest); - } - // When not in widget mode (showing tray) use fixed height - if (!isWidgetMode && currentBigMenuMode == BigMenuMode.Tray) - { - size.Y = 250; + return size; } - - // When Media view is active, ensure the island is large enough to contain the media panel - if (!isWidgetMode && currentBigMenuMode == BigMenuMode.Media) - { - // Keep in sync with the size used when rendering the media UIObject in Update() - float desiredMediaWidth = 440f; - float desiredMediaHeight = 110; - - // Horizontal padding to give the media panel some inset from island edges - float horizontalPadding = 60f; - - // Top container height (approximate) used by the top buttons - float topContainerHeight = 30f; - - size.X = Math.Max(size.X, desiredMediaWidth + horizontalPadding); - - // Add bCD (bottom container offset) + topContainerHeight + topSpacing to ensure vertical space - size.Y = desiredMediaHeight + bCD + topContainerHeight + topSpacing; - } - - return size; } UIObject smallWidgetsContainer; @@ -138,10 +129,13 @@ public override Vec2 IslandSizeBig() // Enum to track which big menu is active public enum BigMenuMode { Widgets, Tray, Media } - public BigMenuMode currentBigMenuMode = BigMenuMode.Widgets; + public BigMenuMode currentBigMenuMode; public override List InitializeMenu(IslandObject island) { + // Set currentBigMenuMode from user setting + currentBigMenuMode = Settings.DefaultBigMenuMode; + var objects = base.InitializeMenu(island); smallWidgetsContainer = new UIObject(island, Vec2.zero, IslandSize(), UIAlignment.Center); diff --git a/DynamicWin/UI/Menu/Menus/SettingsMenu.cs b/DynamicWin/UI/Menu/Menus/SettingsMenu.cs index 84b9d47..5f5c297 100644 --- a/DynamicWin/UI/Menu/Menus/SettingsMenu.cs +++ b/DynamicWin/UI/Menu/Menus/SettingsMenu.cs @@ -39,6 +39,8 @@ public SettingsMenu() bool changedTheme = false; + DWMultiSelectionButton bigMenuModeSelector; + void SaveAndBack() { Settings.AllowBlur = allowBlur.IsChecked; @@ -51,6 +53,18 @@ void SaveAndBack() Settings.RunOnStartup = runOnStartup.IsChecked; Settings.AllowAutomaticUpdates = allowAutomaticUpdates.IsChecked; + // Save the selected default big menu mode + if (bigMenuModeSelector != null) + { + Settings.DefaultBigMenuMode = bigMenuModeSelector.SelectedIndex switch + { + 0 => HomeMenu.BigMenuMode.Widgets, + 1 => HomeMenu.BigMenuMode.Tray, + 2 => HomeMenu.BigMenuMode.Media, + _ => HomeMenu.BigMenuMode.Widgets + }; + } + DynamicWinMain.UpdateStartup(); if (changedTheme) @@ -299,6 +313,35 @@ public override List InitializeMenu(IslandObject island) objects.Add(selectedMonitor); } + { + var bigMenuModeTitle = new DWText(island, "Default Big Menu Mode", new Vec2(25, 0), UIAlignment.TopLeft); + bigMenuModeTitle.Font = Res.SFProBold; + bigMenuModeTitle.TextSize = 15; + bigMenuModeTitle.Anchor.X = 0; + objects.Add(bigMenuModeTitle); + var bigMenuModes = new string[] { "Widgets", "Tray", "Media" }; + bigMenuModeSelector = new DWMultiSelectionButton(island, bigMenuModes, new Vec2(25, 0), new Vec2(IslandSize().X - 50, 25), UIAlignment.TopLeft); + bigMenuModeSelector.SelectedIndex = Settings.DefaultBigMenuMode switch + { + HomeMenu.BigMenuMode.Widgets => 0, + HomeMenu.BigMenuMode.Tray => 1, + HomeMenu.BigMenuMode.Media => 2, + _ => 0 + }; + bigMenuModeSelector.Anchor.X = 0; + bigMenuModeSelector.onClick += (index) => + { + Settings.DefaultBigMenuMode = index switch + { + 0 => HomeMenu.BigMenuMode.Widgets, + 1 => HomeMenu.BigMenuMode.Tray, + 2 => HomeMenu.BigMenuMode.Media, + _ => HomeMenu.BigMenuMode.Widgets + }; + }; + objects.Add(bigMenuModeSelector); + } + { var themeTitle = new DWText(island, "Themes", new Vec2(25, 0), UIAlignment.TopLeft); themeTitle.Font = Res.SFProBold; @@ -476,6 +519,14 @@ public override List InitializeMenu(IslandObject island) Font = Res.SFProBold }); + objects.Add(new DWText(island, $"Software architecture: {DynamicWinMain.ProcessArchitecture.ToString().ToLower()}", new Vec2(25, -25), UIAlignment.TopLeft) + { + Color = Theme.TextMain, + Anchor = new Vec2(0, 0), + TextSize = 13, + Font = Res.SFProBold + }); + objects.Add(new DWText(island, "Maintained and developed by 59xa", new Vec2(25, -25), UIAlignment.TopLeft) { Color = Theme.TextThird, diff --git a/DynamicWin/UI/UIElements/Custom/DWSlider.cs b/DynamicWin/UI/UIElements/Custom/DWSlider.cs index 8537386..f550185 100644 --- a/DynamicWin/UI/UIElements/Custom/DWSlider.cs +++ b/DynamicWin/UI/UIElements/Custom/DWSlider.cs @@ -8,30 +8,24 @@ namespace DynamicWin.UI.UIElements.Custom { - public class DWSlider : DWProgressBar + public class DWSlider(UIObject? parent, Vec2 position, Vec2 size, UIAlignment alignment = UIAlignment.TopCenter) : DWProgressBarEx(parent, position, size, alignment) { public Action clickCallback; - - public DWSlider(UIObject? parent, Vec2 position, Vec2 size, UIAlignment alignment = UIAlignment.TopCenter) : base(parent, position, size, alignment) - { - - } - float valueBefore = 0f; public override void OnMouseDown() { base.OnMouseDown(); - valueBefore = value; + valueBefore = Value; } public override void OnGlobalMouseUp() { base.OnGlobalMouseUp(); - if(valueBefore != value) - clickCallback?.Invoke(this.value); + if(valueBefore != Value) + clickCallback?.Invoke(Value); } public override void Update(float deltaTime) @@ -39,7 +33,7 @@ public override void Update(float deltaTime) base.Update(deltaTime); if (IsMouseDown) - this.value = Mathf.Clamp(Mathf.Remap(RendererMain.CursorPosition.X - Position.X, 0, Size.X, 0, 1), + Value = Mathf.Clamp(Mathf.Remap(RendererMain.CursorPosition.X - Position.X, 0, Size.X, 0, 1), 0.05f, 1); } } diff --git a/DynamicWin/UI/UIElements/Custom/MediaPlayer.cs b/DynamicWin/UI/UIElements/Custom/MediaPlayer.cs index 59dffe0..2d0aeb8 100644 --- a/DynamicWin/UI/UIElements/Custom/MediaPlayer.cs +++ b/DynamicWin/UI/UIElements/Custom/MediaPlayer.cs @@ -16,7 +16,7 @@ * Author: 59xa * GitHub: https://github.com/59xa * Implementation Date: 26 December 2025 - * Last Modified: 27 January 2026 + * Last Modified: 15 February 2026 * */ @@ -1304,13 +1304,7 @@ private void ResetThumbnailState() // Helper to reset animator state private void animatorReset() { - // MediaAnimator does not have a Reset method, so forcibly set state - var field = typeof(MediaAnimator).GetField("State", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public); - if (field != null) field.SetValue(animator, MediaAnimator.AnimState.Idle); - var timerField = typeof(MediaAnimator).GetField("AnimTimer", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public); - if (timerField != null) timerField.SetValue(animator, 0f); - var blurField = typeof(MediaAnimator).GetField("BlurAmount", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public); - if (blurField != null) blurField.SetValue(animator, 0f); + animator.ForceFinish(); } public override void Draw(SKCanvas canvas) diff --git a/DynamicWin/UI/Widgets/Big/WeatherWidget.cs b/DynamicWin/UI/Widgets/Big/WeatherWidget.cs index 046da2e..05b995b 100644 --- a/DynamicWin/UI/Widgets/Big/WeatherWidget.cs +++ b/DynamicWin/UI/Widgets/Big/WeatherWidget.cs @@ -190,7 +190,7 @@ class WeatherWidget : WidgetBase UIObject _LocationTextReplacement; - static WeatherAPI _WeatherAPI; + static WeatherAPI _WeatherAPI => WeatherAPI.Default; DWImage _ForecastIcon; @@ -259,7 +259,7 @@ public WeatherWidget(UIObject? parent, Vec2 position, UIAlignment alignment = UI }; // Initialises weather API - if (_WeatherAPI == null) _WeatherAPI = new WeatherAPI(); + // Always use singleton instance // Updates weather information display _WeatherAPI._OnWeatherDataReceived += OnWeatherDataReceived; @@ -333,6 +333,10 @@ protected override void OnActiveChanged(bool isEnabled) if (isEnabled) { StartOrRestartFetchBasedOnSettings(); + RefreshWeatherDisplay(); + // Force immediate metadata/UI refresh + _ = _WeatherAPI.ForceRefresh(RegisterWeatherWidgetSettings.saveData.countryIndex, RegisterWeatherWidgetSettings.saveData.cityIndex, + RegisterWeatherWidgetSettings.saveData.countryIndex == 0 ? "default" : "city"); } else { @@ -340,6 +344,20 @@ protected override void OnActiveChanged(bool isEnabled) } } + // Force refresh of weather display from last known data and settings + private void RefreshWeatherDisplay() + { + if (!string.IsNullOrEmpty(lastWeatherData.city)) + { + _TemperatureText.SetText(RegisterWeatherWidgetSettings.saveData.useCelsius ? lastWeatherData.celsius : lastWeatherData.fahrenheit); + _ForecastText.SetText(lastWeatherData.weatherText); + _LocationText.SetText(lastWeatherData.city); + UpdateIcon(lastWeatherData.weatherText); + } + _LocationTextReplacement.SilentSetActive(RegisterWeatherWidgetSettings.saveData.hideLocation); + _LocationText.SilentSetActive(!RegisterWeatherWidgetSettings.saveData.hideLocation); + } + // Helper to start fetching using current saved configuration private void StartOrRestartFetchBasedOnSettings() { diff --git a/DynamicWin/UI/Widgets/Small/MediaThumbnailWidget.cs b/DynamicWin/UI/Widgets/Small/MediaThumbnailWidget.cs index 30e8cdb..afdd02e 100644 --- a/DynamicWin/UI/Widgets/Small/MediaThumbnailWidget.cs +++ b/DynamicWin/UI/Widgets/Small/MediaThumbnailWidget.cs @@ -393,6 +393,8 @@ public override void OnDestroy() { base.OnDestroy(); try { MediaThumbnailService.Instance.ThumbnailChanged -= OnThumbnailChanged; } catch { } + // Ensure animation is finished and thumbnail is visible + ForceFinishAnimation(); // Dispose owned bitmaps lock (mediaLock) { @@ -402,6 +404,36 @@ public override void OnDestroy() } } + /// + /// Force the animator to finish and reset blur/flip state, ensuring thumbnail is always visible. + /// + public void ForceFinishAnimation() + { + animator.ForceFinish(); + lock (mediaLock) + { + if (pendingBitmap != null) + { + if (thumbnailBitmap != null) + { + try { thumbnailBitmap.Dispose(); } catch { } + } + thumbnailBitmap = pendingBitmap; + currentBitmapFingerprint = pendingBitmapFingerprint; + pendingBitmap = null; + pendingBitmapFingerprint = null; + currentMediaKey = pendingMediaKey; + pendingMediaKey = null; + pendingMedia = null; + } + if (previousBitmap != null) + { + try { previousBitmap.Dispose(); } catch { } + previousBitmap = null; + } + } + } + // Make this small widget square: width matches height so thumbnail is not stretched protected override float GetWidgetWidth() { diff --git a/DynamicWin/UI/Widgets/Small/UsedDevicesWidget.cs b/DynamicWin/UI/Widgets/Small/UsedDevicesWidget.cs index a60ff26..40a191a 100644 --- a/DynamicWin/UI/Widgets/Small/UsedDevicesWidget.cs +++ b/DynamicWin/UI/Widgets/Small/UsedDevicesWidget.cs @@ -63,7 +63,7 @@ public List SettingsObjects() var loudnessMeter = new LoudnessMeter(null, new Vec2(0, 0), new Vec2(400, 7.5f)); var thresholdSlider = new DWSlider(null, new Vec2(0, 0), new Vec2(400, 25)); - thresholdSlider.value = Mathf.Clamp(saveData.indicatorThreshold, 0.05f, 1f); + thresholdSlider.Value = Mathf.Clamp(saveData.indicatorThreshold, 0.05f, 1f); thresholdSlider.clickCallback = (x) => { saveData.indicatorThreshold = x; diff --git a/DynamicWin/Utils/MediaAnimator.cs b/DynamicWin/Utils/MediaAnimator.cs index 22d1e76..121eb97 100644 --- a/DynamicWin/Utils/MediaAnimator.cs +++ b/DynamicWin/Utils/MediaAnimator.cs @@ -121,5 +121,17 @@ public float GetFlipScale() } public bool IsFlipping => State == AnimState.Flip; + + /// + /// Forces the animation to finish immediately, resetting state and blur. + /// + public void ForceFinish() + { + State = AnimState.Idle; + AnimTimer = 0f; + BlurAmount = 0f; + midSwapCalled = false; + lastHasPending = false; + } } } diff --git a/DynamicWin/Utils/Updater.cs b/DynamicWin/Utils/Updater.cs index 974396d..11762a4 100644 --- a/DynamicWin/Utils/Updater.cs +++ b/DynamicWin/Utils/Updater.cs @@ -4,17 +4,18 @@ using System.Text.Json; using System; using DynamicWin.Main; +using System.Runtime.InteropServices; /* * * Overview: * - Updater logic to handle automatic updates rather than user manually updating - * - from the repository holding the codebase (DynamicWin-Legacy/stable). + * - from the repository holding the codebase (DynamicWin-Legacy/release). * * Author: 59xa * Github: https://github.com/59xa * Implementation Date: 27 November 2025 - * Last Modified: 10 January 2026 + * Last Modified: 15 February 2026 * */ @@ -31,11 +32,23 @@ internal class Updater /// The name of the JSON file to fetch from the remote repository. Cannot be null or empty. /// A task that represents the asynchronous operation. The task result contains an /// object if the file is successfully retrieved and deserialised; otherwise, . - private async Task FetchRemote(string file) + private async Task FetchRemote(string file, Architecture cpuArchitecture) { using HttpClient client = new(); + + string suffix = cpuArchitecture switch + { + Architecture.X64 => "-x64", + Architecture.Arm64 => "-arm64", + _ => string.Empty // In case none matches architecture + }; + string json = await client.GetStringAsync( - $"https://raw.githubusercontent.com/59xa/DynamicWin-Legacy/refs/heads/updater/{file}"); + $"https://raw.githubusercontent.com/59xa/DynamicWin-Legacy/refs/heads/updater/{file}{suffix}.json"); + +#if DEBUG + Debug.WriteLine(json); +#endif var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; return JsonSerializer.Deserialize(json, options); @@ -59,7 +72,7 @@ internal class Updater Debug.WriteLine($"[UPDATER]: selected stream = {(Settings.ReleaseStream == 1 ? "canary" : "release")}"); #endif - var release = await FetchRemote("version.json"); + var release = await FetchRemote("version-release", DynamicWinMain.ProcessArchitecture); #if DEBUG if (release != null) @@ -106,7 +119,7 @@ internal class Updater return null; } - var canary = await FetchRemote("version-canary.json"); + var canary = await FetchRemote("version-canary", DynamicWinMain.ProcessArchitecture); #if DEBUG if (canary != null) diff --git a/DynamicWin/Utils/WeatherAPI.cs b/DynamicWin/Utils/WeatherAPI.cs index 5f14826..0cf86b8 100644 --- a/DynamicWin/Utils/WeatherAPI.cs +++ b/DynamicWin/Utils/WeatherAPI.cs @@ -19,7 +19,7 @@ * Author: 59xa * GitHub: https://github.com/59xa * Implementation Date: 16 May 2025 -* Last Modified: 02 January 2026 +* Last Modified: 15 February 2026 * * TO MAINTAINERS: * - When fetching weather data, the API might hallucinate, and retrieve forecast data from a different city. @@ -69,25 +69,30 @@ private static Task> GetCachedCsvAsync() /// "default" for geo IP, "city" for user-selected public void StartFetching(int countryIndex, int cityIndex, string? type = null) { + bool shouldStart = false; lock (_lock) { _activeClients++; if (_activeClients == 1) { _internalCts = new CancellationTokenSource(); - // Start the loop without blocking the caller - _ = Task.Run(() => Fetch(countryIndex, cityIndex, type, _internalCts.Token, _internalCts)); -#if DEBUG - Debug.WriteLine("[WEATHER API] StartFetching invoked; loop started."); -#endif + shouldStart = true; } - else - { + } + if (shouldStart) + { + // Start the loop without blocking the caller + _ = Task.Run(() => Fetch(countryIndex, cityIndex, type, _internalCts.Token, _internalCts)); #if DEBUG - Debug.WriteLine("[WEATHER API] StartFetching invoked; already running (refcount={0}).", _activeClients); + Debug.WriteLine("[WEATHER API] StartFetching invoked; loop started."); #endif - } } +#if DEBUG + else + { + Debug.WriteLine("[WEATHER API] StartFetching invoked; already running (refcount={0}).", _activeClients); + } +#endif } /// @@ -119,6 +124,18 @@ public void StopFetching() } } + /// + /// Immediately fetch and push the latest weather data, bypassing the background loop. + /// + public async Task ForceRefresh(int countryIndex, int cityIndex, string? type = null) + { + try + { + await Fetch(countryIndex, cityIndex, type, CancellationToken.None, null); + } + catch { } + } + /// /// Handles retrieval of forecast values from a specified city set by user. /// Internal use: prefer StartFetching/StopFetching for controlling the loop lifecycle. diff --git a/DynamicWin/Utils/WindowPositionHelper.cs b/DynamicWin/Utils/WindowPositionHelper.cs index 7fe617e..0ea8235 100644 --- a/DynamicWin/Utils/WindowPositionHelper.cs +++ b/DynamicWin/Utils/WindowPositionHelper.cs @@ -15,8 +15,6 @@ 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); @@ -37,11 +35,12 @@ public static void CenterWindowOnMonitor(Window window, int monitorIndex) double scaleX = dpiX / 96.0; double scaleY = dpiY / 96.0; - double targetLeft = (bounds.Left + (bounds.Width - windowWidth * scaleX) / 2.0) / scaleX; - double targetTop = bounds.Top / scaleY; - - window.Left = targetLeft; - window.Top = targetTop; + // 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; } } } diff --git a/Updater/Updater.csproj b/Updater/Updater.csproj index d32b9de..549b880 100644 --- a/Updater/Updater.csproj +++ b/Updater/Updater.csproj @@ -9,6 +9,7 @@ enable 10.0.19041.0 ..\..\DynamicWin-Legacy\DynamicWin\bin + AnyCPU;ARM64