Add PropertyChanged detection for Dictionary type settings

This commit is contained in:
Michael Bucari-Tovo 2023-01-06 19:46:55 -07:00
parent 1f7000c2c9
commit 5c73beff4b
11 changed files with 211 additions and 179 deletions

View File

@ -48,18 +48,12 @@ namespace AaxDecrypter
TempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc"); TempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc");
DownloadOptions = ArgumentValidator.EnsureNotNull(dlOptions, nameof(dlOptions)); DownloadOptions = ArgumentValidator.EnsureNotNull(dlOptions, nameof(dlOptions));
DownloadOptions.PropertyChanged += DownloadOptions_PropertyChanged; DownloadOptions.DownloadSpeedChanged += (_, speed) => InputFileStream.SpeedLimit = speed;
// delete file after validation is complete // delete file after validation is complete
FileUtility.SaferDelete(OutputFileName); FileUtility.SaferDelete(OutputFileName);
} }
private void DownloadOptions_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(DownloadOptions.DownloadSpeedBps))
InputFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps;
}
public abstract Task CancelAsync(); public abstract Task CancelAsync();
public virtual void SetCoverArt(byte[] coverArt) public virtual void SetCoverArt(byte[] coverArt)
@ -173,7 +167,7 @@ namespace AaxDecrypter
} }
finally finally
{ {
if (nfsp is not null) if (nfsp?.NetworkFileStream is not null)
nfsp.NetworkFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps; nfsp.NetworkFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps;
} }
} }

View File

@ -1,11 +1,12 @@
using AAXClean; using AAXClean;
using System.ComponentModel; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace AaxDecrypter namespace AaxDecrypter
{ {
public interface IDownloadOptions : INotifyPropertyChanged public interface IDownloadOptions
{ {
event EventHandler<long> DownloadSpeedChanged;
FileManager.ReplacementCharacters ReplacementCharacters { get; } FileManager.ReplacementCharacters ReplacementCharacters { get; }
string DownloadUrl { get; } string DownloadUrl { get; }
string UserAgent { get; } string UserAgent { get; }

View File

@ -5,7 +5,6 @@ using DataLayer;
using LibationFileManager; using LibationFileManager;
using FileManager; using FileManager;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.ComponentModel;
using System; using System;
using System.IO; using System.IO;
using ApplicationServices; using ApplicationServices;
@ -14,9 +13,7 @@ namespace FileLiberator
{ {
public class DownloadOptions : IDownloadOptions, IDisposable public class DownloadOptions : IDownloadOptions, IDisposable
{ {
public event EventHandler<long> DownloadSpeedChanged;
public event PropertyChangedEventHandler PropertyChanged;
private readonly IDisposable cancellation;
public LibraryBook LibraryBook { get; } public LibraryBook LibraryBook { get; }
public LibraryBookDto LibraryBookDto { get; } public LibraryBookDto LibraryBookDto { get; }
public string DownloadUrl { get; } public string DownloadUrl { get; }
@ -29,7 +26,7 @@ namespace FileLiberator
public bool StripUnabridged { get; init; } public bool StripUnabridged { get; init; }
public bool CreateCueSheet { get; init; } public bool CreateCueSheet { get; init; }
public bool DownloadClipsBookmarks { get; init; } public bool DownloadClipsBookmarks { get; init; }
public long DownloadSpeedBps { get; set; } public long DownloadSpeedBps { get; init; }
public ChapterInfo ChapterInfo { get; init; } public ChapterInfo ChapterInfo { get; init; }
public bool FixupFile { get; init; } public bool FixupFile { get; init; }
public NAudio.Lame.LameConfig LameConfig { get; init; } public NAudio.Lame.LameConfig LameConfig { get; init; }
@ -72,27 +69,23 @@ namespace FileLiberator
return string.Empty; return string.Empty;
} }
private void DownloadSpeedChanged(string propertyName, long speed) private readonly IDisposable cancellation;
{ public void Dispose() => cancellation?.Dispose();
DownloadSpeedBps = speed;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadSpeedBps)));
}
public void Dispose()
{
cancellation?.Dispose();
}
public DownloadOptions(LibraryBook libraryBook, string downloadUrl, string userAgent) public DownloadOptions(LibraryBook libraryBook, string downloadUrl, string userAgent)
{ {
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook)); LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl)); DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent)); UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
// no null/empty check for key/iv. unencrypted files do not have them
LibraryBookDto = LibraryBook.ToDto(); LibraryBookDto = LibraryBook.ToDto();
cancellation = Configuration.Instance.SubscribeToPropertyChanged<long>(nameof(Configuration.DownloadSpeedLimit), DownloadSpeedChanged); cancellation =
// no null/empty check for key/iv. unencrypted files do not have them Configuration.Instance
.SubscribeToPropertyChanged<long>(
nameof(Configuration.DownloadSpeedLimit),
(_, s) => DownloadSpeedChanged?.Invoke(this, s));
} }
} }
} }

