diff --git a/CHANGELOG.md b/CHANGELOG.md index 56aff85c..dfcbd8bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to Chromatics are documented here. +## 4.3.29 + +- Added a Black layer type, available as both a base layer and a dynamic layer. +- Layers saved for a disabled or disconnected device can now be copied to another device. +- New setting under Settings → General: lower the RGB refresh rate while FFXIV is not running. +- Fixed the update prompt showing the newest version's heading twice in its changelog. + ## 4.3.24 - **New:** Nanoleaf smart-light support (Beta). Covers the panel family (Shapes, Canvas, Elements, Lines, Aurora) and other controllers that speak the Nanoleaf OpenAPI. Enable it from Settings → Device Providers and pair each controller with a one-time button press; effects render across the panels at their real physical positions. Layer assignments stay with their panels if you add or remove panels from the wall later. Essentials bulbs and strips are not supported. diff --git a/Chromatics.Tests/Helpers/LayerCopierTests.cs b/Chromatics.Tests/Helpers/LayerCopierTests.cs new file mode 100644 index 00000000..d881113a --- /dev/null +++ b/Chromatics.Tests/Helpers/LayerCopierTests.cs @@ -0,0 +1,80 @@ +using Chromatics.Helpers; +using RGB.NET.Core; + +namespace Chromatics.Tests.Helpers; + +// Pins the default LedId mapping used when copying layers from a device +// that is disabled or no longer connected: the layer's own LedId set +// stands in for the missing device, so these rules decide where each key +// lands on the destination before the user touches the override dropdowns. +public class LayerCopierTests +{ + [Fact] + public void KeyboardPair_MapsIdentity_OnlyWhereDestinationHasTheKey() + { + var used = new[] { LedId.Keyboard_A, LedId.Keyboard_B, LedId.Keyboard_NumLock }; + var destIds = new List { LedId.Keyboard_A, LedId.Keyboard_B }; // no numpad + + var map = LayerCopier.ComputeDefaultMappingForLayer( + used, RGBDeviceType.Keyboard, RGBDeviceType.Keyboard, destIds); + + Assert.Equal(LedId.Keyboard_A, map[LedId.Keyboard_A]); + Assert.Equal(LedId.Keyboard_B, map[LedId.Keyboard_B]); + // Keyboard pairs never fall back positionally: a missing key is + // dropped so it can't land on an unrelated physical key. + Assert.False(map.ContainsKey(LedId.Keyboard_NumLock)); + } + + [Fact] + public void NonKeyboard_ExactIdMatchWins() + { + var used = new[] { LedId.Custom1, LedId.Custom2 }; + var destIds = new List { LedId.Custom1, LedId.Custom2, LedId.Custom3 }; + + var map = LayerCopier.ComputeDefaultMappingForLayer( + used, RGBDeviceType.LedStripe, RGBDeviceType.LedStripe, destIds); + + Assert.Equal(LedId.Custom1, map[LedId.Custom1]); + Assert.Equal(LedId.Custom2, map[LedId.Custom2]); + } + + [Fact] + public void NonKeyboard_OrdinalFallback_WhenIdsDoNotOverlap() + { + // Source painted Custom5..Custom7; destination only has Custom1..Custom3. + var used = new[] { LedId.Custom5, LedId.Custom6, LedId.Custom7 }; + var destIds = new List { LedId.Custom1, LedId.Custom2, LedId.Custom3 }; + + var map = LayerCopier.ComputeDefaultMappingForLayer( + used, RGBDeviceType.LedStripe, RGBDeviceType.Mouse, destIds); + + Assert.Equal(LedId.Custom1, map[LedId.Custom5]); + Assert.Equal(LedId.Custom2, map[LedId.Custom6]); + Assert.Equal(LedId.Custom3, map[LedId.Custom7]); + } + + [Fact] + public void NonKeyboard_SourceLargerThanDestination_DropsTheTail() + { + var used = new[] { LedId.Custom5, LedId.Custom6, LedId.Custom7 }; + var destIds = new List { LedId.Custom1 }; + + var map = LayerCopier.ComputeDefaultMappingForLayer( + used, RGBDeviceType.LedStripe, RGBDeviceType.LedStripe, destIds); + + Assert.Equal(LedId.Custom1, map[LedId.Custom5]); + Assert.False(map.ContainsKey(LedId.Custom6)); + Assert.False(map.ContainsKey(LedId.Custom7)); + } + + [Fact] + public void EmptyOrMissingInputs_ReturnEmptyMap() + { + Assert.Empty(LayerCopier.ComputeDefaultMappingForLayer( + Array.Empty(), RGBDeviceType.Keyboard, RGBDeviceType.Keyboard, new List { LedId.Keyboard_A })); + Assert.Empty(LayerCopier.ComputeDefaultMappingForLayer( + null, RGBDeviceType.Keyboard, RGBDeviceType.Keyboard, new List())); + Assert.Empty(LayerCopier.ComputeDefaultMappingForLayer( + new[] { LedId.Keyboard_A }, RGBDeviceType.Keyboard, RGBDeviceType.Keyboard, null)); + } +} diff --git a/Chromatics.Tests/Helpers/UpdateNotesTests.cs b/Chromatics.Tests/Helpers/UpdateNotesTests.cs new file mode 100644 index 00000000..bfccdff2 --- /dev/null +++ b/Chromatics.Tests/Helpers/UpdateNotesTests.cs @@ -0,0 +1,62 @@ +using Chromatics.Helpers; + +namespace Chromatics.Tests.Helpers; + +// Pins the release-notes trimming behind the update dialog. Notes embedded +// by older publishes start with their own version heading and carry every +// older changelog section; the dialog adds its own heading per release, so +// rendering them raw doubled the version line and repeated old sections. +public class UpdateNotesTests +{ + [Fact] + public void OldFormatNotes_ReduceToOwnBullets() + { + var notes = "## 4.3.26\n\n- New feature one.\n- Fix two.\n\n## 4.3.24\n\n- Old bullet.\n"; + + var trimmed = UpdateService.TrimNotesToOwnSection(notes, "4.3.26"); + + Assert.Equal("- New feature one.\n- Fix two.", trimmed.Replace("\r\n", "\n")); + } + + [Fact] + public void NewFormatNotes_BulletsOnly_PassThroughUnchanged() + { + var notes = "- New feature one.\n- Fix two."; + + var trimmed = UpdateService.TrimNotesToOwnSection(notes, "4.3.26"); + + Assert.Equal(notes, trimmed.Replace("\r\n", "\n")); + } + + [Theory] + [InlineData("## 4.3.26.0\n\n- Bullet.")] + [InlineData("## [4.3.26]\n\n- Bullet.")] + [InlineData("## v4.3.26\n\n- Bullet.")] + [InlineData("## 4.3.26 - 2026-07-07\n\n- Bullet.")] + public void HeadingVariants_AreAllStripped(string notes) + { + var trimmed = UpdateService.TrimNotesToOwnSection(notes, "4.3.26"); + + Assert.Equal("- Bullet.", trimmed); + } + + [Fact] + public void DifferentVersionHeading_IsNotStripped_ButLaterSectionsAreCut() + { + // A mislabelled asset should not lose its first line; only the + // trailing sections get cut. + var notes = "- Bullet without heading.\n\n## 4.3.20\n\n- Old."; + + var trimmed = UpdateService.TrimNotesToOwnSection(notes, "4.3.26"); + + Assert.Equal("- Bullet without heading.", trimmed.Replace("\r\n", "\n")); + } + + [Fact] + public void EmptyOrWhitespaceNotes_ReturnEmpty() + { + Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection("", "4.3.26")); + Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection(" ", "4.3.26")); + Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection(null, "4.3.26")); + } +} diff --git a/Chromatics/Chromatics.csproj b/Chromatics/Chromatics.csproj index 16e67a91..816fe59d 100644 --- a/Chromatics/Chromatics.csproj +++ b/Chromatics/Chromatics.csproj @@ -10,7 +10,7 @@ net10.0-windows10.0.19041.0 10.0.17763.0 Chromatics.Program - 4.3.24.0 + 4.3.29.0 Danielle Thompson - + + + + @@ -48,7 +56,7 @@ diff --git a/Chromatics/Views/Dialogs/UpdateDialog.axaml.cs b/Chromatics/Views/Dialogs/UpdateDialog.axaml.cs index 476155fc..b16352e9 100644 --- a/Chromatics/Views/Dialogs/UpdateDialog.axaml.cs +++ b/Chromatics/Views/Dialogs/UpdateDialog.axaml.cs @@ -69,12 +69,20 @@ void AddIfNotes(Velopack.SemanticVersion? v, string? notes) var sb = new StringBuilder(); for (int i = 0; i < versions.Count; i++) { - if (i > 0) sb.AppendLine().AppendLine("---").AppendLine(); + // TrimNotesToOwnSection keeps each entry to its own bullets: + // notes from older publishes carry their own version heading + // plus every older section, which doubled the version line + // and repeated content across entries. + var body = UpdateService.TrimNotesToOwnSection( + versions[i].Notes, versions[i].Version.ToString()); + if (string.IsNullOrWhiteSpace(body)) continue; + + if (sb.Length > 0) sb.AppendLine().AppendLine("---").AppendLine(); sb.Append("## ").AppendLine(versions[i].Version.ToString()); sb.AppendLine(); - sb.AppendLine(versions[i].Notes.Trim()); + sb.AppendLine(body); } - return sb.ToString(); + return sb.Length > 0 ? sb.ToString() : "(No release notes provided.)"; } private async void OnInstall(object sender, RoutedEventArgs e) diff --git a/Chromatics/Views/NanoleafAdoptionDialog.axaml b/Chromatics/Views/NanoleafAdoptionDialog.axaml index 2ec6afef..30bb2c54 100644 --- a/Chromatics/Views/NanoleafAdoptionDialog.axaml +++ b/Chromatics/Views/NanoleafAdoptionDialog.axaml @@ -70,7 +70,7 @@