diff --git a/EarTrumpet.Tests/EarTrumpet.Tests.csproj b/EarTrumpet.Tests/EarTrumpet.Tests.csproj
new file mode 100644
index 000000000..8cb2e8ebc
--- /dev/null
+++ b/EarTrumpet.Tests/EarTrumpet.Tests.csproj
@@ -0,0 +1,20 @@
+
+
+ net8.0-windows10.0.19041.0
+ true
+ false
+ x64
+ x64
+ win-x64
+ false
+ ..\Build\Tests\
+
+
+
+
+
+
+
+
+
+
diff --git a/EarTrumpet.Tests/LocalizationTests.cs b/EarTrumpet.Tests/LocalizationTests.cs
new file mode 100644
index 000000000..a110cc2d6
--- /dev/null
+++ b/EarTrumpet.Tests/LocalizationTests.cs
@@ -0,0 +1,61 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+using Xunit;
+
+namespace EarTrumpet.Tests;
+
+// Guards the "no hardcoded text" rule for the settings UI: every resource XAML points at must exist,
+// and the strings this redesign introduced must be translated for the two supported languages.
+public class LocalizationTests
+{
+ private static readonly string s_project = FindProjectDir();
+
+ private static string FindProjectDir()
+ {
+ var dir = new DirectoryInfo(AppContext.BaseDirectory);
+ while (dir != null && !File.Exists(Path.Combine(dir.FullName, "EarTrumpet", "EarTrumpet.csproj")))
+ {
+ dir = dir.Parent;
+ }
+ return Path.Combine(dir?.FullName ?? throw new InvalidOperationException("repo root not found"), "EarTrumpet");
+ }
+
+ private static string[] Keys(string resx)
+ => XDocument.Load(Path.Combine(s_project, "Properties", resx)).Root.Elements("data").Select(e => (string)e.Attribute("name")).ToArray();
+
+ private static string[] XamlResourceRefs()
+ => Directory.GetFiles(s_project, "*.xaml", SearchOption.AllDirectories)
+ .Where(f => !f.Contains(Path.DirectorySeparatorChar + "obj" + Path.DirectorySeparatorChar))
+ .SelectMany(f => Regex.Matches(File.ReadAllText(f), @"resx:Resources\.(\w+)").Select(m => m.Groups[1].Value))
+ .Distinct().ToArray();
+
+ [Fact]
+ public void EveryXamlResourceExistsInDefaultResx()
+ {
+ var keys = Keys("Resources.resx");
+ Assert.Empty(XamlResourceRefs().Except(keys));
+ }
+
+ [Fact]
+ public void EveryDesignerPropertyExistsInDefaultResx()
+ {
+ var designer = File.ReadAllText(Path.Combine(s_project, "Properties", "Resources.Designer.cs"));
+ var props = Regex.Matches(designer, @"public static string (\w+) \{").Select(m => m.Groups[1].Value);
+ Assert.Empty(props.Except(Keys("Resources.resx")));
+ }
+
+ [Fact]
+ public void SettingsRedesignStringsAreTranslatedToSpanish()
+ {
+ var es = Keys("Resources.es-ES.resx");
+ var redesign = Keys("Resources.resx").Where(k =>
+ k == "GeneralSettingsPageText" || k.StartsWith("SettingsSection") ||
+ (k.StartsWith("Settings") && (k.EndsWith("Description") || k.Contains("Volume") || k.Contains("Snap") ||
+ k.Contains("ScrollWheel") || k.Contains("Translucent") || k.Contains("Legacy")))).ToArray();
+ Assert.NotEmpty(redesign);
+ Assert.Empty(redesign.Except(es));
+ }
+}
diff --git a/EarTrumpet.Tests/VolumeMathTests.cs b/EarTrumpet.Tests/VolumeMathTests.cs
new file mode 100644
index 000000000..c9c3496e5
--- /dev/null
+++ b/EarTrumpet.Tests/VolumeMathTests.cs
@@ -0,0 +1,68 @@
+using EarTrumpet.DataModel;
+using EarTrumpet.Extensions;
+using EarTrumpet.UI.Helpers;
+using Xunit;
+
+namespace EarTrumpet.Tests;
+
+public class VolumeMathTests
+{
+ [Theory]
+ [InlineData(0, 5, 5)]
+ [InlineData(3, 5, 5)]
+ [InlineData(5, 5, 10)]
+ [InlineData(98, 5, 100)]
+ [InlineData(100, 5, 100)]
+ public void NextSnapPoint_LandsOnGridAboveAndNeverExceeds100(double current, int step, double expected)
+ => Assert.Equal(expected, VolumeStepper.NextSnapPoint(current, step));
+
+ [Theory]
+ [InlineData(100, 5, 95)]
+ [InlineData(7, 5, 5)]
+ [InlineData(5, 5, 0)]
+ [InlineData(2, 5, 0)]
+ [InlineData(0, 5, 0)]
+ public void PreviousSnapPoint_LandsOnGridBelowAndNeverGoesNegative(double current, int step, double expected)
+ => Assert.Equal(expected, VolumeStepper.PreviousSnapPoint(current, step));
+
+ [Theory]
+ [InlineData(12, 5, 10)]
+ [InlineData(13, 5, 15)]
+ [InlineData(99.6, 3, 100)] // 100 stays reachable when it is not on the grid
+ [InlineData(1, 3, 0)]
+ public void NearestSnapPoint_RoundsToGridButKeepsFullVolumeReachable(double current, int step, double expected)
+ => Assert.Equal(expected, VolumeStepper.NearestSnapPoint(current, step));
+
+ [Fact]
+ public void Bound_ClampsBothEnds()
+ {
+ Assert.Equal(0d, (-5d).Bound(0, 1));
+ Assert.Equal(1d, 5d.Bound(0, 1));
+ Assert.Equal(0.5d, 0.5d.Bound(0, 1));
+ }
+
+ [Theory]
+ [InlineData(1.0)]
+ [InlineData(0.5)]
+ [InlineData(0.01)]
+ public void LogarithmicConversion_RoundTrips(double linear)
+ => Assert.Equal(linear, linear.LinearToLog().LogToLinear(), 9);
+
+ [Fact]
+ public void FullVolumeIs0Db()
+ => Assert.Equal(0d, 1d.LinearToLog(), 9);
+
+ [Fact]
+ public void ForceOpaque_DisablesTransparencyRegardlessOfWindows()
+ {
+ try
+ {
+ SystemSettings.ForceOpaque = true;
+ Assert.False(SystemSettings.IsTransparencyEnabled);
+ }
+ finally
+ {
+ SystemSettings.ForceOpaque = false;
+ }
+ }
+}
diff --git a/EarTrumpet.sln b/EarTrumpet.sln
index 873269e34..f4584c5f1 100644
--- a/EarTrumpet.sln
+++ b/EarTrumpet.sln
@@ -25,6 +25,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Chocolatey", "Chocolatey",
EndProject
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "EarTrumpet.Package", "EarTrumpet.Package\EarTrumpet.Package.wapproj", "{EA5510ED-F014-4587-A505-64C59D5B2627}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EarTrumpet.Tests", "EarTrumpet.Tests\EarTrumpet.Tests.csproj", "{BA949B87-C0DF-4986-99CB-30458590F3FF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -83,6 +85,24 @@ Global
{EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x86.ActiveCfg = Debug|x86
{EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x86.Build.0 = Debug|x86
{EA5510ED-F014-4587-A505-64C59D5B2627}.VSDebug|x86.Deploy.0 = Debug|x86
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.Debug|ARM64.ActiveCfg = Debug|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.Debug|ARM64.Build.0 = Debug|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.Debug|x64.ActiveCfg = Debug|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.Debug|x64.Build.0 = Debug|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.Debug|x86.ActiveCfg = Debug|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.Debug|x86.Build.0 = Debug|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.Release|ARM64.ActiveCfg = Release|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.Release|ARM64.Build.0 = Release|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.Release|x64.ActiveCfg = Release|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.Release|x64.Build.0 = Release|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.Release|x86.ActiveCfg = Release|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.Release|x86.Build.0 = Release|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.VSDebug|ARM64.ActiveCfg = Debug|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.VSDebug|ARM64.Build.0 = Debug|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.VSDebug|x64.ActiveCfg = Debug|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.VSDebug|x64.Build.0 = Debug|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.VSDebug|x86.ActiveCfg = Debug|x64
+ {BA949B87-C0DF-4986-99CB-30458590F3FF}.VSDebug|x86.Build.0 = Debug|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/ActionsCategoryViewModel.cs b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/ActionsCategoryViewModel.cs
index 358b16c78..f07866a80 100644
--- a/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/ActionsCategoryViewModel.cs
+++ b/EarTrumpet/Addons/EarTrumpet.Actions/ViewModel/ActionsCategoryViewModel.cs
@@ -40,11 +40,6 @@ public ActionsCategoryViewModel()
Glyph = "\xE948",
GlyphFontSize = 15,
} ];
-
- if (Pages.Count == 2)
- {
- Toolbar[0].Command.Execute(null);
- }
}
internal void ReloadSavedPages()
diff --git a/EarTrumpet/App.xaml b/EarTrumpet/App.xaml
index fb57d8fd4..3988fa609 100644
--- a/EarTrumpet/App.xaml
+++ b/EarTrumpet/App.xaml
@@ -588,20 +588,21 @@
-
+
-
+
-
+ BorderThickness="1"
+ CornerRadius="8">
@@ -611,12 +612,12 @@
-
+
-
+
@@ -626,7 +627,7 @@
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -410,13 +361,15 @@
-
+
+
+
+