diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..4baaf616 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +## Summary + +## Checklist +- [ ] Tests passed +- [ ] Changes validated (manually or automated) +- [ ] Closes / Fixes #xxx (if applicable) + +## Additional details / comments + +## Manual test steps (if applicable) \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..89ce0b91 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at brndnchong@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..51e0402b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Intro +Thanks for taking the time to read this! +This doc contains guidelines and resources for contributing. It's not too complete but I'll add to it. + +# Ways to contribute +## Reporting Bugs +Feel free to report bugs in the [issues tracker](https://github.com/dsafa/audio-band/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc). +There are templates when creating a new issue to help you get started. Try to search for existing bugs first, the keyword search is quite good. + +## Suggest Features +You can also suggest features and improvements in the issue tracker as well. + +## Development +Development docs are on the [github page](https://dsafa.github.io/audio-band/audioband/development/setup.html) for this repo. +The site will have instructions for setting up a build environment including instructions on how to debug a local setup as well as +an overview of the project itself. + +I try to keep it as up to date as possible but like all docs, they tend to get outdated. +If something is out of date feel free to raise an issue about it. diff --git a/src/AudioBand.Test/AudioBand.Test.csproj b/src/AudioBand.Test/AudioBand.Test.csproj index b4dca433..2af20833 100644 --- a/src/AudioBand.Test/AudioBand.Test.csproj +++ b/src/AudioBand.Test/AudioBand.Test.csproj @@ -91,9 +91,18 @@ C:\Users\bdonc\.nuget\packages\unosquare.swan.lite\0.38.1\lib\net452\Unosquare.Swan.Lite.dll + - + + + + + + + + + diff --git a/src/AudioBand.Test/AudioBand.Test.csproj.DotSettings b/src/AudioBand.Test/AudioBand.Test.csproj.DotSettings index bf95c7ff..868fb131 100644 --- a/src/AudioBand.Test/AudioBand.Test.csproj.DotSettings +++ b/src/AudioBand.Test/AudioBand.Test.csproj.DotSettings @@ -1,5 +1,6 @@  True True + True True True \ No newline at end of file diff --git a/src/AudioBand.Test/Settings/SettingsMigrationTests.cs b/src/AudioBand.Test/Settings/SettingsMigrationTests.cs index 9c28c433..39fd35b1 100644 --- a/src/AudioBand.Test/Settings/SettingsMigrationTests.cs +++ b/src/AudioBand.Test/Settings/SettingsMigrationTests.cs @@ -9,7 +9,7 @@ using V2Settings = AudioBand.Settings.Models.V2.Settings; using Nett; using Xunit; -using AudioSourceSetting = AudioBand.Settings.Models.V1.AudioSourceSetting; +using V1AudioSourceSetting = AudioBand.Settings.Models.V1.AudioSourceSetting; namespace AudioBand.Test { @@ -137,13 +137,13 @@ public void MigrateV1ToV2_AudioSourceSettings() var setting1 = new AudioSourceSettingsCollection { Name = "test", - Settings = new List {new AudioSourceSetting {Name = "key1", Value = "val1"}} + Settings = new List {new V1AudioSourceSetting {Name = "key1", Value = "val1"}} }; var setting2 = new AudioSourceSettingsCollection { Name = "test2", - Settings = new List { new AudioSourceSetting { Name = "key2", Value = "val2" } } + Settings = new List { new V1AudioSourceSetting { Name = "key2", Value = "val2" } } }; var settings = new List {setting1,setting2}; @@ -415,17 +415,7 @@ public void MigrateV2ToV3_MigratesSuccessfully() Name = ""Spotify Client secret"" Value = ""secret"" "; - var settings = TomlSettings.Create(cfg => - { - cfg.ConfigureType(type => type.WithConversionFor(convert => convert - .ToToml(SerializationConversions.ColorToString) - .FromToml(tomlString => SerializationConversions.StringToColor(tomlString.Value)))); - cfg.ConfigureType(type => type.WithConversionFor(convert => convert - .ToToml(SerializationConversions.EnumToString) - .FromToml(str => SerializationConversions.StringToEnum(str.Value)))); - cfg.ConfigureType(type => type.WithConversionFor(c => c - .FromToml(tml => tml.Value))); - }); + var settings = TomlHelper.DefaultSettings; var v2 = Toml.ReadString(settingsFile, settings); var v3 = Migration.MigrateSettings(v2, "2", "3"); @@ -499,5 +489,119 @@ public void MigrateV2ToV3_MigratesSuccessfully() Assert.Equal(v2.CustomLabelSettings[0].Alignment, v3.Profiles[SettingsV3.DefaultProfileName].CustomLabelSettings[0].Alignment); Assert.Equal(v2.CustomLabelSettings[0].FormatString, v3.Profiles[SettingsV3.DefaultProfileName].CustomLabelSettings[0].FormatString); } + + [Fact] + public void ChainedMigrations_V1ToV3_SuccessfulMigration() + { + var v1Settings = @" +Version = ""0.1"" + +[AudioBandAppearance] +Width = 300 +Height = 30 + +[PlayPauseButtonAppearance] +XPosition = 0 +YPosition = 0 +Width = 30 +Height = 10 +IsVisible = true + +[NextSongButtonAppearance] +IsVisible = true +Width = 30 +Height = 10 +XPosition = 0 +YPosition = 0 + +[PreviousSongButtonAppearance] +IsVisible = true +Width = 30 +Height = 10 +XPosition = 0 +YPosition = 0 + +[[TextAppearances]] +IsVisible = true +Width = 100 +Height = 15 +XPosition = 150 +YPosition = 10 +FontSize = 10.0 +Color = ""White"" +Alignment = ""Center"" +ScrollSpeed = 0 +FormatString = ""{song}"" + +[ProgressBarAppearance] +ForegroundColor = ""Blue"" +BackgroundColor = ""Gray"" +IsVisible = true +XPosition = 0 +YPosition = 26 +Width = 200 +Height = 2 + +[AlbumArtAppearance] +IsVisible = true +Width = 30 +Height = 30 +XPosition = 0 +YPosition = 0 + +[AlbumArtPopupAppearance] +IsVisible = true +Width = 500 +Height = 500 +XOffset = 50 +Margin = 6 + +"; + var v1 = Toml.ReadString(v1Settings, TomlHelper.DefaultSettings); + var v3 = Migration.MigrateSettings(v1, "0.1", "3"); + var v3Profile = v3.Profiles[SettingsV3.DefaultProfileName]; + + Assert.Equal(v1.AudioBandAppearance.Width, v3Profile.AudioBandSettings.Width); + Assert.Equal(v1.AudioBandAppearance.Height, v3Profile.AudioBandSettings.Height); + + Assert.Equal(v1.PlayPauseButtonAppearance.Width, v3Profile.PlayPauseButtonSettings.Width); + Assert.Equal(v1.PlayPauseButtonAppearance.Height, v3Profile.PlayPauseButtonSettings.Height); + Assert.Equal(v1.PlayPauseButtonAppearance.XPosition, v3Profile.PlayPauseButtonSettings.XPosition); + Assert.Equal(v1.PlayPauseButtonAppearance.YPosition, v3Profile.PlayPauseButtonSettings.YPosition); + Assert.Equal(v1.PlayPauseButtonAppearance.IsVisible, v3Profile.PlayPauseButtonSettings.IsVisible); + + Assert.Equal(v1.NextSongButtonAppearance.Width, v3Profile.NextButtonSettings.Width); + Assert.Equal(v1.NextSongButtonAppearance.Height, v3Profile.NextButtonSettings.Height); + Assert.Equal(v1.NextSongButtonAppearance.XPosition, v3Profile.NextButtonSettings.XPosition); + Assert.Equal(v1.NextSongButtonAppearance.YPosition, v3Profile.NextButtonSettings.YPosition); + Assert.Equal(v1.NextSongButtonAppearance.IsVisible, v3Profile.NextButtonSettings.IsVisible); + + Assert.Equal(v1.PreviousSongButtonAppearance.Width, v3Profile.PreviousButtonSettings.Width); + Assert.Equal(v1.PreviousSongButtonAppearance.Height, v3Profile.PreviousButtonSettings.Height); + Assert.Equal(v1.PreviousSongButtonAppearance.XPosition, v3Profile.PreviousButtonSettings.XPosition); + Assert.Equal(v1.PreviousSongButtonAppearance.YPosition, v3Profile.PreviousButtonSettings.YPosition); + Assert.Equal(v1.PreviousSongButtonAppearance.IsVisible, v3Profile.PreviousButtonSettings.IsVisible); + + Assert.Single(v3Profile.CustomLabelSettings); + Assert.Equal(v1.TextAppearances[0].Color, v3Profile.CustomLabelSettings[0].Color); + Assert.Equal(v1.TextAppearances[0].Alignment, v3Profile.CustomLabelSettings[0].Alignment); + Assert.Equal(v1.TextAppearances[0].FontFamily, v3Profile.CustomLabelSettings[0].FontFamily); + Assert.Equal(v1.TextAppearances[0].FontSize, v3Profile.CustomLabelSettings[0].FontSize); + Assert.Equal(v1.TextAppearances[0].FormatString, v3Profile.CustomLabelSettings[0].FormatString); + Assert.Equal(v1.TextAppearances[0].Height, v3Profile.CustomLabelSettings[0].Height); + Assert.Equal(v1.TextAppearances[0].IsVisible, v3Profile.CustomLabelSettings[0].IsVisible); + Assert.Equal(v1.TextAppearances[0].Name, v3Profile.CustomLabelSettings[0].Name); + Assert.Equal(v1.TextAppearances[0].ScrollSpeed, v3Profile.CustomLabelSettings[0].ScrollSpeed); + Assert.Equal(v1.TextAppearances[0].Width, v3Profile.CustomLabelSettings[0].Width); + Assert.Equal(v1.TextAppearances[0].XPosition, v3Profile.CustomLabelSettings[0].XPosition); + Assert.Equal(v1.TextAppearances[0].YPosition, v3Profile.CustomLabelSettings[0].YPosition); + + Assert.Equal(v1.AlbumArtAppearance.Height, v3Profile.AlbumArtSettings.Height); + Assert.Equal(v1.AlbumArtAppearance.IsVisible, v3Profile.AlbumArtSettings.IsVisible); + Assert.Equal(v1.AlbumArtAppearance.PlaceholderPath, v3Profile.AlbumArtSettings.PlaceholderPath); + Assert.Equal(v1.AlbumArtAppearance.Width, v3Profile.AlbumArtSettings.Width); + Assert.Equal(v1.AlbumArtAppearance.XPosition, v3Profile.AlbumArtSettings.XPosition); + Assert.Equal(v1.AlbumArtAppearance.YPosition, v3Profile.AlbumArtSettings.YPosition); + } } } diff --git a/src/AudioBand.Test/ValueConverters/BoolToVisibilityConverterTests.cs b/src/AudioBand.Test/ValueConverters/BoolToVisibilityConverterTests.cs new file mode 100644 index 00000000..e2cce975 --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/BoolToVisibilityConverterTests.cs @@ -0,0 +1,101 @@ +using System.Globalization; +using System.Windows; +using AudioBand.ValueConverters; +using Xunit; +using Visibility = System.Windows.Visibility; + +namespace AudioBand.Test +{ + public class BoolToVisibilityConverterTests + { + private readonly BoolToVisibilityConverter _converter = new BoolToVisibilityConverter(); + + [Fact] + public void ConvertTrue_ReturnsVisible() + { + var result = ConvertToVisibility(true, null); + Assert.Equal(Visibility.Visible, result); + } + + [Fact] + public void ConvertFalse_ParameterNull_ReturnsCollapsed() + { + var result = ConvertToVisibility(false, null); + Assert.Equal(Visibility.Collapsed, result); + } + + [Fact] + public void ConvertFalse_ParameterTrue_ReturnsCollapsed() + { + var result = ConvertToVisibility(false, true); + Assert.Equal(Visibility.Collapsed, result); + } + + [Fact] + public void ConvertFalse_ParameterFalse_ReturnsHidden() + { + var result = ConvertToVisibility(false, false); + Assert.Equal(Visibility.Hidden, result); + } + + [Fact] + public void ConvertNull_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.Convert(null, typeof(Visibility), null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void ConvertNonBool_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.Convert(null, typeof(Visibility), null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void ConvertBackVisible_ReturnsTrue() + { + Assert.True(ConvertToBool(Visibility.Visible)); + } + + [Fact] + public void ConvertBackCollapsed_ReturnsFalse() + { + Assert.False(ConvertToBool(Visibility.Collapsed)); + } + + [Fact] + public void ConvertBackHidden_ReturnsFalse() + { + Assert.False(ConvertToBool(Visibility.Hidden)); + } + + [Fact] + public void ConvertBackNull_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.ConvertBack(null, typeof(bool), null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void ConvertBackNonVisibility_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.ConvertBack("invalid", typeof(bool), null, CultureInfo.CurrentCulture)); + } + + private Visibility ConvertToVisibility(bool value, object parameter) + { + var result = _converter.Convert(value, typeof(Visibility), parameter, CultureInfo.CurrentCulture); + + Assert.IsType(result); + + return (Visibility)result; + } + + private bool ConvertToBool(Visibility visibility) + { + var result = _converter.ConvertBack(visibility, typeof(bool), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + + return (bool)result; + } + } +} diff --git a/src/AudioBand.Test/ValueConverters/ColorToBrushConverterTests.cs b/src/AudioBand.Test/ValueConverters/ColorToBrushConverterTests.cs new file mode 100644 index 00000000..588f598d --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/ColorToBrushConverterTests.cs @@ -0,0 +1,45 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Media; +using AudioBand.ValueConverters; +using Xunit; + +namespace AudioBand.Test +{ + public class ColorToBrushConverterTests + { + private readonly ColorToBrushConverter _converter = new ColorToBrushConverter(); + + [Fact] + public void ConvertColor_ReturnsSolidColorBrushWithSameColor() + { + var color = Colors.Red; + var result = _converter.Convert(color, typeof(SolidColorBrush), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(color, ((SolidColorBrush)result).Color); + } + + [Fact] + public void ConvertNull_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.Convert(null, typeof(SolidColorBrush), null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void ConvertBackSolidColorBrush_ReturnsColorWithSameValue() + { + var brush = Brushes.Blue; + var result = _converter.ConvertBack(brush, typeof(Color), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(brush.Color, (Color)result); + } + + [Fact] + public void ConvertBackNull_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.ConvertBack(null, typeof(Color), null, CultureInfo.CurrentCulture)); + } + } +} diff --git a/src/AudioBand.Test/ValueConverters/FlagToBoolConverterTests.cs b/src/AudioBand.Test/ValueConverters/FlagToBoolConverterTests.cs new file mode 100644 index 00000000..2eb7412a --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/FlagToBoolConverterTests.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using AudioBand.ValueConverters; +using Xunit; + +namespace AudioBand.Test +{ + public class FlagToBoolConverterTests + { + private readonly FlagToBoolConverter _converter = new FlagToBoolConverter(); + + [Flags] + private enum TestEnum + { + None = 0, + Flag1 = 1, + Flag2 = 2, + } + + [Fact] + public void ConvertNoFlag_ReturnsFalse() + { + var result = _converter.Convert(TestEnum.None, typeof(bool), TestEnum.Flag2, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.False((bool)result); + } + + [Fact] + public void ConvertHasFlag_ReturnsTrue() + { + var value = TestEnum.Flag1 | TestEnum.Flag2; + var result = _converter.Convert(value, typeof(bool), TestEnum.Flag2, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.True((bool)result); + } + + [Fact] + public void ConvertNull_ReturnsFalse() + { + Assert.False((bool)_converter.Convert(null, typeof(bool), null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void ConvertBack_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.ConvertBack(null, typeof(Enum), null, CultureInfo.CurrentCulture)); + } + } +} diff --git a/src/AudioBand.Test/ValueConverters/MultiplyConverterTests.cs b/src/AudioBand.Test/ValueConverters/MultiplyConverterTests.cs new file mode 100644 index 00000000..1b52614f --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/MultiplyConverterTests.cs @@ -0,0 +1,46 @@ +using System.Globalization; +using AudioBand.ValueConverters; +using Xunit; + +namespace AudioBand.Test +{ + public class MultiplyConverterTests + { + private readonly MultiplierConverter _converter = new MultiplierConverter(); + + [Fact] + public void ConvertSingleValue_ReturnsValue() + { + var result = _converter.Convert(new object[] {5.0}, typeof(double), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(5.0, (double)result, 4); + } + + [Fact] + public void ConvertMultipleValues_ReturnsMultipliedValue() + { + var values = new object[] {1.0, 2, 3}; + var result = _converter.Convert(values, typeof(double), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(6.0, (double)result, 4); + } + + [Fact] + public void ConvertNone_Returns1() + { + var result = _converter.Convert(null, typeof(double), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(1.0, (double)result); + } + + [Fact] + public void ConvertBack_NotSupported_ReturnsNull() + { + var result = _converter.ConvertBack(new object[]{"a", 1}, new[] { typeof(string), typeof(int)}, null, CultureInfo.CurrentCulture); + Assert.Null(result); + } + } +} diff --git a/src/AudioBand.Test/ValueConverters/ObjectToTypeConverterTests.cs b/src/AudioBand.Test/ValueConverters/ObjectToTypeConverterTests.cs new file mode 100644 index 00000000..2e81ac3e --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/ObjectToTypeConverterTests.cs @@ -0,0 +1,33 @@ +using System; +using System.Globalization; +using System.Windows; +using AudioBand.ValueConverters; +using Xunit; + +namespace AudioBand.Test +{ + public class ObjectToTypeConverterTests + { + private readonly ObjectToTypeConverter _converter = new ObjectToTypeConverter(); + + [Fact] + public void ConvertNull_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.Convert(null, typeof(Type), null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void ConvertObject_ReturnsTypeOfObject() + { + var result = _converter.Convert("string", typeof(Type), null, CultureInfo.CurrentCulture); + + Assert.Equal(typeof(string), result); + } + + [Fact] + public void ConvertBack_NotSupported_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.ConvertBack(null, typeof(object), null, CultureInfo.CurrentCulture)); + } + } +} diff --git a/src/AudioBand.Test/ValueConveters/PathToImageConverterTests.cs b/src/AudioBand.Test/ValueConverters/PathToImageConverterTests.cs similarity index 96% rename from src/AudioBand.Test/ValueConveters/PathToImageConverterTests.cs rename to src/AudioBand.Test/ValueConverters/PathToImageConverterTests.cs index 88a43bbf..0bf133a2 100644 --- a/src/AudioBand.Test/ValueConveters/PathToImageConverterTests.cs +++ b/src/AudioBand.Test/ValueConverters/PathToImageConverterTests.cs @@ -1,57 +1,57 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Media; -using AudioBand.ValueConverters; -using Xunit; - -namespace AudioBand.Test -{ - public class PathToImageConverterTests - { - private PathToImageSourceConverter _converter = new PathToImageSourceConverter(); - private string _sampleImagePath = Path.GetFullPath("Assets/imgsource.png"); - - [Fact] - void Convert_EmptyPath_ReturnsNull() - { - Assert.Null(_converter.Convert("", typeof(ImageSource), null, CultureInfo.CurrentCulture)); - } - - [Fact] - void Convert_Null_ReturnsNull() - { - Assert.Null(_converter.Convert((object)null, typeof(ImageSource), null, CultureInfo.CurrentCulture)); - } - - [Fact] - void Convert_NoFile_ReturnsNull() - { - Assert.Null(_converter.Convert("nonexistingfile.png", typeof(ImageSource), null, CultureInfo.CurrentCulture)); - } - - [Fact] - void Convert_ValidFile_ReturnsImageSource() - { - var imgSource = _converter.Convert(_sampleImagePath, typeof(ImageSource), null, CultureInfo.CurrentCulture); - - Assert.NotNull(imgSource); - Assert.IsAssignableFrom(imgSource); - } - - [Fact] - void MultiConvert_InvalidFile_UsesFallback() - { - object fallback = new object(); - var imgSource = _converter.Convert(new[] {"invalidfile.png", fallback}, typeof(ImageSource), null, - CultureInfo.CurrentCulture); - - Assert.NotNull(imgSource); - Assert.Equal(fallback, imgSource); - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media; +using AudioBand.ValueConverters; +using Xunit; + +namespace AudioBand.Test +{ + public class PathToImageConverterTests + { + private PathToImageSourceConverter _converter = new PathToImageSourceConverter(); + private string _sampleImagePath = Path.GetFullPath("Assets/imgsource.png"); + + [Fact] + void Convert_EmptyPath_ReturnsNull() + { + Assert.Null(_converter.Convert("", typeof(ImageSource), null, CultureInfo.CurrentCulture)); + } + + [Fact] + void Convert_Null_ReturnsNull() + { + Assert.Null(_converter.Convert((object)null, typeof(ImageSource), null, CultureInfo.CurrentCulture)); + } + + [Fact] + void Convert_NoFile_ReturnsNull() + { + Assert.Null(_converter.Convert("nonexistingfile.png", typeof(ImageSource), null, CultureInfo.CurrentCulture)); + } + + [Fact] + void Convert_ValidFile_ReturnsImageSource() + { + var imgSource = _converter.Convert(_sampleImagePath, typeof(ImageSource), null, CultureInfo.CurrentCulture); + + Assert.NotNull(imgSource); + Assert.IsAssignableFrom(imgSource); + } + + [Fact] + void MultiConvert_InvalidFile_UsesFallback() + { + object fallback = new object(); + var imgSource = _converter.Convert(new[] {"invalidfile.png", fallback}, typeof(ImageSource), null, + CultureInfo.CurrentCulture); + + Assert.NotNull(imgSource); + Assert.Equal(fallback, imgSource); + } + } +} diff --git a/src/AudioBand.Test/ValueConverters/PointConverterTests.cs b/src/AudioBand.Test/ValueConverters/PointConverterTests.cs new file mode 100644 index 00000000..70394cf5 --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/PointConverterTests.cs @@ -0,0 +1,31 @@ +using System.Globalization; +using System.Windows; +using Xunit; +using PointConverter = AudioBand.ValueConverters.PointConverter; + +namespace AudioBand.Test +{ + public class PointConverterTests + { + private readonly PointConverter _converter = new PointConverter(); + + [Fact] + public void ConvertXAndY_ReturnsPointWithXAndY() + { + var x = 1; + var y = 2; + var result = _converter.Convert(new object[] {x, y}, typeof(Point), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + var point = (Point) result; + Assert.Equal(x, point.X); + Assert.Equal(y, point.Y); + } + + [Fact] + public void ConvertBack_Unsupported_ReturnsNull() + { + Assert.Null(_converter.ConvertBack(null, null, null, CultureInfo.CurrentCulture)); + } + } +} diff --git a/src/AudioBand.Test/ValueConverters/StringToFontFamilyConverterTests.cs b/src/AudioBand.Test/ValueConverters/StringToFontFamilyConverterTests.cs new file mode 100644 index 00000000..5f288534 --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/StringToFontFamilyConverterTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using AudioBand.ValueConverters; +using Xunit; + +namespace AudioBand.Test +{ + public class StringToFontFamilyConverterTests + { + private readonly StringToFontFamilyConverter _converter = new StringToFontFamilyConverter(); + + [Fact] + public void ConvertString_ReturnsFontFamily() + { + var result = _converter.Convert("Arial", typeof(FontFamily), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal("Arial", ((FontFamily)result).Source); + } + + [Fact] + public void ConvertNull_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.Convert(null, typeof(FontFamily), null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void ConvertBackFontFamily_ReturnsStringName() + { + var ff = new FontFamily("Arial"); + + var result = _converter.ConvertBack(ff, typeof(string), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal("Arial", (string)result); + } + + [Fact] + public void ConvertBackNull_ReturnsUnsetValue() + { + Assert.Equal(DependencyProperty.UnsetValue, _converter.ConvertBack(null, typeof(string), null, CultureInfo.CurrentCulture)); + + } + } +} diff --git a/src/AudioBand.Test/ValueConverters/TimeSpanToMsConverterTests.cs b/src/AudioBand.Test/ValueConverters/TimeSpanToMsConverterTests.cs new file mode 100644 index 00000000..cd7099c1 --- /dev/null +++ b/src/AudioBand.Test/ValueConverters/TimeSpanToMsConverterTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AudioBand.ValueConverters; +using Xunit; + +namespace AudioBand.Test +{ + public class TimeSpanToMsConverterTests + { + private readonly TimeSpanToMsConverter _converter = new TimeSpanToMsConverter(); + + [Fact] + public void ConvertTimeSpan_ReturnsMs() + { + var time = TimeSpan.FromMilliseconds(500); + var result = _converter.Convert(time, typeof(double), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(time.TotalMilliseconds, (double)result); + } + + [Fact] + public void ConvertTimeSpanInvalid_Returns0() + { + var result = _converter.Convert(null, typeof(double), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(0.0, (double)result); + } + + [Fact] + public void ConvertBackMs_ReturnsTimespan() + { + var ms = 1000.0; + var result = _converter.ConvertBack(ms, typeof(TimeSpan), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(TimeSpan.FromMilliseconds(ms), (TimeSpan)result); + } + + [Fact] + public void ConvertBackInvalid_ReturnsTimespan0() + { + var result = _converter.ConvertBack(null, typeof(TimeSpan), null, CultureInfo.CurrentCulture); + + Assert.IsType(result); + Assert.Equal(TimeSpan.Zero, (TimeSpan)result); + } + } +} diff --git a/src/AudioBand.Test/ViewModels/AudioSourceSettingsCollectionTests.cs b/src/AudioBand.Test/ViewModels/AudioSourceSettingsCollectionTests.cs index 03e69864..27a50c8e 100644 --- a/src/AudioBand.Test/ViewModels/AudioSourceSettingsCollectionTests.cs +++ b/src/AudioBand.Test/ViewModels/AudioSourceSettingsCollectionTests.cs @@ -4,6 +4,7 @@ using Moq; using System.Collections.Generic; using AudioBand.Messages; +using AudioBand.Settings; using Xunit; namespace AudioBand.Test @@ -12,11 +13,13 @@ public class AudioSourceSettingsCollectionTests { private Mock _audioSourceMock; private Mock _messageBus; + private Mock _appSettings; public AudioSourceSettingsCollectionTests() { _audioSourceMock = new Mock(); _messageBus = new Mock(); + _appSettings = new Mock(); } [Fact] @@ -33,7 +36,7 @@ public void NoMatchingSettings_CreatesNoChildViewModels() }; var settings = new AudioSourceSettings { AudioSourceName = name, Settings = keyVals }; - var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object); + var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object, _appSettings.Object); Assert.Empty(vm.SettingsList); Assert.Equal(name, vm.AudioSourceName); @@ -65,7 +68,7 @@ public void MatchingSettings_ShouldCreateChildViewModelsInOrder() }; var settings = new AudioSourceSettings { Settings = settingModels }; - var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object); + var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object, _appSettings.Object); Assert.Equal(settingModels.Count, vm.SettingsList.Count); Assert.Equal(settingModels[0].Name, vm.SettingsList[0].Name); @@ -88,12 +91,12 @@ public void AudioSourceSettingUpdate_NewValueIsWrittenBackToSettings() object newSettingValue = 1; _audioSourceMock.SetupGet(s => s[It.Is(x => x == setting)]).Returns(newSettingValue); - var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object); + var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object, _appSettings.Object); _audioSourceMock.Raise(s => s.SettingChanged += null, new SettingChangedEventArgs(setting)); - vm.EndEdit(); Assert.Equal(newSettingValue, settingModel.Value); + _appSettings.Verify(m => m.Save(), Times.Once); } [Fact] @@ -121,7 +124,7 @@ public void AudioSourceSettingsUpdated_AudioSourceIsUpdatedInPriority() _audioSourceMock.InSequence(s).SetupSet(source => source[It.Is(x => x == setting1.Name)] = null); _audioSourceMock.InSequence(s).SetupSet(source => source[It.Is(x => x == setting2.Name)] = null); - var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object); + var vm = new AudioSourceSettingsCollectionViewModel(_audioSourceMock.Object, settings, _messageBus.Object, _appSettings.Object); _audioSourceMock.VerifySet(source => source[setting3.Name] = null); _audioSourceMock.VerifySet(source => source[setting1.Name] = null); diff --git a/src/AudioBand.Test/ViewModels/CustomLabelViewModelTests.cs b/src/AudioBand.Test/ViewModels/CustomLabelViewModelTests.cs index 396aef52..6a80d1ed 100644 --- a/src/AudioBand.Test/ViewModels/CustomLabelViewModelTests.cs +++ b/src/AudioBand.Test/ViewModels/CustomLabelViewModelTests.cs @@ -73,5 +73,34 @@ void CustomLabel_ColorChangedThenCanceled_TextSegmentsHaveCorrectColor() Assert.All(vm.TextSegments, segment => Assert.Equal(Colors.Blue, segment.Color)); } + + [Fact] + void CustomLabel_CancelEdit_ModelHasNoChanges() + { + var formatString = "test"; + var model = new CustomLabel {FormatString = formatString}; + var vm = new CustomLabelViewModel(model, _dialogMock.Object, _sessionMock.Object, _messageBusMock.Object); + + vm.FormatString = "new"; + vm.CancelEdit(); + + Assert.Equal(formatString, vm.FormatString); + Assert.Equal(formatString, model.FormatString); + } + + [Fact] + void CustomLabel_SaveEdit_ModelHasNewChanges() + { + var formatString = "test"; + var newFormatstring = "format"; + var model = new CustomLabel { FormatString = formatString }; + var vm = new CustomLabelViewModel(model, _dialogMock.Object, _sessionMock.Object, _messageBusMock.Object); + + vm.FormatString = newFormatstring; + vm.EndEdit(); + + Assert.Equal(newFormatstring, vm.FormatString); + Assert.Equal(newFormatstring, model.FormatString); + } } } diff --git a/src/AudioBand.Test/ViewModels/CustomLabelsViewModelTests.cs b/src/AudioBand.Test/ViewModels/CustomLabelsViewModelTests.cs index 2dafa58d..151bedde 100644 --- a/src/AudioBand.Test/ViewModels/CustomLabelsViewModelTests.cs +++ b/src/AudioBand.Test/ViewModels/CustomLabelsViewModelTests.cs @@ -137,5 +137,29 @@ public void ProfileChanged_NewLabelsHaveCorrectAudioSessionData() _appSettingsMock.Raise(m => m.ProfileChanged += null, null, EventArgs.Empty); Assert.True(vm.CustomLabels[0].IsPlaying); } + + [Fact] + public void RemoveLabel_PublishEdit() + { + _appSettingsMock.SetupGet(x => x.CustomLabels).Returns(new List { new CustomLabel() }); + _dialogMock.Setup(o => o.ShowConfirmationDialog(It.IsAny(), It.IsAny())).Returns(true); + _viewModel = new CustomLabelsViewModel(_appSettingsMock.Object, _dialogMock.Object, _sessionMock.Object, _messageBus.Object); + var label = _viewModel.CustomLabels[0]; + _viewModel.RemoveLabelCommand.Execute(label); + + Assert.True(_viewModel.IsEditing); + _messageBus.Verify(m => m.Publish(It.IsAny(), It.IsAny())); + } + + [Fact] + public void AddLabel_PublishEdit() + { + _appSettingsMock.SetupGet(x => x.CustomLabels).Returns(new List { new CustomLabel() }); + _viewModel = new CustomLabelsViewModel(_appSettingsMock.Object, _dialogMock.Object, _sessionMock.Object, _messageBus.Object); + _viewModel.AddLabelCommand.Execute(null); + + Assert.True(_viewModel.IsEditing); + _messageBus.Verify(m => m.Publish(It.IsAny(), It.IsAny())); + } } } diff --git a/src/AudioBand.sln b/src/AudioBand.sln index 34643bd1..452fd330 100644 --- a/src/AudioBand.sln +++ b/src/AudioBand.sln @@ -21,6 +21,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AudioBand.Logging", "AudioB EndProject Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "AudioBandInstaller", "AudioBandInstaller\AudioBandInstaller.wixproj", "{4E9EEE47-23F6-45B7-9DDB-A34731D6AE4A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AudioBand Core", "AudioBand Core", "{AD1A45A7-F5E2-4657-BA09-99F8A5421FF0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Audio Sources", "Audio Sources", "{31BA7036-81E7-4D02-90CA-753832D950C5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E79F26F3-C51F-4375-BF1C-4090FAB8D16D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -135,6 +141,16 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B69832AD-8373-47AC-A52A-183238903896} = {AD1A45A7-F5E2-4657-BA09-99F8A5421FF0} + {30F2BFEA-788A-494D-88E7-F2070528EBEA} = {AD1A45A7-F5E2-4657-BA09-99F8A5421FF0} + {43B57D81-7FAE-40D0-921E-E29F7E848288} = {31BA7036-81E7-4D02-90CA-753832D950C5} + {E5E9CB59-C8CF-4042-9C13-F216153334B6} = {E79F26F3-C51F-4375-BF1C-4090FAB8D16D} + {6D881B7B-3F3F-4613-A83F-75E31EEA5252} = {31BA7036-81E7-4D02-90CA-753832D950C5} + {741DB79C-921D-4D91-85F1-CD10C746F46E} = {31BA7036-81E7-4D02-90CA-753832D950C5} + {D3F92C3E-E546-4A6B-ADA2-FACD95E229F7} = {AD1A45A7-F5E2-4657-BA09-99F8A5421FF0} + {D8E1D3E5-D0AB-43C4-8AF6-60C14C5C6843} = {AD1A45A7-F5E2-4657-BA09-99F8A5421FF0} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7C4FC539-D568-422E-8445-A0313878CB1F} EndGlobalSection diff --git a/src/AudioBand/AudioBand.csproj b/src/AudioBand/AudioBand.csproj index 5b038a20..31778fc9 100644 --- a/src/AudioBand/AudioBand.csproj +++ b/src/AudioBand/AudioBand.csproj @@ -96,6 +96,11 @@ + + True + True + GlobalSettings.settings + @@ -107,6 +112,9 @@ + + + @@ -303,6 +311,10 @@ Designer + + SettingsSingleFileGenerator + GlobalSettings.Designer.cs + PreserveNewest diff --git a/src/AudioBand/Behaviors/DpiScaling.cs b/src/AudioBand/Behaviors/DpiScaling.cs index 7e71484d..183ace78 100644 --- a/src/AudioBand/Behaviors/DpiScaling.cs +++ b/src/AudioBand/Behaviors/DpiScaling.cs @@ -1,9 +1,11 @@ using System; using System.Diagnostics; +using System.Runtime.InteropServices; using System.Windows; using System.Windows.Interactivity; using System.Windows.Interop; using System.Windows.Media; +using System.Windows.Threading; namespace AudioBand.Behaviors { @@ -108,24 +110,43 @@ private IntPtr HwndSourceHook(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam switch (msg) { case WM_DPICHANGED: - UpdateDpi(HiWord(wparam)); - + var newDpi = HiWord(wparam); + if (AssociatedObject is Window window) + { + UpdateDpi(newDpi, false); + var suggestedRect = Marshal.PtrToStructure(lparam); + window.Left = suggestedRect.Left; + window.Top = suggestedRect.Top; + window.Width = suggestedRect.Right - suggestedRect.Left; + window.Height = suggestedRect.Bottom - suggestedRect.Top; + } + else + { + UpdateDpi(newDpi); + } + + handled = true; break; case WM_DPICHANGED_AFTERPARENT: + // Used for the toolbar since we don't receive WM_DPICHANGED messages there. UpdateDpi(GetParentWindowDpi(AssociatedObject)); + handled = true; break; } return IntPtr.Zero; } - private void UpdateDpi(double newDpi) + private void UpdateDpi(double newDpi, bool updateSize = true) { - var dpiScale = newDpi / CurrentDpi; - CurrentDpi = newDpi; + if (updateSize) + { + var dpiScale = newDpi / CurrentDpi; + AssociatedObject.Width *= dpiScale; + AssociatedObject.Height *= dpiScale; + } - AssociatedObject.Width *= dpiScale; - AssociatedObject.Height *= dpiScale; + CurrentDpi = newDpi; if (VisualTreeHelper.GetChildrenCount(AssociatedObject) == 0) { diff --git a/src/AudioBand/Deskband.cs b/src/AudioBand/Deskband.cs index 60509f52..537c6b62 100644 --- a/src/AudioBand/Deskband.cs +++ b/src/AudioBand/Deskband.cs @@ -16,7 +16,6 @@ using CSDeskBand; using NLog; using SimpleInjector; -using SimpleInjector.Advanced; namespace AudioBand { @@ -39,6 +38,7 @@ public class Deskband : CSDeskBandWpf private AudioBandToolbar _audioBandToolbar; private Container _container; private Window _settingsWindow; + private ILogger _logger; /// /// Initializes a new instance of the class. @@ -55,11 +55,13 @@ public Deskband() Options.HorizontalSize = initialSize; Options.MinHorizontalSize = initialSize; AudioBandLogManager.Initialize(); - var logger = AudioBandLogManager.GetLogger("AudioBand"); - logger.Info("Starting AudioBand. Version: {version}, OS: {os}", GetType().Assembly.GetCustomAttribute().InformationalVersion, Environment.OSVersion); + _logger = AudioBandLogManager.GetLogger("AudioBand"); + _logger.Info("Starting AudioBand. Version: {version}, OS: {os}", GetType().Assembly.GetCustomAttribute().InformationalVersion, Environment.OSVersion); - AppDomain.CurrentDomain.UnhandledException += (sender, args) => AudioBandLogManager.GetLogger("AudioBand").Error((Exception)args.ExceptionObject, "Unhandled Exception"); - AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + StartupCheck(); + + AppDomain.CurrentDomain.UnhandledException += CurrentDomainOnUnhandledException; + AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve; ConfigureDependencies(); @@ -81,7 +83,7 @@ protected override void DeskbandOnClosed() } // Problem with late binding. Fuslogvw shows its not probing the original location. - private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + private static Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args) { // name is in this format Xceed.Wpf.Toolkit, Version=3.4.0.0, Culture=neutral, PublicKeyToken=3e4669d2f30244f4 var asmName = args.Name.Split(',')[0]; @@ -94,6 +96,14 @@ private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEven return File.Exists(filename) ? Assembly.LoadFrom(filename) : null; } + private static void CurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs args) + { + GlobalSettings.Default.UnhandledException = true; + GlobalSettings.Default.Save(); + + AudioBandLogManager.GetLogger("AudioBand").Error((Exception)args.ExceptionObject, "Unhandled Exception"); + } + private void ConfigureDependencies() { try @@ -139,5 +149,21 @@ private void FocusCaptured(FocusChangedMessage msg) UpdateFocus(true); } } + + private void StartupCheck() + { + if (!GlobalSettings.Default.UnhandledException) + { + return; + } + + // Unhandled exception from last run. Prevent audioband from starting in case there is a crash loop. + _logger.Info("Startup prevented due to previous unhandled exception. Open audioband again to ignore."); + GlobalSettings.Default.UnhandledException = false; + GlobalSettings.Default.Save(); + + // Exception should make explorer remove the deskband from being automatically started. + throw new Exception("Startup prevented due to previous unhandled exception."); + } } } diff --git a/src/AudioBand/GlobalSettings.Designer.cs b/src/AudioBand/GlobalSettings.Designer.cs new file mode 100644 index 00000000..24cb5d28 --- /dev/null +++ b/src/AudioBand/GlobalSettings.Designer.cs @@ -0,0 +1,38 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AudioBand { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.1.0.0")] + internal sealed partial class GlobalSettings : global::System.Configuration.ApplicationSettingsBase { + + private static GlobalSettings defaultInstance = ((GlobalSettings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new GlobalSettings()))); + + public static GlobalSettings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool UnhandledException { + get { + return ((bool)(this["UnhandledException"])); + } + set { + this["UnhandledException"] = value; + } + } + } +} diff --git a/src/AudioBand/GlobalSettings.settings b/src/AudioBand/GlobalSettings.settings new file mode 100644 index 00000000..be549678 --- /dev/null +++ b/src/AudioBand/GlobalSettings.settings @@ -0,0 +1,9 @@ + + + + + + False + + + \ No newline at end of file diff --git a/src/AudioBand/NativeMethods.cs b/src/AudioBand/NativeMethods.cs index 056e5a4f..9b29d323 100644 --- a/src/AudioBand/NativeMethods.cs +++ b/src/AudioBand/NativeMethods.cs @@ -195,6 +195,23 @@ internal struct AccentPolicy public uint GradientColor; public int AnimationId; } + + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + + public RECT(int left, int top, int right, int bottom) + { + Left = left; + Top = top; + Right = right; + Bottom = bottom; + } + } } #pragma warning restore } diff --git a/src/AudioBand/Settings/AppSettings.cs b/src/AudioBand/Settings/AppSettings.cs index 6e2b0f20..2ab6e939 100644 --- a/src/AudioBand/Settings/AppSettings.cs +++ b/src/AudioBand/Settings/AppSettings.cs @@ -32,7 +32,6 @@ public class AppSettings : IAppSettings private static readonly string SettingsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AudioBand"); private static readonly string SettingsFilePath = Path.Combine(SettingsDirectory, "audioband.settings"); private static readonly ILogger Logger = AudioBandLogManager.GetLogger(); - private readonly TomlSettings _tomlSettings; private SettingsV3 _settings; private ProfileV3 _currentProfile; @@ -41,31 +40,7 @@ public class AppSettings : IAppSettings /// public AppSettings() { - _tomlSettings = TomlSettings.Create(cfg => - { - cfg.ConfigureType(type => type.WithConversionFor(convert => convert - .ToToml(SerializationConversions.ColorToString) - .FromToml(tomlString => SerializationConversions.StringToColor(tomlString.Value)))); - cfg.ConfigureType(type => type.WithConversionFor(convert => convert - .ToToml(SerializationConversions.EnumToString) - .FromToml(str => SerializationConversions.StringToEnum(str.Value)))); - cfg.ConfigureType(type => type.WithConversionFor(c => c - .FromToml(tml => tml.Value))); - }); - - if (!Directory.Exists(SettingsDirectory)) - { - Directory.CreateDirectory(SettingsDirectory); - } - - if (!File.Exists(SettingsFilePath)) - { - CreateDefaultSettingsFile(); - } - else - { - LoadSettingsFromPath(SettingsFilePath); - } + InitSettings(); if (_settings.AudioSourceSettings == null) { @@ -243,7 +218,7 @@ public void Save() { try { - Toml.WriteFile(_settings, SettingsFilePath, _tomlSettings); + Toml.WriteFile(_settings, SettingsFilePath, TomlHelper.DefaultSettings); } catch (Exception e) { @@ -254,7 +229,7 @@ public void Save() /// public void ImportProfilesFromPath(string path) { - var profilesToImport = Toml.ReadFile(path, _tomlSettings); + var profilesToImport = Toml.ReadFile(path, TomlHelper.DefaultSettings); foreach (var keyVal in profilesToImport.Profiles) { var key = GetUniqueProfileName(keyVal.Key); @@ -266,18 +241,18 @@ public void ImportProfilesFromPath(string path) public void ExportProfilesToPath(string path) { var exportObject = new ProfileExportV3 { Profiles = _settings.Profiles }; - Toml.WriteFile(exportObject, path, _tomlSettings); + Toml.WriteFile(exportObject, path, TomlHelper.DefaultSettings); } private void LoadSettingsFromPath(string path) { - var tomlFile = Toml.ReadFile(path, _tomlSettings); + var tomlFile = Toml.ReadFile(path, TomlHelper.DefaultSettings); var version = tomlFile["Version"].Get(); // Create backup if (version != CurrentVersion) { - Toml.WriteFile(tomlFile, Path.Combine(SettingsDirectory, $"audioband.settings.{version}"), _tomlSettings); + Toml.WriteFile(tomlFile, Path.Combine(SettingsDirectory, $"audioband.settings.{version}"), TomlHelper.DefaultSettings); _settings = Migration.MigrateSettings(tomlFile.Get(SettingsTable[version]), version, CurrentVersion); Save(); } @@ -402,5 +377,32 @@ private string GetUniqueProfileName(string name) return newName; } + + private void InitSettings() + { + if (!Directory.Exists(SettingsDirectory)) + { + Directory.CreateDirectory(SettingsDirectory); + } + + if (!File.Exists(SettingsFilePath)) + { + CreateDefaultSettingsFile(); + return; + } + + try + { + LoadSettingsFromPath(SettingsFilePath); + } + catch (Exception e) + { + Logger.Error(e, "Unable to load settings"); + var backupPath = Path.Combine(SettingsDirectory, "audioband.settings.backup-" + DateTime.Now.Ticks); + File.Copy(SettingsFilePath, backupPath, true); + Logger.Info("Creating new default settings. Backup created at {backup}", backupPath); + CreateDefaultSettingsFile(); + } + } } } diff --git a/src/AudioBand/Settings/Migrations/IdentityMigrator.cs b/src/AudioBand/Settings/Migrations/IdentityMigrator.cs new file mode 100644 index 00000000..d3b7f4fb --- /dev/null +++ b/src/AudioBand/Settings/Migrations/IdentityMigrator.cs @@ -0,0 +1,18 @@ +namespace AudioBand.Settings.Migrations +{ + /// + /// Performs no migration. + /// + internal class IdentityMigrator : ISettingsMigrator + { + /// + /// Migrate settings to new version. + /// + /// Old settings to migrate. + /// The new settings. + public object MigrateSetting(object oldSetting) + { + return oldSetting; + } + } +} diff --git a/src/AudioBand/Settings/Migrations/Migration.cs b/src/AudioBand/Settings/Migrations/Migration.cs index 5e97c4fc..ce1f264e 100644 --- a/src/AudioBand/Settings/Migrations/Migration.cs +++ b/src/AudioBand/Settings/Migrations/Migration.cs @@ -11,10 +11,12 @@ namespace AudioBand.Settings.Migrations /// internal static class Migration { - private static readonly Dictionary<(string From, string To), ISettingsMigrator> SupportedMigrations = new Dictionary<(string From, string To), ISettingsMigrator>() + // Assume that migrations can be applied in order. + private static readonly List<(string version, ISettingsMigrator migrator)> MigrationsList = new List<(string, ISettingsMigrator)> { - { ("0.1", "2"), new V1ToV2() }, - { ("2", "3"), new V2ToV3() }, + ("0.1", new V1ToV2()), + ("2", new V2ToV3()), + ("3", new IdentityMigrator()), }; private static readonly ILogger Logger = AudioBandLogManager.GetLogger(typeof(Migration).FullName); @@ -35,7 +37,7 @@ public static TNew MigrateSettings(object oldSettings, string oldVersion, } var plan = FindPlan(oldVersion, newVersion); - if (!plan.Any()) + if (plan == null || !plan.Any()) { throw new ArgumentException($"No migration plan from {oldVersion} to {newVersion}"); } @@ -48,7 +50,20 @@ public static TNew MigrateSettings(object oldSettings, string oldVersion, private static List FindPlan(string from, string to) { - return SupportedMigrations.Where(x => x.Key.From == from && x.Key.To == to).Select(x => x.Value).ToList(); + var startIndex = MigrationsList.FindIndex(m => m.version == from); + var endIndex = MigrationsList.FindIndex(m => m.version == to); + if (startIndex == -1 || endIndex == -1) + { + return null; + } + + if (endIndex < startIndex) + { + Logger.Error("End index less than start index"); + return null; + } + + return MigrationsList.GetRange(startIndex, endIndex - startIndex).Select(m => m.migrator).ToList(); } } } diff --git a/src/AudioBand/Settings/TomlHelper.cs b/src/AudioBand/Settings/TomlHelper.cs new file mode 100644 index 00000000..62bcb029 --- /dev/null +++ b/src/AudioBand/Settings/TomlHelper.cs @@ -0,0 +1,27 @@ +using System.Windows.Media; +using AudioBand.Models; +using Nett; + +namespace AudioBand.Settings +{ + /// + /// Helper for TOML serialization and deserialization. + /// + internal static class TomlHelper + { + /// + /// Gets the default settings for TOML de/serialization. + /// + public static TomlSettings DefaultSettings { get; } = TomlSettings.Create(cfg => + { + cfg.ConfigureType(type => type.WithConversionFor(convert => convert + .ToToml(SerializationConversions.ColorToString) + .FromToml(tomlString => SerializationConversions.StringToColor(tomlString.Value)))); + cfg.ConfigureType(type => type.WithConversionFor(convert => convert + .ToToml(SerializationConversions.EnumToString) + .FromToml(str => SerializationConversions.StringToEnum(str.Value)))); + cfg.ConfigureType(type => type.WithConversionFor(c => c + .FromToml(tml => tml.Value))); + }); + } +} diff --git a/src/AudioBand/ValueConverters/BoolToVisibilityConverter.cs b/src/AudioBand/ValueConverters/BoolToVisibilityConverter.cs index 9f825efd..0734ea0d 100644 --- a/src/AudioBand/ValueConverters/BoolToVisibilityConverter.cs +++ b/src/AudioBand/ValueConverters/BoolToVisibilityConverter.cs @@ -8,19 +8,53 @@ namespace AudioBand.ValueConverters /// /// Converter from to . /// - [ValueConversion(typeof(bool), typeof(Visibility))] + [ValueConversion(typeof(bool), typeof(Visibility), ParameterType = typeof(bool))] public class BoolToVisibilityConverter : IValueConverter { - /// + /// + /// Converts a to a . + /// + /// The value of the bool to convert. + /// Type is . + /// A bool indicating whether the visibility should be collapsed. + /// The culture info. + /// if is true. + /// Otherwise returns except when is false in which case + /// is returned. public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + if (!ValueConverterHelper.IsValid(value)) + { + return DependencyProperty.UnsetValue; + } + var collapse = parameter == null ? true : System.Convert.ToBoolean(parameter); - return (bool)value ? Visibility.Visible : (collapse ? Visibility.Collapsed : Visibility.Hidden); + var isVisible = (bool)value; + if (isVisible) + { + return Visibility.Visible; + } + else + { + return collapse ? Visibility.Collapsed : Visibility.Hidden; + } } - /// + /// + /// Converts a to a value. + /// + /// The . + /// The type of . + /// Parameter is ignored. + /// The current culture. + /// True if is . False otherwise. public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { + if (!ValueConverterHelper.IsValid(value)) + { + return DependencyProperty.UnsetValue; + } + var visibility = (Visibility)value; return visibility == Visibility.Visible; } diff --git a/src/AudioBand/ValueConverters/ColorToBrushConverter.cs b/src/AudioBand/ValueConverters/ColorToBrushConverter.cs index b219acb7..3c12b8b2 100644 --- a/src/AudioBand/ValueConverters/ColorToBrushConverter.cs +++ b/src/AudioBand/ValueConverters/ColorToBrushConverter.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Windows; using System.Windows.Data; using System.Windows.Media; @@ -8,18 +9,42 @@ namespace AudioBand.ValueConverters /// /// Converter from to . /// - [ValueConversion(typeof(Color), typeof(Brush))] + [ValueConversion(typeof(Color), typeof(SolidColorBrush))] public class ColorToBrushConverter : IValueConverter { - /// + /// + /// Converts a to a . + /// + /// The color. + /// The type of . + /// The converter parameter. + /// The current culture. + /// A representing the color. public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + if (!ValueConverterHelper.IsValid(value)) + { + return DependencyProperty.UnsetValue; + } + return new SolidColorBrush((Color)value); } - /// + /// + /// Converts a to a . + /// + /// The brush. + /// The type of . + /// The converter parameter. + /// The current culture. + /// The color component of a . public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { + if (!ValueConverterHelper.IsValid(value)) + { + return DependencyProperty.UnsetValue; + } + return ((SolidColorBrush)value).Color; } } diff --git a/src/AudioBand/ValueConverters/ColorToNameConverter.cs b/src/AudioBand/ValueConverters/ColorToNameConverter.cs index 9e55f414..622e2171 100644 --- a/src/AudioBand/ValueConverters/ColorToNameConverter.cs +++ b/src/AudioBand/ValueConverters/ColorToNameConverter.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Windows; using System.Windows.Data; using System.Windows.Media; using AudioBand.Extensions; @@ -7,7 +8,7 @@ namespace AudioBand.ValueConverters { /// - /// Convert a string representation to a . + /// Convert a to a string representation. /// [ValueConversion(typeof(Color), typeof(string))] public class ColorToNameConverter : IValueConverter @@ -15,12 +16,22 @@ public class ColorToNameConverter : IValueConverter /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + if (!ValueConverterHelper.IsValid(value)) + { + return DependencyProperty.UnsetValue; + } + return ((Color)value).GetColorName(); } /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { + if (!ValueConverterHelper.IsValid(value)) + { + return DependencyProperty.UnsetValue; + } + return ColorConverter.ConvertFromString((string)value); } } diff --git a/src/AudioBand/ValueConverters/ComparisonConverter.cs b/src/AudioBand/ValueConverters/ComparisonConverter.cs index 2b52cbf2..035ae126 100644 --- a/src/AudioBand/ValueConverters/ComparisonConverter.cs +++ b/src/AudioBand/ValueConverters/ComparisonConverter.cs @@ -18,7 +18,7 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur /// public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return null; } } } diff --git a/src/AudioBand/ValueConverters/DoubleToCornerRadiusConverter.cs b/src/AudioBand/ValueConverters/DoubleToCornerRadiusConverter.cs index fa7d7205..d2822ad0 100644 --- a/src/AudioBand/ValueConverters/DoubleToCornerRadiusConverter.cs +++ b/src/AudioBand/ValueConverters/DoubleToCornerRadiusConverter.cs @@ -11,7 +11,14 @@ namespace AudioBand.ValueConverters [ValueConversion(typeof(double), typeof(CornerRadius))] public class DoubleToCornerRadiusConverter : IValueConverter { - /// + /// + /// Uses a double as the width of a corner radius. + /// + /// The double to convert. + /// The type of . + /// The converter parameter. + /// The culture. + /// A uniform corner radius with each side equal to value / 2. public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return new CornerRadius(System.Convert.ToDouble(value) / 2); @@ -20,7 +27,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } } } diff --git a/src/AudioBand/ValueConverters/EmptyStringToBoolConverter.cs b/src/AudioBand/ValueConverters/EmptyStringToBoolConverter.cs index 4c8b25a3..e576b395 100644 --- a/src/AudioBand/ValueConverters/EmptyStringToBoolConverter.cs +++ b/src/AudioBand/ValueConverters/EmptyStringToBoolConverter.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Windows; using System.Windows.Data; namespace AudioBand.ValueConverters @@ -25,7 +26,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } } } diff --git a/src/AudioBand/ValueConverters/FlagToBoolConverter.cs b/src/AudioBand/ValueConverters/FlagToBoolConverter.cs index 9c25bf0b..3ba12f83 100644 --- a/src/AudioBand/ValueConverters/FlagToBoolConverter.cs +++ b/src/AudioBand/ValueConverters/FlagToBoolConverter.cs @@ -8,13 +8,20 @@ namespace AudioBand.ValueConverters /// /// Converts enum flag to bool. /// - [ValueConversion(typeof(Enum), typeof(bool))] + [ValueConversion(typeof(Enum), typeof(bool), ParameterType = typeof(Enum))] public class FlagToBoolConverter : IValueConverter { - /// + /// + /// Converts an enum flag to a bool value. + /// + /// The value of the flag. + /// The target type. + /// The flag to check if set. + /// The culture. + /// True if the flag specified by is set. public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value == null || value == DependencyProperty.UnsetValue || parameter == null) + if (value == null || parameter == null) { return false; } @@ -25,7 +32,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } } } diff --git a/src/AudioBand/ValueConverters/MultiplierConverter.cs b/src/AudioBand/ValueConverters/MultiplierConverter.cs index d685cc34..33798d3b 100644 --- a/src/AudioBand/ValueConverters/MultiplierConverter.cs +++ b/src/AudioBand/ValueConverters/MultiplierConverter.cs @@ -6,17 +6,24 @@ namespace AudioBand.ValueConverters { /// - /// Mutliples all values together. + /// Multiplies all values together. /// [ValueConversion(typeof(double), typeof(double), ParameterType = typeof(double))] public class MultiplierConverter : IMultiValueConverter { - /// + /// + /// Converts a list of numeric values into a single value equal to multiplying them together. + /// + /// The list of values to multiply together. + /// The target type. + /// The converter parameter. + /// The culture. + /// A single value as the result of multiplying all the values together. Returns 1 if no values. public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { if (values == null) { - return 1; + return 1.0; } double acc = 1; @@ -31,7 +38,7 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur /// public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return null; } } } diff --git a/src/AudioBand/ValueConverters/ObjectToTypeConverter.cs b/src/AudioBand/ValueConverters/ObjectToTypeConverter.cs index bc595057..8a8cae5b 100644 --- a/src/AudioBand/ValueConverters/ObjectToTypeConverter.cs +++ b/src/AudioBand/ValueConverters/ObjectToTypeConverter.cs @@ -1,11 +1,12 @@ using System; using System.Globalization; +using System.Windows; using System.Windows.Data; namespace AudioBand.ValueConverters { /// - /// Converts and object to a type. + /// Converts an object to its type. /// [ValueConversion(typeof(object), typeof(Type))] public class ObjectToTypeConverter : IValueConverter @@ -13,13 +14,18 @@ public class ObjectToTypeConverter : IValueConverter /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return value?.GetType(); + if (value == null) + { + return DependencyProperty.UnsetValue; + } + + return value.GetType(); } /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } } } diff --git a/src/AudioBand/ValueConverters/PathToImageSourceConverter.cs b/src/AudioBand/ValueConverters/PathToImageSourceConverter.cs index ec7b4e48..18e48cfb 100644 --- a/src/AudioBand/ValueConverters/PathToImageSourceConverter.cs +++ b/src/AudioBand/ValueConverters/PathToImageSourceConverter.cs @@ -55,7 +55,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } /// @@ -78,7 +78,7 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur /// public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return null; } } } diff --git a/src/AudioBand/ValueConverters/PointConverter.cs b/src/AudioBand/ValueConverters/PointConverter.cs index 21985fd7..0af10920 100644 --- a/src/AudioBand/ValueConverters/PointConverter.cs +++ b/src/AudioBand/ValueConverters/PointConverter.cs @@ -16,23 +16,30 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur { if (values == null) { - return default(Point); + return DependencyProperty.UnsetValue; } - if (values.Any(v => v == DependencyProperty.UnsetValue)) + if (values.Any(v => v == DependencyProperty.UnsetValue || v == null)) { - return default(Point); + return DependencyProperty.UnsetValue; } - var x = System.Convert.ToDouble(values[0]); - var y = System.Convert.ToDouble(values[1]); - return new Point(x, y); + try + { + var x = System.Convert.ToDouble(values[0]); + var y = System.Convert.ToDouble(values[1]); + return new Point(x, y); + } + catch + { + return DependencyProperty.UnsetValue; + } } /// public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return null; } } } diff --git a/src/AudioBand/ValueConverters/StringFormatConverter.cs b/src/AudioBand/ValueConverters/StringFormatConverter.cs index 0418bf91..bc3887bf 100644 --- a/src/AudioBand/ValueConverters/StringFormatConverter.cs +++ b/src/AudioBand/ValueConverters/StringFormatConverter.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Windows; using System.Windows.Data; namespace AudioBand.ValueConverters @@ -24,7 +25,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } } } diff --git a/src/AudioBand/ValueConverters/StringToFontFamilyConverter.cs b/src/AudioBand/ValueConverters/StringToFontFamilyConverter.cs index 941cf375..17c84fc9 100644 --- a/src/AudioBand/ValueConverters/StringToFontFamilyConverter.cs +++ b/src/AudioBand/ValueConverters/StringToFontFamilyConverter.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Windows; using System.Windows.Data; using System.Windows.Media; @@ -14,21 +15,20 @@ public class StringToFontFamilyConverter : IValueConverter /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value == null) + if (!ValueConverterHelper.IsValid(value)) { - throw new ArgumentNullException(nameof(value)); + return DependencyProperty.UnsetValue; } - var fontFamilyString = (string)value; - return new FontFamily(fontFamilyString); + return new FontFamily((string)value); } /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - if (value == null) + if (!ValueConverterHelper.IsValid(value)) { - throw new ArgumentNullException(nameof(value)); + return DependencyProperty.UnsetValue; } var fontFamily = (FontFamily)value; diff --git a/src/AudioBand/ValueConverters/StringToVisibilityConverter.cs b/src/AudioBand/ValueConverters/StringToVisibilityConverter.cs index bb375d14..5dc600c6 100644 --- a/src/AudioBand/ValueConverters/StringToVisibilityConverter.cs +++ b/src/AudioBand/ValueConverters/StringToVisibilityConverter.cs @@ -8,7 +8,7 @@ namespace AudioBand.ValueConverters /// /// Converts a string to a value. /// - [ValueConversion(typeof(string), typeof(Visibility))] + [ValueConversion(typeof(string), typeof(Visibility), ParameterType = typeof(bool))] public class StringToVisibilityConverter : IValueConverter { /// @@ -21,7 +21,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } } } diff --git a/src/AudioBand/ValueConverters/TextAlignmentConverter.cs b/src/AudioBand/ValueConverters/TextAlignmentConverter.cs index 5d782fff..d87d32fb 100644 --- a/src/AudioBand/ValueConverters/TextAlignmentConverter.cs +++ b/src/AudioBand/ValueConverters/TextAlignmentConverter.cs @@ -15,6 +15,11 @@ public class TextAlignmentConverter : IValueConverter /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + if (!ValueConverterHelper.IsValid(value)) + { + return DependencyProperty.UnsetValue; + } + switch ((CustomLabel.TextAlignment)value) { case CustomLabel.TextAlignment.Center: @@ -31,7 +36,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); + return DependencyProperty.UnsetValue; } } } diff --git a/src/AudioBand/ValueConverters/TimeSpanToMsConverter.cs b/src/AudioBand/ValueConverters/TimeSpanToMsConverter.cs index 25a7530e..b1a037ad 100644 --- a/src/AudioBand/ValueConverters/TimeSpanToMsConverter.cs +++ b/src/AudioBand/ValueConverters/TimeSpanToMsConverter.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using System.Windows; using System.Windows.Data; namespace AudioBand.ValueConverters @@ -14,9 +13,9 @@ public class TimeSpanToMsConverter : IValueConverter /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value == null || value == DependencyProperty.UnsetValue) + if (!ValueConverterHelper.IsValid(value)) { - return 0; + return 0.0; } return ((TimeSpan)value).TotalMilliseconds; @@ -25,7 +24,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - if (value == null || value == DependencyProperty.UnsetValue) + if (!ValueConverterHelper.IsValid(value)) { return TimeSpan.Zero; } diff --git a/src/AudioBand/ValueConverters/ValueConverterHelper.cs b/src/AudioBand/ValueConverters/ValueConverterHelper.cs new file mode 100644 index 00000000..2ed1785b --- /dev/null +++ b/src/AudioBand/ValueConverters/ValueConverterHelper.cs @@ -0,0 +1,23 @@ +using System.Windows; + +namespace AudioBand.ValueConverters +{ + /// + /// Helpers for value conversion. + /// + internal static class ValueConverterHelper + { + /// + /// Provides a standard guard clause for the converter value. + /// + /// Expected type. + /// The value passed to the converter. + /// True if valid, false otherwise. + internal static bool IsValid(object value) + { + return value != null + && value != DependencyProperty.UnsetValue + && value.GetType() == typeof(TType); + } + } +} diff --git a/src/AudioBand/ViewModels/AudioSourceSettingKeyValue.cs b/src/AudioBand/ViewModels/AudioSourceSettingKeyValue.cs index 4bfd94b6..95665f8b 100644 --- a/src/AudioBand/ViewModels/AudioSourceSettingKeyValue.cs +++ b/src/AudioBand/ViewModels/AudioSourceSettingKeyValue.cs @@ -96,6 +96,14 @@ public void PropagateSettingToAudioSource() } } + /// + /// When the audio sources updates the setting itself instead of from audioband. Sync the changes back to the model. + /// + public void SyncToModel() + { + MapSelf(_model, _originalSource); + } + /// protected override void OnBeginEdit() { @@ -124,7 +132,7 @@ protected override void OnEndEdit() return; } - MapSelf(_model, _originalSource); + SyncToModel(); PropagateSettingToAudioSource(); } } diff --git a/src/AudioBand/ViewModels/AudioSourceSettingsCollectionViewModel.cs b/src/AudioBand/ViewModels/AudioSourceSettingsCollectionViewModel.cs index 2f41eae2..0cc8d0f1 100644 --- a/src/AudioBand/ViewModels/AudioSourceSettingsCollectionViewModel.cs +++ b/src/AudioBand/ViewModels/AudioSourceSettingsCollectionViewModel.cs @@ -6,6 +6,7 @@ using AudioBand.AudioSource; using AudioBand.Messages; using AudioBand.Models; +using AudioBand.Settings; namespace AudioBand.ViewModels { @@ -16,6 +17,7 @@ public class AudioSourceSettingsCollectionViewModel : ViewModelBase { private readonly IInternalAudioSource _audioSource; private readonly IMessageBus _messageBus; + private readonly IAppSettings _appSettings; /// /// Initializes a new instance of the class. @@ -23,10 +25,12 @@ public class AudioSourceSettingsCollectionViewModel : ViewModelBase /// The audiosource that these settings belong to. /// The settings model. /// The message bus. - public AudioSourceSettingsCollectionViewModel(IInternalAudioSource audioSource, AudioSourceSettings settingsModel, IMessageBus messageBus) + /// The app settings. + public AudioSourceSettingsCollectionViewModel(IInternalAudioSource audioSource, AudioSourceSettings settingsModel, IMessageBus messageBus, IAppSettings appSettings) { _audioSource = audioSource; _messageBus = messageBus; + _appSettings = appSettings; SettingsList = new ObservableCollection(CreateKeyValuePairs(audioSource, settingsModel)); _audioSource.SettingChanged += AudioSourceOnSettingChanged; @@ -102,6 +106,9 @@ private List CreateKeyValuePairs(IInternalAudioSourc } // Audio sources can change settings themselves so we need to listen for them. + // Usually, settings are saved after the user edits a setting and clicks apply. + // These changes occur outside of the normal settings lifecycle, so the only time that the new values can be saved + // are when the application closes but there are some issues with detected that, so instead just save now. private void AudioSourceOnSettingChanged(object sender, SettingChangedEventArgs e) { var settingThatChanged = SettingsList.FirstOrDefault(s => s.Name == e.SettingName); @@ -114,6 +121,8 @@ private void AudioSourceOnSettingChanged(object sender, SettingChangedEventArgs try { settingThatChanged.Value = _audioSource[e.SettingName]; + settingThatChanged.SyncToModel(); + _appSettings.Save(); } catch (Exception ex) { diff --git a/src/AudioBand/ViewModels/AudioSourceSettingsViewModel.cs b/src/AudioBand/ViewModels/AudioSourceSettingsViewModel.cs index be4ae67f..2f01512b 100644 --- a/src/AudioBand/ViewModels/AudioSourceSettingsViewModel.cs +++ b/src/AudioBand/ViewModels/AudioSourceSettingsViewModel.cs @@ -48,14 +48,14 @@ public void CreateViewModelForAudioSource(IInternalAudioSource audioSource) var matchingSetting = _appSettings.AudioSourceSettings.FirstOrDefault(s => s.AudioSourceName == audioSource.Name); if (matchingSetting != null) { - var viewModel = new AudioSourceSettingsCollectionViewModel(audioSource, matchingSetting, _messageBus); + var viewModel = new AudioSourceSettingsCollectionViewModel(audioSource, matchingSetting, _messageBus, _appSettings); AudioSourcesSettings.Add(viewModel); } else { var newSettingsModel = new AudioSourceSettings { AudioSourceName = audioSource.Name }; _appSettings.AudioSourceSettings.Add(newSettingsModel); - var newViewModel = new AudioSourceSettingsCollectionViewModel(audioSource, newSettingsModel, _messageBus); + var newViewModel = new AudioSourceSettingsCollectionViewModel(audioSource, newSettingsModel, _messageBus, _appSettings); AudioSourcesSettings.Add(newViewModel); } diff --git a/src/AudioBand/ViewModels/CustomLabelViewModel.cs b/src/AudioBand/ViewModels/CustomLabelViewModel.cs index 7cd5526c..59fd5720 100644 --- a/src/AudioBand/ViewModels/CustomLabelViewModel.cs +++ b/src/AudioBand/ViewModels/CustomLabelViewModel.cs @@ -17,6 +17,7 @@ namespace AudioBand.ViewModels /// public class CustomLabelViewModel : LayoutViewModelBase { + private readonly CustomLabel _source; private readonly IAudioSession _audioSession; private bool _isPlaying; private IEnumerable _textSegments; @@ -31,6 +32,7 @@ public class CustomLabelViewModel : LayoutViewModelBase public CustomLabelViewModel(CustomLabel source, IDialogService dialogService, IAudioSession audioSession, IMessageBus messageBus) : base(messageBus, source) { + _source = source; _audioSession = audioSession; _audioSession.PropertyChanged += AudioSessionOnPropertyChanged; @@ -243,6 +245,13 @@ protected override void OnReset() ReParseSegments(); } + /// + protected override void OnEndEdit() + { + base.OnEndEdit(); + MapSelf(Model, _source); + } + private void AudioSessionOnPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName != nameof(IAudioSession.IsPlaying)) diff --git a/src/AudioBand/ViewModels/CustomLabelsViewModel.cs b/src/AudioBand/ViewModels/CustomLabelsViewModel.cs index 206b4101..46749b76 100644 --- a/src/AudioBand/ViewModels/CustomLabelsViewModel.cs +++ b/src/AudioBand/ViewModels/CustomLabelsViewModel.cs @@ -61,6 +61,8 @@ public CustomLabelsViewModel(IAppSettings appsettings, IDialogService dialogServ /// protected override void OnBeginEdit() { + base.OnBeginEdit(); + _added.Clear(); _removed.Clear(); } @@ -68,6 +70,8 @@ protected override void OnBeginEdit() /// protected override void OnCancelEdit() { + base.OnCancelEdit(); + foreach (var label in _added) { CustomLabels.Remove(label); @@ -85,6 +89,8 @@ protected override void OnCancelEdit() /// protected override void OnEndEdit() { + base.OnEndEdit(); + _added.Clear(); _removed.Clear(); @@ -108,13 +114,13 @@ private void AddLabelCommandOnExecute() private void RemoveLabelCommandOnExecute(CustomLabelViewModel labelViewModel) { - BeginEdit(); - if (!_dialogService.ShowConfirmationDialog(ConfirmationDialogType.DeleteLabel, labelViewModel.Name)) { return; } + BeginEdit(); + CustomLabels.Remove(labelViewModel); // Only add to removed if not a new label diff --git a/src/AudioBand/app.config b/src/AudioBand/app.config index 74f5bf7e..71d88d87 100644 --- a/src/AudioBand/app.config +++ b/src/AudioBand/app.config @@ -1,5 +1,13 @@  + + +
+ + +
+ + @@ -12,4 +20,18 @@ + + + + False + + + + + + + False + + + \ No newline at end of file diff --git a/src/AudioBandRules.ruleset b/src/AudioBandRules.ruleset index 0a65f425..b68806b6 100644 --- a/src/AudioBandRules.ruleset +++ b/src/AudioBandRules.ruleset @@ -19,5 +19,6 @@ + \ No newline at end of file