From 5c73beff4b315de6e1aedc23a9fd9ede81ad8d6f Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Fri, 6 Jan 2023 19:46:55 -0700 Subject: [PATCH] Add PropertyChanged detection for Dictionary type settings --- Source/AaxDecrypter/AudiobookDownloadBase.cs | 10 +- Source/AaxDecrypter/IDownloadOptions.cs | 5 +- Source/FileLiberator/DownloadOptions.cs | 29 +- Source/FileManager/PersistentDictionary.cs | 7 +- Source/FileManager/ReplacementCharacters.cs | 16 +- Source/LibationAvalonia/FormSaveExtension.cs | 3 +- .../Configuration.PersistentSettings.cs | 270 +++++++++--------- .../Configuration.PropertyChange.cs | 25 ++ Source/LibationFileManager/Configuration.cs | 7 +- .../Dialogs/EditReplacementChars.cs | 2 +- Source/LibationWinForms/FormSaveExtension.cs | 16 +- 11 files changed, 211 insertions(+), 179 deletions(-) diff --git a/Source/AaxDecrypter/AudiobookDownloadBase.cs b/Source/AaxDecrypter/AudiobookDownloadBase.cs index 69a69407..121520e7 100644 --- a/Source/AaxDecrypter/AudiobookDownloadBase.cs +++ b/Source/AaxDecrypter/AudiobookDownloadBase.cs @@ -48,18 +48,12 @@ namespace AaxDecrypter TempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc"); DownloadOptions = ArgumentValidator.EnsureNotNull(dlOptions, nameof(dlOptions)); - DownloadOptions.PropertyChanged += DownloadOptions_PropertyChanged; + DownloadOptions.DownloadSpeedChanged += (_, speed) => InputFileStream.SpeedLimit = speed; // delete file after validation is complete 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 virtual void SetCoverArt(byte[] coverArt) @@ -173,7 +167,7 @@ namespace AaxDecrypter } finally { - if (nfsp is not null) + if (nfsp?.NetworkFileStream is not null) nfsp.NetworkFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps; } } diff --git a/Source/AaxDecrypter/IDownloadOptions.cs b/Source/AaxDecrypter/IDownloadOptions.cs index ff24c1b7..6972f067 100644 --- a/Source/AaxDecrypter/IDownloadOptions.cs +++ b/Source/AaxDecrypter/IDownloadOptions.cs @@ -1,11 +1,12 @@ using AAXClean; -using System.ComponentModel; +using System; using System.Threading.Tasks; namespace AaxDecrypter { - public interface IDownloadOptions : INotifyPropertyChanged + public interface IDownloadOptions { + event EventHandler DownloadSpeedChanged; FileManager.ReplacementCharacters ReplacementCharacters { get; } string DownloadUrl { get; } string UserAgent { get; } diff --git a/Source/FileLiberator/DownloadOptions.cs b/Source/FileLiberator/DownloadOptions.cs index 0575764c..3b74aa2b 100644 --- a/Source/FileLiberator/DownloadOptions.cs +++ b/Source/FileLiberator/DownloadOptions.cs @@ -5,7 +5,6 @@ using DataLayer; using LibationFileManager; using FileManager; using System.Threading.Tasks; -using System.ComponentModel; using System; using System.IO; using ApplicationServices; @@ -14,9 +13,7 @@ namespace FileLiberator { public class DownloadOptions : IDownloadOptions, IDisposable { - - public event PropertyChangedEventHandler PropertyChanged; - private readonly IDisposable cancellation; + public event EventHandler DownloadSpeedChanged; public LibraryBook LibraryBook { get; } public LibraryBookDto LibraryBookDto { get; } public string DownloadUrl { get; } @@ -29,7 +26,7 @@ namespace FileLiberator public bool StripUnabridged { get; init; } public bool CreateCueSheet { get; init; } public bool DownloadClipsBookmarks { get; init; } - public long DownloadSpeedBps { get; set; } + public long DownloadSpeedBps { get; init; } public ChapterInfo ChapterInfo { get; init; } public bool FixupFile { get; init; } public NAudio.Lame.LameConfig LameConfig { get; init; } @@ -72,27 +69,23 @@ namespace FileLiberator return string.Empty; } - private void DownloadSpeedChanged(string propertyName, long speed) - { - DownloadSpeedBps = speed; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadSpeedBps))); - } - - public void Dispose() - { - cancellation?.Dispose(); - } + private readonly IDisposable cancellation; + public void Dispose() => cancellation?.Dispose(); public DownloadOptions(LibraryBook libraryBook, string downloadUrl, string userAgent) { LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook)); DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl)); UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent)); + // no null/empty check for key/iv. unencrypted files do not have them LibraryBookDto = LibraryBook.ToDto(); - cancellation = Configuration.Instance.SubscribeToPropertyChanged(nameof(Configuration.DownloadSpeedLimit), DownloadSpeedChanged); - // no null/empty check for key/iv. unencrypted files do not have them + cancellation = + Configuration.Instance + .SubscribeToPropertyChanged( + nameof(Configuration.DownloadSpeedLimit), + (_, s) => DownloadSpeedChanged?.Invoke(this, s)); } - } + } } diff --git a/Source/FileManager/PersistentDictionary.cs b/Source/FileManager/PersistentDictionary.cs index db66983a..8e8ee518 100644 --- a/Source/FileManager/PersistentDictionary.cs +++ b/Source/FileManager/PersistentDictionary.cs @@ -49,8 +49,10 @@ namespace FileManager public T GetNonString(string 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(); if (obj is JValue jValue) { if (jValue.Type == JTokenType.String && typeof(T).IsAssignableTo(typeof(Enum))) @@ -62,8 +64,7 @@ namespace FileManager } return jValue.Value(); } - if (obj is JObject jObject) return jObject.ToObject(); - return (T)obj; + throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}"); } public object GetObject(string propertyName) diff --git a/Source/FileManager/ReplacementCharacters.cs b/Source/FileManager/ReplacementCharacters.cs index 85b65317..4457e19f 100644 --- a/Source/FileManager/ReplacementCharacters.cs +++ b/Source/FileManager/ReplacementCharacters.cs @@ -7,7 +7,7 @@ using System.Linq; namespace FileManager { - public class Replacement : ICloneable + public record Replacement { public const int FIXED_COUNT = 6; @@ -30,8 +30,6 @@ namespace FileManager Mandatory = mandatory; } - public object Clone() => new Replacement(CharacterToReplace, ReplacementString, Description, Mandatory); - public void Update(char charToReplace, string replacementString, string description) { ReplacementString = replacementString; @@ -61,10 +59,20 @@ namespace FileManager [JsonConverter(typeof(ReplacementCharactersConverter))] 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 = IsWindows ? new() diff --git a/Source/LibationAvalonia/FormSaveExtension.cs b/Source/LibationAvalonia/FormSaveExtension.cs index 3dbd64f1..384f3323 100644 --- a/Source/LibationAvalonia/FormSaveExtension.cs +++ b/Source/LibationAvalonia/FormSaveExtension.cs @@ -101,7 +101,7 @@ namespace LibationAvalonia } } - class FormSizeAndPosition + private record FormSizeAndPosition { public int X; public int Y; @@ -110,7 +110,6 @@ namespace LibationAvalonia public bool IsMaximized; } - public static void HideMinMaxBtns(this Window form) { if (Design.IsDesignMode || !Configuration.IsWindows) diff --git a/Source/LibationFileManager/Configuration.PersistentSettings.cs b/Source/LibationFileManager/Configuration.PersistentSettings.cs index 5de25ef3..98458102 100644 --- a/Source/LibationFileManager/Configuration.PersistentSettings.cs +++ b/Source/LibationFileManager/Configuration.PersistentSettings.cs @@ -10,31 +10,32 @@ using Newtonsoft.Json.Converters; namespace LibationFileManager { - public partial class Configuration + public partial class Configuration { - // note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app + // note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app - // default setting and directory creation occur in class responsible for files. - // config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation - // exceptions: appsettings.json, LibationFiles dir, Settings.json + // default setting and directory creation occur in class responsible for files. + // config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation + // exceptions: appsettings.json, LibationFiles dir, Settings.json - private PersistentDictionary persistentDictionary { get; set; } + private PersistentDictionary persistentDictionary; - public T GetNonString([CallerMemberName] string propertyName = "") => persistentDictionary.GetNonString(propertyName); - public object GetObject([CallerMemberName] string propertyName = "") => persistentDictionary.GetObject(propertyName); - public string GetString([CallerMemberName] string propertyName = "") => persistentDictionary.GetString(propertyName); - public void SetNonString(object newValue, [CallerMemberName] string propertyName = "") + public T GetNonString([CallerMemberName] string propertyName = "") => persistentDictionary.GetNonString(propertyName); + public object GetObject([CallerMemberName] string propertyName = "") => persistentDictionary.GetObject(propertyName); + public string GetString([CallerMemberName] string propertyName = "") => persistentDictionary.GetString(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; PropertyChanging?.Invoke(this, new PropertyChangingEventArgsEx(propertyName, existing, newValue)); persistentDictionary.SetNonString(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; PropertyChanging?.Invoke(this, new PropertyChangingEventArgsEx(propertyName, existing, newValue)); @@ -42,101 +43,108 @@ namespace LibationFileManager 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); + } + /// WILL ONLY set if already present. WILL NOT create new public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false) - { + { var settingWasChanged = persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging); - if (settingWasChanged) + if (settingWasChanged) configuration?.Reload(); - } + } - public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json"); + public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json"); - public static string GetDescription(string propertyName) - { - var attribute = typeof(Configuration) - .GetProperty(propertyName) - ?.GetCustomAttributes(typeof(DescriptionAttribute), true) - .SingleOrDefault() - as DescriptionAttribute; + public static string GetDescription(string propertyName) + { + var attribute = typeof(Configuration) + .GetProperty(propertyName) + ?.GetCustomAttributes(typeof(DescriptionAttribute), true) + .SingleOrDefault() + as DescriptionAttribute; - return attribute?.Description; - } + return attribute?.Description; + } - public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName); + public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName); - [Description("Set cover art as the folder's icon. (Windows only)")] - public bool UseCoverAsFolderIcon { get => GetNonString(); set => SetNonString(value); } + [Description("Set cover art as the folder's icon. (Windows only)")] + public bool UseCoverAsFolderIcon { get => GetNonString(); set => SetNonString(value); } - [Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")] - public bool BetaOptIn { get => GetNonString(); set => SetNonString(value); } + [Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")] + public bool BetaOptIn { get => GetNonString(); set => SetNonString(value); } - [Description("Location for book storage. Includes destination of newly liberated books")] - public string Books { get => GetString(); set => SetString(value); } + [Description("Location for book storage. Includes destination of newly liberated books")] + public string Books { get => GetString(); set => SetString(value); } - // temp/working dir(s) should be outside of dropbox - [Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")] - public string InProgress { get => GetString(); set => SetString(value); } + // temp/working dir(s) should be outside of dropbox + [Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")] + public string InProgress { get => GetString(); set => SetString(value); } - [Description("Allow Libation to fix up audiobook metadata")] - public bool AllowLibationFixup { get => GetNonString(); set => SetNonString(value); } + [Description("Allow Libation to fix up audiobook metadata")] + public bool AllowLibationFixup { get => GetNonString(); set => SetNonString(value); } [Description("Create a cue sheet (.cue)")] - public bool CreateCueSheet { get => GetNonString(); set => SetNonString(value); } + public bool CreateCueSheet { get => GetNonString(); set => SetNonString(value); } [Description("Retain the Aax file after successfully decrypting")] - public bool RetainAaxFile { get => GetNonString(); set => SetNonString(value); } + public bool RetainAaxFile { get => GetNonString(); set => SetNonString(value); } [Description("Split my books into multiple files by chapter")] - public bool SplitFilesByChapter { get => GetNonString(); set => SetNonString(value); } + public bool SplitFilesByChapter { get => GetNonString(); set => SetNonString(value); } [Description("Merge Opening/End Credits into the following/preceding chapters")] - public bool MergeOpeningAndEndCredits { get => GetNonString(); set => SetNonString(value); } + public bool MergeOpeningAndEndCredits { get => GetNonString(); set => SetNonString(value); } [Description("Strip \"(Unabridged)\" from audiobook metadata tags")] - public bool StripUnabridged { get => GetNonString(); set => SetNonString(value); } + public bool StripUnabridged { get => GetNonString(); set => SetNonString(value); } [Description("Strip audible branding from the start and end of audiobooks.\r\n(e.g. \"This is Audible\")")] - public bool StripAudibleBrandAudio { get => GetNonString(); set => SetNonString(value); } + public bool StripAudibleBrandAudio { get => GetNonString(); set => SetNonString(value); } [Description("Decrypt to lossy format?")] - public bool DecryptToLossy { get => GetNonString(); set => SetNonString(value); } + public bool DecryptToLossy { get => GetNonString(); set => SetNonString(value); } [Description("Lame encoder target. true = Bitrate, false = Quality")] - public bool LameTargetBitrate { get => GetNonString(); set => SetNonString(value); } + public bool LameTargetBitrate { get => GetNonString(); set => SetNonString(value); } [Description("Lame encoder downsamples to mono")] - public bool LameDownsampleMono { get => GetNonString(); set => SetNonString(value); } + public bool LameDownsampleMono { get => GetNonString(); set => SetNonString(value); } - [Description("Lame target bitrate [16,320]")] - public int LameBitrate { get => GetNonString(); set => SetNonString(value); } + [Description("Lame target bitrate [16,320]")] + public int LameBitrate { get => GetNonString(); set => SetNonString(value); } - [Description("Restrict encoder to constant bitrate?")] - public bool LameConstantBitrate { get => GetNonString(); set => SetNonString(value); } + [Description("Restrict encoder to constant bitrate?")] + public bool LameConstantBitrate { get => GetNonString(); set => SetNonString(value); } [Description("Match the source bitrate?")] - public bool LameMatchSourceBR { get => GetNonString(); set => SetNonString(value); } + public bool LameMatchSourceBR { get => GetNonString(); set => SetNonString(value); } [Description("Lame target VBR quality [10,100]")] - public int LameVBRQuality { get => GetNonString(); set => SetNonString(value); } + public int LameVBRQuality { get => GetNonString(); set => SetNonString(value); } [Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")] - public Dictionary GridColumnsVisibilities { get => GetNonString>(); set => SetNonString(value); } + public Dictionary GridColumnsVisibilities { get => GetNonString>().Clone(); set => SetNonString(value); } - [Description("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")] - public Dictionary GridColumnsDisplayIndices { get => GetNonString>(); set => SetNonString(value); } + [Description("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")] + public Dictionary GridColumnsDisplayIndices { get => GetNonString>().Clone(); set => SetNonString(value); } - [Description("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")] - public Dictionary GridColumnsWidths { get => GetNonString>(); set => SetNonString(value); } + [Description("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")] + public Dictionary GridColumnsWidths { get => GetNonString>().Clone(); set => SetNonString(value); } - [Description("Save cover image alongside audiobook?")] - public bool DownloadCoverArt { get => GetNonString(); set => SetNonString(value); } + [Description("Save cover image alongside audiobook?")] + public bool DownloadCoverArt { get => GetNonString(); set => SetNonString(value); } [Description("Download clips and bookmarks?")] - public bool DownloadClipsBookmarks { get => GetNonString(); set => SetNonString(value); } - - [Description("File format to save clips and bookmarks")] - public ClipBookmarkFormat ClipsBookmarksFileFormat { get => GetNonString(); set => SetNonString(value); } + public bool DownloadClipsBookmarks { get => GetNonString(); set => SetNonString(value); } + + [Description("File format to save clips and bookmarks")] + public ClipBookmarkFormat ClipsBookmarksFileFormat { get => GetNonString(); set => SetNonString(value); } [JsonConverter(typeof(StringEnumConverter))] public enum ClipBookmarkFormat @@ -145,100 +153,100 @@ namespace LibationFileManager CSV, [Description("Microsoft Excel Spreadsheet")] Xlsx, - [Description("JavaScript Object Notation (JSON)")] - Json - } + [Description("JavaScript Object Notation (JSON)")] + Json + } [JsonConverter(typeof(StringEnumConverter))] public enum BadBookAction - { - [Description("Ask each time what action to take.")] - Ask = 0, - [Description("Stop processing books.")] - Abort = 1, - [Description("Retry book later. Skip for now. Continue processing books.")] - Retry = 2, - [Description("Permanently ignore book. Continue processing books. Do not try book again.")] - Ignore = 3 - } + { + [Description("Ask each time what action to take.")] + Ask = 0, + [Description("Stop processing books.")] + Abort = 1, + [Description("Retry book later. Skip for now. Continue processing books.")] + Retry = 2, + [Description("Permanently ignore book. Continue processing books. Do not try book again.")] + Ignore = 3 + } - [Description("When liberating books and there is an error, Libation should:")] - public BadBookAction BadBook { get => GetNonString(); set => SetNonString(value); } + [Description("When liberating books and there is an error, Libation should:")] + public BadBookAction BadBook { get => GetNonString(); set => SetNonString(value); } - [Description("Show number of newly imported titles? When unchecked, no pop-up will appear after library scan.")] - public bool ShowImportedStats { get => GetNonString(); set => SetNonString(value); } + [Description("Show number of newly imported titles? When unchecked, no pop-up will appear after library scan.")] + public bool ShowImportedStats { get => GetNonString(); set => SetNonString(value); } - [Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")] - public bool ImportEpisodes { get => GetNonString(); set => SetNonString(value); } + [Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")] + public bool ImportEpisodes { get => GetNonString(); set => SetNonString(value); } [Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")] - public bool DownloadEpisodes { get => GetNonString(); set => SetNonString(value); } + public bool DownloadEpisodes { get => GetNonString(); set => SetNonString(value); } - [Description("Automatically run periodic scans in the background?")] - public bool AutoScan { get => GetNonString(); set => SetNonString(value); } + [Description("Automatically run periodic scans in the background?")] + public bool AutoScan { get => GetNonString(); set => SetNonString(value); } - [Description("Auto download books? After scan, download new books in 'checked' accounts.")] - // poorly named setting. Should just be 'AutoDownload'. It is NOT episode specific - public bool AutoDownloadEpisodes { get => GetNonString(); set => SetNonString(value); } + [Description("Auto download books? After scan, download new books in 'checked' accounts.")] + // poorly named setting. Should just be 'AutoDownload'. It is NOT episode specific + public bool AutoDownloadEpisodes { get => GetNonString(); set => SetNonString(value); } [Description("Save all podcast episodes in a series to the series parent folder?")] - public bool SavePodcastsToParentFolder { get => GetNonString(); set => SetNonString(value); } + public bool SavePodcastsToParentFolder { get => GetNonString(); set => SetNonString(value); } [Description("Global download speed limit in bytes per second.")] public long DownloadSpeedLimit { - get - { - var limit = GetNonString(); - return limit <= 0 ? 0 : Math.Max(limit, AaxDecrypter.NetworkFileStream.MIN_BYTES_PER_SECOND); + get + { + var limit = GetNonString(); + return limit <= 0 ? 0 : Math.Max(limit, AaxDecrypter.NetworkFileStream.MIN_BYTES_PER_SECOND); } - set - { + set + { var limit = value <= 0 ? 0 : Math.Max(value, AaxDecrypter.NetworkFileStream.MIN_BYTES_PER_SECOND); SetNonString(limit); - } + } } #region templates: custom file naming [Description("Edit how filename characters are replaced")] - public ReplacementCharacters ReplacementCharacters { get => GetNonString(); set => SetNonString(value); } + public ReplacementCharacters ReplacementCharacters { get => GetNonString(); set => SetNonString(value); } - [Description("How to format the folders in which files will be saved")] - public string FolderTemplate - { - get => getTemplate(nameof(FolderTemplate), Templates.Folder); - set => setTemplate(nameof(FolderTemplate), Templates.Folder, value); - } + [Description("How to format the folders in which files will be saved")] + public string FolderTemplate + { + get => getTemplate(nameof(FolderTemplate), Templates.Folder); + set => setTemplate(nameof(FolderTemplate), Templates.Folder, value); + } - [Description("How to format the saved pdf and audio files")] - public string FileTemplate - { - get => getTemplate(nameof(FileTemplate), Templates.File); - set => setTemplate(nameof(FileTemplate), Templates.File, value); - } + [Description("How to format the saved pdf and audio files")] + public string FileTemplate + { + get => getTemplate(nameof(FileTemplate), Templates.File); + set => setTemplate(nameof(FileTemplate), Templates.File, value); + } - [Description("How to format the saved audio files when split by chapters")] - public string ChapterFileTemplate - { - get => getTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile); - set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value); - } + [Description("How to format the saved audio files when split by chapters")] + public string ChapterFileTemplate + { + get => getTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile); + set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value); + } - [Description("How to format the file's Tile stored in metadata")] - public string ChapterTitleTemplate - { - get => getTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle); - set => setTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle, value); - } + [Description("How to format the file's Tile stored in metadata")] + public string ChapterTitleTemplate + { + get => getTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle); + set => setTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle, value); + } - private string getTemplate(string settingName, Templates templ) => templ.GetValid(GetString(settingName)); - private void setTemplate(string settingName, Templates templ, string newValue) - { - var template = newValue?.Trim(); - if (templ.IsValid(template)) - SetString(template, settingName); - } - #endregion - } + private string getTemplate(string settingName, Templates templ) => templ.GetValid(GetString(settingName)); + private void setTemplate(string settingName, Templates templ, string newValue) + { + var template = newValue?.Trim(); + if (templ.IsValid(template)) + SetString(template, settingName); + } + #endregion + } } diff --git a/Source/LibationFileManager/Configuration.PropertyChange.cs b/Source/LibationFileManager/Configuration.PropertyChange.cs index b3d032c9..e923b12d 100644 --- a/Source/LibationFileManager/Configuration.PropertyChange.cs +++ b/Source/LibationFileManager/Configuration.PropertyChange.cs @@ -1,4 +1,5 @@ using Dinah.Core; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.ComponentModel; @@ -151,5 +152,29 @@ namespace LibationFileManager _observers.Remove(_observer); } } + + /* + * Use this type in the getter for any Dictionary 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 : Dictionary + { + public EquatableDictionary(IEnumerable> keyValuePairs) : base(keyValuePairs) { } + public EquatableDictionary Clone() => new(this); + public override bool Equals(object obj) + { + if (obj is Dictionary 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(); + } } } diff --git a/Source/LibationFileManager/Configuration.cs b/Source/LibationFileManager/Configuration.cs index 518dac78..517bce8d 100644 --- a/Source/LibationFileManager/Configuration.cs +++ b/Source/LibationFileManager/Configuration.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using Dinah.Core; @@ -33,7 +32,11 @@ namespace LibationFileManager #region singleton stuff public static Configuration Instance { get; } = new Configuration(); - private Configuration() { } + private Configuration() + { + PropertyChanging += Configuration_PropertyChanging; + PropertyChanged += Configuration_PropertyChanged; + } #endregion } } diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.cs b/Source/LibationWinForms/Dialogs/EditReplacementChars.cs index 75762a90..68edd2f6 100644 --- a/Source/LibationWinForms/Dialogs/EditReplacementChars.cs +++ b/Source/LibationWinForms/Dialogs/EditReplacementChars.cs @@ -30,7 +30,7 @@ namespace LibationWinForms.Dialogs var r = replacements[i]; 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) diff --git a/Source/LibationWinForms/FormSaveExtension.cs b/Source/LibationWinForms/FormSaveExtension.cs index d4b8feee..9789e9eb 100644 --- a/Source/LibationWinForms/FormSaveExtension.cs +++ b/Source/LibationWinForms/FormSaveExtension.cs @@ -90,13 +90,13 @@ namespace LibationWinForms config.SetNonString(saveState, form.Name); } - } - class FormSizeAndPosition - { - public int X; - public int Y; - public int Height; - public int Width; - public bool IsMaximized; + private record FormSizeAndPosition + { + public int X; + public int Y; + public int Height; + public int Width; + public bool IsMaximized; + } } }