View File

@ -49,8 +49,10 @@ namespace FileManager
public T GetNonString<T>(string propertyName) public T GetNonString<T>(string propertyName)
{ {
var obj = GetObject(propertyName); var obj = GetObject(propertyName);
if (obj is null) return default;
if (obj is null) return default;
if (obj.GetType().IsAssignableTo(typeof(T))) return (T)obj;
if (obj is JObject jObject) return jObject.ToObject<T>();
if (obj is JValue jValue) if (obj is JValue jValue)
{ {
if (jValue.Type == JTokenType.String && typeof(T).IsAssignableTo(typeof(Enum))) if (jValue.Type == JTokenType.String && typeof(T).IsAssignableTo(typeof(Enum)))
@ -62,8 +64,7 @@ namespace FileManager
} }
return jValue.Value<T>(); return jValue.Value<T>();
} }
if (obj is JObject jObject) return jObject.ToObject<T>(); throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}");
return (T)obj;
} }
public object GetObject(string propertyName) public object GetObject(string propertyName)

View File

@ -7,7 +7,7 @@ using System.Linq;
namespace FileManager namespace FileManager
{ {
public class Replacement : ICloneable public record Replacement
{ {
public const int FIXED_COUNT = 6; public const int FIXED_COUNT = 6;
@ -30,8 +30,6 @@ namespace FileManager
Mandatory = mandatory; Mandatory = mandatory;
} }
public object Clone() => new Replacement(CharacterToReplace, ReplacementString, Description, Mandatory);
public void Update(char charToReplace, string replacementString, string description) public void Update(char charToReplace, string replacementString, string description)
{ {
ReplacementString = replacementString; ReplacementString = replacementString;
@ -61,10 +59,20 @@ namespace FileManager
[JsonConverter(typeof(ReplacementCharactersConverter))] [JsonConverter(typeof(ReplacementCharactersConverter))]
public class ReplacementCharacters public class ReplacementCharacters
{ {
static ReplacementCharacters() public override bool Equals(object obj)
{ {
if (obj is ReplacementCharacters second && Replacements.Count == second.Replacements.Count)
{
for (int i = 0; i < Replacements.Count; i++)
if (Replacements[i] != second.Replacements[i])
return false;
return true;
} }
return false;
}
public override int GetHashCode() => Replacements.GetHashCode();
public static readonly ReplacementCharacters Default public static readonly ReplacementCharacters Default
= IsWindows = IsWindows
? new() ? new()

View File

@ -101,7 +101,7 @@ namespace LibationAvalonia
} }
} }
class FormSizeAndPosition private record FormSizeAndPosition
{ {
public int X; public int X;
public int Y; public int Y;
@ -110,7 +110,6 @@ namespace LibationAvalonia
public bool IsMaximized; public bool IsMaximized;
} }
public static void HideMinMaxBtns(this Window form) public static void HideMinMaxBtns(this Window form)
{ {
if (Design.IsDesignMode || !Configuration.IsWindows) if (Design.IsDesignMode || !Configuration.IsWindows)

View File

@ -18,23 +18,24 @@ namespace LibationFileManager
// config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation // config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation
// exceptions: appsettings.json, LibationFiles dir, Settings.json // exceptions: appsettings.json, LibationFiles dir, Settings.json
private PersistentDictionary persistentDictionary { get; set; } private PersistentDictionary persistentDictionary;
public T GetNonString<T>([CallerMemberName] string propertyName = "") => persistentDictionary.GetNonString<T>(propertyName); public T GetNonString<T>([CallerMemberName] string propertyName = "") => persistentDictionary.GetNonString<T>(propertyName);
public object GetObject([CallerMemberName] string propertyName = "") => persistentDictionary.GetObject(propertyName); public object GetObject([CallerMemberName] string propertyName = "") => persistentDictionary.GetObject(propertyName);
public string GetString([CallerMemberName] string propertyName = "") => persistentDictionary.GetString(propertyName); public string GetString([CallerMemberName] string propertyName = "") => persistentDictionary.GetString(propertyName);
public void SetNonString(object newValue, [CallerMemberName] string propertyName = "") public void SetNonString(object newValue, [CallerMemberName] string propertyName = "")
{ {
var existing = GetType().GetProperty(propertyName)?.GetValue(this); var existing = getExistingValue(propertyName);
if (existing?.Equals(newValue) is true) return; if (existing?.Equals(newValue) is true) return;
PropertyChanging?.Invoke(this, new PropertyChangingEventArgsEx(propertyName, existing, newValue)); PropertyChanging?.Invoke(this, new PropertyChangingEventArgsEx(propertyName, existing, newValue));
persistentDictionary.SetNonString(propertyName, newValue); persistentDictionary.SetNonString(propertyName, newValue);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgsEx(propertyName, newValue)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgsEx(propertyName, newValue));
} }
public void SetString(string newValue, [CallerMemberName] string propertyName = "") public void SetString(string newValue, [CallerMemberName] string propertyName = "")
{ {
var existing = GetType().GetProperty(propertyName)?.GetValue(this); var existing = getExistingValue(propertyName);
if (existing?.Equals(newValue) is true) return; if (existing?.Equals(newValue) is true) return;
PropertyChanging?.Invoke(this, new PropertyChangingEventArgsEx(propertyName, existing, newValue)); PropertyChanging?.Invoke(this, new PropertyChangingEventArgsEx(propertyName, existing, newValue));
@ -42,6 +43,13 @@ namespace LibationFileManager
PropertyChanged?.Invoke(this, new PropertyChangedEventArgsEx(propertyName, newValue)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgsEx(propertyName, newValue));
} }
private object getExistingValue(string propertyName)
{
var property = GetType().GetProperty(propertyName);
if (property is not null) return property.GetValue(this);
return GetObject(propertyName);
}
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary> /// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false) public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
{ {
@ -121,13 +129,13 @@ namespace LibationFileManager
public int LameVBRQuality { get => GetNonString<int>(); set => SetNonString(value); } public int LameVBRQuality { get => GetNonString<int>(); set => SetNonString(value); }
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")] [Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
public Dictionary<string, bool> GridColumnsVisibilities { get => GetNonString<Dictionary<string, bool>>(); set => SetNonString(value); } public Dictionary<string, bool> GridColumnsVisibilities { get => GetNonString<EquatableDictionary<string, bool>>().Clone(); set => SetNonString(value); }
[Description("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")] [Description("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")]
public Dictionary<string, int> GridColumnsDisplayIndices { get => GetNonString<Dictionary<string, int>>(); set => SetNonString(value); } public Dictionary<string, int> GridColumnsDisplayIndices { get => GetNonString<EquatableDictionary<string, int>>().Clone(); set => SetNonString(value); }
[Description("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")] [Description("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")]
public Dictionary<string, int> GridColumnsWidths { get => GetNonString<Dictionary<string, int>>(); set => SetNonString(value); } public Dictionary<string, int> GridColumnsWidths { get => GetNonString<EquatableDictionary<string, int>>().Clone(); set => SetNonString(value); }
[Description("Save cover image alongside audiobook?")] [Description("Save cover image alongside audiobook?")]
public bool DownloadCoverArt { get => GetNonString<bool>(); set => SetNonString(value); } public bool DownloadCoverArt { get => GetNonString<bool>(); set => SetNonString(value); }

View File

@ -1,4 +1,5 @@
using Dinah.Core; using Dinah.Core;
using Newtonsoft.Json.Linq;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
@ -151,5 +152,29 @@ namespace LibationFileManager
_observers.Remove(_observer); _observers.Remove(_observer);
} }
} }
/*
* Use this type in the getter for any Dictionary<TKey, TValue> settings,
* and be sure to clone it before returning. This allows Configuration to
* accurately detect if an of the Dictionary's elements have changed.
*/
private class EquatableDictionary<TKey, TValue> : Dictionary<TKey, TValue>
{
public EquatableDictionary(IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs) : base(keyValuePairs) { }
public EquatableDictionary<TKey, TValue> Clone() => new(this);
public override bool Equals(object obj)
{
if (obj is Dictionary<TKey, TValue> dic && Count == dic.Count)
{
foreach (var pair in this)
if (!dic.TryGetValue(pair.Key, out var value) || !pair.Value.Equals(value))
return false;
return true;
}
return false;
}
public override int GetHashCode() => base.GetHashCode();
}
} }
} }

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dinah.Core; using Dinah.Core;
@ -33,7 +32,11 @@ namespace LibationFileManager
#region singleton stuff #region singleton stuff
public static Configuration Instance { get; } = new Configuration(); public static Configuration Instance { get; } = new Configuration();
private Configuration() { } private Configuration()
{
PropertyChanging += Configuration_PropertyChanging;
PropertyChanged += Configuration_PropertyChanged;
}
#endregion #endregion
} }
} }

View File

@ -30,7 +30,7 @@ namespace LibationWinForms.Dialogs
var r = replacements[i]; var r = replacements[i];
int row = dataGridView1.Rows.Add(r.CharacterToReplace.ToString(), r.ReplacementString, r.Description); int row = dataGridView1.Rows.Add(r.CharacterToReplace.ToString(), r.ReplacementString, r.Description);
dataGridView1.Rows[row].Tag = r.Clone(); dataGridView1.Rows[row].Tag = r with { };
if (r.Mandatory) if (r.Mandatory)

View File

@ -90,8 +90,7 @@ namespace LibationWinForms
config.SetNonString(saveState, form.Name); config.SetNonString(saveState, form.Name);
} }
} private record FormSizeAndPosition
class FormSizeAndPosition
{ {
public int X; public int X;
public int Y; public int Y;
@ -99,4 +98,5 @@ namespace LibationWinForms
public int Width; public int Width;
public bool IsMaximized; public bool IsMaximized;
} }
}
} }