? setup = null)
+ {
+ lock (poolLock)
+ {
+ if (pooledBitmap != null)
+ {
+ var bmp = pooledBitmap;
+ pooledBitmap = null;
+ setup?.Invoke(bmp);
+ return bmp;
+ }
+ }
+ return null;
+ }
+
+ public static void Return(SKBitmap? bmp)
+ {
+ if (bmp == null) return;
+ lock (poolLock)
+ {
+ if (pooledBitmap == null)
+ {
+ pooledBitmap = bmp;
+ }
+ else
+ {
+ bmp.Dispose();
+ }
+ }
+ }
+
+ public static void Clear()
+ {
+ lock (poolLock)
+ {
+ pooledBitmap?.Dispose();
+ pooledBitmap = null;
+ }
+ }
}
///
- /// Decode image bytes into an owned SKImage and compute fingerprint from a temporary SKBitmap.
- /// Returns null on failure.
+ /// Decode image bytes directly into an SKImage with computed fingerprint.
+ /// Returns null on failure. Fingerprint is computed from temporary SKBitmap.
+ /// Optimized for minimal allocations and memory overhead.
///
public static SKImage? DecodeBytesToImageAndFingerprint(byte[] bytes, out ulong? fingerprint)
{
fingerprint = null;
if (bytes == null || bytes.Length == 0) return null;
+ SKBitmap? decoded = null;
try
{
+ // Decode bytes directly to SKBitmap
using var ms = new SKMemoryStream(bytes);
- var bmp = SKBitmap.Decode(ms);
- if (bmp == null) return null;
+ decoded = SKBitmap.Decode(ms);
- try { fingerprint = BitmapUtils.GetBitmapFingerprint(bmp); } catch { fingerprint = null; }
+ if (decoded == null) return null;
+ // Compute fingerprint from bitmap
+ try { fingerprint = BitmapUtils.GetBitmapFingerprint(decoded); }
+ catch { }
+
+ // Convert to SKImage (more efficient than keeping bitmap)
SKImage? img = null;
- try
- {
- img = SKImage.FromBitmap(bmp);
- }
- catch
+ try { img = SKImage.FromBitmap(decoded); }
+ catch { }
+
+ return img;
+ }
+ finally
+ {
+ // Always dispose temporary bitmap
+ decoded?.Dispose();
+ }
+ }
+
+ ///
+ /// Batch decode multiple thumbnail bytes for mass imports.
+ /// Returns tuples of (SKImage, fingerprint) for each input, null on individual failures.
+ /// Useful for thumbnail gallery loading.
+ ///
+ public static (SKImage?, ulong?)[] DecodeBytesArrayToImagesAndFingerprints(byte[][] bytesArray)
+ {
+ if (bytesArray == null || bytesArray.Length == 0)
+ return Array.Empty<(SKImage?, ulong?)>();
+
+ var results = new (SKImage?, ulong?)[bytesArray.Length];
+
+ for (int i = 0; i < bytesArray.Length; i++)
+ {
+ if (bytesArray[i] == null || bytesArray[i].Length == 0)
{
- img = null;
+ results[i] = (null, null);
+ continue;
}
- try { bmp.Dispose(); } catch { }
+ var img = DecodeBytesToImageAndFingerprint(bytesArray[i], out var fp);
+ results[i] = (img, fp);
+ }
- return img;
+ return results;
+ }
+
+ ///
+ /// Fast check if two thumbnail byte arrays produce the same fingerprint.
+ /// Useful for deduplication without full decode.
+ ///
+ public static bool AreThumbnailBytesEquivalent(byte[]? bytes1, byte[]? bytes2)
+ {
+ if (bytes1 == null && bytes2 == null) return true;
+ if (bytes1 == null || bytes2 == null) return false;
+ if (bytes1.Length != bytes2.Length) return false;
+
+ // For small arrays, just compare directly
+ if (bytes1.Length < 1024)
+ {
+ return bytes1.AsSpan().SequenceEqual(bytes2);
}
- catch
+
+ // For larger arrays, use fast byte hash
+ return ComputeQuickByteHash(bytes1) == ComputeQuickByteHash(bytes2);
+ }
+
+ ///
+ /// Lightweight FNV-1a hash for byte array comparison.
+ ///
+ private static ulong ComputeQuickByteHash(byte[] bytes)
+ {
+ const ulong fnvOffset = 14695981039346656037UL;
+ const ulong fnvPrime = 1099511628211UL;
+ ulong hash = fnvOffset;
+
+ for (int i = 0; i < bytes.Length; i++)
{
- return null;
+ hash ^= bytes[i];
+ hash *= fnvPrime;
}
+
+ return hash;
+ }
+
+ ///
+ /// Validate if bytes represent valid image data without full decode.
+ /// Performs format magic number check only.
+ ///
+ public static bool IsValidImageBytes(byte[]? bytes)
+ {
+ if (bytes == null || bytes.Length < 4) return false;
+
+ // Check for common image format magic numbers
+ // JPEG: FF D8 FF
+ if (bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF) return true;
+
+ // PNG: 89 50 4E 47 (‰PNG)
+ if (bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47) return true;
+
+ // WebP: RIFF ... WEBP
+ if (bytes.Length >= 12 &&
+ bytes[0] == 0x52 && bytes[1] == 0x49 && bytes[2] == 0x46 && bytes[3] == 0x46 &&
+ bytes[8] == 0x57 && bytes[9] == 0x45 && bytes[10] == 0x42 && bytes[11] == 0x50)
+ return true;
+
+ // BMP: 42 4D (BM)
+ if (bytes[0] == 0x42 && bytes[1] == 0x4D) return true;
+
+ // GIF: 47 49 46 (GIF)
+ if (bytes[0] == 0x47 && bytes[1] == 0x49 && bytes[2] == 0x46) return true;
+
+ return false;
}
}
}
diff --git a/DynamicWin/Utils/MediaTypes.cs b/DynamicWin/Utils/MediaTypes.cs
index a609d3f..fc880b9 100644
--- a/DynamicWin/Utils/MediaTypes.cs
+++ b/DynamicWin/Utils/MediaTypes.cs
@@ -8,6 +8,8 @@ public class MediaTimeline
public System.TimeSpan Position { get; set; }
public System.TimeSpan StartTime { get; set; }
public System.TimeSpan EndTime { get; set; }
+ public System.DateTimeOffset LastUpdatedTime { get; set; }
+ public System.DateTimeOffset CachedAt { get; set; }
public Windows.Media.Control.GlobalSystemMediaTransportControlsSessionPlaybackStatus PlaybackStatus { get; set; }
}
diff --git a/DynamicWin/Utils/WindowPositionHelper.cs b/DynamicWin/Utils/WindowPositionHelper.cs
index 0ea8235..5995458 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.AlwaysTopmost ? screenBounds.Height / scaleY : 500.0;
+ if (double.IsNaN(window.Height) || Math.Abs(window.Height - desiredHeight) > epsilon)
+ window.Height = desiredHeight;
}
}
}
diff --git a/DynamicWin/WPFBinders/SKElement.cs b/DynamicWin/WPFBinders/SKElement.cs
index f3d1557..cbb6a7b 100644
--- a/DynamicWin/WPFBinders/SKElement.cs
+++ b/DynamicWin/WPFBinders/SKElement.cs
@@ -30,25 +30,7 @@ public SKElement()
{
designMode = DesignerProperties.GetIsInDesignMode(this);
- // Attempt to use OpenGL if possible. If GL fails, log exception and leave GRContext null to use CPU as fallback
- try
- {
- var glInterface = GRGlInterface.Create();
- if (glInterface != null)
- {
- GrContext = GRContext.CreateGl(glInterface);
- Debug.WriteLine("SKElement: Created GL GRContext successfully.");
- }
- else
- {
- Debug.WriteLine("SKElement: GRGlInterface.Create returned null - GL not available.");
- }
- }
- catch (Exception ex)
- {
- Debug.WriteLine($"SKElement: Failed to create GL GRContext: {ex}");
- GrContext = null;
- }
+ GrContext = null;
}
public SKSize CanvasSize { get; private set; }
diff --git a/MODDING.md b/MODDING.md
index 6b50844..f51eaa9 100644
--- a/MODDING.md
+++ b/MODDING.md
@@ -1,5 +1,13 @@
# Creating/modifying DynamicWin-Legacy with custom extensions
+**We support mod extensions. You can add your own small widgets and big widgets by creating a custom extension.**
+Loading an extension from someone else is very simple. Drag the **`Mod.dll`** file you have created to the `Extensions` folder located in the `%appdata%/DynamicWin` directory.
+
+> [!WARNING]
+> **Please never load a mod that is not tested to be safe.**
+
+Mods may contain malicious code that can mess up your system, so always check a mod's source code or let a trustworthy person check it for you.
+
To create an extension you need an IDE like [Visual Studio 2026](https://visualstudio.microsoft.com/vs/community/).
- Create a new C# project of the type `Class Library`. Ensure that the target framework is **`.NET 9.0`**.
- It is required to add `DynamicWin.dll` and SkiaSharp DLLs as assembly dependencies to your project. [More information regarding this through here.](https://learn.microsoft.com/en-gb/visualstudio/ide/how-to-add-or-remove-references-by-using-the-reference-manager?view=vs-2022)
diff --git a/README.md b/README.md
index 82ca252..7e5c8d3 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# DynamicWin Legacy
+# DynamicWin-Legacy
@@ -8,20 +8,23 @@
-
+
-DynamicWin Legacy by Florian Butz is maintained by 59xa and is licenced under CC BY-SA 4.0


-
-> [!NOTE]
-> This repository holds the legacy code and releases for DynamicWin developed by [FlorianButz](https://github.com/FlorianButz), and is maintained by [59xa](https://github.com/59xa). Please do not report issues and missing features in this repository regarding version 2.0 as this repository only accepts version 1.0 issues. For version 2.0 releases, click [here](https://github.com/FlorianButz/DynamicWin).
+DynamicWin-Legacy developed by Florian Butz and 59xa is licenced under CC BY-SA 4.0


> [!WARNING]
-> This is a legacy application that is being maintained by one developer. Do not expect most features to be fixed whatsoever. However, this does not mean groundbreaking issues and feature requests will be turned down immediately. Open an issue ticket for a new feature or an existing issue, they will be added/fixed eventually.
+> As of **31st May, 2026**, DynamicWin-Legacy is feature complete, and will no longer receive further updates moving forward. The maintainer of this repository will instead shift development focus on DynamicWin's successor. Stay tuned for more details [here](https://github.com/project-vibrance).
+
+> [!NOTE]
+> This repository holds the legacy code and releases for DynamicWin developed by [FlorianButz](https://github.com/FlorianButz), and is maintained by [59xa](https://github.com/59xa). Please do not report issues and missing features in this repository regarding V2 as this repository only accepts version 1.0 issues. For V2 releases, click [here](https://github.com/FlorianButz/DynamicWin/releases).
### What is it?
-A [Dynamic Island](https://support.apple.com/de-de/guide/iphone/iph28f50d10d/ios) inspired Windows App that brings in a bunch of features like widgets or a file tray that works like a clipboard.
-Similar to dynamic notches that you can find on macOS like [NotchNook](https://lo.cafe/notchnook), this application brings the concept on Windows devices to life.
+A [Dynamic Island](https://support.apple.com/en-gb/guide/iphone/iph28f50d10d/ios)-inspired Windows software that brings in a bunch of features like widgets or a file tray that works like a clipboard.
+
+Similar to dynamic notches that you can find on macOS like [NotchNook](https://lo.cafe/notchnook), this software brings the concept on Windows devices to life.
+
+_DynamicWin-Legacy supports both **`x64`** and **`arm64`** releases. [Get the latest version for your Windows device here](https://github.com/59xa/DynamicWin-Legacy/releases)._
### Implementation and build
This application is developed using C# for the logic, Windows Presentation Foundation (WPF) for windowing, and [SkiaSharp](https://github.com/mono/SkiaSharp) to display the graphical interface.
@@ -33,93 +36,55 @@ git pull https://github.com/59xa/DynamicWin-Legacy.git
```
### Future plans/continued support:
-- While [version 2.0](https://github.com/FlorianButz/DynamicWin) of this software has been made public, the legacy codebase will continue to exist and maintained by me until FlorianButz decides to pull the legacy support.
-- This repository is no longer connected to the original repository's fork network. Please report your issues regarding V2 [here](https://github.com/FlorianButz/DynamicWin).
+- DynamicWin-Legacy is now considered abandonware. Development focus has been shifted to **V3** instead, [more information here](https://github.com/project-vibrance).
+- While [V2](https://github.com/FlorianButz/DynamicWin) has been made public, the legacy codebase will continue to exist for other developers and maintainers.
+- This repository is not linked to the original repository's fork network. Please report your issues regarding V2 [here](https://github.com/FlorianButz/DynamicWin).
- V1 (this repository) will co-exist with V2, and will not serve as a replacement but an alternative for users to use.
-- Your support truly means a lot to keep maintaining DynamicWin Legacy. Keep an eye out whenever a new release comes out.
- Feel free to contribute to this project as you wish. Open any issues on the issues page if you encounter any bugs.
-
-
-**Quick disclaimer**: The codebase is currently structured terribly and almost un-maintainable. Codebase refactoring is currently in the works starting with **`v1.4.0b`**.
# Features
-> [!NOTE]
-> Only checkboxed features are currently available. Unimplemented features will be introduced as time passes.
-
DynamicWin-Legacy has a variety of features, currently including:
## Shortcuts
-- [x] `Ctrl + Win` Will hide the island (or show it again).
-- [ ] ~~`Shift + Win` Will open a quick search menu.~~ (Please consider using an alternative such as [Powertoys Run](https://learn.microsoft.com/en-us/windows/powertoys/run))
-
-## Big Widgets
-- [x] Media Playback Widget
-- [x] Timer Widget
-- [x] Weather Widget
-- [x] Shortcuts Widget (Can be configured to open a file, e.g. Shortcut, .EXE or any other filetype.)
-- [ ] Calendar Widget
-
-## Small Widgets
-- [x] Time Display
-- [x] Music Visualizer
-- [x] Device Usage Detector (Indicates if camera / microphone is in use)
-- [x] Power State Display (Shows battery in form of icons. If no battery is found it shows a connector icon instead)
-- [x] Timer (Displaying current running timer)
-- [x] CPU/GPU Usage Display
-
-## File Distribution & Management
+- [x] `CTRL + Win` hides the interface (or show it again).
+
+## Big widgets
+- [x] Media playback (deprecated)
+- [x] Timer
+- [x] Weather
+- [x] Shortcuts (can be configured to open a file, e.g. shortcut, `.exe`, and/or any other filetype)
+
+## Small widgets
+- [x] Time display
+- [x] Audio visualiser
+- [x] Device usage detector (indicates if camera / microphone is in use)
+- [x] Power state display (shows battery in form of icons, displays connector if battery is not found)
+- [x] Timer (displays current running timer)
+- [x] Resource usage display
+
+## File distribution & management
-
+
- [x] File Tray
Files can be dragged over the island to add them to the file tray. The tray can be accessed when hovering over the island and clicking on the 'Tray' button. The files are stored until they are dragged out again. They can also be removed by selecting the file and right clicking. A context menu will popup and you can click on - **"Remove Selected Files"** or **"Remove Selected Files"** to copy the files.
-- [ ] SnapDrop API implementation
-While this feature is low-priority, please expect the introduction of this feature in the near future.
-
-
> [!WARNING]
> If you are using the file tray to import files in to an app (e.g. After Effects) make sure to not remove the files from the tray. Apps that only copy a link to the file will be lost after you remove the file from the tray.
-## Spotify Integration
+## Media player
-
The Media Playback Widget automatically detects when an instance of the Spotify app is running (Desktop version only). It will display the current playing song name and the artist. Login to the Spotify service on the app is not required.
+
The media player uses the GSMTC interop to control and display metadata regardless of media source. Integration with other applications through sign-in or API is not required.
-
-## Mod Support
-**We support mod extensions. You can add your own small widgets and big widgets by creating a custom extension.**
-Loading an extension from someone else is very simple. You just need to drag the **Mod.dll** file in to the *Extensions* folder that is located in the `%appdata%/DynamicWin` directory.
-
-> [!WARNING]
-> **Please never load a mod that is not tested to be safe.**
-
-Mods may contain malicious code that can mess up your system, so always check a mod's source code or let a trustworthy person check it for you.
-
-## Custom Themes
-
-
-
-
-
-> [!NOTE]
-> Custom themes are not the main priority for this repository, but will remain supported for use. Visit Florian's Discord server to get access to more themes like the ones shown from above.
-
-You can use the built-in dark / light theme. You can also create custom themes that fit your liking by going to the `%appdata%/DynamicWin/Theme.json` file. After editing the colors you need to select the `Custom` theme option in the settings. If you already did that, you will need to go back to the settings and click on it again. Otherwise you would have to restart the app.
-This is an example of a color:
-`"IslandColor": "#000000"`
-
-The hex code is structured this way: `#rrggbb`. If you want to change the alpha of the color, it is **always** at the start of the code. `#aarrggbb`.
-
-# Known Issues
-The performance might not be the best. Slowly expect codebase optimisations starting with **`v1.4.0b`**.
-The app might suddenly disappear and upon trying to reopen it a message box will tell you that only one instance of the app can run at the same time. To fix this, open task manager and find the process `DynamicWin`. Kill it and start the app again.
+## Custom themes
+> [!NOTE]
+> Custom themes are not the main priority for this repository, but will remain supported for use. Visit FlorianButz's [Discord server](https://discord.gg/UHFuqB9NqR) to get access to more themes.
+- Read [THEMING.md](THEMING.md) to get started on decorating your interface.
-Too fast interactions might confuse the animation system and will result in an empty menu. To fix this, usually moving the mouse away from the island and then over it again will fix it.
+## Modding DynamicWin-Legacy (making extensions)
-# Modding DynamicWin (making Extensions)
-- While extension support and compatibility is not a focus for the maintainer, users are still able to make their own extensions as needed.
- Read [MODDING.md](MODDING.md) for more information on how to get started.
diff --git a/ReadmeFiles/IslandGif-1_Volume.gif b/ReadmeFiles/IslandGif-1_Volume.gif
deleted file mode 100644
index 63b9af2..0000000
Binary files a/ReadmeFiles/IslandGif-1_Volume.gif and /dev/null differ
diff --git a/ReadmeFiles/IslandGif-2_Tray.gif b/ReadmeFiles/IslandGif-2_Tray.gif
deleted file mode 100644
index 0e5acb4..0000000
Binary files a/ReadmeFiles/IslandGif-2_Tray.gif and /dev/null differ
diff --git a/ReadmeFiles/IslandGif-3_Spotify.gif b/ReadmeFiles/IslandGif-3_Spotify.gif
deleted file mode 100644
index 1a31384..0000000
Binary files a/ReadmeFiles/IslandGif-3_Spotify.gif and /dev/null differ
diff --git a/ReadmeFiles/Themes.png b/ReadmeFiles/Themes.png
deleted file mode 100644
index 026b7f4..0000000
Binary files a/ReadmeFiles/Themes.png and /dev/null differ
diff --git a/THEMING.md b/THEMING.md
new file mode 100644
index 0000000..a0ac1a9
--- /dev/null
+++ b/THEMING.md
@@ -0,0 +1,15 @@
+# Theming and customising your interface
+
+
+
+
+
+> [!NOTE]
+> Custom themes are not the main priority for this repository, but will remain supported for use. Visit FlorianButz's [Discord server](https://discord.gg/UHFuqB9NqR) to get access to more themes like the ones shown from above.
+
+You can use the built-in dark / light theme. You can also create custom themes that fit your liking by going to the `%appdata%/DynamicWin/Theme.json` file. After editing the colors you need to select the `Custom` theme option in the settings. If you already did that, you will need to go back to the settings and click on it again. Otherwise you would have to restart the app.
+
+This is an example of a color:
+`"IslandColor": "#000000"`
+
+The hex code is structured this way: `#rrggbb`. If you want to change the alpha of the color, it is **always** at the start of the code. `#aarrggbb`.
\ No newline at end of file
diff --git a/readme-files/media.gif b/readme-files/media.gif
new file mode 100644
index 0000000..505f7ee
Binary files /dev/null and b/readme-files/media.gif differ
diff --git a/readme-files/themes.png b/readme-files/themes.png
new file mode 100644
index 0000000..fe336c6
Binary files /dev/null and b/readme-files/themes.png differ
diff --git a/readme-files/tray.gif b/readme-files/tray.gif
new file mode 100644
index 0000000..67c43c4
Binary files /dev/null and b/readme-files/tray.gif differ
diff --git a/readme-files/volume.gif b/readme-files/volume.gif
new file mode 100644
index 0000000..cc63479
Binary files /dev/null and b/readme-files/volume.gif differ