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