From 1470aefd42ebbea782b8056252a4952df2a81081 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Tue, 21 Jun 2022 18:34:47 -0600 Subject: [PATCH 01/20] Add character replacement --- .../AaxcDownloadMultiConverter.cs | 2 +- Source/AaxDecrypter/AudiobookDownloadBase.cs | 2 +- Source/AaxDecrypter/IDownloadOptions.cs | 1 + .../MultiConvertFileProperties.cs | 10 - .../UnencryptedAudiobookDownloader.cs | 4 +- Source/AppScaffolding/LibationScaffolding.cs | 3 + Source/FileLiberator/ConvertToMp3.cs | 2 +- Source/FileLiberator/DownloadDecryptBook.cs | 2 +- Source/FileLiberator/DownloadOptions.cs | 2 + Source/FileManager/FileNamingTemplate.cs | 4 +- Source/FileManager/FileUtility.cs | 100 +++------- Source/FileManager/ReplacementCharacters.cs | 155 +++++++++++++++ Source/LibationFileManager/Configuration.cs | 7 + Source/LibationFileManager/Templates.cs | 8 +- .../Dialogs/EditReplacementChars.Designer.cs | 156 +++++++++++++++ .../Dialogs/EditReplacementChars.cs | 177 ++++++++++++++++++ .../Dialogs/EditReplacementChars.resx | 69 +++++++ .../Dialogs/SettingsDialog.Designer.cs | 56 +++--- .../Dialogs/SettingsDialog.DownloadDecrypt.cs | 9 + 19 files changed, 654 insertions(+), 115 deletions(-) create mode 100644 Source/FileManager/ReplacementCharacters.cs create mode 100644 Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs create mode 100644 Source/LibationWinForms/Dialogs/EditReplacementChars.cs create mode 100644 Source/LibationWinForms/Dialogs/EditReplacementChars.resx diff --git a/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs b/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs index 25cac87b..ea11c319 100644 --- a/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs +++ b/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs @@ -141,7 +141,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea private FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties) { var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties); - fileName = FileUtility.GetValidFilename(fileName); + fileName = FileUtility.GetValidFilename(fileName, DownloadOptions.ReplacementCharacters); multiPartFilePaths.Add(fileName); diff --git a/Source/AaxDecrypter/AudiobookDownloadBase.cs b/Source/AaxDecrypter/AudiobookDownloadBase.cs index 27ce3025..08d4f3d1 100644 --- a/Source/AaxDecrypter/AudiobookDownloadBase.cs +++ b/Source/AaxDecrypter/AudiobookDownloadBase.cs @@ -103,7 +103,7 @@ namespace AaxDecrypter try { var path = Path.ChangeExtension(OutputFileName, ".cue"); - path = FileUtility.GetValidFilename(path); + path = FileUtility.GetValidFilename(path, DownloadOptions.ReplacementCharacters); File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo)); OnFileCreated(path); } diff --git a/Source/AaxDecrypter/IDownloadOptions.cs b/Source/AaxDecrypter/IDownloadOptions.cs index 64ae08e3..58d6e90f 100644 --- a/Source/AaxDecrypter/IDownloadOptions.cs +++ b/Source/AaxDecrypter/IDownloadOptions.cs @@ -4,6 +4,7 @@ namespace AaxDecrypter { public interface IDownloadOptions { + FileManager.ReplacementCharacters ReplacementCharacters { get; } string DownloadUrl { get; } string UserAgent { get; } string AudibleKey { get; } diff --git a/Source/AaxDecrypter/MultiConvertFileProperties.cs b/Source/AaxDecrypter/MultiConvertFileProperties.cs index febc2a49..71bafe21 100644 --- a/Source/AaxDecrypter/MultiConvertFileProperties.cs +++ b/Source/AaxDecrypter/MultiConvertFileProperties.cs @@ -11,15 +11,5 @@ namespace AaxDecrypter public int PartsTotal { get; set; } public string Title { get; set; } - public static string DefaultMultipartFilename(MultiConvertFileProperties multiConvertFileProperties) - { - var template = Path.ChangeExtension(multiConvertFileProperties.OutputFileName, null) + " - - " + Path.GetExtension(multiConvertFileProperties.OutputFileName); - - var fileNamingTemplate = new FileNamingTemplate(template) { IllegalCharacterReplacements = " " }; - fileNamingTemplate.AddParameterReplacement("ch# 0", FileUtility.GetSequenceFormatted(multiConvertFileProperties.PartsPosition, multiConvertFileProperties.PartsTotal)); - fileNamingTemplate.AddParameterReplacement("title", multiConvertFileProperties.Title ?? ""); - - return fileNamingTemplate.GetFilePath(); - } } } diff --git a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs index cd6dee88..142ccf44 100644 --- a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs +++ b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs @@ -67,8 +67,8 @@ namespace AaxDecrypter } CloseInputFileStream(); - - var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName); + + var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName, DownloadOptions.ReplacementCharacters); SetOutputFileName(realOutputFileName); OnFileCreated(realOutputFileName); diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 411bc886..0d338351 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -117,6 +117,9 @@ namespace AppScaffolding if (!config.Exists(nameof(config.DownloadEpisodes))) config.DownloadEpisodes = true; + if (!config.Exists(nameof(config.ReplacementCharacters))) + config.ReplacementCharacters = FileManager.ReplacementCharacters.Default; + if (!config.Exists(nameof(config.FolderTemplate))) config.FolderTemplate = Templates.Folder.DefaultTemplate; diff --git a/Source/FileLiberator/ConvertToMp3.cs b/Source/FileLiberator/ConvertToMp3.cs index 14a75b9b..bb191986 100644 --- a/Source/FileLiberator/ConvertToMp3.cs +++ b/Source/FileLiberator/ConvertToMp3.cs @@ -70,7 +70,7 @@ namespace FileLiberator return new StatusHandler { "Cancelled" }; } - var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path); + var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path, Configuration.Instance.ReplacementCharacters); OnFileCreated(libraryBook, realMp3Path); } return new StatusHandler(); diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index 906acca3..e759386d 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -258,7 +258,7 @@ namespace FileLiberator { var entry = entries[i]; - var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path))); + var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)), Configuration.Instance.ReplacementCharacters); FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest); // propogate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop) diff --git a/Source/FileLiberator/DownloadOptions.cs b/Source/FileLiberator/DownloadOptions.cs index 86b0a778..dc2fd940 100644 --- a/Source/FileLiberator/DownloadOptions.cs +++ b/Source/FileLiberator/DownloadOptions.cs @@ -3,6 +3,7 @@ using AAXClean; using Dinah.Core; using DataLayer; using LibationFileManager; +using FileManager; namespace FileLiberator { @@ -23,6 +24,7 @@ namespace FileLiberator public NAudio.Lame.LameConfig LameConfig { get; set; } public bool Downsample { get; set; } public bool MatchSourceBitrate { get; set; } + public ReplacementCharacters ReplacementCharacters => Configuration.Instance.ReplacementCharacters; public string GetMultipartFileName(MultiConvertFileProperties props) => Templates.ChapterFile.GetFilename(LibraryBookDto, props); diff --git a/Source/FileManager/FileNamingTemplate.cs b/Source/FileManager/FileNamingTemplate.cs index 1f472b6d..c55e2f8f 100644 --- a/Source/FileManager/FileNamingTemplate.cs +++ b/Source/FileManager/FileNamingTemplate.cs @@ -16,7 +16,7 @@ namespace FileManager public string IllegalCharacterReplacements { get; set; } /// <summary>Generate a valid path for this file or directory</summary> - public LongPath GetFilePath(bool returnFirstExisting = false) + public LongPath GetFilePath(ReplacementCharacters replacements, bool returnFirstExisting = false) { string fileName = Template.EndsWith(Path.DirectorySeparatorChar) ? Template[..^1] : Template; @@ -43,7 +43,7 @@ namespace FileManager pathParts.Reverse(); - return FileUtility.GetValidFilename(Path.Join(pathParts.ToArray()), IllegalCharacterReplacements, returnFirstExisting); + return FileUtility.GetValidFilename(Path.Join(pathParts.ToArray()), replacements, returnFirstExisting); } private string replaceFileName(string filename, Dictionary<string,string> paramReplacements) diff --git a/Source/FileManager/FileUtility.cs b/Source/FileManager/FileUtility.cs index 54bb3320..550f46e0 100644 --- a/Source/FileManager/FileUtility.cs +++ b/Source/FileManager/FileUtility.cs @@ -46,12 +46,12 @@ namespace FileManager /// <br/>- ensure uniqueness /// <br/>- enforce max file length /// </summary> - public static LongPath GetValidFilename(LongPath path, string illegalCharacterReplacements = "", bool returnFirstExisting = false) + public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, bool returnFirstExisting = false) { ArgumentValidator.EnsureNotNull(path, nameof(path)); // remove invalid chars - path = GetSafePath(path, illegalCharacterReplacements); + path = GetSafePath(path, replacements); // ensure uniqueness and check lengths var dir = Path.GetDirectoryName(path); @@ -77,35 +77,45 @@ namespace FileManager return fullfilename; } - // GetInvalidFileNameChars contains everything in GetInvalidPathChars plus ':', '*', '?', '\\', '/' - - /// <summary>Use with file name, not full path. Valid path charaters which are invalid file name characters will be replaced: ':', '\\', '/'</summary> - public static string GetSafeFileName(string str, string illegalCharacterReplacements = "") - => string.Join(illegalCharacterReplacements ?? "", str.Split(Path.GetInvalidFileNameChars())); - /// <summary>Use with full path, not file name. Valid path charaters which are invalid file name characters will be retained: '\\', '/'</summary> - public static LongPath GetSafePath(LongPath path, string illegalCharacterReplacements = "") + public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements) { ArgumentValidator.EnsureNotNull(path, nameof(path)); var pathNoPrefix = path.PathWithoutPrefix; - pathNoPrefix = replaceColons(pathNoPrefix, "꞉"); - pathNoPrefix = replaceIllegalWithUnicodeAnalog(pathNoPrefix); - pathNoPrefix = replaceInvalidChars(pathNoPrefix, illegalCharacterReplacements); + pathNoPrefix = replaceInvalidChars(pathNoPrefix, replacements); pathNoPrefix = removeDoubleSlashes(pathNoPrefix); return pathNoPrefix; } - private static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] { - '*', '?', + public static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] { + '*', '?', ':', // these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included. // In live code, Path.GetInvalidPathChars() does not include them '"', '<', '>' }).ToArray(); - private static string replaceInvalidChars(string path, string illegalCharacterReplacements) - => string.Join(illegalCharacterReplacements ?? "", path.Split(invalidChars)); + private static string replaceInvalidChars(string path, ReplacementCharacters replacements) + { + // replace all colons except within the first 2 chars + var builder = new System.Text.StringBuilder(); + for (var i = 0; i < path.Length; i++) + { + var c = path[i]; + + if (!invalidChars.Contains(c) || (i <= 2 && Path.IsPathRooted(path))) + builder.Append(c); + else + { + char preceding = i > 0 ? path[i - 1] : default; + char succeeding = i < path.Length - 1 ? path[i + 1] : default; + builder.Append(replacements.GetReplacement(c, preceding, succeeding)); + } + + } + return builder.ToString(); + } private static string removeDoubleSlashes(string path) { @@ -122,60 +132,6 @@ namespace FileManager return path[0] + remainder; } - private static string replaceIllegalWithUnicodeAnalog(string path) - { - char[] replaced = path.ToCharArray(); - - char GetQuote(int position) - { - if ( - position == 0 - || (position > 0 - && position < replaced.Length - && !char.IsLetter(replaced[position - 1]) - && !char.IsNumber(replaced[position - 1]) - ) - ) return '“'; - else if ( - position == replaced.Length - 1 - || (position >= 0 - && position < replaced.Length - 1 - && !char.IsLetter(replaced[position + 1]) - && !char.IsNumber(replaced[position + 1]) - ) - ) return '”'; - else return '"'; - } - - for (int i = 0; i < replaced.Length; i++) - { - replaced[i] = replaced[i] switch - { - '?' => '?', - '*' => '✱', - '<' => '<', - '>' => '>', - '"' => GetQuote(i), - _ => replaced[i] - }; - } - return new string(replaced); - } - - private static string replaceColons(string path, string illegalCharacterReplacements) - { - // replace all colons except within the first 2 chars - var builder = new System.Text.StringBuilder(); - for (var i = 0; i < path.Length; i++) - { - var c = path[i]; - if (i >= 2 && c == ':') - builder.Append(illegalCharacterReplacements); - else - builder.Append(c); - } - return builder.ToString(); - } private static string removeInvalidWhitespace_pattern { get; } = $@"[\s\.]*\{Path.DirectorySeparatorChar}\s*"; private static Regex removeInvalidWhitespace_regex { get; } = new(removeInvalidWhitespace_pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace); @@ -206,9 +162,9 @@ namespace FileManager /// <br/>- Perform <see cref="SaferMove"/> /// <br/>- Return valid path /// </summary> - public static string SaferMoveToValidPath(LongPath source, LongPath destination) + public static string SaferMoveToValidPath(LongPath source, LongPath destination, ReplacementCharacters replacements) { - destination = GetValidFilename(destination); + destination = GetValidFilename(destination, replacements); SaferMove(source, destination); return destination; } diff --git a/Source/FileManager/ReplacementCharacters.cs b/Source/FileManager/ReplacementCharacters.cs new file mode 100644 index 00000000..0fb91189 --- /dev/null +++ b/Source/FileManager/ReplacementCharacters.cs @@ -0,0 +1,155 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FileManager +{ + public class Replacement + { + [JsonIgnore] + public bool Mandatory { get; set; } + [JsonProperty] + public char CharacterToReplace { get; init; } + [JsonProperty] + public string ReplacementString { get; set; } + [JsonProperty] + public string Description { get; set; } + + public Replacement Clone() => new() + { + Mandatory = Mandatory, + CharacterToReplace = CharacterToReplace, + ReplacementString = ReplacementString, + Description = Description + }; + + public override string ToString() => $"{CharacterToReplace} → {ReplacementString} ({Description})"; + + public static Replacement Colon(string replacement) => new Replacement { CharacterToReplace = ':', Description = "Colon", ReplacementString = replacement}; + public static Replacement Asterisk(string replacement) => new Replacement { CharacterToReplace = '*', Description = "Asterisk", ReplacementString = replacement }; + public static Replacement QuestionMark(string replacement) => new Replacement { CharacterToReplace = '?', Description = "Question Mark", ReplacementString = replacement }; + public static Replacement OpenAngleBracket(string replacement) => new Replacement { CharacterToReplace = '<', Description = "Open Angle Bracket", ReplacementString = replacement }; + public static Replacement CloseAngleBracket(string replacement) => new Replacement { CharacterToReplace = '>', Description = "Close Angle Bracket", ReplacementString = replacement }; + public static Replacement OpenQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Open Quote", ReplacementString = replacement }; + public static Replacement CloseQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Close Quote", ReplacementString = replacement }; + public static Replacement OtherQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Other Quote", ReplacementString = replacement }; + public static Replacement Pipe(string replacement) => new Replacement { CharacterToReplace = '|', Description = "Vertical Line", ReplacementString = replacement }; + public static Replacement OtherInvalid(string replacement) => new Replacement { CharacterToReplace = default, Description = "Any other invalid characters", ReplacementString = replacement }; + } + + internal class ReplacementCharactersConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + => objectType == typeof(ReplacementCharacters); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jObj = JObject.Load(reader); + var replaceArr = jObj[nameof(Replacement)]; + var dict = replaceArr + .ToObject<Replacement[]>().ToList(); + + //Add any missing defaults and ensure they are in the expected order. + for (int i = 0; i < ReplacementCharacters.Default.Replacements.Count; i++) + { + var rep = ReplacementCharacters.Default.Replacements[i].Clone(); + + if (i < dict.Count) + { + var replacementStr = dict[i].ReplacementString; + dict[i] = rep; + dict[i].ReplacementString = replacementStr; + } + else + { + dict.Insert(i, rep); + } + } + + return new ReplacementCharacters { Replacements = dict }; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + ReplacementCharacters replacements = (ReplacementCharacters)value; + + var propertyNames = replacements.Replacements + .Select(c => JObject.FromObject(c)).ToList(); + + var prop = new JProperty(nameof(Replacement), new JArray(propertyNames)); + + var obj = new JObject(); + obj.AddFirst(prop); + obj.WriteTo(writer); + } + } + + [JsonConverter(typeof(ReplacementCharactersConverter))] + public class ReplacementCharacters + { + public static readonly ReplacementCharacters Default = new() + { + Replacements = new() + { + Replacement.OtherInvalid("_"), + Replacement.OpenQuote("“"), + Replacement.CloseQuote("”"), + Replacement.OtherQuote("""), + Replacement.Colon("꞉"), + Replacement.Asterisk("✱"), + Replacement.QuestionMark("?"), + Replacement.OpenAngleBracket("<"), + Replacement.CloseAngleBracket(">"), + Replacement.Pipe("⏐"), + } + }; + + public static readonly ReplacementCharacters LoFiDefault = new() + { + Replacements = new() + { + Replacement.OtherInvalid("_"), + Replacement.OpenQuote("'"), + Replacement.CloseQuote("'"), + Replacement.OtherQuote("'"), + Replacement.Colon("-"), + Replacement.Asterisk(""), + Replacement.QuestionMark(""), + Replacement.OpenAngleBracket("["), + Replacement.CloseAngleBracket("]"), + Replacement.Pipe("_"), + } + }; + + public List<Replacement> Replacements { get; init; } + public string DefaultReplacement => Replacements[0].ReplacementString; + public string OpenQuote => Replacements[1].ReplacementString; + public string CloseQuote => Replacements[2].ReplacementString; + public string OtherQuote => Replacements[3].ReplacementString; + + private const char QuoteMark = '"'; + + public string GetReplacement(char toReplace, char preceding, char succeding) + { + if (toReplace == QuoteMark) + { + if (preceding != default && !char.IsLetter(preceding) && !char.IsNumber(preceding)) + return OpenQuote; + else if (succeding != default && !char.IsLetter(succeding) && !char.IsNumber(succeding)) + return CloseQuote; + else + return OtherQuote; + } + + for (int i = 4; i < Replacements.Count; i++) + { + var r = Replacements[i]; + if (r.CharacterToReplace == toReplace) + return r.ReplacementString; + } + return DefaultReplacement; + } + } +} diff --git a/Source/LibationFileManager/Configuration.cs b/Source/LibationFileManager/Configuration.cs index 9ce601a6..d545df22 100644 --- a/Source/LibationFileManager/Configuration.cs +++ b/Source/LibationFileManager/Configuration.cs @@ -284,6 +284,13 @@ namespace LibationFileManager #region templates: custom file naming + [Description("Edit how illegal filename characters are replaced")] + public ReplacementCharacters ReplacementCharacters + { + get => persistentDictionary.GetNonString<ReplacementCharacters>(nameof(ReplacementCharacters)); + set => persistentDictionary.SetNonString(nameof(ReplacementCharacters), value); + } + [Description("How to format the folders in which files will be saved")] public string FolderTemplate { diff --git a/Source/LibationFileManager/Templates.cs b/Source/LibationFileManager/Templates.cs index 5ee917af..7410a8b9 100644 --- a/Source/LibationFileManager/Templates.cs +++ b/Source/LibationFileManager/Templates.cs @@ -106,7 +106,7 @@ namespace LibationFileManager => string.IsNullOrWhiteSpace(template) ? "" : getFileNamingTemplate(libraryBookDto, template, null, null) - .GetFilePath().PathWithoutPrefix; + .GetFilePath(Configuration.Instance.ReplacementCharacters).PathWithoutPrefix; private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -210,7 +210,7 @@ namespace LibationFileManager /// <summary>USES LIVE CONFIGURATION VALUES</summary> public string GetFilename(LibraryBookDto libraryBookDto, string baseDir = null) => getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, baseDir ?? AudibleFileStorage.BooksDirectory, null) - .GetFilePath(); + .GetFilePath(Configuration.Instance.ReplacementCharacters); #endregion } @@ -233,7 +233,7 @@ namespace LibationFileManager /// <summary>USES LIVE CONFIGURATION VALUES</summary> public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension, bool returnFirstExisting = false) => getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension) - .GetFilePath(returnFirstExisting); + .GetFilePath(Configuration.Instance.ReplacementCharacters, returnFirstExisting); #endregion } @@ -277,7 +277,7 @@ namespace LibationFileManager fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal)); fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? ""); - return fileNamingTemplate.GetFilePath().PathWithoutPrefix; + return fileNamingTemplate.GetFilePath(Configuration.Instance.ReplacementCharacters).PathWithoutPrefix; } #endregion } diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs b/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs new file mode 100644 index 00000000..4bfe2c91 --- /dev/null +++ b/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs @@ -0,0 +1,156 @@ +namespace LibationWinForms.Dialogs +{ + partial class EditReplacementChars + { + /// <summary> + /// Required designer variable. + /// </summary> + private System.ComponentModel.IContainer components = null; + + /// <summary> + /// Clean up any resources being used. + /// </summary> + /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// <summary> + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// </summary> + private void InitializeComponent() + { + this.dataGridView1 = new System.Windows.Forms.DataGridView(); + this.charToReplaceCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.replacementStringCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.descriptionCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.defaultsBtn = new System.Windows.Forms.Button(); + this.loFiDefaultsBtn = new System.Windows.Forms.Button(); + this.saveBtn = new System.Windows.Forms.Button(); + this.cancelBtn = new System.Windows.Forms.Button(); + ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit(); + this.SuspendLayout(); + // + // dataGridView1 + // + this.dataGridView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this.charToReplaceCol, + this.replacementStringCol, + this.descriptionCol}); + this.dataGridView1.Location = new System.Drawing.Point(12, 12); + this.dataGridView1.Name = "dataGridView1"; + this.dataGridView1.RowTemplate.Height = 25; + this.dataGridView1.Size = new System.Drawing.Size(416, 393); + this.dataGridView1.TabIndex = 0; + this.dataGridView1.CellEndEdit += new System.Windows.Forms.DataGridViewCellEventHandler(this.dataGridView1_CellEndEdit); + this.dataGridView1.CellValidating += new System.Windows.Forms.DataGridViewCellValidatingEventHandler(this.dataGridView1_CellValidating); + this.dataGridView1.UserDeletingRow += new System.Windows.Forms.DataGridViewRowCancelEventHandler(this.dataGridView1_UserDeletingRow); + this.dataGridView1.Resize += new System.EventHandler(this.dataGridView1_Resize); + // + // charToReplaceCol + // + this.charToReplaceCol.HeaderText = "Char to Replace"; + this.charToReplaceCol.MinimumWidth = 70; + this.charToReplaceCol.Name = "charToReplaceCol"; + this.charToReplaceCol.Resizable = System.Windows.Forms.DataGridViewTriState.False; + this.charToReplaceCol.Width = 70; + // + // replacementStringCol + // + this.replacementStringCol.HeaderText = "Replacement String"; + this.replacementStringCol.MinimumWidth = 85; + this.replacementStringCol.Name = "replacementStringCol"; + this.replacementStringCol.Width = 85; + // + // descriptionCol + // + this.descriptionCol.HeaderText = "Description"; + this.descriptionCol.MinimumWidth = 100; + this.descriptionCol.Name = "descriptionCol"; + this.descriptionCol.Width = 200; + // + // defaultsBtn + // + this.defaultsBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.defaultsBtn.Location = new System.Drawing.Point(12, 430); + this.defaultsBtn.Name = "defaultsBtn"; + this.defaultsBtn.Size = new System.Drawing.Size(64, 25); + this.defaultsBtn.TabIndex = 1; + this.defaultsBtn.Text = "Defaults"; + this.defaultsBtn.UseVisualStyleBackColor = true; + this.defaultsBtn.Click += new System.EventHandler(this.defaultsBtn_Click); + // + // loFiDefaultsBtn + // + this.loFiDefaultsBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.loFiDefaultsBtn.Location = new System.Drawing.Point(82, 430); + this.loFiDefaultsBtn.Name = "loFiDefaultsBtn"; + this.loFiDefaultsBtn.Size = new System.Drawing.Size(84, 25); + this.loFiDefaultsBtn.TabIndex = 1; + this.loFiDefaultsBtn.Text = "LoFi Defaults"; + this.loFiDefaultsBtn.UseVisualStyleBackColor = true; + this.loFiDefaultsBtn.Click += new System.EventHandler(this.loFiDefaultsBtn_Click); + // + // saveBtn + // + this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.saveBtn.Location = new System.Drawing.Point(346, 430); + this.saveBtn.Name = "saveBtn"; + this.saveBtn.Size = new System.Drawing.Size(82, 25); + this.saveBtn.TabIndex = 1; + this.saveBtn.Text = "Save"; + this.saveBtn.UseVisualStyleBackColor = true; + this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click); + // + // cancelBtn + // + this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.cancelBtn.Location = new System.Drawing.Point(258, 430); + this.cancelBtn.Name = "cancelBtn"; + this.cancelBtn.Size = new System.Drawing.Size(82, 25); + this.cancelBtn.TabIndex = 1; + this.cancelBtn.Text = "Cancel"; + this.cancelBtn.UseVisualStyleBackColor = true; + this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click); + // + // EditReplacementChars + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(440, 467); + this.Controls.Add(this.loFiDefaultsBtn); + this.Controls.Add(this.cancelBtn); + this.Controls.Add(this.saveBtn); + this.Controls.Add(this.defaultsBtn); + this.Controls.Add(this.dataGridView1); + this.Name = "EditReplacementChars"; + this.Text = "Character Replacements"; + ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.DataGridView dataGridView1; + private System.Windows.Forms.DataGridViewTextBoxColumn charToReplaceCol; + private System.Windows.Forms.DataGridViewTextBoxColumn replacementStringCol; + private System.Windows.Forms.DataGridViewTextBoxColumn descriptionCol; + private System.Windows.Forms.Button defaultsBtn; + private System.Windows.Forms.Button loFiDefaultsBtn; + private System.Windows.Forms.Button saveBtn; + private System.Windows.Forms.Button cancelBtn; + } +} \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.cs b/Source/LibationWinForms/Dialogs/EditReplacementChars.cs new file mode 100644 index 00000000..0322ce64 --- /dev/null +++ b/Source/LibationWinForms/Dialogs/EditReplacementChars.cs @@ -0,0 +1,177 @@ +using FileManager; +using LibationFileManager; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; + +namespace LibationWinForms.Dialogs +{ + public partial class EditReplacementChars : Form + { + Configuration config; + public EditReplacementChars() + { + InitializeComponent(); + dataGridView1_Resize(this, EventArgs.Empty); + } + public EditReplacementChars(Configuration config) : this() + { + this.config = config; + LoadTable(config.ReplacementCharacters.Replacements); + } + + private void LoadTable(List<Replacement> replacements) + { + dataGridView1.Rows.Clear(); + foreach (var r in replacements) + { + int row = dataGridView1.Rows.Add(r.CharacterToReplace, r.ReplacementString, r.Description); + dataGridView1.Rows[row].Tag = r; + + if (ReplacementCharacters.Default.Replacements.Any(rep => rep.CharacterToReplace == r.CharacterToReplace)) + { + r.Mandatory = true; + dataGridView1.Rows[row].Cells[charToReplaceCol.Index].ReadOnly = true; + dataGridView1.Rows[row].Cells[descriptionCol.Index].ReadOnly = true; + } + } + } + + private void dataGridView1_UserDeletingRow(object sender, DataGridViewRowCancelEventArgs e) + { + if (e.Row?.Tag is Replacement r && r.Mandatory) + e.Cancel = true; + } + + private void loFiDefaultsBtn_Click(object sender, EventArgs e) + { + LoadTable(ReplacementCharacters.LoFiDefault.Replacements); + } + + private void defaultsBtn_Click(object sender, EventArgs e) + { + LoadTable(ReplacementCharacters.Default.Replacements); + } + + private void dataGridView1_CellValidating(object sender, DataGridViewCellValidatingEventArgs e) + { + if (e.RowIndex < 0) return; + + var cellValue = e.FormattedValue?.ToString(); + + if (dataGridView1.Rows[e.RowIndex].Tag is Replacement row && row.Mandatory) + { + if (e.ColumnIndex == replacementStringCol.Index) + { + //Ensure replacement string doesn't contain an illegal character. + var replaceString = cellValue ?? string.Empty; + if (replaceString != string.Empty && replaceString.Any(c => FileUtility.invalidChars.Contains(c))) + { + dataGridView1.Rows[e.RowIndex].ErrorText = $"{replaceString} contains an illegal path character"; + e.Cancel = true; + } + } + return; + } + + + + if (e.ColumnIndex == charToReplaceCol.Index) + { + if (cellValue.Length != 1) + { + dataGridView1.Rows[e.RowIndex].ErrorText = "Only 1 character to replace per entry"; + e.Cancel = true; + } + else if ( + dataGridView1.Rows + .Cast<DataGridViewRow>() + .Where(r => r.Index != e.RowIndex) + .OfType<Replacement>() + .Any(r => r.CharacterToReplace == cellValue[0]) + ) + { + dataGridView1.Rows[e.RowIndex].ErrorText = $"The {cellValue[0]} character is already being replaced"; + e.Cancel = true; + } + } + else if (e.ColumnIndex == descriptionCol.Index || e.ColumnIndex == replacementStringCol.Index) + { + var value = dataGridView1.Rows[e.RowIndex].Cells[charToReplaceCol.Index].Value; + if (value is null || value is string str && string.IsNullOrEmpty(str)) + { + dataGridView1.Rows[e.RowIndex].ErrorText = $"You must choose a character to replace"; + e.Cancel = true; + } + } + } + + private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e) + { + if (e.RowIndex < 0) return; + + dataGridView1.Rows[e.RowIndex].ErrorText = string.Empty; + + var cellValue = dataGridView1.Rows[e.RowIndex].Cells[charToReplaceCol.Index].Value?.ToString(); + + if (string.IsNullOrEmpty(cellValue) || cellValue.Length > 1) + { + var row = dataGridView1.Rows[e.RowIndex]; + if (!row.IsNewRow) + { + BeginInvoke(new MethodInvoker(delegate + { + dataGridView1.Rows.Remove(row); + })); + } + } + else + { + char charToReplace = cellValue[0]; + string description = dataGridView1.Rows[e.RowIndex].Cells[descriptionCol.Index].Value?.ToString() ?? string.Empty; + string replacement = dataGridView1.Rows[e.RowIndex].Cells[replacementStringCol.Index].Value?.ToString() ?? string.Empty; + + var mandatory = false; + if (dataGridView1.Rows[e.RowIndex].Tag is Replacement existing) + { + mandatory = existing.Mandatory; + } + + dataGridView1.Rows[e.RowIndex].Tag = + new Replacement() + { + CharacterToReplace = charToReplace, + ReplacementString = replacement, + Description = description, + Mandatory = mandatory + }; + } + } + + private void saveBtn_Click(object sender, EventArgs e) + { + var replacements = dataGridView1.Rows + .Cast<DataGridViewRow>() + .Select(r => r.Tag) + .OfType<Replacement>() + .Where(r => r.ReplacementString != null && (r.ReplacementString == string.Empty || !r.ReplacementString.Any(c => FileUtility.invalidChars.Contains(c)))) + .ToList(); + + config.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements }; + DialogResult = DialogResult.OK; + Close(); + } + + private void cancelBtn_Click(object sender, EventArgs e) + { + DialogResult = DialogResult.Cancel; + Close(); + } + + private void dataGridView1_Resize(object sender, EventArgs e) + { + dataGridView1.Columns[^1].Width = dataGridView1.Width - dataGridView1.Columns.Cast<DataGridViewColumn>().Sum(c => c == dataGridView1.Columns[^1] ? 0 : c.Width) - dataGridView1.RowHeadersWidth - 2; + } + } +} diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.resx b/Source/LibationWinForms/Dialogs/EditReplacementChars.resx new file mode 100644 index 00000000..fa41cc36 --- /dev/null +++ b/Source/LibationWinForms/Dialogs/EditReplacementChars.resx @@ -0,0 +1,69 @@ +<root> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <metadata name="charToReplaceCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> + <value>True</value> + </metadata> + <metadata name="replacementStringCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> + <value>True</value> + </metadata> + <metadata name="descriptionCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> + <value>True</value> + </metadata> +</root> \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs index f64f3c1a..d117e4fe 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs @@ -60,6 +60,7 @@ this.tab3DownloadDecrypt = new System.Windows.Forms.TabPage(); this.inProgressFilesGb = new System.Windows.Forms.GroupBox(); this.customFileNamingGb = new System.Windows.Forms.GroupBox(); + this.editCharreplacementBtn = new System.Windows.Forms.Button(); this.chapterFileTemplateBtn = new System.Windows.Forms.Button(); this.chapterFileTemplateTb = new System.Windows.Forms.TextBox(); this.chapterFileTemplateLbl = new System.Windows.Forms.Label(); @@ -148,7 +149,7 @@ // saveBtn // this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.saveBtn.Location = new System.Drawing.Point(667, 441); + this.saveBtn.Location = new System.Drawing.Point(667, 461); this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.saveBtn.Name = "saveBtn"; this.saveBtn.Size = new System.Drawing.Size(88, 27); @@ -161,7 +162,7 @@ // this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.cancelBtn.Location = new System.Drawing.Point(785, 441); + this.cancelBtn.Location = new System.Drawing.Point(785, 461); this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.cancelBtn.Name = "cancelBtn"; this.cancelBtn.Size = new System.Drawing.Size(88, 27); @@ -308,7 +309,7 @@ // inProgressSelectControl // this.inProgressSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.inProgressSelectControl.Location = new System.Drawing.Point(7, 68); this.inProgressSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.inProgressSelectControl.Name = "inProgressSelectControl"; @@ -328,7 +329,7 @@ // booksSelectControl // this.booksSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.booksSelectControl.Location = new System.Drawing.Point(7, 37); this.booksSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.booksSelectControl.Name = "booksSelectControl"; @@ -356,8 +357,8 @@ // tabControl // this.tabControl.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); this.tabControl.Controls.Add(this.tab1ImportantSettings); this.tabControl.Controls.Add(this.tab2ImportLibrary); this.tabControl.Controls.Add(this.tab3DownloadDecrypt); @@ -365,7 +366,7 @@ this.tabControl.Location = new System.Drawing.Point(12, 12); this.tabControl.Name = "tabControl"; this.tabControl.SelectedIndex = 0; - this.tabControl.Size = new System.Drawing.Size(862, 423); + this.tabControl.Size = new System.Drawing.Size(862, 443); this.tabControl.TabIndex = 100; // // tab1ImportantSettings @@ -377,7 +378,7 @@ this.tab1ImportantSettings.Location = new System.Drawing.Point(4, 24); this.tab1ImportantSettings.Name = "tab1ImportantSettings"; this.tab1ImportantSettings.Padding = new System.Windows.Forms.Padding(3); - this.tab1ImportantSettings.Size = new System.Drawing.Size(854, 395); + this.tab1ImportantSettings.Size = new System.Drawing.Size(854, 415); this.tab1ImportantSettings.TabIndex = 0; this.tab1ImportantSettings.Text = "Important settings"; this.tab1ImportantSettings.UseVisualStyleBackColor = true; @@ -385,7 +386,7 @@ // booksGb // this.booksGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.booksGb.Controls.Add(this.saveEpisodesToSeriesFolderCbox); this.booksGb.Controls.Add(this.booksSelectControl); this.booksGb.Controls.Add(this.booksLocationDescLbl); @@ -416,7 +417,7 @@ this.tab2ImportLibrary.Location = new System.Drawing.Point(4, 24); this.tab2ImportLibrary.Name = "tab2ImportLibrary"; this.tab2ImportLibrary.Padding = new System.Windows.Forms.Padding(3); - this.tab2ImportLibrary.Size = new System.Drawing.Size(854, 395); + this.tab2ImportLibrary.Size = new System.Drawing.Size(854, 415); this.tab2ImportLibrary.TabIndex = 1; this.tab2ImportLibrary.Text = "Import library"; this.tab2ImportLibrary.UseVisualStyleBackColor = true; @@ -459,7 +460,7 @@ this.tab3DownloadDecrypt.Location = new System.Drawing.Point(4, 24); this.tab3DownloadDecrypt.Name = "tab3DownloadDecrypt"; this.tab3DownloadDecrypt.Padding = new System.Windows.Forms.Padding(3); - this.tab3DownloadDecrypt.Size = new System.Drawing.Size(854, 395); + this.tab3DownloadDecrypt.Size = new System.Drawing.Size(854, 415); this.tab3DownloadDecrypt.TabIndex = 2; this.tab3DownloadDecrypt.Text = "Download/Decrypt"; this.tab3DownloadDecrypt.UseVisualStyleBackColor = true; @@ -467,10 +468,10 @@ // inProgressFilesGb // this.inProgressFilesGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.inProgressFilesGb.Controls.Add(this.inProgressDescLbl); this.inProgressFilesGb.Controls.Add(this.inProgressSelectControl); - this.inProgressFilesGb.Location = new System.Drawing.Point(7, 251); + this.inProgressFilesGb.Location = new System.Drawing.Point(6, 281); this.inProgressFilesGb.Name = "inProgressFilesGb"; this.inProgressFilesGb.Size = new System.Drawing.Size(841, 128); this.inProgressFilesGb.TabIndex = 21; @@ -480,7 +481,8 @@ // customFileNamingGb // this.customFileNamingGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); + this.customFileNamingGb.Controls.Add(this.editCharreplacementBtn); this.customFileNamingGb.Controls.Add(this.chapterFileTemplateBtn); this.customFileNamingGb.Controls.Add(this.chapterFileTemplateTb); this.customFileNamingGb.Controls.Add(this.chapterFileTemplateLbl); @@ -492,11 +494,22 @@ this.customFileNamingGb.Controls.Add(this.folderTemplateLbl); this.customFileNamingGb.Location = new System.Drawing.Point(7, 88); this.customFileNamingGb.Name = "customFileNamingGb"; - this.customFileNamingGb.Size = new System.Drawing.Size(841, 157); + this.customFileNamingGb.Size = new System.Drawing.Size(841, 187); this.customFileNamingGb.TabIndex = 20; this.customFileNamingGb.TabStop = false; this.customFileNamingGb.Text = "Custom file naming"; // + // editCharreplacementBtn + // + this.editCharreplacementBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.editCharreplacementBtn.Location = new System.Drawing.Point(5, 158); + this.editCharreplacementBtn.Name = "editCharreplacementBtn"; + this.editCharreplacementBtn.Size = new System.Drawing.Size(387, 23); + this.editCharreplacementBtn.TabIndex = 8; + this.editCharreplacementBtn.Text = "[edit char replacement desc]"; + this.editCharreplacementBtn.UseVisualStyleBackColor = true; + this.editCharreplacementBtn.Click += new System.EventHandler(this.editCharreplacementBtn_Click); + // // chapterFileTemplateBtn // this.chapterFileTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); @@ -511,7 +524,7 @@ // chapterFileTemplateTb // this.chapterFileTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.chapterFileTemplateTb.Location = new System.Drawing.Point(6, 125); this.chapterFileTemplateTb.Name = "chapterFileTemplateTb"; this.chapterFileTemplateTb.ReadOnly = true; @@ -541,7 +554,7 @@ // fileTemplateTb // this.fileTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.fileTemplateTb.Location = new System.Drawing.Point(6, 81); this.fileTemplateTb.Name = "fileTemplateTb"; this.fileTemplateTb.ReadOnly = true; @@ -571,7 +584,7 @@ // folderTemplateTb // this.folderTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.folderTemplateTb.Location = new System.Drawing.Point(5, 37); this.folderTemplateTb.Name = "folderTemplateTb"; this.folderTemplateTb.ReadOnly = true; @@ -603,7 +616,7 @@ this.tab4AudioFileOptions.Location = new System.Drawing.Point(4, 24); this.tab4AudioFileOptions.Name = "tab4AudioFileOptions"; this.tab4AudioFileOptions.Padding = new System.Windows.Forms.Padding(3); - this.tab4AudioFileOptions.Size = new System.Drawing.Size(854, 395); + this.tab4AudioFileOptions.Size = new System.Drawing.Size(854, 415); this.tab4AudioFileOptions.TabIndex = 3; this.tab4AudioFileOptions.Text = "Audio File Options"; this.tab4AudioFileOptions.UseVisualStyleBackColor = true; @@ -633,7 +646,7 @@ // chapterTitleTemplateTb // this.chapterTitleTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.chapterTitleTemplateTb.Location = new System.Drawing.Point(6, 22); this.chapterTitleTemplateTb.Name = "chapterTitleTemplateTb"; this.chapterTitleTemplateTb.ReadOnly = true; @@ -1017,7 +1030,7 @@ this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.cancelBtn; - this.ClientSize = new System.Drawing.Size(886, 484); + this.ClientSize = new System.Drawing.Size(886, 504); this.Controls.Add(this.tabControl); this.Controls.Add(this.cancelBtn); this.Controls.Add(this.saveBtn); @@ -1141,5 +1154,6 @@ private System.Windows.Forms.GroupBox chapterTitleTemplateGb; private System.Windows.Forms.Button chapterTitleTemplateBtn; private System.Windows.Forms.TextBox chapterTitleTemplateTb; + private System.Windows.Forms.Button editCharreplacementBtn; } } \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs index adba6f9d..8a5266e9 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs @@ -11,9 +11,18 @@ namespace LibationWinForms.Dialogs private void fileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.File, fileTemplateTb); private void chapterFileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.ChapterFile, chapterFileTemplateTb); + + private void editCharreplacementBtn_Click(object sender, EventArgs e) + { + var form = new EditReplacementChars(config); + form.ShowDialog(); + } + private void Load_DownloadDecrypt(Configuration config) { inProgressDescLbl.Text = desc(nameof(config.InProgress)); + editCharreplacementBtn.Text = desc(nameof(config.ReplacementCharacters)); + badBookGb.Text = desc(nameof(config.BadBook)); badBookAskRb.Text = Configuration.BadBookAction.Ask.GetDescription(); badBookAbortRb.Text = Configuration.BadBookAction.Abort.GetDescription(); From b69869725661dbb2f76b45701aa809089b2a9d11 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Tue, 21 Jun 2022 23:39:24 -0600 Subject: [PATCH 02/20] Improve display and function of character replacement --- Source/FileManager/FileUtility.cs | 29 +- Source/FileManager/ReplacementCharacters.cs | 281 +++++++++++------- .../Dialogs/EditReplacementChars.Designer.cs | 81 +++-- .../Dialogs/EditReplacementChars.cs | 127 +++----- .../Dialogs/SettingsDialog.DownloadDecrypt.cs | 3 +- 5 files changed, 272 insertions(+), 249 deletions(-) diff --git a/Source/FileManager/FileUtility.cs b/Source/FileManager/FileUtility.cs index 550f46e0..80905aaa 100644 --- a/Source/FileManager/FileUtility.cs +++ b/Source/FileManager/FileUtility.cs @@ -84,39 +84,12 @@ namespace FileManager var pathNoPrefix = path.PathWithoutPrefix; - pathNoPrefix = replaceInvalidChars(pathNoPrefix, replacements); + pathNoPrefix = replacements.ReplaceInvalidChars(pathNoPrefix); pathNoPrefix = removeDoubleSlashes(pathNoPrefix); return pathNoPrefix; } - public static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] { - '*', '?', ':', - // these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included. - // In live code, Path.GetInvalidPathChars() does not include them - '"', '<', '>' - }).ToArray(); - private static string replaceInvalidChars(string path, ReplacementCharacters replacements) - { - // replace all colons except within the first 2 chars - var builder = new System.Text.StringBuilder(); - for (var i = 0; i < path.Length; i++) - { - var c = path[i]; - - if (!invalidChars.Contains(c) || (i <= 2 && Path.IsPathRooted(path))) - builder.Append(c); - else - { - char preceding = i > 0 ? path[i - 1] : default; - char succeeding = i < path.Length - 1 ? path[i + 1] : default; - builder.Append(replacements.GetReplacement(c, preceding, succeeding)); - } - - } - return builder.ToString(); - } - private static string removeDoubleSlashes(string path) { if (path.Length < 2) diff --git a/Source/FileManager/ReplacementCharacters.cs b/Source/FileManager/ReplacementCharacters.cs index 0fb91189..a689e6f2 100644 --- a/Source/FileManager/ReplacementCharacters.cs +++ b/Source/FileManager/ReplacementCharacters.cs @@ -2,43 +2,177 @@ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.IO; using System.Linq; namespace FileManager { public class Replacement { - [JsonIgnore] - public bool Mandatory { get; set; } - [JsonProperty] - public char CharacterToReplace { get; init; } - [JsonProperty] - public string ReplacementString { get; set; } - [JsonProperty] - public string Description { get; set; } - - public Replacement Clone() => new() - { - Mandatory = Mandatory, - CharacterToReplace = CharacterToReplace, - ReplacementString = ReplacementString, - Description = Description - }; + public const int FIXED_COUNT = 4; + internal const char QUOTE_MARK = '"'; + internal const string DEFAULT_DESCRIPTION = "Any other invalid characters"; + internal const string OPEN_QUOTE_DESCRIPTION = "Open Quote"; + internal const string CLOSE_QUOTE_DESCRIPTION = "Close Quote"; + internal const string OTHER_QUOTE_DESCRIPTION = "Other Quote"; + [JsonIgnore] public bool Mandatory { get; internal set; } + [JsonProperty] public char CharacterToReplace { get; private set; } + [JsonProperty] public string ReplacementString { get; private set; } + [JsonProperty] public string Description { get; private set; } public override string ToString() => $"{CharacterToReplace} → {ReplacementString} ({Description})"; - public static Replacement Colon(string replacement) => new Replacement { CharacterToReplace = ':', Description = "Colon", ReplacementString = replacement}; - public static Replacement Asterisk(string replacement) => new Replacement { CharacterToReplace = '*', Description = "Asterisk", ReplacementString = replacement }; - public static Replacement QuestionMark(string replacement) => new Replacement { CharacterToReplace = '?', Description = "Question Mark", ReplacementString = replacement }; - public static Replacement OpenAngleBracket(string replacement) => new Replacement { CharacterToReplace = '<', Description = "Open Angle Bracket", ReplacementString = replacement }; - public static Replacement CloseAngleBracket(string replacement) => new Replacement { CharacterToReplace = '>', Description = "Close Angle Bracket", ReplacementString = replacement }; - public static Replacement OpenQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Open Quote", ReplacementString = replacement }; - public static Replacement CloseQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Close Quote", ReplacementString = replacement }; - public static Replacement OtherQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Other Quote", ReplacementString = replacement }; - public static Replacement Pipe(string replacement) => new Replacement { CharacterToReplace = '|', Description = "Vertical Line", ReplacementString = replacement }; - public static Replacement OtherInvalid(string replacement) => new Replacement { CharacterToReplace = default, Description = "Any other invalid characters", ReplacementString = replacement }; + public Replacement(char charToReplace, string replacementString, string description) + { + CharacterToReplace = charToReplace; + ReplacementString = replacementString; + Description = description; + } + private Replacement(char charToReplace, string replacementString, string description, bool mandatory) + : this(charToReplace, replacementString, description) + { + Mandatory = mandatory; + } + + public void Update(char charToReplace, string replacementString, string description) + { + ReplacementString = replacementString; + + if (!Mandatory) + { + CharacterToReplace = charToReplace; + Description = description; + } + } + + public static Replacement OtherInvalid(string replacement) => new(default, replacement, DEFAULT_DESCRIPTION, true); + public static Replacement OpenQuote(string replacement) => new('"', replacement, OPEN_QUOTE_DESCRIPTION, true); + public static Replacement CloseQuote(string replacement) => new('"', replacement, CLOSE_QUOTE_DESCRIPTION, true); + public static Replacement OtherQuote(string replacement) => new('"', replacement, OTHER_QUOTE_DESCRIPTION, true); + public static Replacement Colon(string replacement) => new(':', replacement, "Colon"); + public static Replacement Asterisk(string replacement) => new('*', replacement, "Asterisk"); + public static Replacement QuestionMark(string replacement) => new('?', replacement, "Question Mark"); + public static Replacement OpenAngleBracket(string replacement) => new('<', replacement, "Open Angle Bracket"); + public static Replacement CloseAngleBracket(string replacement) => new('>', replacement, "Close Angle Bracket"); + public static Replacement Pipe(string replacement) => new('|', replacement, "Vertical Line"); } + [JsonConverter(typeof(ReplacementCharactersConverter))] + public class ReplacementCharacters + { + public static readonly ReplacementCharacters Default = new() + { + Replacements = new List<Replacement>() + { + Replacement.OtherInvalid("_"), + Replacement.OpenQuote("“"), + Replacement.CloseQuote("”"), + Replacement.OtherQuote("""), + Replacement.OpenAngleBracket("<"), + Replacement.CloseAngleBracket(">"), + Replacement.Colon("꞉"), + Replacement.Asterisk("✱"), + Replacement.QuestionMark("?"), + Replacement.Pipe("⏐"), + } + }; + + public static readonly ReplacementCharacters LoFiDefault = new() + { + Replacements = new List<Replacement>() + { + Replacement.OtherInvalid("_"), + Replacement.OpenQuote("'"), + Replacement.CloseQuote("'"), + Replacement.OtherQuote("'"), + Replacement.OpenAngleBracket("{"), + Replacement.CloseAngleBracket("}"), + Replacement.Colon("-"), + Replacement.Asterisk(""), + Replacement.QuestionMark(""), + } + }; + + public static readonly ReplacementCharacters Minimum = new() + { + Replacements = new List<Replacement>() + { + Replacement.OtherInvalid("_"), + Replacement.OpenQuote("_"), + Replacement.CloseQuote("_"), + Replacement.OtherQuote("_"), + } + }; + + private static readonly char[] invalidChars = Path.GetInvalidPathChars().Union(new[] { + '*', '?', ':', + // these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included. + // In live code, Path.GetInvalidPathChars() does not include them + '"', '<', '>' + }).ToArray(); + + public IReadOnlyList<Replacement> Replacements { get; init; } + private string DefaultReplacement => Replacements[0].ReplacementString; + private string OpenQuote => Replacements[1].ReplacementString; + private string CloseQuote => Replacements[2].ReplacementString; + private string OtherQuote => Replacements[3].ReplacementString; + + private string GetReplacement(char toReplace, char preceding, char succeding) + { + if (toReplace == Replacement.QUOTE_MARK) + { + if ( + preceding != default + && !char.IsLetter(preceding) + && !char.IsNumber(preceding) + && (char.IsLetter(succeding) || char.IsNumber(succeding)) + ) + return OpenQuote; + else if ( + succeding != default + && !char.IsLetter(succeding) + && !char.IsNumber(succeding) + && (char.IsLetter(preceding) || char.IsNumber(preceding)) + ) + return CloseQuote; + else + return OtherQuote; + } + + for (int i = Replacement.FIXED_COUNT; i < Replacements.Count; i++) + { + var r = Replacements[i]; + if (r.CharacterToReplace == toReplace) + return r.ReplacementString; + } + return DefaultReplacement; + } + + public static bool ContainsInvalid(string path) + => path.Any(c => invalidChars.Contains(c)); + + public string ReplaceInvalidChars(string pathStr) + { + // replace all colons except within the first 2 chars + var builder = new System.Text.StringBuilder(); + for (var i = 0; i < pathStr.Length; i++) + { + var c = pathStr[i]; + + if (!invalidChars.Contains(c) || (i <= 2 && Path.IsPathRooted(pathStr))) + builder.Append(c); + else + { + char preceding = i > 0 ? pathStr[i - 1] : default; + char succeeding = i < pathStr.Length - 1 ? pathStr[i + 1] : default; + builder.Append(GetReplacement(c, preceding, succeeding)); + } + + } + return builder.ToString(); + } + } + #region JSON Converter internal class ReplacementCharactersConverter : JsonConverter { public override bool CanConvert(Type objectType) @@ -51,22 +185,21 @@ namespace FileManager var dict = replaceArr .ToObject<Replacement[]>().ToList(); - //Add any missing defaults and ensure they are in the expected order. - for (int i = 0; i < ReplacementCharacters.Default.Replacements.Count; i++) + //Ensure that the first 4 replacements are for the expected chars and that all replacement strings are valid. + //If not, reset to default. + if (dict.Count < Replacement.FIXED_COUNT || + dict[0].CharacterToReplace != default || dict[0].Description != Replacement.DEFAULT_DESCRIPTION || + dict[1].CharacterToReplace != Replacement.QUOTE_MARK || dict[1].Description != Replacement.OPEN_QUOTE_DESCRIPTION || + dict[2].CharacterToReplace != Replacement.QUOTE_MARK || dict[2].Description != Replacement.CLOSE_QUOTE_DESCRIPTION || + dict[3].CharacterToReplace != Replacement.QUOTE_MARK || dict[3].Description != Replacement.OTHER_QUOTE_DESCRIPTION || + dict.Any(r => ReplacementCharacters.ContainsInvalid(r.ReplacementString)) + ) { - var rep = ReplacementCharacters.Default.Replacements[i].Clone(); - - if (i < dict.Count) - { - var replacementStr = dict[i].ReplacementString; - dict[i] = rep; - dict[i].ReplacementString = replacementStr; - } - else - { - dict.Insert(i, rep); - } + dict = ReplacementCharacters.Default.Replacements; } + //First 4 are mandatory + for (int i = 0; i < Replacement.FIXED_COUNT; i++) + dict[i].Mandatory = true; return new ReplacementCharacters { Replacements = dict }; } @@ -85,71 +218,5 @@ namespace FileManager obj.WriteTo(writer); } } - - [JsonConverter(typeof(ReplacementCharactersConverter))] - public class ReplacementCharacters - { - public static readonly ReplacementCharacters Default = new() - { - Replacements = new() - { - Replacement.OtherInvalid("_"), - Replacement.OpenQuote("“"), - Replacement.CloseQuote("”"), - Replacement.OtherQuote("""), - Replacement.Colon("꞉"), - Replacement.Asterisk("✱"), - Replacement.QuestionMark("?"), - Replacement.OpenAngleBracket("<"), - Replacement.CloseAngleBracket(">"), - Replacement.Pipe("⏐"), - } - }; - - public static readonly ReplacementCharacters LoFiDefault = new() - { - Replacements = new() - { - Replacement.OtherInvalid("_"), - Replacement.OpenQuote("'"), - Replacement.CloseQuote("'"), - Replacement.OtherQuote("'"), - Replacement.Colon("-"), - Replacement.Asterisk(""), - Replacement.QuestionMark(""), - Replacement.OpenAngleBracket("["), - Replacement.CloseAngleBracket("]"), - Replacement.Pipe("_"), - } - }; - - public List<Replacement> Replacements { get; init; } - public string DefaultReplacement => Replacements[0].ReplacementString; - public string OpenQuote => Replacements[1].ReplacementString; - public string CloseQuote => Replacements[2].ReplacementString; - public string OtherQuote => Replacements[3].ReplacementString; - - private const char QuoteMark = '"'; - - public string GetReplacement(char toReplace, char preceding, char succeding) - { - if (toReplace == QuoteMark) - { - if (preceding != default && !char.IsLetter(preceding) && !char.IsNumber(preceding)) - return OpenQuote; - else if (succeding != default && !char.IsLetter(succeding) && !char.IsNumber(succeding)) - return CloseQuote; - else - return OtherQuote; - } - - for (int i = 4; i < Replacements.Count; i++) - { - var r = Replacements[i]; - if (r.CharacterToReplace == toReplace) - return r.ReplacementString; - } - return DefaultReplacement; - } - } + #endregion } diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs b/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs index 4bfe2c91..07bced72 100644 --- a/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs +++ b/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs @@ -29,18 +29,21 @@ private void InitializeComponent() { this.dataGridView1 = new System.Windows.Forms.DataGridView(); - this.charToReplaceCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.replacementStringCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.descriptionCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.defaultsBtn = new System.Windows.Forms.Button(); this.loFiDefaultsBtn = new System.Windows.Forms.Button(); this.saveBtn = new System.Windows.Forms.Button(); this.cancelBtn = new System.Windows.Forms.Button(); + this.minDefaultBtn = new System.Windows.Forms.Button(); + this.charToReplaceCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.replacementStringCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.descriptionCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit(); this.SuspendLayout(); // // dataGridView1 // + this.dataGridView1.AllowUserToResizeColumns = false; + this.dataGridView1.AllowUserToResizeRows = false; this.dataGridView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); @@ -52,35 +55,12 @@ this.dataGridView1.Location = new System.Drawing.Point(12, 12); this.dataGridView1.Name = "dataGridView1"; this.dataGridView1.RowTemplate.Height = 25; - this.dataGridView1.Size = new System.Drawing.Size(416, 393); + this.dataGridView1.Size = new System.Drawing.Size(498, 393); this.dataGridView1.TabIndex = 0; this.dataGridView1.CellEndEdit += new System.Windows.Forms.DataGridViewCellEventHandler(this.dataGridView1_CellEndEdit); - this.dataGridView1.CellValidating += new System.Windows.Forms.DataGridViewCellValidatingEventHandler(this.dataGridView1_CellValidating); this.dataGridView1.UserDeletingRow += new System.Windows.Forms.DataGridViewRowCancelEventHandler(this.dataGridView1_UserDeletingRow); this.dataGridView1.Resize += new System.EventHandler(this.dataGridView1_Resize); // - // charToReplaceCol - // - this.charToReplaceCol.HeaderText = "Char to Replace"; - this.charToReplaceCol.MinimumWidth = 70; - this.charToReplaceCol.Name = "charToReplaceCol"; - this.charToReplaceCol.Resizable = System.Windows.Forms.DataGridViewTriState.False; - this.charToReplaceCol.Width = 70; - // - // replacementStringCol - // - this.replacementStringCol.HeaderText = "Replacement String"; - this.replacementStringCol.MinimumWidth = 85; - this.replacementStringCol.Name = "replacementStringCol"; - this.replacementStringCol.Width = 85; - // - // descriptionCol - // - this.descriptionCol.HeaderText = "Description"; - this.descriptionCol.MinimumWidth = 100; - this.descriptionCol.Name = "descriptionCol"; - this.descriptionCol.Width = 200; - // // defaultsBtn // this.defaultsBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); @@ -106,7 +86,7 @@ // saveBtn // this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.saveBtn.Location = new System.Drawing.Point(346, 430); + this.saveBtn.Location = new System.Drawing.Point(428, 430); this.saveBtn.Name = "saveBtn"; this.saveBtn.Size = new System.Drawing.Size(82, 25); this.saveBtn.TabIndex = 1; @@ -117,7 +97,7 @@ // cancelBtn // this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.cancelBtn.Location = new System.Drawing.Point(258, 430); + this.cancelBtn.Location = new System.Drawing.Point(340, 430); this.cancelBtn.Name = "cancelBtn"; this.cancelBtn.Size = new System.Drawing.Size(82, 25); this.cancelBtn.TabIndex = 1; @@ -125,11 +105,45 @@ this.cancelBtn.UseVisualStyleBackColor = true; this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click); // + // minDefaultBtn + // + this.minDefaultBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.minDefaultBtn.Location = new System.Drawing.Point(172, 430); + this.minDefaultBtn.Name = "minDefaultBtn"; + this.minDefaultBtn.Size = new System.Drawing.Size(80, 25); + this.minDefaultBtn.TabIndex = 1; + this.minDefaultBtn.Text = "Barebones"; + this.minDefaultBtn.UseVisualStyleBackColor = true; + this.minDefaultBtn.Click += new System.EventHandler(this.minDefaultBtn_Click); + // + // charToReplaceCol + // + this.charToReplaceCol.HeaderText = "Char to Replace"; + this.charToReplaceCol.MinimumWidth = 70; + this.charToReplaceCol.Name = "charToReplaceCol"; + this.charToReplaceCol.Resizable = System.Windows.Forms.DataGridViewTriState.False; + this.charToReplaceCol.Width = 70; + // + // replacementStringCol + // + this.replacementStringCol.HeaderText = "Replacement Text"; + this.replacementStringCol.MinimumWidth = 85; + this.replacementStringCol.Name = "replacementStringCol"; + this.replacementStringCol.Width = 85; + // + // descriptionCol + // + this.descriptionCol.HeaderText = "Description"; + this.descriptionCol.MinimumWidth = 100; + this.descriptionCol.Name = "descriptionCol"; + this.descriptionCol.Width = 200; + // // EditReplacementChars // this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(440, 467); + this.ClientSize = new System.Drawing.Size(522, 467); + this.Controls.Add(this.minDefaultBtn); this.Controls.Add(this.loFiDefaultsBtn); this.Controls.Add(this.cancelBtn); this.Controls.Add(this.saveBtn); @@ -145,12 +159,13 @@ #endregion private System.Windows.Forms.DataGridView dataGridView1; - private System.Windows.Forms.DataGridViewTextBoxColumn charToReplaceCol; - private System.Windows.Forms.DataGridViewTextBoxColumn replacementStringCol; - private System.Windows.Forms.DataGridViewTextBoxColumn descriptionCol; private System.Windows.Forms.Button defaultsBtn; private System.Windows.Forms.Button loFiDefaultsBtn; private System.Windows.Forms.Button saveBtn; private System.Windows.Forms.Button cancelBtn; + private System.Windows.Forms.Button minDefaultBtn; + private System.Windows.Forms.DataGridViewTextBoxColumn charToReplaceCol; + private System.Windows.Forms.DataGridViewTextBoxColumn replacementStringCol; + private System.Windows.Forms.DataGridViewTextBoxColumn descriptionCol; } } \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.cs b/Source/LibationWinForms/Dialogs/EditReplacementChars.cs index 0322ce64..11a0bf6c 100644 --- a/Source/LibationWinForms/Dialogs/EditReplacementChars.cs +++ b/Source/LibationWinForms/Dialogs/EditReplacementChars.cs @@ -15,25 +15,30 @@ namespace LibationWinForms.Dialogs InitializeComponent(); dataGridView1_Resize(this, EventArgs.Empty); } + public EditReplacementChars(Configuration config) : this() { this.config = config; LoadTable(config.ReplacementCharacters.Replacements); } - private void LoadTable(List<Replacement> replacements) + private void LoadTable(IReadOnlyList<Replacement> replacements) { dataGridView1.Rows.Clear(); - foreach (var r in replacements) + for (int i = 0; i < replacements.Count; i++) { + var r = replacements[i]; + int row = dataGridView1.Rows.Add(r.CharacterToReplace, r.ReplacementString, r.Description); dataGridView1.Rows[row].Tag = r; - if (ReplacementCharacters.Default.Replacements.Any(rep => rep.CharacterToReplace == r.CharacterToReplace)) + + if (r.Mandatory) { - r.Mandatory = true; dataGridView1.Rows[row].Cells[charToReplaceCol.Index].ReadOnly = true; dataGridView1.Rows[row].Cells[descriptionCol.Index].ReadOnly = true; + dataGridView1.Rows[row].Cells[charToReplaceCol.Index].Style.BackColor = System.Drawing.Color.LightGray; + dataGridView1.Rows[row].Cells[descriptionCol.Index].Style.BackColor = System.Drawing.Color.LightGray; } } } @@ -45,67 +50,14 @@ namespace LibationWinForms.Dialogs } private void loFiDefaultsBtn_Click(object sender, EventArgs e) - { - LoadTable(ReplacementCharacters.LoFiDefault.Replacements); - } + => LoadTable(ReplacementCharacters.LoFiDefault.Replacements); private void defaultsBtn_Click(object sender, EventArgs e) - { - LoadTable(ReplacementCharacters.Default.Replacements); - } + => LoadTable(ReplacementCharacters.Default.Replacements); - private void dataGridView1_CellValidating(object sender, DataGridViewCellValidatingEventArgs e) - { - if (e.RowIndex < 0) return; + private void minDefaultBtn_Click(object sender, EventArgs e) + => LoadTable(ReplacementCharacters.Minimum.Replacements); - var cellValue = e.FormattedValue?.ToString(); - - if (dataGridView1.Rows[e.RowIndex].Tag is Replacement row && row.Mandatory) - { - if (e.ColumnIndex == replacementStringCol.Index) - { - //Ensure replacement string doesn't contain an illegal character. - var replaceString = cellValue ?? string.Empty; - if (replaceString != string.Empty && replaceString.Any(c => FileUtility.invalidChars.Contains(c))) - { - dataGridView1.Rows[e.RowIndex].ErrorText = $"{replaceString} contains an illegal path character"; - e.Cancel = true; - } - } - return; - } - - - - if (e.ColumnIndex == charToReplaceCol.Index) - { - if (cellValue.Length != 1) - { - dataGridView1.Rows[e.RowIndex].ErrorText = "Only 1 character to replace per entry"; - e.Cancel = true; - } - else if ( - dataGridView1.Rows - .Cast<DataGridViewRow>() - .Where(r => r.Index != e.RowIndex) - .OfType<Replacement>() - .Any(r => r.CharacterToReplace == cellValue[0]) - ) - { - dataGridView1.Rows[e.RowIndex].ErrorText = $"The {cellValue[0]} character is already being replaced"; - e.Cancel = true; - } - } - else if (e.ColumnIndex == descriptionCol.Index || e.ColumnIndex == replacementStringCol.Index) - { - var value = dataGridView1.Rows[e.RowIndex].Cells[charToReplaceCol.Index].Value; - if (value is null || value is string str && string.IsNullOrEmpty(str)) - { - dataGridView1.Rows[e.RowIndex].ErrorText = $"You must choose a character to replace"; - e.Cancel = true; - } - } - } private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e) { @@ -113,10 +65,14 @@ namespace LibationWinForms.Dialogs dataGridView1.Rows[e.RowIndex].ErrorText = string.Empty; - var cellValue = dataGridView1.Rows[e.RowIndex].Cells[charToReplaceCol.Index].Value?.ToString(); + var charToReplaceStr = dataGridView1.Rows[e.RowIndex].Cells[charToReplaceCol.Index].Value?.ToString(); + var replacement = dataGridView1.Rows[e.RowIndex].Cells[replacementStringCol.Index].Value?.ToString() ?? string.Empty; + var description = dataGridView1.Rows[e.RowIndex].Cells[descriptionCol.Index].Value?.ToString() ?? string.Empty; - if (string.IsNullOrEmpty(cellValue) || cellValue.Length > 1) + //Validate the whole row. If it passes all validation, add or update the row's tag. + if (string.IsNullOrEmpty(charToReplaceStr) && replacement == string.Empty && description == string.Empty) { + //Invalid entry, so delete row var row = dataGridView1.Rows[e.RowIndex]; if (!row.IsNewRow) { @@ -126,26 +82,38 @@ namespace LibationWinForms.Dialogs })); } } + else if (string.IsNullOrEmpty(charToReplaceStr)) + { + dataGridView1.Rows[e.RowIndex].ErrorText = $"You must choose a character to replace"; + } + else if (charToReplaceStr.Length > 1) + { + dataGridView1.Rows[e.RowIndex].ErrorText = $"Only 1 {charToReplaceCol.HeaderText} per entry"; + } + else if (e.RowIndex >= Replacement.FIXED_COUNT && + dataGridView1.Rows + .Cast<DataGridViewRow>() + .Where(r => r.Index != e.RowIndex) + .Select(r => r.Tag) + .OfType<Replacement>() + .Any(r => r.CharacterToReplace == charToReplaceStr[0]) + ) + { + dataGridView1.Rows[e.RowIndex].ErrorText = $"The {charToReplaceStr[0]} character is already being replaced"; + } + else if (ReplacementCharacters.ContainsInvalid(replacement)) + { + dataGridView1.Rows[e.RowIndex].ErrorText = $"Your {replacementStringCol.HeaderText} contains illegal characters"; + } else { - char charToReplace = cellValue[0]; - string description = dataGridView1.Rows[e.RowIndex].Cells[descriptionCol.Index].Value?.ToString() ?? string.Empty; - string replacement = dataGridView1.Rows[e.RowIndex].Cells[replacementStringCol.Index].Value?.ToString() ?? string.Empty; + //valid entry. Add or update Replacement in row's Tag + var charToReplace = charToReplaceStr[0]; - var mandatory = false; if (dataGridView1.Rows[e.RowIndex].Tag is Replacement existing) - { - mandatory = existing.Mandatory; - } - - dataGridView1.Rows[e.RowIndex].Tag = - new Replacement() - { - CharacterToReplace = charToReplace, - ReplacementString = replacement, - Description = description, - Mandatory = mandatory - }; + existing.Update(charToReplace, replacement, description); + else + dataGridView1.Rows[e.RowIndex].Tag = new Replacement(charToReplace, replacement, description); } } @@ -155,7 +123,6 @@ namespace LibationWinForms.Dialogs .Cast<DataGridViewRow>() .Select(r => r.Tag) .OfType<Replacement>() - .Where(r => r.ReplacementString != null && (r.ReplacementString == string.Empty || !r.ReplacementString.Any(c => FileUtility.invalidChars.Contains(c)))) .ToList(); config.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements }; diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs index 8a5266e9..035fdf98 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs @@ -15,7 +15,8 @@ namespace LibationWinForms.Dialogs private void editCharreplacementBtn_Click(object sender, EventArgs e) { var form = new EditReplacementChars(config); - form.ShowDialog(); + form.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + form.ShowDialog(this); } private void Load_DownloadDecrypt(Configuration config) From 839a62cb07d9d23188eb63b1dc33ec62f19ddd57 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Wed, 22 Jun 2022 08:01:39 -0600 Subject: [PATCH 03/20] Change type --- Source/FileManager/ReplacementCharacters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/FileManager/ReplacementCharacters.cs b/Source/FileManager/ReplacementCharacters.cs index a689e6f2..947ec085 100644 --- a/Source/FileManager/ReplacementCharacters.cs +++ b/Source/FileManager/ReplacementCharacters.cs @@ -182,7 +182,7 @@ namespace FileManager { var jObj = JObject.Load(reader); var replaceArr = jObj[nameof(Replacement)]; - var dict = replaceArr + IReadOnlyList<Replacement> dict = replaceArr .ToObject<Replacement[]>().ToList(); //Ensure that the first 4 replacements are for the expected chars and that all replacement strings are valid. From 52622fadbb50a14141098fbf809573ab569593b2 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 11:07:10 -0600 Subject: [PATCH 04/20] Match rmcrackan's changes --- Source/AaxDecrypter/NetworkFileStream.cs | 2 +- Source/AppScaffolding/LibationScaffolding.cs | 2 +- Source/ApplicationServices/LibraryCommands.cs | 10 +++++----- Source/AudibleUtilities/ApiExtended.cs | 2 +- Source/AudibleUtilities/AudibleUtilities.csproj | 2 +- Source/DataLayer/DataLayer.csproj | 2 +- Source/DtoImporterService/BookImporter.cs | 3 ++- Source/FileLiberator/DownloadDecryptBook.cs | 2 +- Source/FileManager/FileManager.csproj | 2 +- Source/FileManager/FileUtility.cs | 4 ++-- Source/LibationWinForms/Dialogs/AccountsDialog.cs | 4 ++-- Source/LibationWinForms/Form1.ScanNotification.cs | 2 +- Source/LibationWinForms/LibationWinForms.csproj | 2 +- Source/LibationWinForms/Program.cs | 2 +- Source/LibationWinForms/Updater.cs | 2 +- 15 files changed, 22 insertions(+), 21 deletions(-) diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs index 8f2079f1..d03e44f8 100644 --- a/Source/AaxDecrypter/NetworkFileStream.cs +++ b/Source/AaxDecrypter/NetworkFileStream.cs @@ -218,7 +218,7 @@ namespace AaxDecrypter } /// <summary> - /// Downlod <see cref="Uri"/> to <see cref="SaveFilePath"/>. + /// Download <see cref="Uri"/> to <see cref="SaveFilePath"/>. /// </summary> private void DownloadFile() { diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 0d338351..9af8b75f 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -473,7 +473,7 @@ namespace AppScaffolding } catch (Exception ex) { - Serilog.Log.Logger.Error(ex, "An error occured while running database migrations in {0}", nameof(migrate_from_7_10_1)); + Serilog.Log.Logger.Error(ex, "An error occurred while running database migrations in {0}", nameof(migrate_from_7_10_1)); config.SetObject($"{nameof(migrate_from_7_10_1)}_ThrewError", true); } } diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs index 65e6d117..56c64e2d 100644 --- a/Source/ApplicationServices/LibraryCommands.cs +++ b/Source/ApplicationServices/LibraryCommands.cs @@ -134,7 +134,7 @@ namespace ApplicationServices if (newParents >= 0) { //If any episodes are still orphaned, their series have been - //removed from the catalog and wel'll never be able to find them. + //removed from the catalog and we'll never be able to find them. //only do this if findAndAddMissingParents returned >= 0. If it //returned < 0, an error happened and there's still a chance that @@ -251,7 +251,7 @@ namespace ApplicationServices } catch (Exception ex) { - Serilog.Log.Logger.Error(ex, "An error occured while trying to remove orphaned episodes from the database"); + Serilog.Log.Logger.Error(ex, "An error occurred while trying to remove orphaned episodes from the database"); } } @@ -274,7 +274,7 @@ namespace ApplicationServices .DistinctBy(s => s.Series.AudibleSeriesId) .ToList(); - // The Catalog endpointdoes not require authentication. + // The Catalog endpoint does not require authentication. var api = new ApiUnauthenticated(accounts[0].Locale); var seriesParents = orphanedSeries.Select(o => o.Series.AudibleSeriesId).ToList(); @@ -308,7 +308,7 @@ namespace ApplicationServices } catch (Exception ex) { - Serilog.Log.Logger.Error(ex, "An error occured while trying to scan for orphaned episode parents."); + Serilog.Log.Logger.Error(ex, "An error occurred while trying to scan for orphaned episode parents."); return -1; } } @@ -321,7 +321,7 @@ namespace ApplicationServices } catch (Microsoft.EntityFrameworkCore.DbUpdateException ex) { - // DbUpdateException exceptions can wreck serilog. Condense it until we can find a better solution. I suspect the culpret is the "WithExceptionDetails" serilog extension + // DbUpdateException exceptions can wreck serilog. Condense it until we can find a better solution. I suspect the culprit is the "WithExceptionDetails" serilog extension static string format(Exception ex) => $"\r\nMessage: {ex.Message}\r\nStack Trace:\r\n{ex.StackTrace}"; diff --git a/Source/AudibleUtilities/ApiExtended.cs b/Source/AudibleUtilities/ApiExtended.cs index 4290be72..28fbd14f 100644 --- a/Source/AudibleUtilities/ApiExtended.cs +++ b/Source/AudibleUtilities/ApiExtended.cs @@ -145,7 +145,7 @@ namespace AudibleUtilities Serilog.Log.Logger.Debug("Library scan complete. Found {count} books and series. Waiting on {getChildEpisodesTasksCount} series episode scans to complete.", count, getChildEpisodesTasks.Count); - //await and add all episides from all parents + //await and add all episodes from all parents foreach (var epList in await Task.WhenAll(getChildEpisodesTasks)) items.AddRange(epList); diff --git a/Source/AudibleUtilities/AudibleUtilities.csproj b/Source/AudibleUtilities/AudibleUtilities.csproj index 94e16429..f3d58a40 100644 --- a/Source/AudibleUtilities/AudibleUtilities.csproj +++ b/Source/AudibleUtilities/AudibleUtilities.csproj @@ -5,7 +5,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="AudibleApi" Version="4.1.0.1" /> + <PackageReference Include="AudibleApi" Version="4.1.1.1" /> </ItemGroup> <ItemGroup> diff --git a/Source/DataLayer/DataLayer.csproj b/Source/DataLayer/DataLayer.csproj index eb2f1b50..1690e74f 100644 --- a/Source/DataLayer/DataLayer.csproj +++ b/Source/DataLayer/DataLayer.csproj @@ -12,7 +12,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Dinah.EntityFrameworkCore" Version="4.1.0.1" /> + <PackageReference Include="Dinah.EntityFrameworkCore" Version="4.1.1.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> diff --git a/Source/DtoImporterService/BookImporter.cs b/Source/DtoImporterService/BookImporter.cs index 297ae659..67d18f4e 100644 --- a/Source/DtoImporterService/BookImporter.cs +++ b/Source/DtoImporterService/BookImporter.cs @@ -101,7 +101,8 @@ namespace DtoImporterService // absence of categories is also possible // CATEGORY HACK: only use the 1st 2 categories - // (real impl: var lastCategory = item.Categories.LastOrDefault()?.CategoryId ?? "";) + // after we support full arbitrary-depth category trees and multiple categories per book, the real impl will be something like this + // var lastCategory = item.Categories.LastOrDefault()?.CategoryId ?? ""; var lastCategory = item.Categories.Length == 0 ? "" : item.Categories.Length == 1 ? item.Categories[0].CategoryId diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index e759386d..4bea8538 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -261,7 +261,7 @@ namespace FileLiberator var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)), Configuration.Instance.ReplacementCharacters); FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest); - // propogate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop) + // propagate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop) entries[i] = entry with { Path = realDest }; } diff --git a/Source/FileManager/FileManager.csproj b/Source/FileManager/FileManager.csproj index 12601f9a..16363aa5 100644 --- a/Source/FileManager/FileManager.csproj +++ b/Source/FileManager/FileManager.csproj @@ -5,7 +5,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Dinah.Core" Version="4.4.0.1" /> + <PackageReference Include="Dinah.Core" Version="4.4.1.1" /> <PackageReference Include="Polly" Version="7.2.3" /> </ItemGroup> diff --git a/Source/FileManager/FileUtility.cs b/Source/FileManager/FileUtility.cs index 550f46e0..7a33dd91 100644 --- a/Source/FileManager/FileUtility.cs +++ b/Source/FileManager/FileUtility.cs @@ -77,7 +77,7 @@ namespace FileManager return fullfilename; } - /// <summary>Use with full path, not file name. Valid path charaters which are invalid file name characters will be retained: '\\', '/'</summary> + /// <summary>Use with full path, not file name. Valid path characters which are invalid file name characters will be retained: '\\', '/'</summary> public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements) { ArgumentValidator.EnsureNotNull(path, nameof(path)); @@ -147,7 +147,7 @@ namespace FileManager // regex is easier by ending with separator fullfilename += Path.DirectorySeparatorChar; fullfilename = removeInvalidWhitespace_regex.Replace(fullfilename, Path.DirectorySeparatorChar.ToString()); - // take seperator back off + // take separator back off fullfilename = RemoveLastCharacter(fullfilename); fullfilename = removeDoubleSlashes(fullfilename); diff --git a/Source/LibationWinForms/Dialogs/AccountsDialog.cs b/Source/LibationWinForms/Dialogs/AccountsDialog.cs index 0f04bb7c..d60c2f1e 100644 --- a/Source/LibationWinForms/Dialogs/AccountsDialog.cs +++ b/Source/LibationWinForms/Dialogs/AccountsDialog.cs @@ -252,7 +252,7 @@ namespace LibationWinForms.Dialogs { MessageBoxLib.ShowAdminAlert( this, - $"An error occured while exporting account:\r\n{account.AccountName}", + $"An error occurred while exporting account:\r\n{account.AccountName}", "Error Exporting Account", ex); } @@ -294,7 +294,7 @@ namespace LibationWinForms.Dialogs { MessageBoxLib.ShowAdminAlert( this, - $"An error occured while importing an account from:\r\n{ofd.FileName}\r\n\r\nIs the file encrypted?", + $"An error occurred while importing an account from:\r\n{ofd.FileName}\r\n\r\nIs the file encrypted?", "Error Importing Account", ex); } diff --git a/Source/LibationWinForms/Form1.ScanNotification.cs b/Source/LibationWinForms/Form1.ScanNotification.cs index dc70537d..8537053d 100644 --- a/Source/LibationWinForms/Form1.ScanNotification.cs +++ b/Source/LibationWinForms/Form1.ScanNotification.cs @@ -3,7 +3,7 @@ using ApplicationServices; namespace LibationWinForms { - // This is for the Scanning notificationin the upper right. This shown for manual scanning and auto-scan + // This is for the Scanning notification in the upper right. This shown for manual scanning and auto-scan public partial class Form1 { private void Configure_ScanNotification() diff --git a/Source/LibationWinForms/LibationWinForms.csproj b/Source/LibationWinForms/LibationWinForms.csproj index 94dd14ae..b21cadd3 100644 --- a/Source/LibationWinForms/LibationWinForms.csproj +++ b/Source/LibationWinForms/LibationWinForms.csproj @@ -29,7 +29,7 @@ <ItemGroup> <PackageReference Include="Autoupdater.NET.Official" Version="1.7.3" /> - <PackageReference Include="Dinah.Core.WindowsDesktop" Version="4.2.2.1" /> + <PackageReference Include="Dinah.Core.WindowsDesktop" Version="4.2.3.1" /> </ItemGroup> <ItemGroup> diff --git a/Source/LibationWinForms/Program.cs b/Source/LibationWinForms/Program.cs index 2abbea83..fd933c32 100644 --- a/Source/LibationWinForms/Program.cs +++ b/Source/LibationWinForms/Program.cs @@ -87,7 +87,7 @@ namespace LibationWinForms var defaultLibationFilesDir = Configuration.UserProfile; - // check for existing settigns in default location + // check for existing settings in default location var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json"); if (Configuration.SettingsFileIsValid(defaultSettingsFile)) config.SetLibationFiles(defaultLibationFilesDir); diff --git a/Source/LibationWinForms/Updater.cs b/Source/LibationWinForms/Updater.cs index 1506f924..9660c7ce 100644 --- a/Source/LibationWinForms/Updater.cs +++ b/Source/LibationWinForms/Updater.cs @@ -29,7 +29,7 @@ namespace LibationWinForms return; var dialogResult = MessageBox.Show(string.Format( - $"There is a new version avilable. Would you like to update?\r\n\r\nAfter you close Libation, the upgrade will start automatically."), + $"There is a new version available. Would you like to update?\r\n\r\nAfter you close Libation, the upgrade will start automatically."), "Update Available", MessageBoxButtons.YesNo, MessageBoxIcon.Information); From fd7c833de03a05c3a7124458f2c889ad03499764 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 11:34:00 -0600 Subject: [PATCH 05/20] Revert "make auto-scan more fault-tolerant" This reverts commit f802d1524f94e12b1682cbe4d5b9671386f915ac. --- Source/LibationWinForms/Form1.ScanAuto.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Source/LibationWinForms/Form1.ScanAuto.cs b/Source/LibationWinForms/Form1.ScanAuto.cs index 5fb10932..30cfa7e1 100644 --- a/Source/LibationWinForms/Form1.ScanAuto.cs +++ b/Source/LibationWinForms/Form1.ScanAuto.cs @@ -33,18 +33,7 @@ namespace LibationWinForms .ToArray(); // in autoScan, new books SHALL NOT show dialog - try - { - Task importAsync() => LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts); - if (InvokeRequired) - await Invoke(importAsync); - else - await importAsync(); - } - catch (Exception ex) - { - Serilog.Log.Logger.Error(ex, "Error invoking auto-scan"); - } + await Invoke(async () => await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts)); }; // load init state to menu checkbox From 7c11ecb3a7cec28d3cea27f15494e820b603d509 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 11:34:07 -0600 Subject: [PATCH 06/20] Revert "Change type" This reverts commit 839a62cb07d9d23188eb63b1dc33ec62f19ddd57. --- Source/FileManager/ReplacementCharacters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/FileManager/ReplacementCharacters.cs b/Source/FileManager/ReplacementCharacters.cs index 947ec085..a689e6f2 100644 --- a/Source/FileManager/ReplacementCharacters.cs +++ b/Source/FileManager/ReplacementCharacters.cs @@ -182,7 +182,7 @@ namespace FileManager { var jObj = JObject.Load(reader); var replaceArr = jObj[nameof(Replacement)]; - IReadOnlyList<Replacement> dict = replaceArr + var dict = replaceArr .ToObject<Replacement[]>().ToList(); //Ensure that the first 4 replacements are for the expected chars and that all replacement strings are valid. From 1a74736115e1e872dccd6f9aaf0b27a79b1af5e2 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 11:34:11 -0600 Subject: [PATCH 07/20] Revert "Improve display and function of character replacement" This reverts commit b69869725661dbb2f76b45701aa809089b2a9d11. --- Source/FileManager/FileUtility.cs | 29 +- Source/FileManager/ReplacementCharacters.cs | 281 +++++++----------- .../Dialogs/EditReplacementChars.Designer.cs | 81 ++--- .../Dialogs/EditReplacementChars.cs | 127 +++++--- .../Dialogs/SettingsDialog.DownloadDecrypt.cs | 3 +- 5 files changed, 249 insertions(+), 272 deletions(-) diff --git a/Source/FileManager/FileUtility.cs b/Source/FileManager/FileUtility.cs index 6cc42380..7a33dd91 100644 --- a/Source/FileManager/FileUtility.cs +++ b/Source/FileManager/FileUtility.cs @@ -84,12 +84,39 @@ namespace FileManager var pathNoPrefix = path.PathWithoutPrefix; - pathNoPrefix = replacements.ReplaceInvalidChars(pathNoPrefix); + pathNoPrefix = replaceInvalidChars(pathNoPrefix, replacements); pathNoPrefix = removeDoubleSlashes(pathNoPrefix); return pathNoPrefix; } + public static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] { + '*', '?', ':', + // these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included. + // In live code, Path.GetInvalidPathChars() does not include them + '"', '<', '>' + }).ToArray(); + private static string replaceInvalidChars(string path, ReplacementCharacters replacements) + { + // replace all colons except within the first 2 chars + var builder = new System.Text.StringBuilder(); + for (var i = 0; i < path.Length; i++) + { + var c = path[i]; + + if (!invalidChars.Contains(c) || (i <= 2 && Path.IsPathRooted(path))) + builder.Append(c); + else + { + char preceding = i > 0 ? path[i - 1] : default; + char succeeding = i < path.Length - 1 ? path[i + 1] : default; + builder.Append(replacements.GetReplacement(c, preceding, succeeding)); + } + + } + return builder.ToString(); + } + private static string removeDoubleSlashes(string path) { if (path.Length < 2) diff --git a/Source/FileManager/ReplacementCharacters.cs b/Source/FileManager/ReplacementCharacters.cs index a689e6f2..0fb91189 100644 --- a/Source/FileManager/ReplacementCharacters.cs +++ b/Source/FileManager/ReplacementCharacters.cs @@ -2,177 +2,43 @@ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; -using System.IO; using System.Linq; namespace FileManager { public class Replacement { - public const int FIXED_COUNT = 4; + [JsonIgnore] + public bool Mandatory { get; set; } + [JsonProperty] + public char CharacterToReplace { get; init; } + [JsonProperty] + public string ReplacementString { get; set; } + [JsonProperty] + public string Description { get; set; } + + public Replacement Clone() => new() + { + Mandatory = Mandatory, + CharacterToReplace = CharacterToReplace, + ReplacementString = ReplacementString, + Description = Description + }; - internal const char QUOTE_MARK = '"'; - internal const string DEFAULT_DESCRIPTION = "Any other invalid characters"; - internal const string OPEN_QUOTE_DESCRIPTION = "Open Quote"; - internal const string CLOSE_QUOTE_DESCRIPTION = "Close Quote"; - internal const string OTHER_QUOTE_DESCRIPTION = "Other Quote"; - [JsonIgnore] public bool Mandatory { get; internal set; } - [JsonProperty] public char CharacterToReplace { get; private set; } - [JsonProperty] public string ReplacementString { get; private set; } - [JsonProperty] public string Description { get; private set; } public override string ToString() => $"{CharacterToReplace} → {ReplacementString} ({Description})"; - public Replacement(char charToReplace, string replacementString, string description) - { - CharacterToReplace = charToReplace; - ReplacementString = replacementString; - Description = description; - } - private Replacement(char charToReplace, string replacementString, string description, bool mandatory) - : this(charToReplace, replacementString, description) - { - Mandatory = mandatory; - } - - public void Update(char charToReplace, string replacementString, string description) - { - ReplacementString = replacementString; - - if (!Mandatory) - { - CharacterToReplace = charToReplace; - Description = description; - } - } - - public static Replacement OtherInvalid(string replacement) => new(default, replacement, DEFAULT_DESCRIPTION, true); - public static Replacement OpenQuote(string replacement) => new('"', replacement, OPEN_QUOTE_DESCRIPTION, true); - public static Replacement CloseQuote(string replacement) => new('"', replacement, CLOSE_QUOTE_DESCRIPTION, true); - public static Replacement OtherQuote(string replacement) => new('"', replacement, OTHER_QUOTE_DESCRIPTION, true); - public static Replacement Colon(string replacement) => new(':', replacement, "Colon"); - public static Replacement Asterisk(string replacement) => new('*', replacement, "Asterisk"); - public static Replacement QuestionMark(string replacement) => new('?', replacement, "Question Mark"); - public static Replacement OpenAngleBracket(string replacement) => new('<', replacement, "Open Angle Bracket"); - public static Replacement CloseAngleBracket(string replacement) => new('>', replacement, "Close Angle Bracket"); - public static Replacement Pipe(string replacement) => new('|', replacement, "Vertical Line"); + public static Replacement Colon(string replacement) => new Replacement { CharacterToReplace = ':', Description = "Colon", ReplacementString = replacement}; + public static Replacement Asterisk(string replacement) => new Replacement { CharacterToReplace = '*', Description = "Asterisk", ReplacementString = replacement }; + public static Replacement QuestionMark(string replacement) => new Replacement { CharacterToReplace = '?', Description = "Question Mark", ReplacementString = replacement }; + public static Replacement OpenAngleBracket(string replacement) => new Replacement { CharacterToReplace = '<', Description = "Open Angle Bracket", ReplacementString = replacement }; + public static Replacement CloseAngleBracket(string replacement) => new Replacement { CharacterToReplace = '>', Description = "Close Angle Bracket", ReplacementString = replacement }; + public static Replacement OpenQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Open Quote", ReplacementString = replacement }; + public static Replacement CloseQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Close Quote", ReplacementString = replacement }; + public static Replacement OtherQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Other Quote", ReplacementString = replacement }; + public static Replacement Pipe(string replacement) => new Replacement { CharacterToReplace = '|', Description = "Vertical Line", ReplacementString = replacement }; + public static Replacement OtherInvalid(string replacement) => new Replacement { CharacterToReplace = default, Description = "Any other invalid characters", ReplacementString = replacement }; } - [JsonConverter(typeof(ReplacementCharactersConverter))] - public class ReplacementCharacters - { - public static readonly ReplacementCharacters Default = new() - { - Replacements = new List<Replacement>() - { - Replacement.OtherInvalid("_"), - Replacement.OpenQuote("“"), - Replacement.CloseQuote("”"), - Replacement.OtherQuote("""), - Replacement.OpenAngleBracket("<"), - Replacement.CloseAngleBracket(">"), - Replacement.Colon("꞉"), - Replacement.Asterisk("✱"), - Replacement.QuestionMark("?"), - Replacement.Pipe("⏐"), - } - }; - - public static readonly ReplacementCharacters LoFiDefault = new() - { - Replacements = new List<Replacement>() - { - Replacement.OtherInvalid("_"), - Replacement.OpenQuote("'"), - Replacement.CloseQuote("'"), - Replacement.OtherQuote("'"), - Replacement.OpenAngleBracket("{"), - Replacement.CloseAngleBracket("}"), - Replacement.Colon("-"), - Replacement.Asterisk(""), - Replacement.QuestionMark(""), - } - }; - - public static readonly ReplacementCharacters Minimum = new() - { - Replacements = new List<Replacement>() - { - Replacement.OtherInvalid("_"), - Replacement.OpenQuote("_"), - Replacement.CloseQuote("_"), - Replacement.OtherQuote("_"), - } - }; - - private static readonly char[] invalidChars = Path.GetInvalidPathChars().Union(new[] { - '*', '?', ':', - // these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included. - // In live code, Path.GetInvalidPathChars() does not include them - '"', '<', '>' - }).ToArray(); - - public IReadOnlyList<Replacement> Replacements { get; init; } - private string DefaultReplacement => Replacements[0].ReplacementString; - private string OpenQuote => Replacements[1].ReplacementString; - private string CloseQuote => Replacements[2].ReplacementString; - private string OtherQuote => Replacements[3].ReplacementString; - - private string GetReplacement(char toReplace, char preceding, char succeding) - { - if (toReplace == Replacement.QUOTE_MARK) - { - if ( - preceding != default - && !char.IsLetter(preceding) - && !char.IsNumber(preceding) - && (char.IsLetter(succeding) || char.IsNumber(succeding)) - ) - return OpenQuote; - else if ( - succeding != default - && !char.IsLetter(succeding) - && !char.IsNumber(succeding) - && (char.IsLetter(preceding) || char.IsNumber(preceding)) - ) - return CloseQuote; - else - return OtherQuote; - } - - for (int i = Replacement.FIXED_COUNT; i < Replacements.Count; i++) - { - var r = Replacements[i]; - if (r.CharacterToReplace == toReplace) - return r.ReplacementString; - } - return DefaultReplacement; - } - - public static bool ContainsInvalid(string path) - => path.Any(c => invalidChars.Contains(c)); - - public string ReplaceInvalidChars(string pathStr) - { - // replace all colons except within the first 2 chars - var builder = new System.Text.StringBuilder(); - for (var i = 0; i < pathStr.Length; i++) - { - var c = pathStr[i]; - - if (!invalidChars.Contains(c) || (i <= 2 && Path.IsPathRooted(pathStr))) - builder.Append(c); - else - { - char preceding = i > 0 ? pathStr[i - 1] : default; - char succeeding = i < pathStr.Length - 1 ? pathStr[i + 1] : default; - builder.Append(GetReplacement(c, preceding, succeeding)); - } - - } - return builder.ToString(); - } - } - #region JSON Converter internal class ReplacementCharactersConverter : JsonConverter { public override bool CanConvert(Type objectType) @@ -185,21 +51,22 @@ namespace FileManager var dict = replaceArr .ToObject<Replacement[]>().ToList(); - //Ensure that the first 4 replacements are for the expected chars and that all replacement strings are valid. - //If not, reset to default. - if (dict.Count < Replacement.FIXED_COUNT || - dict[0].CharacterToReplace != default || dict[0].Description != Replacement.DEFAULT_DESCRIPTION || - dict[1].CharacterToReplace != Replacement.QUOTE_MARK || dict[1].Description != Replacement.OPEN_QUOTE_DESCRIPTION || - dict[2].CharacterToReplace != Replacement.QUOTE_MARK || dict[2].Description != Replacement.CLOSE_QUOTE_DESCRIPTION || - dict[3].CharacterToReplace != Replacement.QUOTE_MARK || dict[3].Description != Replacement.OTHER_QUOTE_DESCRIPTION || - dict.Any(r => ReplacementCharacters.ContainsInvalid(r.ReplacementString)) - ) + //Add any missing defaults and ensure they are in the expected order. + for (int i = 0; i < ReplacementCharacters.Default.Replacements.Count; i++) { - dict = ReplacementCharacters.Default.Replacements; + var rep = ReplacementCharacters.Default.Replacements[i].Clone(); + + if (i < dict.Count) + { + var replacementStr = dict[i].ReplacementString; + dict[i] = rep; + dict[i].ReplacementString = replacementStr; + } + else + { + dict.Insert(i, rep); + } } - //First 4 are mandatory - for (int i = 0; i < Replacement.FIXED_COUNT; i++) - dict[i].Mandatory = true; return new ReplacementCharacters { Replacements = dict }; } @@ -218,5 +85,71 @@ namespace FileManager obj.WriteTo(writer); } } - #endregion + + [JsonConverter(typeof(ReplacementCharactersConverter))] + public class ReplacementCharacters + { + public static readonly ReplacementCharacters Default = new() + { + Replacements = new() + { + Replacement.OtherInvalid("_"), + Replacement.OpenQuote("“"), + Replacement.CloseQuote("”"), + Replacement.OtherQuote("""), + Replacement.Colon("꞉"), + Replacement.Asterisk("✱"), + Replacement.QuestionMark("?"), + Replacement.OpenAngleBracket("<"), + Replacement.CloseAngleBracket(">"), + Replacement.Pipe("⏐"), + } + }; + + public static readonly ReplacementCharacters LoFiDefault = new() + { + Replacements = new() + { + Replacement.OtherInvalid("_"), + Replacement.OpenQuote("'"), + Replacement.CloseQuote("'"), + Replacement.OtherQuote("'"), + Replacement.Colon("-"), + Replacement.Asterisk(""), + Replacement.QuestionMark(""), + Replacement.OpenAngleBracket("["), + Replacement.CloseAngleBracket("]"), + Replacement.Pipe("_"), + } + }; + + public List<Replacement> Replacements { get; init; } + public string DefaultReplacement => Replacements[0].ReplacementString; + public string OpenQuote => Replacements[1].ReplacementString; + public string CloseQuote => Replacements[2].ReplacementString; + public string OtherQuote => Replacements[3].ReplacementString; + + private const char QuoteMark = '"'; + + public string GetReplacement(char toReplace, char preceding, char succeding) + { + if (toReplace == QuoteMark) + { + if (preceding != default && !char.IsLetter(preceding) && !char.IsNumber(preceding)) + return OpenQuote; + else if (succeding != default && !char.IsLetter(succeding) && !char.IsNumber(succeding)) + return CloseQuote; + else + return OtherQuote; + } + + for (int i = 4; i < Replacements.Count; i++) + { + var r = Replacements[i]; + if (r.CharacterToReplace == toReplace) + return r.ReplacementString; + } + return DefaultReplacement; + } + } } diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs b/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs index 07bced72..4bfe2c91 100644 --- a/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs +++ b/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs @@ -29,21 +29,18 @@ private void InitializeComponent() { this.dataGridView1 = new System.Windows.Forms.DataGridView(); + this.charToReplaceCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.replacementStringCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.descriptionCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.defaultsBtn = new System.Windows.Forms.Button(); this.loFiDefaultsBtn = new System.Windows.Forms.Button(); this.saveBtn = new System.Windows.Forms.Button(); this.cancelBtn = new System.Windows.Forms.Button(); - this.minDefaultBtn = new System.Windows.Forms.Button(); - this.charToReplaceCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.replacementStringCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.descriptionCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit(); this.SuspendLayout(); // // dataGridView1 // - this.dataGridView1.AllowUserToResizeColumns = false; - this.dataGridView1.AllowUserToResizeRows = false; this.dataGridView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); @@ -55,12 +52,35 @@ this.dataGridView1.Location = new System.Drawing.Point(12, 12); this.dataGridView1.Name = "dataGridView1"; this.dataGridView1.RowTemplate.Height = 25; - this.dataGridView1.Size = new System.Drawing.Size(498, 393); + this.dataGridView1.Size = new System.Drawing.Size(416, 393); this.dataGridView1.TabIndex = 0; this.dataGridView1.CellEndEdit += new System.Windows.Forms.DataGridViewCellEventHandler(this.dataGridView1_CellEndEdit); + this.dataGridView1.CellValidating += new System.Windows.Forms.DataGridViewCellValidatingEventHandler(this.dataGridView1_CellValidating); this.dataGridView1.UserDeletingRow += new System.Windows.Forms.DataGridViewRowCancelEventHandler(this.dataGridView1_UserDeletingRow); this.dataGridView1.Resize += new System.EventHandler(this.dataGridView1_Resize); // + // charToReplaceCol + // + this.charToReplaceCol.HeaderText = "Char to Replace"; + this.charToReplaceCol.MinimumWidth = 70; + this.charToReplaceCol.Name = "charToReplaceCol"; + this.charToReplaceCol.Resizable = System.Windows.Forms.DataGridViewTriState.False; + this.charToReplaceCol.Width = 70; + // + // replacementStringCol + // + this.replacementStringCol.HeaderText = "Replacement String"; + this.replacementStringCol.MinimumWidth = 85; + this.replacementStringCol.Name = "replacementStringCol"; + this.replacementStringCol.Width = 85; + // + // descriptionCol + // + this.descriptionCol.HeaderText = "Description"; + this.descriptionCol.MinimumWidth = 100; + this.descriptionCol.Name = "descriptionCol"; + this.descriptionCol.Width = 200; + // // defaultsBtn // this.defaultsBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); @@ -86,7 +106,7 @@ // saveBtn // this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.saveBtn.Location = new System.Drawing.Point(428, 430); + this.saveBtn.Location = new System.Drawing.Point(346, 430); this.saveBtn.Name = "saveBtn"; this.saveBtn.Size = new System.Drawing.Size(82, 25); this.saveBtn.TabIndex = 1; @@ -97,7 +117,7 @@ // cancelBtn // this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.cancelBtn.Location = new System.Drawing.Point(340, 430); + this.cancelBtn.Location = new System.Drawing.Point(258, 430); this.cancelBtn.Name = "cancelBtn"; this.cancelBtn.Size = new System.Drawing.Size(82, 25); this.cancelBtn.TabIndex = 1; @@ -105,45 +125,11 @@ this.cancelBtn.UseVisualStyleBackColor = true; this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click); // - // minDefaultBtn - // - this.minDefaultBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.minDefaultBtn.Location = new System.Drawing.Point(172, 430); - this.minDefaultBtn.Name = "minDefaultBtn"; - this.minDefaultBtn.Size = new System.Drawing.Size(80, 25); - this.minDefaultBtn.TabIndex = 1; - this.minDefaultBtn.Text = "Barebones"; - this.minDefaultBtn.UseVisualStyleBackColor = true; - this.minDefaultBtn.Click += new System.EventHandler(this.minDefaultBtn_Click); - // - // charToReplaceCol - // - this.charToReplaceCol.HeaderText = "Char to Replace"; - this.charToReplaceCol.MinimumWidth = 70; - this.charToReplaceCol.Name = "charToReplaceCol"; - this.charToReplaceCol.Resizable = System.Windows.Forms.DataGridViewTriState.False; - this.charToReplaceCol.Width = 70; - // - // replacementStringCol - // - this.replacementStringCol.HeaderText = "Replacement Text"; - this.replacementStringCol.MinimumWidth = 85; - this.replacementStringCol.Name = "replacementStringCol"; - this.replacementStringCol.Width = 85; - // - // descriptionCol - // - this.descriptionCol.HeaderText = "Description"; - this.descriptionCol.MinimumWidth = 100; - this.descriptionCol.Name = "descriptionCol"; - this.descriptionCol.Width = 200; - // // EditReplacementChars // this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(522, 467); - this.Controls.Add(this.minDefaultBtn); + this.ClientSize = new System.Drawing.Size(440, 467); this.Controls.Add(this.loFiDefaultsBtn); this.Controls.Add(this.cancelBtn); this.Controls.Add(this.saveBtn); @@ -159,13 +145,12 @@ #endregion private System.Windows.Forms.DataGridView dataGridView1; + private System.Windows.Forms.DataGridViewTextBoxColumn charToReplaceCol; + private System.Windows.Forms.DataGridViewTextBoxColumn replacementStringCol; + private System.Windows.Forms.DataGridViewTextBoxColumn descriptionCol; private System.Windows.Forms.Button defaultsBtn; private System.Windows.Forms.Button loFiDefaultsBtn; private System.Windows.Forms.Button saveBtn; private System.Windows.Forms.Button cancelBtn; - private System.Windows.Forms.Button minDefaultBtn; - private System.Windows.Forms.DataGridViewTextBoxColumn charToReplaceCol; - private System.Windows.Forms.DataGridViewTextBoxColumn replacementStringCol; - private System.Windows.Forms.DataGridViewTextBoxColumn descriptionCol; } } \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.cs b/Source/LibationWinForms/Dialogs/EditReplacementChars.cs index 11a0bf6c..0322ce64 100644 --- a/Source/LibationWinForms/Dialogs/EditReplacementChars.cs +++ b/Source/LibationWinForms/Dialogs/EditReplacementChars.cs @@ -15,30 +15,25 @@ namespace LibationWinForms.Dialogs InitializeComponent(); dataGridView1_Resize(this, EventArgs.Empty); } - public EditReplacementChars(Configuration config) : this() { this.config = config; LoadTable(config.ReplacementCharacters.Replacements); } - private void LoadTable(IReadOnlyList<Replacement> replacements) + private void LoadTable(List<Replacement> replacements) { dataGridView1.Rows.Clear(); - for (int i = 0; i < replacements.Count; i++) + foreach (var r in replacements) { - var r = replacements[i]; - int row = dataGridView1.Rows.Add(r.CharacterToReplace, r.ReplacementString, r.Description); dataGridView1.Rows[row].Tag = r; - - if (r.Mandatory) + if (ReplacementCharacters.Default.Replacements.Any(rep => rep.CharacterToReplace == r.CharacterToReplace)) { + r.Mandatory = true; dataGridView1.Rows[row].Cells[charToReplaceCol.Index].ReadOnly = true; dataGridView1.Rows[row].Cells[descriptionCol.Index].ReadOnly = true; - dataGridView1.Rows[row].Cells[charToReplaceCol.Index].Style.BackColor = System.Drawing.Color.LightGray; - dataGridView1.Rows[row].Cells[descriptionCol.Index].Style.BackColor = System.Drawing.Color.LightGray; } } } @@ -50,14 +45,67 @@ namespace LibationWinForms.Dialogs } private void loFiDefaultsBtn_Click(object sender, EventArgs e) - => LoadTable(ReplacementCharacters.LoFiDefault.Replacements); + { + LoadTable(ReplacementCharacters.LoFiDefault.Replacements); + } private void defaultsBtn_Click(object sender, EventArgs e) - => LoadTable(ReplacementCharacters.Default.Replacements); + { + LoadTable(ReplacementCharacters.Default.Replacements); + } - private void minDefaultBtn_Click(object sender, EventArgs e) - => LoadTable(ReplacementCharacters.Minimum.Replacements); + private void dataGridView1_CellValidating(object sender, DataGridViewCellValidatingEventArgs e) + { + if (e.RowIndex < 0) return; + var cellValue = e.FormattedValue?.ToString(); + + if (dataGridView1.Rows[e.RowIndex].Tag is Replacement row && row.Mandatory) + { + if (e.ColumnIndex == replacementStringCol.Index) + { + //Ensure replacement string doesn't contain an illegal character. + var replaceString = cellValue ?? string.Empty; + if (replaceString != string.Empty && replaceString.Any(c => FileUtility.invalidChars.Contains(c))) + { + dataGridView1.Rows[e.RowIndex].ErrorText = $"{replaceString} contains an illegal path character"; + e.Cancel = true; + } + } + return; + } + + + + if (e.ColumnIndex == charToReplaceCol.Index) + { + if (cellValue.Length != 1) + { + dataGridView1.Rows[e.RowIndex].ErrorText = "Only 1 character to replace per entry"; + e.Cancel = true; + } + else if ( + dataGridView1.Rows + .Cast<DataGridViewRow>() + .Where(r => r.Index != e.RowIndex) + .OfType<Replacement>() + .Any(r => r.CharacterToReplace == cellValue[0]) + ) + { + dataGridView1.Rows[e.RowIndex].ErrorText = $"The {cellValue[0]} character is already being replaced"; + e.Cancel = true; + } + } + else if (e.ColumnIndex == descriptionCol.Index || e.ColumnIndex == replacementStringCol.Index) + { + var value = dataGridView1.Rows[e.RowIndex].Cells[charToReplaceCol.Index].Value; + if (value is null || value is string str && string.IsNullOrEmpty(str)) + { + dataGridView1.Rows[e.RowIndex].ErrorText = $"You must choose a character to replace"; + e.Cancel = true; + } + } + } private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e) { @@ -65,14 +113,10 @@ namespace LibationWinForms.Dialogs dataGridView1.Rows[e.RowIndex].ErrorText = string.Empty; - var charToReplaceStr = dataGridView1.Rows[e.RowIndex].Cells[charToReplaceCol.Index].Value?.ToString(); - var replacement = dataGridView1.Rows[e.RowIndex].Cells[replacementStringCol.Index].Value?.ToString() ?? string.Empty; - var description = dataGridView1.Rows[e.RowIndex].Cells[descriptionCol.Index].Value?.ToString() ?? string.Empty; + var cellValue = dataGridView1.Rows[e.RowIndex].Cells[charToReplaceCol.Index].Value?.ToString(); - //Validate the whole row. If it passes all validation, add or update the row's tag. - if (string.IsNullOrEmpty(charToReplaceStr) && replacement == string.Empty && description == string.Empty) + if (string.IsNullOrEmpty(cellValue) || cellValue.Length > 1) { - //Invalid entry, so delete row var row = dataGridView1.Rows[e.RowIndex]; if (!row.IsNewRow) { @@ -82,38 +126,26 @@ namespace LibationWinForms.Dialogs })); } } - else if (string.IsNullOrEmpty(charToReplaceStr)) - { - dataGridView1.Rows[e.RowIndex].ErrorText = $"You must choose a character to replace"; - } - else if (charToReplaceStr.Length > 1) - { - dataGridView1.Rows[e.RowIndex].ErrorText = $"Only 1 {charToReplaceCol.HeaderText} per entry"; - } - else if (e.RowIndex >= Replacement.FIXED_COUNT && - dataGridView1.Rows - .Cast<DataGridViewRow>() - .Where(r => r.Index != e.RowIndex) - .Select(r => r.Tag) - .OfType<Replacement>() - .Any(r => r.CharacterToReplace == charToReplaceStr[0]) - ) - { - dataGridView1.Rows[e.RowIndex].ErrorText = $"The {charToReplaceStr[0]} character is already being replaced"; - } - else if (ReplacementCharacters.ContainsInvalid(replacement)) - { - dataGridView1.Rows[e.RowIndex].ErrorText = $"Your {replacementStringCol.HeaderText} contains illegal characters"; - } else { - //valid entry. Add or update Replacement in row's Tag - var charToReplace = charToReplaceStr[0]; + char charToReplace = cellValue[0]; + string description = dataGridView1.Rows[e.RowIndex].Cells[descriptionCol.Index].Value?.ToString() ?? string.Empty; + string replacement = dataGridView1.Rows[e.RowIndex].Cells[replacementStringCol.Index].Value?.ToString() ?? string.Empty; + var mandatory = false; if (dataGridView1.Rows[e.RowIndex].Tag is Replacement existing) - existing.Update(charToReplace, replacement, description); - else - dataGridView1.Rows[e.RowIndex].Tag = new Replacement(charToReplace, replacement, description); + { + mandatory = existing.Mandatory; + } + + dataGridView1.Rows[e.RowIndex].Tag = + new Replacement() + { + CharacterToReplace = charToReplace, + ReplacementString = replacement, + Description = description, + Mandatory = mandatory + }; } } @@ -123,6 +155,7 @@ namespace LibationWinForms.Dialogs .Cast<DataGridViewRow>() .Select(r => r.Tag) .OfType<Replacement>() + .Where(r => r.ReplacementString != null && (r.ReplacementString == string.Empty || !r.ReplacementString.Any(c => FileUtility.invalidChars.Contains(c)))) .ToList(); config.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements }; diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs index 035fdf98..8a5266e9 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs @@ -15,8 +15,7 @@ namespace LibationWinForms.Dialogs private void editCharreplacementBtn_Click(object sender, EventArgs e) { var form = new EditReplacementChars(config); - form.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; - form.ShowDialog(this); + form.ShowDialog(); } private void Load_DownloadDecrypt(Configuration config) From 6411d23744e1c04548c7b5d3dde24b52e4e3b7cd Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 11:34:20 -0600 Subject: [PATCH 08/20] Revert "Non-null disposed BlockingCollection can throw exception" This reverts commit ba722487d8c7be977d8246e152131e9c1d829b2f. --- Source/FileManager/BackgroundFileSystem.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Source/FileManager/BackgroundFileSystem.cs b/Source/FileManager/BackgroundFileSystem.cs index f3f9a747..4f68003d 100644 --- a/Source/FileManager/BackgroundFileSystem.cs +++ b/Source/FileManager/BackgroundFileSystem.cs @@ -79,13 +79,8 @@ namespace FileManager //Stop raising events fileSystemWatcher?.Dispose(); - try - { - //Calling CompleteAdding() will cause background scanner to terminate. - directoryChangesEvents?.CompleteAdding(); - } - // if directoryChangesEvents is non-null and isDisposed, this exception is thrown. there's no other way to check >:( - catch (ObjectDisposedException) { } + //Calling CompleteAdding() will cause background scanner to terminate. + directoryChangesEvents?.CompleteAdding(); //Wait for background scanner to terminate before reinitializing. backgroundScanner?.Wait(); From 29c73445404bb0567bbe9c352d7864a93e62b59d Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 11:34:24 -0600 Subject: [PATCH 09/20] Revert "linux + WINE link" This reverts commit eff2634b32e698a5beee1dc94a0de71952944b66. --- Documentation/Advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/Advanced.md b/Documentation/Advanced.md index b7954f7d..66117b29 100644 --- a/Documentation/Advanced.md +++ b/Documentation/Advanced.md @@ -27,7 +27,7 @@ To make upgrades and reinstalls easier, Libation separates all of its responsibi ### Linux and Mac -Although Libation only currently officially supports Windows, some users have had success with WINE. ([Linux](https://github.com/rmcrackan/Libation/issues/28#issuecomment-890594158), [OSX Crossover and WINE](https://github.com/rmcrackan/Libation/issues/150#issuecomment-1004918592), [Linux and WINE](https://github.com/rmcrackan/Libation/issues/28#issuecomment-1161111014)) +Although Libation only currently officially supports Windows, some users have had success with WINE. ([Linux](https://github.com/rmcrackan/Libation/issues/28#issuecomment-890594158), [OSX Crossover and WINE](https://github.com/rmcrackan/Libation/issues/150#issuecomment-1004918592)) ### Settings From 71192cc2ee507442721de3beb7cac1b46672b4ae Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 11:34:29 -0600 Subject: [PATCH 10/20] Revert "Match rmcrackan's changes" This reverts commit 52622fadbb50a14141098fbf809573ab569593b2. --- Source/AaxDecrypter/NetworkFileStream.cs | 2 +- Source/AppScaffolding/LibationScaffolding.cs | 2 +- Source/ApplicationServices/LibraryCommands.cs | 10 +++++----- Source/AudibleUtilities/ApiExtended.cs | 2 +- Source/AudibleUtilities/AudibleUtilities.csproj | 2 +- Source/DataLayer/DataLayer.csproj | 2 +- Source/DtoImporterService/BookImporter.cs | 3 +-- Source/FileLiberator/DownloadDecryptBook.cs | 2 +- Source/FileManager/FileManager.csproj | 2 +- Source/FileManager/FileUtility.cs | 4 ++-- Source/LibationWinForms/Dialogs/AccountsDialog.cs | 4 ++-- Source/LibationWinForms/Form1.ScanNotification.cs | 2 +- Source/LibationWinForms/LibationWinForms.csproj | 2 +- Source/LibationWinForms/Program.cs | 2 +- Source/LibationWinForms/Updater.cs | 2 +- 15 files changed, 21 insertions(+), 22 deletions(-) diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs index d03e44f8..8f2079f1 100644 --- a/Source/AaxDecrypter/NetworkFileStream.cs +++ b/Source/AaxDecrypter/NetworkFileStream.cs @@ -218,7 +218,7 @@ namespace AaxDecrypter } /// <summary> - /// Download <see cref="Uri"/> to <see cref="SaveFilePath"/>. + /// Downlod <see cref="Uri"/> to <see cref="SaveFilePath"/>. /// </summary> private void DownloadFile() { diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 9af8b75f..0d338351 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -473,7 +473,7 @@ namespace AppScaffolding } catch (Exception ex) { - Serilog.Log.Logger.Error(ex, "An error occurred while running database migrations in {0}", nameof(migrate_from_7_10_1)); + Serilog.Log.Logger.Error(ex, "An error occured while running database migrations in {0}", nameof(migrate_from_7_10_1)); config.SetObject($"{nameof(migrate_from_7_10_1)}_ThrewError", true); } } diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs index 56c64e2d..65e6d117 100644 --- a/Source/ApplicationServices/LibraryCommands.cs +++ b/Source/ApplicationServices/LibraryCommands.cs @@ -134,7 +134,7 @@ namespace ApplicationServices if (newParents >= 0) { //If any episodes are still orphaned, their series have been - //removed from the catalog and we'll never be able to find them. + //removed from the catalog and wel'll never be able to find them. //only do this if findAndAddMissingParents returned >= 0. If it //returned < 0, an error happened and there's still a chance that @@ -251,7 +251,7 @@ namespace ApplicationServices } catch (Exception ex) { - Serilog.Log.Logger.Error(ex, "An error occurred while trying to remove orphaned episodes from the database"); + Serilog.Log.Logger.Error(ex, "An error occured while trying to remove orphaned episodes from the database"); } } @@ -274,7 +274,7 @@ namespace ApplicationServices .DistinctBy(s => s.Series.AudibleSeriesId) .ToList(); - // The Catalog endpoint does not require authentication. + // The Catalog endpointdoes not require authentication. var api = new ApiUnauthenticated(accounts[0].Locale); var seriesParents = orphanedSeries.Select(o => o.Series.AudibleSeriesId).ToList(); @@ -308,7 +308,7 @@ namespace ApplicationServices } catch (Exception ex) { - Serilog.Log.Logger.Error(ex, "An error occurred while trying to scan for orphaned episode parents."); + Serilog.Log.Logger.Error(ex, "An error occured while trying to scan for orphaned episode parents."); return -1; } } @@ -321,7 +321,7 @@ namespace ApplicationServices } catch (Microsoft.EntityFrameworkCore.DbUpdateException ex) { - // DbUpdateException exceptions can wreck serilog. Condense it until we can find a better solution. I suspect the culprit is the "WithExceptionDetails" serilog extension + // DbUpdateException exceptions can wreck serilog. Condense it until we can find a better solution. I suspect the culpret is the "WithExceptionDetails" serilog extension static string format(Exception ex) => $"\r\nMessage: {ex.Message}\r\nStack Trace:\r\n{ex.StackTrace}"; diff --git a/Source/AudibleUtilities/ApiExtended.cs b/Source/AudibleUtilities/ApiExtended.cs index 28fbd14f..4290be72 100644 --- a/Source/AudibleUtilities/ApiExtended.cs +++ b/Source/AudibleUtilities/ApiExtended.cs @@ -145,7 +145,7 @@ namespace AudibleUtilities Serilog.Log.Logger.Debug("Library scan complete. Found {count} books and series. Waiting on {getChildEpisodesTasksCount} series episode scans to complete.", count, getChildEpisodesTasks.Count); - //await and add all episodes from all parents + //await and add all episides from all parents foreach (var epList in await Task.WhenAll(getChildEpisodesTasks)) items.AddRange(epList); diff --git a/Source/AudibleUtilities/AudibleUtilities.csproj b/Source/AudibleUtilities/AudibleUtilities.csproj index f3d58a40..94e16429 100644 --- a/Source/AudibleUtilities/AudibleUtilities.csproj +++ b/Source/AudibleUtilities/AudibleUtilities.csproj @@ -5,7 +5,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="AudibleApi" Version="4.1.1.1" /> + <PackageReference Include="AudibleApi" Version="4.1.0.1" /> </ItemGroup> <ItemGroup> diff --git a/Source/DataLayer/DataLayer.csproj b/Source/DataLayer/DataLayer.csproj index 1690e74f..eb2f1b50 100644 --- a/Source/DataLayer/DataLayer.csproj +++ b/Source/DataLayer/DataLayer.csproj @@ -12,7 +12,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Dinah.EntityFrameworkCore" Version="4.1.1.1" /> + <PackageReference Include="Dinah.EntityFrameworkCore" Version="4.1.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> diff --git a/Source/DtoImporterService/BookImporter.cs b/Source/DtoImporterService/BookImporter.cs index 67d18f4e..297ae659 100644 --- a/Source/DtoImporterService/BookImporter.cs +++ b/Source/DtoImporterService/BookImporter.cs @@ -101,8 +101,7 @@ namespace DtoImporterService // absence of categories is also possible // CATEGORY HACK: only use the 1st 2 categories - // after we support full arbitrary-depth category trees and multiple categories per book, the real impl will be something like this - // var lastCategory = item.Categories.LastOrDefault()?.CategoryId ?? ""; + // (real impl: var lastCategory = item.Categories.LastOrDefault()?.CategoryId ?? "";) var lastCategory = item.Categories.Length == 0 ? "" : item.Categories.Length == 1 ? item.Categories[0].CategoryId diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index 4bea8538..e759386d 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -261,7 +261,7 @@ namespace FileLiberator var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)), Configuration.Instance.ReplacementCharacters); FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest); - // propagate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop) + // propogate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop) entries[i] = entry with { Path = realDest }; } diff --git a/Source/FileManager/FileManager.csproj b/Source/FileManager/FileManager.csproj index 16363aa5..12601f9a 100644 --- a/Source/FileManager/FileManager.csproj +++ b/Source/FileManager/FileManager.csproj @@ -5,7 +5,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Dinah.Core" Version="4.4.1.1" /> + <PackageReference Include="Dinah.Core" Version="4.4.0.1" /> <PackageReference Include="Polly" Version="7.2.3" /> </ItemGroup> diff --git a/Source/FileManager/FileUtility.cs b/Source/FileManager/FileUtility.cs index 7a33dd91..550f46e0 100644 --- a/Source/FileManager/FileUtility.cs +++ b/Source/FileManager/FileUtility.cs @@ -77,7 +77,7 @@ namespace FileManager return fullfilename; } - /// <summary>Use with full path, not file name. Valid path characters which are invalid file name characters will be retained: '\\', '/'</summary> + /// <summary>Use with full path, not file name. Valid path charaters which are invalid file name characters will be retained: '\\', '/'</summary> public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements) { ArgumentValidator.EnsureNotNull(path, nameof(path)); @@ -147,7 +147,7 @@ namespace FileManager // regex is easier by ending with separator fullfilename += Path.DirectorySeparatorChar; fullfilename = removeInvalidWhitespace_regex.Replace(fullfilename, Path.DirectorySeparatorChar.ToString()); - // take separator back off + // take seperator back off fullfilename = RemoveLastCharacter(fullfilename); fullfilename = removeDoubleSlashes(fullfilename); diff --git a/Source/LibationWinForms/Dialogs/AccountsDialog.cs b/Source/LibationWinForms/Dialogs/AccountsDialog.cs index d60c2f1e..0f04bb7c 100644 --- a/Source/LibationWinForms/Dialogs/AccountsDialog.cs +++ b/Source/LibationWinForms/Dialogs/AccountsDialog.cs @@ -252,7 +252,7 @@ namespace LibationWinForms.Dialogs { MessageBoxLib.ShowAdminAlert( this, - $"An error occurred while exporting account:\r\n{account.AccountName}", + $"An error occured while exporting account:\r\n{account.AccountName}", "Error Exporting Account", ex); } @@ -294,7 +294,7 @@ namespace LibationWinForms.Dialogs { MessageBoxLib.ShowAdminAlert( this, - $"An error occurred while importing an account from:\r\n{ofd.FileName}\r\n\r\nIs the file encrypted?", + $"An error occured while importing an account from:\r\n{ofd.FileName}\r\n\r\nIs the file encrypted?", "Error Importing Account", ex); } diff --git a/Source/LibationWinForms/Form1.ScanNotification.cs b/Source/LibationWinForms/Form1.ScanNotification.cs index 8537053d..dc70537d 100644 --- a/Source/LibationWinForms/Form1.ScanNotification.cs +++ b/Source/LibationWinForms/Form1.ScanNotification.cs @@ -3,7 +3,7 @@ using ApplicationServices; namespace LibationWinForms { - // This is for the Scanning notification in the upper right. This shown for manual scanning and auto-scan + // This is for the Scanning notificationin the upper right. This shown for manual scanning and auto-scan public partial class Form1 { private void Configure_ScanNotification() diff --git a/Source/LibationWinForms/LibationWinForms.csproj b/Source/LibationWinForms/LibationWinForms.csproj index b21cadd3..94dd14ae 100644 --- a/Source/LibationWinForms/LibationWinForms.csproj +++ b/Source/LibationWinForms/LibationWinForms.csproj @@ -29,7 +29,7 @@ <ItemGroup> <PackageReference Include="Autoupdater.NET.Official" Version="1.7.3" /> - <PackageReference Include="Dinah.Core.WindowsDesktop" Version="4.2.3.1" /> + <PackageReference Include="Dinah.Core.WindowsDesktop" Version="4.2.2.1" /> </ItemGroup> <ItemGroup> diff --git a/Source/LibationWinForms/Program.cs b/Source/LibationWinForms/Program.cs index fd933c32..2abbea83 100644 --- a/Source/LibationWinForms/Program.cs +++ b/Source/LibationWinForms/Program.cs @@ -87,7 +87,7 @@ namespace LibationWinForms var defaultLibationFilesDir = Configuration.UserProfile; - // check for existing settings in default location + // check for existing settigns in default location var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json"); if (Configuration.SettingsFileIsValid(defaultSettingsFile)) config.SetLibationFiles(defaultLibationFilesDir); diff --git a/Source/LibationWinForms/Updater.cs b/Source/LibationWinForms/Updater.cs index 9660c7ce..1506f924 100644 --- a/Source/LibationWinForms/Updater.cs +++ b/Source/LibationWinForms/Updater.cs @@ -29,7 +29,7 @@ namespace LibationWinForms return; var dialogResult = MessageBox.Show(string.Format( - $"There is a new version available. Would you like to update?\r\n\r\nAfter you close Libation, the upgrade will start automatically."), + $"There is a new version avilable. Would you like to update?\r\n\r\nAfter you close Libation, the upgrade will start automatically."), "Update Available", MessageBoxButtons.YesNo, MessageBoxIcon.Information); From 48eca3f5afec562a2b5b57c7f96fc82b39f81d05 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 11:34:39 -0600 Subject: [PATCH 11/20] Revert "Add character replacement" This reverts commit 1470aefd42ebbea782b8056252a4952df2a81081. --- .../AaxcDownloadMultiConverter.cs | 2 +- Source/AaxDecrypter/AudiobookDownloadBase.cs | 2 +- Source/AaxDecrypter/IDownloadOptions.cs | 1 - .../MultiConvertFileProperties.cs | 10 + .../UnencryptedAudiobookDownloader.cs | 4 +- Source/AppScaffolding/LibationScaffolding.cs | 3 - Source/FileLiberator/ConvertToMp3.cs | 2 +- Source/FileLiberator/DownloadDecryptBook.cs | 2 +- Source/FileLiberator/DownloadOptions.cs | 2 - Source/FileManager/FileNamingTemplate.cs | 4 +- Source/FileManager/FileUtility.cs | 100 +++++++--- Source/FileManager/ReplacementCharacters.cs | 155 --------------- Source/LibationFileManager/Configuration.cs | 7 - Source/LibationFileManager/Templates.cs | 8 +- .../Dialogs/EditReplacementChars.Designer.cs | 156 --------------- .../Dialogs/EditReplacementChars.cs | 177 ------------------ .../Dialogs/EditReplacementChars.resx | 69 ------- .../Dialogs/SettingsDialog.Designer.cs | 56 +++--- .../Dialogs/SettingsDialog.DownloadDecrypt.cs | 9 - 19 files changed, 115 insertions(+), 654 deletions(-) delete mode 100644 Source/FileManager/ReplacementCharacters.cs delete mode 100644 Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs delete mode 100644 Source/LibationWinForms/Dialogs/EditReplacementChars.cs delete mode 100644 Source/LibationWinForms/Dialogs/EditReplacementChars.resx diff --git a/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs b/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs index ea11c319..25cac87b 100644 --- a/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs +++ b/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs @@ -141,7 +141,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea private FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties) { var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties); - fileName = FileUtility.GetValidFilename(fileName, DownloadOptions.ReplacementCharacters); + fileName = FileUtility.GetValidFilename(fileName); multiPartFilePaths.Add(fileName); diff --git a/Source/AaxDecrypter/AudiobookDownloadBase.cs b/Source/AaxDecrypter/AudiobookDownloadBase.cs index 08d4f3d1..27ce3025 100644 --- a/Source/AaxDecrypter/AudiobookDownloadBase.cs +++ b/Source/AaxDecrypter/AudiobookDownloadBase.cs @@ -103,7 +103,7 @@ namespace AaxDecrypter try { var path = Path.ChangeExtension(OutputFileName, ".cue"); - path = FileUtility.GetValidFilename(path, DownloadOptions.ReplacementCharacters); + path = FileUtility.GetValidFilename(path); File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo)); OnFileCreated(path); } diff --git a/Source/AaxDecrypter/IDownloadOptions.cs b/Source/AaxDecrypter/IDownloadOptions.cs index 58d6e90f..64ae08e3 100644 --- a/Source/AaxDecrypter/IDownloadOptions.cs +++ b/Source/AaxDecrypter/IDownloadOptions.cs @@ -4,7 +4,6 @@ namespace AaxDecrypter { public interface IDownloadOptions { - FileManager.ReplacementCharacters ReplacementCharacters { get; } string DownloadUrl { get; } string UserAgent { get; } string AudibleKey { get; } diff --git a/Source/AaxDecrypter/MultiConvertFileProperties.cs b/Source/AaxDecrypter/MultiConvertFileProperties.cs index 71bafe21..febc2a49 100644 --- a/Source/AaxDecrypter/MultiConvertFileProperties.cs +++ b/Source/AaxDecrypter/MultiConvertFileProperties.cs @@ -11,5 +11,15 @@ namespace AaxDecrypter public int PartsTotal { get; set; } public string Title { get; set; } + public static string DefaultMultipartFilename(MultiConvertFileProperties multiConvertFileProperties) + { + var template = Path.ChangeExtension(multiConvertFileProperties.OutputFileName, null) + " - <ch# 0> - <title>" + Path.GetExtension(multiConvertFileProperties.OutputFileName); + + var fileNamingTemplate = new FileNamingTemplate(template) { IllegalCharacterReplacements = " " }; + fileNamingTemplate.AddParameterReplacement("ch# 0", FileUtility.GetSequenceFormatted(multiConvertFileProperties.PartsPosition, multiConvertFileProperties.PartsTotal)); + fileNamingTemplate.AddParameterReplacement("title", multiConvertFileProperties.Title ?? ""); + + return fileNamingTemplate.GetFilePath(); + } } } diff --git a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs index 142ccf44..cd6dee88 100644 --- a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs +++ b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs @@ -67,8 +67,8 @@ namespace AaxDecrypter } CloseInputFileStream(); - - var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName, DownloadOptions.ReplacementCharacters); + + var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName); SetOutputFileName(realOutputFileName); OnFileCreated(realOutputFileName); diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 0d338351..411bc886 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -117,9 +117,6 @@ namespace AppScaffolding if (!config.Exists(nameof(config.DownloadEpisodes))) config.DownloadEpisodes = true; - if (!config.Exists(nameof(config.ReplacementCharacters))) - config.ReplacementCharacters = FileManager.ReplacementCharacters.Default; - if (!config.Exists(nameof(config.FolderTemplate))) config.FolderTemplate = Templates.Folder.DefaultTemplate; diff --git a/Source/FileLiberator/ConvertToMp3.cs b/Source/FileLiberator/ConvertToMp3.cs index bb191986..14a75b9b 100644 --- a/Source/FileLiberator/ConvertToMp3.cs +++ b/Source/FileLiberator/ConvertToMp3.cs @@ -70,7 +70,7 @@ namespace FileLiberator return new StatusHandler { "Cancelled" }; } - var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path, Configuration.Instance.ReplacementCharacters); + var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path); OnFileCreated(libraryBook, realMp3Path); } return new StatusHandler(); diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index e759386d..906acca3 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -258,7 +258,7 @@ namespace FileLiberator { var entry = entries[i]; - var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)), Configuration.Instance.ReplacementCharacters); + var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path))); FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest); // propogate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop) diff --git a/Source/FileLiberator/DownloadOptions.cs b/Source/FileLiberator/DownloadOptions.cs index dc2fd940..86b0a778 100644 --- a/Source/FileLiberator/DownloadOptions.cs +++ b/Source/FileLiberator/DownloadOptions.cs @@ -3,7 +3,6 @@ using AAXClean; using Dinah.Core; using DataLayer; using LibationFileManager; -using FileManager; namespace FileLiberator { @@ -24,7 +23,6 @@ namespace FileLiberator public NAudio.Lame.LameConfig LameConfig { get; set; } public bool Downsample { get; set; } public bool MatchSourceBitrate { get; set; } - public ReplacementCharacters ReplacementCharacters => Configuration.Instance.ReplacementCharacters; public string GetMultipartFileName(MultiConvertFileProperties props) => Templates.ChapterFile.GetFilename(LibraryBookDto, props); diff --git a/Source/FileManager/FileNamingTemplate.cs b/Source/FileManager/FileNamingTemplate.cs index c55e2f8f..1f472b6d 100644 --- a/Source/FileManager/FileNamingTemplate.cs +++ b/Source/FileManager/FileNamingTemplate.cs @@ -16,7 +16,7 @@ namespace FileManager public string IllegalCharacterReplacements { get; set; } /// <summary>Generate a valid path for this file or directory</summary> - public LongPath GetFilePath(ReplacementCharacters replacements, bool returnFirstExisting = false) + public LongPath GetFilePath(bool returnFirstExisting = false) { string fileName = Template.EndsWith(Path.DirectorySeparatorChar) ? Template[..^1] : Template; @@ -43,7 +43,7 @@ namespace FileManager pathParts.Reverse(); - return FileUtility.GetValidFilename(Path.Join(pathParts.ToArray()), replacements, returnFirstExisting); + return FileUtility.GetValidFilename(Path.Join(pathParts.ToArray()), IllegalCharacterReplacements, returnFirstExisting); } private string replaceFileName(string filename, Dictionary<string,string> paramReplacements) diff --git a/Source/FileManager/FileUtility.cs b/Source/FileManager/FileUtility.cs index 550f46e0..54bb3320 100644 --- a/Source/FileManager/FileUtility.cs +++ b/Source/FileManager/FileUtility.cs @@ -46,12 +46,12 @@ namespace FileManager /// <br/>- ensure uniqueness /// <br/>- enforce max file length /// </summary> - public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, bool returnFirstExisting = false) + public static LongPath GetValidFilename(LongPath path, string illegalCharacterReplacements = "", bool returnFirstExisting = false) { ArgumentValidator.EnsureNotNull(path, nameof(path)); // remove invalid chars - path = GetSafePath(path, replacements); + path = GetSafePath(path, illegalCharacterReplacements); // ensure uniqueness and check lengths var dir = Path.GetDirectoryName(path); @@ -77,45 +77,35 @@ namespace FileManager return fullfilename; } + // GetInvalidFileNameChars contains everything in GetInvalidPathChars plus ':', '*', '?', '\\', '/' + + /// <summary>Use with file name, not full path. Valid path charaters which are invalid file name characters will be replaced: ':', '\\', '/'</summary> + public static string GetSafeFileName(string str, string illegalCharacterReplacements = "") + => string.Join(illegalCharacterReplacements ?? "", str.Split(Path.GetInvalidFileNameChars())); + /// <summary>Use with full path, not file name. Valid path charaters which are invalid file name characters will be retained: '\\', '/'</summary> - public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements) + public static LongPath GetSafePath(LongPath path, string illegalCharacterReplacements = "") { ArgumentValidator.EnsureNotNull(path, nameof(path)); var pathNoPrefix = path.PathWithoutPrefix; - pathNoPrefix = replaceInvalidChars(pathNoPrefix, replacements); + pathNoPrefix = replaceColons(pathNoPrefix, "꞉"); + pathNoPrefix = replaceIllegalWithUnicodeAnalog(pathNoPrefix); + pathNoPrefix = replaceInvalidChars(pathNoPrefix, illegalCharacterReplacements); pathNoPrefix = removeDoubleSlashes(pathNoPrefix); return pathNoPrefix; } - public static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] { - '*', '?', ':', + private static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] { + '*', '?', // these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included. // In live code, Path.GetInvalidPathChars() does not include them '"', '<', '>' }).ToArray(); - private static string replaceInvalidChars(string path, ReplacementCharacters replacements) - { - // replace all colons except within the first 2 chars - var builder = new System.Text.StringBuilder(); - for (var i = 0; i < path.Length; i++) - { - var c = path[i]; - - if (!invalidChars.Contains(c) || (i <= 2 && Path.IsPathRooted(path))) - builder.Append(c); - else - { - char preceding = i > 0 ? path[i - 1] : default; - char succeeding = i < path.Length - 1 ? path[i + 1] : default; - builder.Append(replacements.GetReplacement(c, preceding, succeeding)); - } - - } - return builder.ToString(); - } + private static string replaceInvalidChars(string path, string illegalCharacterReplacements) + => string.Join(illegalCharacterReplacements ?? "", path.Split(invalidChars)); private static string removeDoubleSlashes(string path) { @@ -132,6 +122,60 @@ namespace FileManager return path[0] + remainder; } + private static string replaceIllegalWithUnicodeAnalog(string path) + { + char[] replaced = path.ToCharArray(); + + char GetQuote(int position) + { + if ( + position == 0 + || (position > 0 + && position < replaced.Length + && !char.IsLetter(replaced[position - 1]) + && !char.IsNumber(replaced[position - 1]) + ) + ) return '“'; + else if ( + position == replaced.Length - 1 + || (position >= 0 + && position < replaced.Length - 1 + && !char.IsLetter(replaced[position + 1]) + && !char.IsNumber(replaced[position + 1]) + ) + ) return '”'; + else return '"'; + } + + for (int i = 0; i < replaced.Length; i++) + { + replaced[i] = replaced[i] switch + { + '?' => '?', + '*' => '✱', + '<' => '<', + '>' => '>', + '"' => GetQuote(i), + _ => replaced[i] + }; + } + return new string(replaced); + } + + private static string replaceColons(string path, string illegalCharacterReplacements) + { + // replace all colons except within the first 2 chars + var builder = new System.Text.StringBuilder(); + for (var i = 0; i < path.Length; i++) + { + var c = path[i]; + if (i >= 2 && c == ':') + builder.Append(illegalCharacterReplacements); + else + builder.Append(c); + } + return builder.ToString(); + } private static string removeInvalidWhitespace_pattern { get; } = $@"[\s\.]*\{Path.DirectorySeparatorChar}\s*"; private static Regex removeInvalidWhitespace_regex { get; } = new(removeInvalidWhitespace_pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace); @@ -162,9 +206,9 @@ namespace FileManager /// <br/>- Perform <see cref="SaferMove"/> /// <br/>- Return valid path /// </summary> - public static string SaferMoveToValidPath(LongPath source, LongPath destination, ReplacementCharacters replacements) + public static string SaferMoveToValidPath(LongPath source, LongPath destination) { - destination = GetValidFilename(destination, replacements); + destination = GetValidFilename(destination); SaferMove(source, destination); return destination; } diff --git a/Source/FileManager/ReplacementCharacters.cs b/Source/FileManager/ReplacementCharacters.cs deleted file mode 100644 index 0fb91189..00000000 --- a/Source/FileManager/ReplacementCharacters.cs +++ /dev/null @@ -1,155 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace FileManager -{ - public class Replacement - { - [JsonIgnore] - public bool Mandatory { get; set; } - [JsonProperty] - public char CharacterToReplace { get; init; } - [JsonProperty] - public string ReplacementString { get; set; } - [JsonProperty] - public string Description { get; set; } - - public Replacement Clone() => new() - { - Mandatory = Mandatory, - CharacterToReplace = CharacterToReplace, - ReplacementString = ReplacementString, - Description = Description - }; - - public override string ToString() => $"{CharacterToReplace} → {ReplacementString} ({Description})"; - - public static Replacement Colon(string replacement) => new Replacement { CharacterToReplace = ':', Description = "Colon", ReplacementString = replacement}; - public static Replacement Asterisk(string replacement) => new Replacement { CharacterToReplace = '*', Description = "Asterisk", ReplacementString = replacement }; - public static Replacement QuestionMark(string replacement) => new Replacement { CharacterToReplace = '?', Description = "Question Mark", ReplacementString = replacement }; - public static Replacement OpenAngleBracket(string replacement) => new Replacement { CharacterToReplace = '<', Description = "Open Angle Bracket", ReplacementString = replacement }; - public static Replacement CloseAngleBracket(string replacement) => new Replacement { CharacterToReplace = '>', Description = "Close Angle Bracket", ReplacementString = replacement }; - public static Replacement OpenQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Open Quote", ReplacementString = replacement }; - public static Replacement CloseQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Close Quote", ReplacementString = replacement }; - public static Replacement OtherQuote(string replacement) => new Replacement { CharacterToReplace = '"', Description = "Other Quote", ReplacementString = replacement }; - public static Replacement Pipe(string replacement) => new Replacement { CharacterToReplace = '|', Description = "Vertical Line", ReplacementString = replacement }; - public static Replacement OtherInvalid(string replacement) => new Replacement { CharacterToReplace = default, Description = "Any other invalid characters", ReplacementString = replacement }; - } - - internal class ReplacementCharactersConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - => objectType == typeof(ReplacementCharacters); - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var jObj = JObject.Load(reader); - var replaceArr = jObj[nameof(Replacement)]; - var dict = replaceArr - .ToObject<Replacement[]>().ToList(); - - //Add any missing defaults and ensure they are in the expected order. - for (int i = 0; i < ReplacementCharacters.Default.Replacements.Count; i++) - { - var rep = ReplacementCharacters.Default.Replacements[i].Clone(); - - if (i < dict.Count) - { - var replacementStr = dict[i].ReplacementString; - dict[i] = rep; - dict[i].ReplacementString = replacementStr; - } - else - { - dict.Insert(i, rep); - } - } - - return new ReplacementCharacters { Replacements = dict }; - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - ReplacementCharacters replacements = (ReplacementCharacters)value; - - var propertyNames = replacements.Replacements - .Select(c => JObject.FromObject(c)).ToList(); - - var prop = new JProperty(nameof(Replacement), new JArray(propertyNames)); - - var obj = new JObject(); - obj.AddFirst(prop); - obj.WriteTo(writer); - } - } - - [JsonConverter(typeof(ReplacementCharactersConverter))] - public class ReplacementCharacters - { - public static readonly ReplacementCharacters Default = new() - { - Replacements = new() - { - Replacement.OtherInvalid("_"), - Replacement.OpenQuote("“"), - Replacement.CloseQuote("”"), - Replacement.OtherQuote("""), - Replacement.Colon("꞉"), - Replacement.Asterisk("✱"), - Replacement.QuestionMark("?"), - Replacement.OpenAngleBracket("<"), - Replacement.CloseAngleBracket(">"), - Replacement.Pipe("⏐"), - } - }; - - public static readonly ReplacementCharacters LoFiDefault = new() - { - Replacements = new() - { - Replacement.OtherInvalid("_"), - Replacement.OpenQuote("'"), - Replacement.CloseQuote("'"), - Replacement.OtherQuote("'"), - Replacement.Colon("-"), - Replacement.Asterisk(""), - Replacement.QuestionMark(""), - Replacement.OpenAngleBracket("["), - Replacement.CloseAngleBracket("]"), - Replacement.Pipe("_"), - } - }; - - public List<Replacement> Replacements { get; init; } - public string DefaultReplacement => Replacements[0].ReplacementString; - public string OpenQuote => Replacements[1].ReplacementString; - public string CloseQuote => Replacements[2].ReplacementString; - public string OtherQuote => Replacements[3].ReplacementString; - - private const char QuoteMark = '"'; - - public string GetReplacement(char toReplace, char preceding, char succeding) - { - if (toReplace == QuoteMark) - { - if (preceding != default && !char.IsLetter(preceding) && !char.IsNumber(preceding)) - return OpenQuote; - else if (succeding != default && !char.IsLetter(succeding) && !char.IsNumber(succeding)) - return CloseQuote; - else - return OtherQuote; - } - - for (int i = 4; i < Replacements.Count; i++) - { - var r = Replacements[i]; - if (r.CharacterToReplace == toReplace) - return r.ReplacementString; - } - return DefaultReplacement; - } - } -} diff --git a/Source/LibationFileManager/Configuration.cs b/Source/LibationFileManager/Configuration.cs index d545df22..9ce601a6 100644 --- a/Source/LibationFileManager/Configuration.cs +++ b/Source/LibationFileManager/Configuration.cs @@ -284,13 +284,6 @@ namespace LibationFileManager #region templates: custom file naming - [Description("Edit how illegal filename characters are replaced")] - public ReplacementCharacters ReplacementCharacters - { - get => persistentDictionary.GetNonString<ReplacementCharacters>(nameof(ReplacementCharacters)); - set => persistentDictionary.SetNonString(nameof(ReplacementCharacters), value); - } - [Description("How to format the folders in which files will be saved")] public string FolderTemplate { diff --git a/Source/LibationFileManager/Templates.cs b/Source/LibationFileManager/Templates.cs index 7410a8b9..5ee917af 100644 --- a/Source/LibationFileManager/Templates.cs +++ b/Source/LibationFileManager/Templates.cs @@ -106,7 +106,7 @@ namespace LibationFileManager => string.IsNullOrWhiteSpace(template) ? "" : getFileNamingTemplate(libraryBookDto, template, null, null) - .GetFilePath(Configuration.Instance.ReplacementCharacters).PathWithoutPrefix; + .GetFilePath().PathWithoutPrefix; private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -210,7 +210,7 @@ namespace LibationFileManager /// <summary>USES LIVE CONFIGURATION VALUES</summary> public string GetFilename(LibraryBookDto libraryBookDto, string baseDir = null) => getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, baseDir ?? AudibleFileStorage.BooksDirectory, null) - .GetFilePath(Configuration.Instance.ReplacementCharacters); + .GetFilePath(); #endregion } @@ -233,7 +233,7 @@ namespace LibationFileManager /// <summary>USES LIVE CONFIGURATION VALUES</summary> public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension, bool returnFirstExisting = false) => getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension) - .GetFilePath(Configuration.Instance.ReplacementCharacters, returnFirstExisting); + .GetFilePath(returnFirstExisting); #endregion } @@ -277,7 +277,7 @@ namespace LibationFileManager fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal)); fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? ""); - return fileNamingTemplate.GetFilePath(Configuration.Instance.ReplacementCharacters).PathWithoutPrefix; + return fileNamingTemplate.GetFilePath().PathWithoutPrefix; } #endregion } diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs b/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs deleted file mode 100644 index 4bfe2c91..00000000 --- a/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs +++ /dev/null @@ -1,156 +0,0 @@ -namespace LibationWinForms.Dialogs -{ - partial class EditReplacementChars - { - /// <summary> - /// Required designer variable. - /// </summary> - private System.ComponentModel.IContainer components = null; - - /// <summary> - /// Clean up any resources being used. - /// </summary> - /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// <summary> - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// </summary> - private void InitializeComponent() - { - this.dataGridView1 = new System.Windows.Forms.DataGridView(); - this.charToReplaceCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.replacementStringCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.descriptionCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.defaultsBtn = new System.Windows.Forms.Button(); - this.loFiDefaultsBtn = new System.Windows.Forms.Button(); - this.saveBtn = new System.Windows.Forms.Button(); - this.cancelBtn = new System.Windows.Forms.Button(); - ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit(); - this.SuspendLayout(); - // - // dataGridView1 - // - this.dataGridView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; - this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { - this.charToReplaceCol, - this.replacementStringCol, - this.descriptionCol}); - this.dataGridView1.Location = new System.Drawing.Point(12, 12); - this.dataGridView1.Name = "dataGridView1"; - this.dataGridView1.RowTemplate.Height = 25; - this.dataGridView1.Size = new System.Drawing.Size(416, 393); - this.dataGridView1.TabIndex = 0; - this.dataGridView1.CellEndEdit += new System.Windows.Forms.DataGridViewCellEventHandler(this.dataGridView1_CellEndEdit); - this.dataGridView1.CellValidating += new System.Windows.Forms.DataGridViewCellValidatingEventHandler(this.dataGridView1_CellValidating); - this.dataGridView1.UserDeletingRow += new System.Windows.Forms.DataGridViewRowCancelEventHandler(this.dataGridView1_UserDeletingRow); - this.dataGridView1.Resize += new System.EventHandler(this.dataGridView1_Resize); - // - // charToReplaceCol - // - this.charToReplaceCol.HeaderText = "Char to Replace"; - this.charToReplaceCol.MinimumWidth = 70; - this.charToReplaceCol.Name = "charToReplaceCol"; - this.charToReplaceCol.Resizable = System.Windows.Forms.DataGridViewTriState.False; - this.charToReplaceCol.Width = 70; - // - // replacementStringCol - // - this.replacementStringCol.HeaderText = "Replacement String"; - this.replacementStringCol.MinimumWidth = 85; - this.replacementStringCol.Name = "replacementStringCol"; - this.replacementStringCol.Width = 85; - // - // descriptionCol - // - this.descriptionCol.HeaderText = "Description"; - this.descriptionCol.MinimumWidth = 100; - this.descriptionCol.Name = "descriptionCol"; - this.descriptionCol.Width = 200; - // - // defaultsBtn - // - this.defaultsBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.defaultsBtn.Location = new System.Drawing.Point(12, 430); - this.defaultsBtn.Name = "defaultsBtn"; - this.defaultsBtn.Size = new System.Drawing.Size(64, 25); - this.defaultsBtn.TabIndex = 1; - this.defaultsBtn.Text = "Defaults"; - this.defaultsBtn.UseVisualStyleBackColor = true; - this.defaultsBtn.Click += new System.EventHandler(this.defaultsBtn_Click); - // - // loFiDefaultsBtn - // - this.loFiDefaultsBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.loFiDefaultsBtn.Location = new System.Drawing.Point(82, 430); - this.loFiDefaultsBtn.Name = "loFiDefaultsBtn"; - this.loFiDefaultsBtn.Size = new System.Drawing.Size(84, 25); - this.loFiDefaultsBtn.TabIndex = 1; - this.loFiDefaultsBtn.Text = "LoFi Defaults"; - this.loFiDefaultsBtn.UseVisualStyleBackColor = true; - this.loFiDefaultsBtn.Click += new System.EventHandler(this.loFiDefaultsBtn_Click); - // - // saveBtn - // - this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.saveBtn.Location = new System.Drawing.Point(346, 430); - this.saveBtn.Name = "saveBtn"; - this.saveBtn.Size = new System.Drawing.Size(82, 25); - this.saveBtn.TabIndex = 1; - this.saveBtn.Text = "Save"; - this.saveBtn.UseVisualStyleBackColor = true; - this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click); - // - // cancelBtn - // - this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.cancelBtn.Location = new System.Drawing.Point(258, 430); - this.cancelBtn.Name = "cancelBtn"; - this.cancelBtn.Size = new System.Drawing.Size(82, 25); - this.cancelBtn.TabIndex = 1; - this.cancelBtn.Text = "Cancel"; - this.cancelBtn.UseVisualStyleBackColor = true; - this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click); - // - // EditReplacementChars - // - this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(440, 467); - this.Controls.Add(this.loFiDefaultsBtn); - this.Controls.Add(this.cancelBtn); - this.Controls.Add(this.saveBtn); - this.Controls.Add(this.defaultsBtn); - this.Controls.Add(this.dataGridView1); - this.Name = "EditReplacementChars"; - this.Text = "Character Replacements"; - ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit(); - this.ResumeLayout(false); - - } - - #endregion - - private System.Windows.Forms.DataGridView dataGridView1; - private System.Windows.Forms.DataGridViewTextBoxColumn charToReplaceCol; - private System.Windows.Forms.DataGridViewTextBoxColumn replacementStringCol; - private System.Windows.Forms.DataGridViewTextBoxColumn descriptionCol; - private System.Windows.Forms.Button defaultsBtn; - private System.Windows.Forms.Button loFiDefaultsBtn; - private System.Windows.Forms.Button saveBtn; - private System.Windows.Forms.Button cancelBtn; - } -} \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.cs b/Source/LibationWinForms/Dialogs/EditReplacementChars.cs deleted file mode 100644 index 0322ce64..00000000 --- a/Source/LibationWinForms/Dialogs/EditReplacementChars.cs +++ /dev/null @@ -1,177 +0,0 @@ -using FileManager; -using LibationFileManager; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows.Forms; - -namespace LibationWinForms.Dialogs -{ - public partial class EditReplacementChars : Form - { - Configuration config; - public EditReplacementChars() - { - InitializeComponent(); - dataGridView1_Resize(this, EventArgs.Empty); - } - public EditReplacementChars(Configuration config) : this() - { - this.config = config; - LoadTable(config.ReplacementCharacters.Replacements); - } - - private void LoadTable(List<Replacement> replacements) - { - dataGridView1.Rows.Clear(); - foreach (var r in replacements) - { - int row = dataGridView1.Rows.Add(r.CharacterToReplace, r.ReplacementString, r.Description); - dataGridView1.Rows[row].Tag = r; - - if (ReplacementCharacters.Default.Replacements.Any(rep => rep.CharacterToReplace == r.CharacterToReplace)) - { - r.Mandatory = true; - dataGridView1.Rows[row].Cells[charToReplaceCol.Index].ReadOnly = true; - dataGridView1.Rows[row].Cells[descriptionCol.Index].ReadOnly = true; - } - } - } - - private void dataGridView1_UserDeletingRow(object sender, DataGridViewRowCancelEventArgs e) - { - if (e.Row?.Tag is Replacement r && r.Mandatory) - e.Cancel = true; - } - - private void loFiDefaultsBtn_Click(object sender, EventArgs e) - { - LoadTable(ReplacementCharacters.LoFiDefault.Replacements); - } - - private void defaultsBtn_Click(object sender, EventArgs e) - { - LoadTable(ReplacementCharacters.Default.Replacements); - } - - private void dataGridView1_CellValidating(object sender, DataGridViewCellValidatingEventArgs e) - { - if (e.RowIndex < 0) return; - - var cellValue = e.FormattedValue?.ToString(); - - if (dataGridView1.Rows[e.RowIndex].Tag is Replacement row && row.Mandatory) - { - if (e.ColumnIndex == replacementStringCol.Index) - { - //Ensure replacement string doesn't contain an illegal character. - var replaceString = cellValue ?? string.Empty; - if (replaceString != string.Empty && replaceString.Any(c => FileUtility.invalidChars.Contains(c))) - { - dataGridView1.Rows[e.RowIndex].ErrorText = $"{replaceString} contains an illegal path character"; - e.Cancel = true; - } - } - return; - } - - - - if (e.ColumnIndex == charToReplaceCol.Index) - { - if (cellValue.Length != 1) - { - dataGridView1.Rows[e.RowIndex].ErrorText = "Only 1 character to replace per entry"; - e.Cancel = true; - } - else if ( - dataGridView1.Rows - .Cast<DataGridViewRow>() - .Where(r => r.Index != e.RowIndex) - .OfType<Replacement>() - .Any(r => r.CharacterToReplace == cellValue[0]) - ) - { - dataGridView1.Rows[e.RowIndex].ErrorText = $"The {cellValue[0]} character is already being replaced"; - e.Cancel = true; - } - } - else if (e.ColumnIndex == descriptionCol.Index || e.ColumnIndex == replacementStringCol.Index) - { - var value = dataGridView1.Rows[e.RowIndex].Cells[charToReplaceCol.Index].Value; - if (value is null || value is string str && string.IsNullOrEmpty(str)) - { - dataGridView1.Rows[e.RowIndex].ErrorText = $"You must choose a character to replace"; - e.Cancel = true; - } - } - } - - private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e) - { - if (e.RowIndex < 0) return; - - dataGridView1.Rows[e.RowIndex].ErrorText = string.Empty; - - var cellValue = dataGridView1.Rows[e.RowIndex].Cells[charToReplaceCol.Index].Value?.ToString(); - - if (string.IsNullOrEmpty(cellValue) || cellValue.Length > 1) - { - var row = dataGridView1.Rows[e.RowIndex]; - if (!row.IsNewRow) - { - BeginInvoke(new MethodInvoker(delegate - { - dataGridView1.Rows.Remove(row); - })); - } - } - else - { - char charToReplace = cellValue[0]; - string description = dataGridView1.Rows[e.RowIndex].Cells[descriptionCol.Index].Value?.ToString() ?? string.Empty; - string replacement = dataGridView1.Rows[e.RowIndex].Cells[replacementStringCol.Index].Value?.ToString() ?? string.Empty; - - var mandatory = false; - if (dataGridView1.Rows[e.RowIndex].Tag is Replacement existing) - { - mandatory = existing.Mandatory; - } - - dataGridView1.Rows[e.RowIndex].Tag = - new Replacement() - { - CharacterToReplace = charToReplace, - ReplacementString = replacement, - Description = description, - Mandatory = mandatory - }; - } - } - - private void saveBtn_Click(object sender, EventArgs e) - { - var replacements = dataGridView1.Rows - .Cast<DataGridViewRow>() - .Select(r => r.Tag) - .OfType<Replacement>() - .Where(r => r.ReplacementString != null && (r.ReplacementString == string.Empty || !r.ReplacementString.Any(c => FileUtility.invalidChars.Contains(c)))) - .ToList(); - - config.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements }; - DialogResult = DialogResult.OK; - Close(); - } - - private void cancelBtn_Click(object sender, EventArgs e) - { - DialogResult = DialogResult.Cancel; - Close(); - } - - private void dataGridView1_Resize(object sender, EventArgs e) - { - dataGridView1.Columns[^1].Width = dataGridView1.Width - dataGridView1.Columns.Cast<DataGridViewColumn>().Sum(c => c == dataGridView1.Columns[^1] ? 0 : c.Width) - dataGridView1.RowHeadersWidth - 2; - } - } -} diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.resx b/Source/LibationWinForms/Dialogs/EditReplacementChars.resx deleted file mode 100644 index fa41cc36..00000000 --- a/Source/LibationWinForms/Dialogs/EditReplacementChars.resx +++ /dev/null @@ -1,69 +0,0 @@ -<root> - <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> - <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> - <xsd:element name="root" msdata:IsDataSet="true"> - <xsd:complexType> - <xsd:choice maxOccurs="unbounded"> - <xsd:element name="metadata"> - <xsd:complexType> - <xsd:sequence> - <xsd:element name="value" type="xsd:string" minOccurs="0" /> - </xsd:sequence> - <xsd:attribute name="name" use="required" type="xsd:string" /> - <xsd:attribute name="type" type="xsd:string" /> - <xsd:attribute name="mimetype" type="xsd:string" /> - <xsd:attribute ref="xml:space" /> - </xsd:complexType> - </xsd:element> - <xsd:element name="assembly"> - <xsd:complexType> - <xsd:attribute name="alias" type="xsd:string" /> - <xsd:attribute name="name" type="xsd:string" /> - </xsd:complexType> - </xsd:element> - <xsd:element name="data"> - <xsd:complexType> - <xsd:sequence> - <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> - <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> - </xsd:sequence> - <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> - <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> - <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> - <xsd:attribute ref="xml:space" /> - </xsd:complexType> - </xsd:element> - <xsd:element name="resheader"> - <xsd:complexType> - <xsd:sequence> - <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> - </xsd:sequence> - <xsd:attribute name="name" type="xsd:string" use="required" /> - </xsd:complexType> - </xsd:element> - </xsd:choice> - </xsd:complexType> - </xsd:element> - </xsd:schema> - <resheader name="resmimetype"> - <value>text/microsoft-resx</value> - </resheader> - <resheader name="version"> - <value>2.0</value> - </resheader> - <resheader name="reader"> - <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> - </resheader> - <resheader name="writer"> - <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> - </resheader> - <metadata name="charToReplaceCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> - <value>True</value> - </metadata> - <metadata name="replacementStringCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> - <value>True</value> - </metadata> - <metadata name="descriptionCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> - <value>True</value> - </metadata> -</root> \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs index d117e4fe..f64f3c1a 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs @@ -60,7 +60,6 @@ this.tab3DownloadDecrypt = new System.Windows.Forms.TabPage(); this.inProgressFilesGb = new System.Windows.Forms.GroupBox(); this.customFileNamingGb = new System.Windows.Forms.GroupBox(); - this.editCharreplacementBtn = new System.Windows.Forms.Button(); this.chapterFileTemplateBtn = new System.Windows.Forms.Button(); this.chapterFileTemplateTb = new System.Windows.Forms.TextBox(); this.chapterFileTemplateLbl = new System.Windows.Forms.Label(); @@ -149,7 +148,7 @@ // saveBtn // this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.saveBtn.Location = new System.Drawing.Point(667, 461); + this.saveBtn.Location = new System.Drawing.Point(667, 441); this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.saveBtn.Name = "saveBtn"; this.saveBtn.Size = new System.Drawing.Size(88, 27); @@ -162,7 +161,7 @@ // this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.cancelBtn.Location = new System.Drawing.Point(785, 461); + this.cancelBtn.Location = new System.Drawing.Point(785, 441); this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.cancelBtn.Name = "cancelBtn"; this.cancelBtn.Size = new System.Drawing.Size(88, 27); @@ -309,7 +308,7 @@ // inProgressSelectControl // this.inProgressSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.inProgressSelectControl.Location = new System.Drawing.Point(7, 68); this.inProgressSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.inProgressSelectControl.Name = "inProgressSelectControl"; @@ -329,7 +328,7 @@ // booksSelectControl // this.booksSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.booksSelectControl.Location = new System.Drawing.Point(7, 37); this.booksSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.booksSelectControl.Name = "booksSelectControl"; @@ -357,8 +356,8 @@ // tabControl // this.tabControl.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); this.tabControl.Controls.Add(this.tab1ImportantSettings); this.tabControl.Controls.Add(this.tab2ImportLibrary); this.tabControl.Controls.Add(this.tab3DownloadDecrypt); @@ -366,7 +365,7 @@ this.tabControl.Location = new System.Drawing.Point(12, 12); this.tabControl.Name = "tabControl"; this.tabControl.SelectedIndex = 0; - this.tabControl.Size = new System.Drawing.Size(862, 443); + this.tabControl.Size = new System.Drawing.Size(862, 423); this.tabControl.TabIndex = 100; // // tab1ImportantSettings @@ -378,7 +377,7 @@ this.tab1ImportantSettings.Location = new System.Drawing.Point(4, 24); this.tab1ImportantSettings.Name = "tab1ImportantSettings"; this.tab1ImportantSettings.Padding = new System.Windows.Forms.Padding(3); - this.tab1ImportantSettings.Size = new System.Drawing.Size(854, 415); + this.tab1ImportantSettings.Size = new System.Drawing.Size(854, 395); this.tab1ImportantSettings.TabIndex = 0; this.tab1ImportantSettings.Text = "Important settings"; this.tab1ImportantSettings.UseVisualStyleBackColor = true; @@ -386,7 +385,7 @@ // booksGb // this.booksGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.booksGb.Controls.Add(this.saveEpisodesToSeriesFolderCbox); this.booksGb.Controls.Add(this.booksSelectControl); this.booksGb.Controls.Add(this.booksLocationDescLbl); @@ -417,7 +416,7 @@ this.tab2ImportLibrary.Location = new System.Drawing.Point(4, 24); this.tab2ImportLibrary.Name = "tab2ImportLibrary"; this.tab2ImportLibrary.Padding = new System.Windows.Forms.Padding(3); - this.tab2ImportLibrary.Size = new System.Drawing.Size(854, 415); + this.tab2ImportLibrary.Size = new System.Drawing.Size(854, 395); this.tab2ImportLibrary.TabIndex = 1; this.tab2ImportLibrary.Text = "Import library"; this.tab2ImportLibrary.UseVisualStyleBackColor = true; @@ -460,7 +459,7 @@ this.tab3DownloadDecrypt.Location = new System.Drawing.Point(4, 24); this.tab3DownloadDecrypt.Name = "tab3DownloadDecrypt"; this.tab3DownloadDecrypt.Padding = new System.Windows.Forms.Padding(3); - this.tab3DownloadDecrypt.Size = new System.Drawing.Size(854, 415); + this.tab3DownloadDecrypt.Size = new System.Drawing.Size(854, 395); this.tab3DownloadDecrypt.TabIndex = 2; this.tab3DownloadDecrypt.Text = "Download/Decrypt"; this.tab3DownloadDecrypt.UseVisualStyleBackColor = true; @@ -468,10 +467,10 @@ // inProgressFilesGb // this.inProgressFilesGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.inProgressFilesGb.Controls.Add(this.inProgressDescLbl); this.inProgressFilesGb.Controls.Add(this.inProgressSelectControl); - this.inProgressFilesGb.Location = new System.Drawing.Point(6, 281); + this.inProgressFilesGb.Location = new System.Drawing.Point(7, 251); this.inProgressFilesGb.Name = "inProgressFilesGb"; this.inProgressFilesGb.Size = new System.Drawing.Size(841, 128); this.inProgressFilesGb.TabIndex = 21; @@ -481,8 +480,7 @@ // customFileNamingGb // this.customFileNamingGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.customFileNamingGb.Controls.Add(this.editCharreplacementBtn); + | System.Windows.Forms.AnchorStyles.Right))); this.customFileNamingGb.Controls.Add(this.chapterFileTemplateBtn); this.customFileNamingGb.Controls.Add(this.chapterFileTemplateTb); this.customFileNamingGb.Controls.Add(this.chapterFileTemplateLbl); @@ -494,22 +492,11 @@ this.customFileNamingGb.Controls.Add(this.folderTemplateLbl); this.customFileNamingGb.Location = new System.Drawing.Point(7, 88); this.customFileNamingGb.Name = "customFileNamingGb"; - this.customFileNamingGb.Size = new System.Drawing.Size(841, 187); + this.customFileNamingGb.Size = new System.Drawing.Size(841, 157); this.customFileNamingGb.TabIndex = 20; this.customFileNamingGb.TabStop = false; this.customFileNamingGb.Text = "Custom file naming"; // - // editCharreplacementBtn - // - this.editCharreplacementBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.editCharreplacementBtn.Location = new System.Drawing.Point(5, 158); - this.editCharreplacementBtn.Name = "editCharreplacementBtn"; - this.editCharreplacementBtn.Size = new System.Drawing.Size(387, 23); - this.editCharreplacementBtn.TabIndex = 8; - this.editCharreplacementBtn.Text = "[edit char replacement desc]"; - this.editCharreplacementBtn.UseVisualStyleBackColor = true; - this.editCharreplacementBtn.Click += new System.EventHandler(this.editCharreplacementBtn_Click); - // // chapterFileTemplateBtn // this.chapterFileTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); @@ -524,7 +511,7 @@ // chapterFileTemplateTb // this.chapterFileTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.chapterFileTemplateTb.Location = new System.Drawing.Point(6, 125); this.chapterFileTemplateTb.Name = "chapterFileTemplateTb"; this.chapterFileTemplateTb.ReadOnly = true; @@ -554,7 +541,7 @@ // fileTemplateTb // this.fileTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.fileTemplateTb.Location = new System.Drawing.Point(6, 81); this.fileTemplateTb.Name = "fileTemplateTb"; this.fileTemplateTb.ReadOnly = true; @@ -584,7 +571,7 @@ // folderTemplateTb // this.folderTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.folderTemplateTb.Location = new System.Drawing.Point(5, 37); this.folderTemplateTb.Name = "folderTemplateTb"; this.folderTemplateTb.ReadOnly = true; @@ -616,7 +603,7 @@ this.tab4AudioFileOptions.Location = new System.Drawing.Point(4, 24); this.tab4AudioFileOptions.Name = "tab4AudioFileOptions"; this.tab4AudioFileOptions.Padding = new System.Windows.Forms.Padding(3); - this.tab4AudioFileOptions.Size = new System.Drawing.Size(854, 415); + this.tab4AudioFileOptions.Size = new System.Drawing.Size(854, 395); this.tab4AudioFileOptions.TabIndex = 3; this.tab4AudioFileOptions.Text = "Audio File Options"; this.tab4AudioFileOptions.UseVisualStyleBackColor = true; @@ -646,7 +633,7 @@ // chapterTitleTemplateTb // this.chapterTitleTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.chapterTitleTemplateTb.Location = new System.Drawing.Point(6, 22); this.chapterTitleTemplateTb.Name = "chapterTitleTemplateTb"; this.chapterTitleTemplateTb.ReadOnly = true; @@ -1030,7 +1017,7 @@ this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.cancelBtn; - this.ClientSize = new System.Drawing.Size(886, 504); + this.ClientSize = new System.Drawing.Size(886, 484); this.Controls.Add(this.tabControl); this.Controls.Add(this.cancelBtn); this.Controls.Add(this.saveBtn); @@ -1154,6 +1141,5 @@ private System.Windows.Forms.GroupBox chapterTitleTemplateGb; private System.Windows.Forms.Button chapterTitleTemplateBtn; private System.Windows.Forms.TextBox chapterTitleTemplateTb; - private System.Windows.Forms.Button editCharreplacementBtn; } } \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs index 8a5266e9..adba6f9d 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs @@ -11,18 +11,9 @@ namespace LibationWinForms.Dialogs private void fileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.File, fileTemplateTb); private void chapterFileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.ChapterFile, chapterFileTemplateTb); - - private void editCharreplacementBtn_Click(object sender, EventArgs e) - { - var form = new EditReplacementChars(config); - form.ShowDialog(); - } - private void Load_DownloadDecrypt(Configuration config) { inProgressDescLbl.Text = desc(nameof(config.InProgress)); - editCharreplacementBtn.Text = desc(nameof(config.ReplacementCharacters)); - badBookGb.Text = desc(nameof(config.BadBook)); badBookAskRb.Text = Configuration.BadBookAction.Ask.GetDescription(); badBookAbortRb.Text = Configuration.BadBookAction.Abort.GetDescription(); From 99dddb1af47fd258a4fdd3aedaa159df4c70144d Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 11:34:50 -0600 Subject: [PATCH 12/20] Revert "* bug fix: occasional hang bug in process queue" This reverts commit b7fd87b09c02eb556660d20aa513119cf63d0783. --- Source/AppScaffolding/AppScaffolding.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/AppScaffolding/AppScaffolding.csproj b/Source/AppScaffolding/AppScaffolding.csproj index b7882bd1..b0604e58 100644 --- a/Source/AppScaffolding/AppScaffolding.csproj +++ b/Source/AppScaffolding/AppScaffolding.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <TargetFramework>net6.0-windows</TargetFramework> - <Version>8.1.1.1</Version> + <Version>8.1.0.1</Version> </PropertyGroup> <ItemGroup> From 2ab466c570ae87168623c6fd0eb19e6d968adc72 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 13:01:24 -0600 Subject: [PATCH 13/20] Custom illegal character replacement --- .../AaxcDownloadMultiConverter.cs | 2 +- Source/AaxDecrypter/AudiobookDownloadBase.cs | 2 +- Source/AaxDecrypter/IDownloadOptions.cs | 1 + .../MultiConvertFileProperties.cs | 10 - .../UnencryptedAudiobookDownloader.cs | 4 +- Source/AppScaffolding/LibationScaffolding.cs | 3 + Source/FileLiberator/ConvertToMp3.cs | 2 +- Source/FileLiberator/DownloadDecryptBook.cs | 2 +- Source/FileLiberator/DownloadOptions.cs | 2 + Source/FileManager/BackgroundFileSystem.cs | 9 +- Source/FileManager/FileNamingTemplate.cs | 17 +- Source/FileManager/FileUtility.cs | 83 +----- Source/FileManager/ReplacementCharacters.cs | 269 ++++++++++++++++++ Source/LibationFileManager/Configuration.cs | 7 + Source/LibationFileManager/Templates.cs | 13 +- .../Dialogs/EditReplacementChars.Designer.cs | 171 +++++++++++ .../Dialogs/EditReplacementChars.cs | 144 ++++++++++ .../Dialogs/EditReplacementChars.resx | 69 +++++ .../Dialogs/SettingsDialog.Designer.cs | 56 ++-- .../Dialogs/SettingsDialog.DownloadDecrypt.cs | 10 + Source/LibationWinForms/Form1.ScanAuto.cs | 13 +- .../FileNamingTemplateTests.cs | 62 +--- .../FileManager.Tests/FileUtilityTests.cs | 89 ++++-- .../TemplatesTests.cs | 16 +- 24 files changed, 838 insertions(+), 218 deletions(-) create mode 100644 Source/FileManager/ReplacementCharacters.cs create mode 100644 Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs create mode 100644 Source/LibationWinForms/Dialogs/EditReplacementChars.cs create mode 100644 Source/LibationWinForms/Dialogs/EditReplacementChars.resx diff --git a/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs b/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs index 25cac87b..ea11c319 100644 --- a/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs +++ b/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs @@ -141,7 +141,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea private FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties) { var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties); - fileName = FileUtility.GetValidFilename(fileName); + fileName = FileUtility.GetValidFilename(fileName, DownloadOptions.ReplacementCharacters); multiPartFilePaths.Add(fileName); diff --git a/Source/AaxDecrypter/AudiobookDownloadBase.cs b/Source/AaxDecrypter/AudiobookDownloadBase.cs index 27ce3025..08d4f3d1 100644 --- a/Source/AaxDecrypter/AudiobookDownloadBase.cs +++ b/Source/AaxDecrypter/AudiobookDownloadBase.cs @@ -103,7 +103,7 @@ namespace AaxDecrypter try { var path = Path.ChangeExtension(OutputFileName, ".cue"); - path = FileUtility.GetValidFilename(path); + path = FileUtility.GetValidFilename(path, DownloadOptions.ReplacementCharacters); File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo)); OnFileCreated(path); } diff --git a/Source/AaxDecrypter/IDownloadOptions.cs b/Source/AaxDecrypter/IDownloadOptions.cs index 64ae08e3..58d6e90f 100644 --- a/Source/AaxDecrypter/IDownloadOptions.cs +++ b/Source/AaxDecrypter/IDownloadOptions.cs @@ -4,6 +4,7 @@ namespace AaxDecrypter { public interface IDownloadOptions { + FileManager.ReplacementCharacters ReplacementCharacters { get; } string DownloadUrl { get; } string UserAgent { get; } string AudibleKey { get; } diff --git a/Source/AaxDecrypter/MultiConvertFileProperties.cs b/Source/AaxDecrypter/MultiConvertFileProperties.cs index febc2a49..71bafe21 100644 --- a/Source/AaxDecrypter/MultiConvertFileProperties.cs +++ b/Source/AaxDecrypter/MultiConvertFileProperties.cs @@ -11,15 +11,5 @@ namespace AaxDecrypter public int PartsTotal { get; set; } public string Title { get; set; } - public static string DefaultMultipartFilename(MultiConvertFileProperties multiConvertFileProperties) - { - var template = Path.ChangeExtension(multiConvertFileProperties.OutputFileName, null) + " - <ch# 0> - <title>" + Path.GetExtension(multiConvertFileProperties.OutputFileName); - - var fileNamingTemplate = new FileNamingTemplate(template) { IllegalCharacterReplacements = " " }; - fileNamingTemplate.AddParameterReplacement("ch# 0", FileUtility.GetSequenceFormatted(multiConvertFileProperties.PartsPosition, multiConvertFileProperties.PartsTotal)); - fileNamingTemplate.AddParameterReplacement("title", multiConvertFileProperties.Title ?? ""); - - return fileNamingTemplate.GetFilePath(); - } } } diff --git a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs index cd6dee88..142ccf44 100644 --- a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs +++ b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs @@ -67,8 +67,8 @@ namespace AaxDecrypter } CloseInputFileStream(); - - var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName); + + var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName, DownloadOptions.ReplacementCharacters); SetOutputFileName(realOutputFileName); OnFileCreated(realOutputFileName); diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 9d368246..9af8b75f 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -117,6 +117,9 @@ namespace AppScaffolding if (!config.Exists(nameof(config.DownloadEpisodes))) config.DownloadEpisodes = true; + if (!config.Exists(nameof(config.ReplacementCharacters))) + config.ReplacementCharacters = FileManager.ReplacementCharacters.Default; + if (!config.Exists(nameof(config.FolderTemplate))) config.FolderTemplate = Templates.Folder.DefaultTemplate; diff --git a/Source/FileLiberator/ConvertToMp3.cs b/Source/FileLiberator/ConvertToMp3.cs index 14a75b9b..bb191986 100644 --- a/Source/FileLiberator/ConvertToMp3.cs +++ b/Source/FileLiberator/ConvertToMp3.cs @@ -70,7 +70,7 @@ namespace FileLiberator return new StatusHandler { "Cancelled" }; } - var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path); + var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path, Configuration.Instance.ReplacementCharacters); OnFileCreated(libraryBook, realMp3Path); } return new StatusHandler(); diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index 45eda535..4bea8538 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -258,7 +258,7 @@ namespace FileLiberator { var entry = entries[i]; - var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path))); + var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)), Configuration.Instance.ReplacementCharacters); FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest); // propagate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop) diff --git a/Source/FileLiberator/DownloadOptions.cs b/Source/FileLiberator/DownloadOptions.cs index 86b0a778..dc2fd940 100644 --- a/Source/FileLiberator/DownloadOptions.cs +++ b/Source/FileLiberator/DownloadOptions.cs @@ -3,6 +3,7 @@ using AAXClean; using Dinah.Core; using DataLayer; using LibationFileManager; +using FileManager; namespace FileLiberator { @@ -23,6 +24,7 @@ namespace FileLiberator public NAudio.Lame.LameConfig LameConfig { get; set; } public bool Downsample { get; set; } public bool MatchSourceBitrate { get; set; } + public ReplacementCharacters ReplacementCharacters => Configuration.Instance.ReplacementCharacters; public string GetMultipartFileName(MultiConvertFileProperties props) => Templates.ChapterFile.GetFilename(LibraryBookDto, props); diff --git a/Source/FileManager/BackgroundFileSystem.cs b/Source/FileManager/BackgroundFileSystem.cs index 4f68003d..f3f9a747 100644 --- a/Source/FileManager/BackgroundFileSystem.cs +++ b/Source/FileManager/BackgroundFileSystem.cs @@ -79,8 +79,13 @@ namespace FileManager //Stop raising events fileSystemWatcher?.Dispose(); - //Calling CompleteAdding() will cause background scanner to terminate. - directoryChangesEvents?.CompleteAdding(); + try + { + //Calling CompleteAdding() will cause background scanner to terminate. + directoryChangesEvents?.CompleteAdding(); + } + // if directoryChangesEvents is non-null and isDisposed, this exception is thrown. there's no other way to check >:( + catch (ObjectDisposedException) { } //Wait for background scanner to terminate before reinitializing. backgroundScanner?.Wait(); diff --git a/Source/FileManager/FileNamingTemplate.cs b/Source/FileManager/FileNamingTemplate.cs index 1f472b6d..e1a782ea 100644 --- a/Source/FileManager/FileNamingTemplate.cs +++ b/Source/FileManager/FileNamingTemplate.cs @@ -12,17 +12,13 @@ namespace FileManager /// <param name="template">Proposed file name with optional html-styled template tags.</param> public FileNamingTemplate(string template) : base(template) { } - /// <summary>Optional step 2: Replace all illegal characters with this. Default=<see cref="string.Empty"/></summary> - public string IllegalCharacterReplacements { get; set; } - /// <summary>Generate a valid path for this file or directory</summary> - public LongPath GetFilePath(bool returnFirstExisting = false) + public LongPath GetFilePath(ReplacementCharacters replacements, bool returnFirstExisting = false) { - string fileName = Template.EndsWith(Path.DirectorySeparatorChar) ? Template[..^1] : Template; List<string> pathParts = new(); - var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value)); + var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value, replacements)); while (!string.IsNullOrEmpty(fileName)) { @@ -43,7 +39,7 @@ namespace FileManager pathParts.Reverse(); - return FileUtility.GetValidFilename(Path.Join(pathParts.ToArray()), IllegalCharacterReplacements, returnFirstExisting); + return FileUtility.GetValidFilename(Path.Join(pathParts.ToArray()), replacements, returnFirstExisting); } private string replaceFileName(string filename, Dictionary<string,string> paramReplacements) @@ -92,17 +88,14 @@ namespace FileManager return string.Join("", filenameParts); } - private string formatValue(object value) + private string formatValue(object value, ReplacementCharacters replacements) { if (value is null) return ""; // Other illegal characters will be taken care of later. Must take care of slashes now so params can't introduce new folders. // Esp important for file templates. - return value - .ToString() - .Replace($"{System.IO.Path.DirectorySeparatorChar}", IllegalCharacterReplacements) - .Replace($"{System.IO.Path.AltDirectorySeparatorChar}", IllegalCharacterReplacements); + return replacements.ReplaceInvalidFilenameChars(value.ToString()); } } } diff --git a/Source/FileManager/FileUtility.cs b/Source/FileManager/FileUtility.cs index e4577e65..6cc42380 100644 --- a/Source/FileManager/FileUtility.cs +++ b/Source/FileManager/FileUtility.cs @@ -46,12 +46,12 @@ namespace FileManager /// <br/>- ensure uniqueness /// <br/>- enforce max file length /// </summary> - public static LongPath GetValidFilename(LongPath path, string illegalCharacterReplacements = "", bool returnFirstExisting = false) + public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, bool returnFirstExisting = false) { ArgumentValidator.EnsureNotNull(path, nameof(path)); // remove invalid chars - path = GetSafePath(path, illegalCharacterReplacements); + path = GetSafePath(path, replacements); // ensure uniqueness and check lengths var dir = Path.GetDirectoryName(path); @@ -77,36 +77,19 @@ namespace FileManager return fullfilename; } - // GetInvalidFileNameChars contains everything in GetInvalidPathChars plus ':', '*', '?', '\\', '/' - - /// <summary>Use with file name, not full path. Valid path characters which are invalid file name characters will be replaced: ':', '\\', '/'</summary> - public static string GetSafeFileName(string str, string illegalCharacterReplacements = "") - => string.Join(illegalCharacterReplacements ?? "", str.Split(Path.GetInvalidFileNameChars())); - /// <summary>Use with full path, not file name. Valid path characters which are invalid file name characters will be retained: '\\', '/'</summary> - public static LongPath GetSafePath(LongPath path, string illegalCharacterReplacements = "") + public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements) { ArgumentValidator.EnsureNotNull(path, nameof(path)); var pathNoPrefix = path.PathWithoutPrefix; - pathNoPrefix = replaceColons(pathNoPrefix, "꞉"); - pathNoPrefix = replaceIllegalWithUnicodeAnalog(pathNoPrefix); - pathNoPrefix = replaceInvalidChars(pathNoPrefix, illegalCharacterReplacements); + pathNoPrefix = replacements.ReplaceInvalidChars(pathNoPrefix); pathNoPrefix = removeDoubleSlashes(pathNoPrefix); return pathNoPrefix; } - private static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] { - '*', '?', - // these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included. - // In live code, Path.GetInvalidPathChars() does not include them - '"', '<', '>' - }).ToArray(); - private static string replaceInvalidChars(string path, string illegalCharacterReplacements) - => string.Join(illegalCharacterReplacements ?? "", path.Split(invalidChars)); - private static string removeDoubleSlashes(string path) { if (path.Length < 2) @@ -122,60 +105,6 @@ namespace FileManager return path[0] + remainder; } - private static string replaceIllegalWithUnicodeAnalog(string path) - { - char[] replaced = path.ToCharArray(); - - char GetQuote(int position) - { - if ( - position == 0 - || (position > 0 - && position < replaced.Length - && !char.IsLetter(replaced[position - 1]) - && !char.IsNumber(replaced[position - 1]) - ) - ) return '“'; - else if ( - position == replaced.Length - 1 - || (position >= 0 - && position < replaced.Length - 1 - && !char.IsLetter(replaced[position + 1]) - && !char.IsNumber(replaced[position + 1]) - ) - ) return '”'; - else return '"'; - } - - for (int i = 0; i < replaced.Length; i++) - { - replaced[i] = replaced[i] switch - { - '?' => '?', - '*' => '✱', - '<' => '<', - '>' => '>', - '"' => GetQuote(i), - _ => replaced[i] - }; - } - return new string(replaced); - } - - private static string replaceColons(string path, string illegalCharacterReplacements) - { - // replace all colons except within the first 2 chars - var builder = new System.Text.StringBuilder(); - for (var i = 0; i < path.Length; i++) - { - var c = path[i]; - if (i >= 2 && c == ':') - builder.Append(illegalCharacterReplacements); - else - builder.Append(c); - } - return builder.ToString(); - } private static string removeInvalidWhitespace_pattern { get; } = $@"[\s\.]*\{Path.DirectorySeparatorChar}\s*"; private static Regex removeInvalidWhitespace_regex { get; } = new(removeInvalidWhitespace_pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace); @@ -206,9 +135,9 @@ namespace FileManager /// <br/>- Perform <see cref="SaferMove"/> /// <br/>- Return valid path /// </summary> - public static string SaferMoveToValidPath(LongPath source, LongPath destination) + public static string SaferMoveToValidPath(LongPath source, LongPath destination, ReplacementCharacters replacements) { - destination = GetValidFilename(destination); + destination = GetValidFilename(destination, replacements); SaferMove(source, destination); return destination; } diff --git a/Source/FileManager/ReplacementCharacters.cs b/Source/FileManager/ReplacementCharacters.cs new file mode 100644 index 00000000..ffdd493b --- /dev/null +++ b/Source/FileManager/ReplacementCharacters.cs @@ -0,0 +1,269 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace FileManager +{ + public class Replacement + { + public const int FIXED_COUNT = 6; + + internal const char QUOTE_MARK = '"'; + [JsonIgnore] public bool Mandatory { get; internal set; } + [JsonProperty] public char CharacterToReplace { get; private set; } + [JsonProperty] public string ReplacementString { get; private set; } + [JsonProperty] public string Description { get; private set; } + public override string ToString() => $"{CharacterToReplace} → {ReplacementString} ({Description})"; + + public Replacement(char charToReplace, string replacementString, string description) + { + CharacterToReplace = charToReplace; + ReplacementString = replacementString; + Description = description; + } + private Replacement(char charToReplace, string replacementString, string description, bool mandatory) + : this(charToReplace, replacementString, description) + { + Mandatory = mandatory; + } + + public void Update(char charToReplace, string replacementString, string description) + { + ReplacementString = replacementString; + + if (!Mandatory) + { + CharacterToReplace = charToReplace; + Description = description; + } + } + + public static Replacement OtherInvalid(string replacement) => new(default, replacement, "All other invalid characters", true); + public static Replacement FilenameForwardSlash(string replacement) => new('/', replacement, "Forward Slash (Filename Only)", true); + public static Replacement FilenameBackSlash(string replacement) => new('\\', replacement, "Back Slash (Filename Only)", true); + public static Replacement OpenQuote(string replacement) => new('"', replacement, "Open Quote", true); + public static Replacement CloseQuote(string replacement) => new('"', replacement, "Close Quote", true); + public static Replacement OtherQuote(string replacement) => new('"', replacement, "Other Quote", true); + public static Replacement Colon(string replacement) => new(':', replacement, "Colon"); + public static Replacement Asterisk(string replacement) => new('*', replacement, "Asterisk"); + public static Replacement QuestionMark(string replacement) => new('?', replacement, "Question Mark"); + public static Replacement OpenAngleBracket(string replacement) => new('<', replacement, "Open Angle Bracket"); + public static Replacement CloseAngleBracket(string replacement) => new('>', replacement, "Close Angle Bracket"); + public static Replacement Pipe(string replacement) => new('|', replacement, "Vertical Line"); + } + + [JsonConverter(typeof(ReplacementCharactersConverter))] + public class ReplacementCharacters + { + public static readonly ReplacementCharacters Default = new() + { + Replacements = new List<Replacement>() + { + Replacement.OtherInvalid("_"), + Replacement.FilenameForwardSlash("∕"), + Replacement.FilenameBackSlash(""), + Replacement.OpenQuote("“"), + Replacement.CloseQuote("”"), + Replacement.OtherQuote("""), + Replacement.OpenAngleBracket("<"), + Replacement.CloseAngleBracket(">"), + Replacement.Colon("꞉"), + Replacement.Asterisk("✱"), + Replacement.QuestionMark("?"), + Replacement.Pipe("⏐"), + } + }; + + public static readonly ReplacementCharacters LoFiDefault = new() + { + Replacements = new List<Replacement>() + { + Replacement.OtherInvalid("_"), + Replacement.FilenameForwardSlash("_"), + Replacement.FilenameBackSlash("_"), + Replacement.OpenQuote("'"), + Replacement.CloseQuote("'"), + Replacement.OtherQuote("'"), + Replacement.OpenAngleBracket("{"), + Replacement.CloseAngleBracket("}"), + Replacement.Colon("-"), + } + }; + + public static readonly ReplacementCharacters Minimum = new() + { + Replacements = new List<Replacement>() + { + Replacement.OtherInvalid("_"), + Replacement.FilenameForwardSlash("_"), + Replacement.FilenameBackSlash("_"), + Replacement.OpenQuote("_"), + Replacement.CloseQuote("_"), + Replacement.OtherQuote("_"), + } + }; + + private static readonly char[] invalidChars = Path.GetInvalidPathChars().Union(new[] { + '*', '?', ':', + // these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included. + // In live code, Path.GetInvalidPathChars() does not include them + '"', '<', '>' + }).ToArray(); + + public IReadOnlyList<Replacement> Replacements { get; init; } + private string DefaultReplacement => Replacements[0].ReplacementString; + private Replacement ForwardSlash => Replacements[1]; + private Replacement BackSlash => Replacements[2]; + private string OpenQuote => Replacements[3].ReplacementString; + private string CloseQuote => Replacements[4].ReplacementString; + private string OtherQuote => Replacements[5].ReplacementString; + + private string GetFilenameCharReplacement(char toReplace, char preceding, char succeding) + { + if (toReplace == ForwardSlash.CharacterToReplace) + return ForwardSlash.ReplacementString; + else if (toReplace == BackSlash.CharacterToReplace) + return BackSlash.ReplacementString; + else return GetPathCharReplacement(toReplace, preceding, succeding); + } + private string GetPathCharReplacement(char toReplace, char preceding, char succeding) + { + if (toReplace == Replacement.QUOTE_MARK) + { + if ( + preceding != default + && !char.IsLetter(preceding) + && !char.IsNumber(preceding) + && (char.IsLetter(succeding) || char.IsNumber(succeding)) + ) + return OpenQuote; + else if ( + succeding != default + && !char.IsLetter(succeding) + && !char.IsNumber(succeding) + && (char.IsLetter(preceding) || char.IsNumber(preceding)) + ) + return CloseQuote; + else + return OtherQuote; + } + + for (int i = Replacement.FIXED_COUNT; i < Replacements.Count; i++) + { + var r = Replacements[i]; + if (r.CharacterToReplace == toReplace) + return r.ReplacementString; + } + return DefaultReplacement; + } + + + public static bool ContainsInvalid(string path) + => path.Any(c => invalidChars.Contains(c)); + + public string ReplaceInvalidFilenameChars(string fileName) + { + if (string.IsNullOrEmpty(fileName)) return string.Empty; + var builder = new System.Text.StringBuilder(); + for (var i = 0; i < fileName.Length; i++) + { + var c = fileName[i]; + + if (invalidChars.Contains(c) || c == ForwardSlash.CharacterToReplace || c == BackSlash.CharacterToReplace) + { + char preceding = i > 0 ? fileName[i - 1] : default; + char succeeding = i < fileName.Length - 1 ? fileName[i + 1] : default; + builder.Append(GetFilenameCharReplacement(c, preceding, succeeding)); + } + else + builder.Append(c); + + } + return builder.ToString(); + } + + public string ReplaceInvalidChars(string pathStr) + { + if (string.IsNullOrEmpty(pathStr)) return string.Empty; + + // replace all colons except within the first 2 chars + var builder = new System.Text.StringBuilder(); + for (var i = 0; i < pathStr.Length; i++) + { + var c = pathStr[i]; + + if (!invalidChars.Contains(c) || (i <= 2 && Path.IsPathRooted(pathStr))) + builder.Append(c); + else + { + char preceding = i > 0 ? pathStr[i - 1] : default; + char succeeding = i < pathStr.Length - 1 ? pathStr[i + 1] : default; + builder.Append(GetPathCharReplacement(c, preceding, succeeding)); + } + + } + return builder.ToString(); + } + } + + #region JSON Converter + internal class ReplacementCharactersConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + => objectType == typeof(ReplacementCharacters); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jObj = JObject.Load(reader); + var replaceArr = jObj[nameof(Replacement)]; + IReadOnlyList<Replacement> dict = replaceArr + .ToObject<Replacement[]>().ToList(); + + //Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid. + //If not, reset to default. + + var default0 = Replacement.OtherInvalid(""); + var default1 = Replacement.FilenameForwardSlash(""); + var default2 = Replacement.FilenameBackSlash(""); + var default3 = Replacement.OpenQuote(""); + var default4 = Replacement.CloseQuote(""); + var default5 = Replacement.OtherQuote(""); + + if (dict.Count < Replacement.FIXED_COUNT || + dict[0].CharacterToReplace != default0.CharacterToReplace || dict[0].Description != default0.Description || + dict[1].CharacterToReplace != default1.CharacterToReplace || dict[1].Description != default1.Description || + dict[2].CharacterToReplace != default2.CharacterToReplace || dict[2].Description != default2.Description || + dict[3].CharacterToReplace != default3.CharacterToReplace || dict[3].Description != default3.Description || + dict[4].CharacterToReplace != default4.CharacterToReplace || dict[4].Description != default4.Description || + dict[5].CharacterToReplace != default5.CharacterToReplace || dict[5].Description != default5.Description || + dict.Any(r => ReplacementCharacters.ContainsInvalid(r.ReplacementString)) + ) + { + dict = ReplacementCharacters.Default.Replacements; + } + //First FIXED_COUNT are mandatory + for (int i = 0; i < Replacement.FIXED_COUNT; i++) + dict[i].Mandatory = true; + + return new ReplacementCharacters { Replacements = dict }; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + ReplacementCharacters replacements = (ReplacementCharacters)value; + + var propertyNames = replacements.Replacements + .Select(c => JObject.FromObject(c)).ToList(); + + var prop = new JProperty(nameof(Replacement), new JArray(propertyNames)); + + var obj = new JObject(); + obj.AddFirst(prop); + obj.WriteTo(writer); + } + } + #endregion +} diff --git a/Source/LibationFileManager/Configuration.cs b/Source/LibationFileManager/Configuration.cs index 9ce601a6..d545df22 100644 --- a/Source/LibationFileManager/Configuration.cs +++ b/Source/LibationFileManager/Configuration.cs @@ -284,6 +284,13 @@ namespace LibationFileManager #region templates: custom file naming + [Description("Edit how illegal filename characters are replaced")] + public ReplacementCharacters ReplacementCharacters + { + get => persistentDictionary.GetNonString<ReplacementCharacters>(nameof(ReplacementCharacters)); + set => persistentDictionary.SetNonString(nameof(ReplacementCharacters), value); + } + [Description("How to format the folders in which files will be saved")] public string FolderTemplate { diff --git a/Source/LibationFileManager/Templates.cs b/Source/LibationFileManager/Templates.cs index 5ee917af..7a856c73 100644 --- a/Source/LibationFileManager/Templates.cs +++ b/Source/LibationFileManager/Templates.cs @@ -106,7 +106,7 @@ namespace LibationFileManager => string.IsNullOrWhiteSpace(template) ? "" : getFileNamingTemplate(libraryBookDto, template, null, null) - .GetFilePath().PathWithoutPrefix; + .GetFilePath(Configuration.Instance.ReplacementCharacters).PathWithoutPrefix; private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -126,7 +126,7 @@ namespace LibationFileManager var t = template + FileUtility.GetStandardizedExtension(extension); var fullfilename = dirFullPath == "" ? t : Path.Combine(dirFullPath, t); - var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" }; + var fileNamingTemplate = new FileNamingTemplate(fullfilename); var title = libraryBookDto.Title ?? ""; var titleShort = title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':')); @@ -210,7 +210,7 @@ namespace LibationFileManager /// <summary>USES LIVE CONFIGURATION VALUES</summary> public string GetFilename(LibraryBookDto libraryBookDto, string baseDir = null) => getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, baseDir ?? AudibleFileStorage.BooksDirectory, null) - .GetFilePath(); + .GetFilePath(Configuration.Instance.ReplacementCharacters); #endregion } @@ -233,7 +233,7 @@ namespace LibationFileManager /// <summary>USES LIVE CONFIGURATION VALUES</summary> public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension, bool returnFirstExisting = false) => getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension) - .GetFilePath(returnFirstExisting); + .GetFilePath(Configuration.Instance.ReplacementCharacters, returnFirstExisting); #endregion } @@ -268,8 +268,9 @@ namespace LibationFileManager public string GetFilename(LibraryBookDto libraryBookDto, AaxDecrypter.MultiConvertFileProperties props) => GetPortionFilename(libraryBookDto, Configuration.Instance.ChapterFileTemplate, props, AudibleFileStorage.DecryptInProgressDirectory); - public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props, string fullDirPath) + public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props, string fullDirPath, ReplacementCharacters replacements = null) { + replacements ??= Configuration.Instance.ReplacementCharacters; var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, Path.GetExtension(props.OutputFileName)); fileNamingTemplate.AddParameterReplacement(TemplateTags.ChCount, props.PartsTotal); @@ -277,7 +278,7 @@ namespace LibationFileManager fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal)); fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? ""); - return fileNamingTemplate.GetFilePath().PathWithoutPrefix; + return fileNamingTemplate.GetFilePath(replacements).PathWithoutPrefix; } #endregion } diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs b/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs new file mode 100644 index 00000000..07bced72 --- /dev/null +++ b/Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs @@ -0,0 +1,171 @@ +namespace LibationWinForms.Dialogs +{ + partial class EditReplacementChars + { + /// <summary> + /// Required designer variable. + /// </summary> + private System.ComponentModel.IContainer components = null; + + /// <summary> + /// Clean up any resources being used. + /// </summary> + /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// <summary> + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// </summary> + private void InitializeComponent() + { + this.dataGridView1 = new System.Windows.Forms.DataGridView(); + this.defaultsBtn = new System.Windows.Forms.Button(); + this.loFiDefaultsBtn = new System.Windows.Forms.Button(); + this.saveBtn = new System.Windows.Forms.Button(); + this.cancelBtn = new System.Windows.Forms.Button(); + this.minDefaultBtn = new System.Windows.Forms.Button(); + this.charToReplaceCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.replacementStringCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.descriptionCol = new System.Windows.Forms.DataGridViewTextBoxColumn(); + ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit(); + this.SuspendLayout(); + // + // dataGridView1 + // + this.dataGridView1.AllowUserToResizeColumns = false; + this.dataGridView1.AllowUserToResizeRows = false; + this.dataGridView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this.charToReplaceCol, + this.replacementStringCol, + this.descriptionCol}); + this.dataGridView1.Location = new System.Drawing.Point(12, 12); + this.dataGridView1.Name = "dataGridView1"; + this.dataGridView1.RowTemplate.Height = 25; + this.dataGridView1.Size = new System.Drawing.Size(498, 393); + this.dataGridView1.TabIndex = 0; + this.dataGridView1.CellEndEdit += new System.Windows.Forms.DataGridViewCellEventHandler(this.dataGridView1_CellEndEdit); + this.dataGridView1.UserDeletingRow += new System.Windows.Forms.DataGridViewRowCancelEventHandler(this.dataGridView1_UserDeletingRow); + this.dataGridView1.Resize += new System.EventHandler(this.dataGridView1_Resize); + // + // defaultsBtn + // + this.defaultsBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.defaultsBtn.Location = new System.Drawing.Point(12, 430); + this.defaultsBtn.Name = "defaultsBtn"; + this.defaultsBtn.Size = new System.Drawing.Size(64, 25); + this.defaultsBtn.TabIndex = 1; + this.defaultsBtn.Text = "Defaults"; + this.defaultsBtn.UseVisualStyleBackColor = true; + this.defaultsBtn.Click += new System.EventHandler(this.defaultsBtn_Click); + // + // loFiDefaultsBtn + // + this.loFiDefaultsBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.loFiDefaultsBtn.Location = new System.Drawing.Point(82, 430); + this.loFiDefaultsBtn.Name = "loFiDefaultsBtn"; + this.loFiDefaultsBtn.Size = new System.Drawing.Size(84, 25); + this.loFiDefaultsBtn.TabIndex = 1; + this.loFiDefaultsBtn.Text = "LoFi Defaults"; + this.loFiDefaultsBtn.UseVisualStyleBackColor = true; + this.loFiDefaultsBtn.Click += new System.EventHandler(this.loFiDefaultsBtn_Click); + // + // saveBtn + // + this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.saveBtn.Location = new System.Drawing.Point(428, 430); + this.saveBtn.Name = "saveBtn"; + this.saveBtn.Size = new System.Drawing.Size(82, 25); + this.saveBtn.TabIndex = 1; + this.saveBtn.Text = "Save"; + this.saveBtn.UseVisualStyleBackColor = true; + this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click); + // + // cancelBtn + // + this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.cancelBtn.Location = new System.Drawing.Point(340, 430); + this.cancelBtn.Name = "cancelBtn"; + this.cancelBtn.Size = new System.Drawing.Size(82, 25); + this.cancelBtn.TabIndex = 1; + this.cancelBtn.Text = "Cancel"; + this.cancelBtn.UseVisualStyleBackColor = true; + this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click); + // + // minDefaultBtn + // + this.minDefaultBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.minDefaultBtn.Location = new System.Drawing.Point(172, 430); + this.minDefaultBtn.Name = "minDefaultBtn"; + this.minDefaultBtn.Size = new System.Drawing.Size(80, 25); + this.minDefaultBtn.TabIndex = 1; + this.minDefaultBtn.Text = "Barebones"; + this.minDefaultBtn.UseVisualStyleBackColor = true; + this.minDefaultBtn.Click += new System.EventHandler(this.minDefaultBtn_Click); + // + // charToReplaceCol + // + this.charToReplaceCol.HeaderText = "Char to Replace"; + this.charToReplaceCol.MinimumWidth = 70; + this.charToReplaceCol.Name = "charToReplaceCol"; + this.charToReplaceCol.Resizable = System.Windows.Forms.DataGridViewTriState.False; + this.charToReplaceCol.Width = 70; + // + // replacementStringCol + // + this.replacementStringCol.HeaderText = "Replacement Text"; + this.replacementStringCol.MinimumWidth = 85; + this.replacementStringCol.Name = "replacementStringCol"; + this.replacementStringCol.Width = 85; + // + // descriptionCol + // + this.descriptionCol.HeaderText = "Description"; + this.descriptionCol.MinimumWidth = 100; + this.descriptionCol.Name = "descriptionCol"; + this.descriptionCol.Width = 200; + // + // EditReplacementChars + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(522, 467); + this.Controls.Add(this.minDefaultBtn); + this.Controls.Add(this.loFiDefaultsBtn); + this.Controls.Add(this.cancelBtn); + this.Controls.Add(this.saveBtn); + this.Controls.Add(this.defaultsBtn); + this.Controls.Add(this.dataGridView1); + this.Name = "EditReplacementChars"; + this.Text = "Character Replacements"; + ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.DataGridView dataGridView1; + private System.Windows.Forms.Button defaultsBtn; + private System.Windows.Forms.Button loFiDefaultsBtn; + private System.Windows.Forms.Button saveBtn; + private System.Windows.Forms.Button cancelBtn; + private System.Windows.Forms.Button minDefaultBtn; + private System.Windows.Forms.DataGridViewTextBoxColumn charToReplaceCol; + private System.Windows.Forms.DataGridViewTextBoxColumn replacementStringCol; + private System.Windows.Forms.DataGridViewTextBoxColumn descriptionCol; + } +} \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.cs b/Source/LibationWinForms/Dialogs/EditReplacementChars.cs new file mode 100644 index 00000000..11a0bf6c --- /dev/null +++ b/Source/LibationWinForms/Dialogs/EditReplacementChars.cs @@ -0,0 +1,144 @@ +using FileManager; +using LibationFileManager; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; + +namespace LibationWinForms.Dialogs +{ + public partial class EditReplacementChars : Form + { + Configuration config; + public EditReplacementChars() + { + InitializeComponent(); + dataGridView1_Resize(this, EventArgs.Empty); + } + + public EditReplacementChars(Configuration config) : this() + { + this.config = config; + LoadTable(config.ReplacementCharacters.Replacements); + } + + private void LoadTable(IReadOnlyList<Replacement> replacements) + { + dataGridView1.Rows.Clear(); + for (int i = 0; i < replacements.Count; i++) + { + var r = replacements[i]; + + int row = dataGridView1.Rows.Add(r.CharacterToReplace, r.ReplacementString, r.Description); + dataGridView1.Rows[row].Tag = r; + + + if (r.Mandatory) + { + dataGridView1.Rows[row].Cells[charToReplaceCol.Index].ReadOnly = true; + dataGridView1.Rows[row].Cells[descriptionCol.Index].ReadOnly = true; + dataGridView1.Rows[row].Cells[charToReplaceCol.Index].Style.BackColor = System.Drawing.Color.LightGray; + dataGridView1.Rows[row].Cells[descriptionCol.Index].Style.BackColor = System.Drawing.Color.LightGray; + } + } + } + + private void dataGridView1_UserDeletingRow(object sender, DataGridViewRowCancelEventArgs e) + { + if (e.Row?.Tag is Replacement r && r.Mandatory) + e.Cancel = true; + } + + private void loFiDefaultsBtn_Click(object sender, EventArgs e) + => LoadTable(ReplacementCharacters.LoFiDefault.Replacements); + + private void defaultsBtn_Click(object sender, EventArgs e) + => LoadTable(ReplacementCharacters.Default.Replacements); + + private void minDefaultBtn_Click(object sender, EventArgs e) + => LoadTable(ReplacementCharacters.Minimum.Replacements); + + + private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e) + { + if (e.RowIndex < 0) return; + + dataGridView1.Rows[e.RowIndex].ErrorText = string.Empty; + + var charToReplaceStr = dataGridView1.Rows[e.RowIndex].Cells[charToReplaceCol.Index].Value?.ToString(); + var replacement = dataGridView1.Rows[e.RowIndex].Cells[replacementStringCol.Index].Value?.ToString() ?? string.Empty; + var description = dataGridView1.Rows[e.RowIndex].Cells[descriptionCol.Index].Value?.ToString() ?? string.Empty; + + //Validate the whole row. If it passes all validation, add or update the row's tag. + if (string.IsNullOrEmpty(charToReplaceStr) && replacement == string.Empty && description == string.Empty) + { + //Invalid entry, so delete row + var row = dataGridView1.Rows[e.RowIndex]; + if (!row.IsNewRow) + { + BeginInvoke(new MethodInvoker(delegate + { + dataGridView1.Rows.Remove(row); + })); + } + } + else if (string.IsNullOrEmpty(charToReplaceStr)) + { + dataGridView1.Rows[e.RowIndex].ErrorText = $"You must choose a character to replace"; + } + else if (charToReplaceStr.Length > 1) + { + dataGridView1.Rows[e.RowIndex].ErrorText = $"Only 1 {charToReplaceCol.HeaderText} per entry"; + } + else if (e.RowIndex >= Replacement.FIXED_COUNT && + dataGridView1.Rows + .Cast<DataGridViewRow>() + .Where(r => r.Index != e.RowIndex) + .Select(r => r.Tag) + .OfType<Replacement>() + .Any(r => r.CharacterToReplace == charToReplaceStr[0]) + ) + { + dataGridView1.Rows[e.RowIndex].ErrorText = $"The {charToReplaceStr[0]} character is already being replaced"; + } + else if (ReplacementCharacters.ContainsInvalid(replacement)) + { + dataGridView1.Rows[e.RowIndex].ErrorText = $"Your {replacementStringCol.HeaderText} contains illegal characters"; + } + else + { + //valid entry. Add or update Replacement in row's Tag + var charToReplace = charToReplaceStr[0]; + + if (dataGridView1.Rows[e.RowIndex].Tag is Replacement existing) + existing.Update(charToReplace, replacement, description); + else + dataGridView1.Rows[e.RowIndex].Tag = new Replacement(charToReplace, replacement, description); + } + } + + private void saveBtn_Click(object sender, EventArgs e) + { + var replacements = dataGridView1.Rows + .Cast<DataGridViewRow>() + .Select(r => r.Tag) + .OfType<Replacement>() + .ToList(); + + config.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements }; + DialogResult = DialogResult.OK; + Close(); + } + + private void cancelBtn_Click(object sender, EventArgs e) + { + DialogResult = DialogResult.Cancel; + Close(); + } + + private void dataGridView1_Resize(object sender, EventArgs e) + { + dataGridView1.Columns[^1].Width = dataGridView1.Width - dataGridView1.Columns.Cast<DataGridViewColumn>().Sum(c => c == dataGridView1.Columns[^1] ? 0 : c.Width) - dataGridView1.RowHeadersWidth - 2; + } + } +} diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.resx b/Source/LibationWinForms/Dialogs/EditReplacementChars.resx new file mode 100644 index 00000000..fa41cc36 --- /dev/null +++ b/Source/LibationWinForms/Dialogs/EditReplacementChars.resx @@ -0,0 +1,69 @@ +<root> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <metadata name="charToReplaceCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> + <value>True</value> + </metadata> + <metadata name="replacementStringCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> + <value>True</value> + </metadata> + <metadata name="descriptionCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> + <value>True</value> + </metadata> +</root> \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs index f64f3c1a..d117e4fe 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs @@ -60,6 +60,7 @@ this.tab3DownloadDecrypt = new System.Windows.Forms.TabPage(); this.inProgressFilesGb = new System.Windows.Forms.GroupBox(); this.customFileNamingGb = new System.Windows.Forms.GroupBox(); + this.editCharreplacementBtn = new System.Windows.Forms.Button(); this.chapterFileTemplateBtn = new System.Windows.Forms.Button(); this.chapterFileTemplateTb = new System.Windows.Forms.TextBox(); this.chapterFileTemplateLbl = new System.Windows.Forms.Label(); @@ -148,7 +149,7 @@ // saveBtn // this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.saveBtn.Location = new System.Drawing.Point(667, 441); + this.saveBtn.Location = new System.Drawing.Point(667, 461); this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.saveBtn.Name = "saveBtn"; this.saveBtn.Size = new System.Drawing.Size(88, 27); @@ -161,7 +162,7 @@ // this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.cancelBtn.Location = new System.Drawing.Point(785, 441); + this.cancelBtn.Location = new System.Drawing.Point(785, 461); this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.cancelBtn.Name = "cancelBtn"; this.cancelBtn.Size = new System.Drawing.Size(88, 27); @@ -308,7 +309,7 @@ // inProgressSelectControl // this.inProgressSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.inProgressSelectControl.Location = new System.Drawing.Point(7, 68); this.inProgressSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.inProgressSelectControl.Name = "inProgressSelectControl"; @@ -328,7 +329,7 @@ // booksSelectControl // this.booksSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.booksSelectControl.Location = new System.Drawing.Point(7, 37); this.booksSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.booksSelectControl.Name = "booksSelectControl"; @@ -356,8 +357,8 @@ // tabControl // this.tabControl.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); this.tabControl.Controls.Add(this.tab1ImportantSettings); this.tabControl.Controls.Add(this.tab2ImportLibrary); this.tabControl.Controls.Add(this.tab3DownloadDecrypt); @@ -365,7 +366,7 @@ this.tabControl.Location = new System.Drawing.Point(12, 12); this.tabControl.Name = "tabControl"; this.tabControl.SelectedIndex = 0; - this.tabControl.Size = new System.Drawing.Size(862, 423); + this.tabControl.Size = new System.Drawing.Size(862, 443); this.tabControl.TabIndex = 100; // // tab1ImportantSettings @@ -377,7 +378,7 @@ this.tab1ImportantSettings.Location = new System.Drawing.Point(4, 24); this.tab1ImportantSettings.Name = "tab1ImportantSettings"; this.tab1ImportantSettings.Padding = new System.Windows.Forms.Padding(3); - this.tab1ImportantSettings.Size = new System.Drawing.Size(854, 395); + this.tab1ImportantSettings.Size = new System.Drawing.Size(854, 415); this.tab1ImportantSettings.TabIndex = 0; this.tab1ImportantSettings.Text = "Important settings"; this.tab1ImportantSettings.UseVisualStyleBackColor = true; @@ -385,7 +386,7 @@ // booksGb // this.booksGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.booksGb.Controls.Add(this.saveEpisodesToSeriesFolderCbox); this.booksGb.Controls.Add(this.booksSelectControl); this.booksGb.Controls.Add(this.booksLocationDescLbl); @@ -416,7 +417,7 @@ this.tab2ImportLibrary.Location = new System.Drawing.Point(4, 24); this.tab2ImportLibrary.Name = "tab2ImportLibrary"; this.tab2ImportLibrary.Padding = new System.Windows.Forms.Padding(3); - this.tab2ImportLibrary.Size = new System.Drawing.Size(854, 395); + this.tab2ImportLibrary.Size = new System.Drawing.Size(854, 415); this.tab2ImportLibrary.TabIndex = 1; this.tab2ImportLibrary.Text = "Import library"; this.tab2ImportLibrary.UseVisualStyleBackColor = true; @@ -459,7 +460,7 @@ this.tab3DownloadDecrypt.Location = new System.Drawing.Point(4, 24); this.tab3DownloadDecrypt.Name = "tab3DownloadDecrypt"; this.tab3DownloadDecrypt.Padding = new System.Windows.Forms.Padding(3); - this.tab3DownloadDecrypt.Size = new System.Drawing.Size(854, 395); + this.tab3DownloadDecrypt.Size = new System.Drawing.Size(854, 415); this.tab3DownloadDecrypt.TabIndex = 2; this.tab3DownloadDecrypt.Text = "Download/Decrypt"; this.tab3DownloadDecrypt.UseVisualStyleBackColor = true; @@ -467,10 +468,10 @@ // inProgressFilesGb // this.inProgressFilesGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.inProgressFilesGb.Controls.Add(this.inProgressDescLbl); this.inProgressFilesGb.Controls.Add(this.inProgressSelectControl); - this.inProgressFilesGb.Location = new System.Drawing.Point(7, 251); + this.inProgressFilesGb.Location = new System.Drawing.Point(6, 281); this.inProgressFilesGb.Name = "inProgressFilesGb"; this.inProgressFilesGb.Size = new System.Drawing.Size(841, 128); this.inProgressFilesGb.TabIndex = 21; @@ -480,7 +481,8 @@ // customFileNamingGb // this.customFileNamingGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); + this.customFileNamingGb.Controls.Add(this.editCharreplacementBtn); this.customFileNamingGb.Controls.Add(this.chapterFileTemplateBtn); this.customFileNamingGb.Controls.Add(this.chapterFileTemplateTb); this.customFileNamingGb.Controls.Add(this.chapterFileTemplateLbl); @@ -492,11 +494,22 @@ this.customFileNamingGb.Controls.Add(this.folderTemplateLbl); this.customFileNamingGb.Location = new System.Drawing.Point(7, 88); this.customFileNamingGb.Name = "customFileNamingGb"; - this.customFileNamingGb.Size = new System.Drawing.Size(841, 157); + this.customFileNamingGb.Size = new System.Drawing.Size(841, 187); this.customFileNamingGb.TabIndex = 20; this.customFileNamingGb.TabStop = false; this.customFileNamingGb.Text = "Custom file naming"; // + // editCharreplacementBtn + // + this.editCharreplacementBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.editCharreplacementBtn.Location = new System.Drawing.Point(5, 158); + this.editCharreplacementBtn.Name = "editCharreplacementBtn"; + this.editCharreplacementBtn.Size = new System.Drawing.Size(387, 23); + this.editCharreplacementBtn.TabIndex = 8; + this.editCharreplacementBtn.Text = "[edit char replacement desc]"; + this.editCharreplacementBtn.UseVisualStyleBackColor = true; + this.editCharreplacementBtn.Click += new System.EventHandler(this.editCharreplacementBtn_Click); + // // chapterFileTemplateBtn // this.chapterFileTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); @@ -511,7 +524,7 @@ // chapterFileTemplateTb // this.chapterFileTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.chapterFileTemplateTb.Location = new System.Drawing.Point(6, 125); this.chapterFileTemplateTb.Name = "chapterFileTemplateTb"; this.chapterFileTemplateTb.ReadOnly = true; @@ -541,7 +554,7 @@ // fileTemplateTb // this.fileTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.fileTemplateTb.Location = new System.Drawing.Point(6, 81); this.fileTemplateTb.Name = "fileTemplateTb"; this.fileTemplateTb.ReadOnly = true; @@ -571,7 +584,7 @@ // folderTemplateTb // this.folderTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.folderTemplateTb.Location = new System.Drawing.Point(5, 37); this.folderTemplateTb.Name = "folderTemplateTb"; this.folderTemplateTb.ReadOnly = true; @@ -603,7 +616,7 @@ this.tab4AudioFileOptions.Location = new System.Drawing.Point(4, 24); this.tab4AudioFileOptions.Name = "tab4AudioFileOptions"; this.tab4AudioFileOptions.Padding = new System.Windows.Forms.Padding(3); - this.tab4AudioFileOptions.Size = new System.Drawing.Size(854, 395); + this.tab4AudioFileOptions.Size = new System.Drawing.Size(854, 415); this.tab4AudioFileOptions.TabIndex = 3; this.tab4AudioFileOptions.Text = "Audio File Options"; this.tab4AudioFileOptions.UseVisualStyleBackColor = true; @@ -633,7 +646,7 @@ // chapterTitleTemplateTb // this.chapterTitleTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + | System.Windows.Forms.AnchorStyles.Right))); this.chapterTitleTemplateTb.Location = new System.Drawing.Point(6, 22); this.chapterTitleTemplateTb.Name = "chapterTitleTemplateTb"; this.chapterTitleTemplateTb.ReadOnly = true; @@ -1017,7 +1030,7 @@ this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.cancelBtn; - this.ClientSize = new System.Drawing.Size(886, 484); + this.ClientSize = new System.Drawing.Size(886, 504); this.Controls.Add(this.tabControl); this.Controls.Add(this.cancelBtn); this.Controls.Add(this.saveBtn); @@ -1141,5 +1154,6 @@ private System.Windows.Forms.GroupBox chapterTitleTemplateGb; private System.Windows.Forms.Button chapterTitleTemplateBtn; private System.Windows.Forms.TextBox chapterTitleTemplateTb; + private System.Windows.Forms.Button editCharreplacementBtn; } } \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs index adba6f9d..035fdf98 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs @@ -11,9 +11,19 @@ namespace LibationWinForms.Dialogs private void fileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.File, fileTemplateTb); private void chapterFileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.ChapterFile, chapterFileTemplateTb); + + private void editCharreplacementBtn_Click(object sender, EventArgs e) + { + var form = new EditReplacementChars(config); + form.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + form.ShowDialog(this); + } + private void Load_DownloadDecrypt(Configuration config) { inProgressDescLbl.Text = desc(nameof(config.InProgress)); + editCharreplacementBtn.Text = desc(nameof(config.ReplacementCharacters)); + badBookGb.Text = desc(nameof(config.BadBook)); badBookAskRb.Text = Configuration.BadBookAction.Ask.GetDescription(); badBookAbortRb.Text = Configuration.BadBookAction.Abort.GetDescription(); diff --git a/Source/LibationWinForms/Form1.ScanAuto.cs b/Source/LibationWinForms/Form1.ScanAuto.cs index 30cfa7e1..5fb10932 100644 --- a/Source/LibationWinForms/Form1.ScanAuto.cs +++ b/Source/LibationWinForms/Form1.ScanAuto.cs @@ -33,7 +33,18 @@ namespace LibationWinForms .ToArray(); // in autoScan, new books SHALL NOT show dialog - await Invoke(async () => await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts)); + try + { + Task importAsync() => LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts); + if (InvokeRequired) + await Invoke(importAsync); + else + await importAsync(); + } + catch (Exception ex) + { + Serilog.Log.Logger.Error(ex, "Error invoking auto-scan"); + } }; // load init state to menu checkbox diff --git a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs index 4672cdcf..8db45ca4 100644 --- a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs +++ b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs @@ -12,6 +12,8 @@ namespace FileNamingTemplateTests [TestClass] public class GetFilePath { + static ReplacementCharacters Replacements = ReplacementCharacters.Default; + [TestMethod] public void equiv_GetValidFilename() { @@ -19,81 +21,33 @@ namespace FileNamingTemplateTests sb.Append('0', 300); var longText = sb.ToString(); - var expectedOld = "C:\\foo\\bar\\my_ book 00000000000000000000000000000000000000000 [ID123456].txt"; var expectedNew = "C:\\foo\\bar\\my꞉ book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 [ID123456].txt"; - var f1 = OLD_GetValidFilename(@"C:\foo\bar", "my: book " + longText, "txt", "ID123456"); var f2 = NEW_GetValidFilename_FileNamingTemplate(@"C:\foo\bar", "my: book " + longText, "txt", "ID123456"); - f1.Should().Be(expectedOld); f2.Should().Be(expectedNew); } - private static string OLD_GetValidFilename(string dirFullPath, string filename, string extension, string metadataSuffix) - { - ArgumentValidator.EnsureNotNullOrWhiteSpace(dirFullPath, nameof(dirFullPath)); - filename ??= ""; - - // sanitize. omit invalid characters. exception: colon => underscore - filename = filename.Replace(":", "_"); - filename = FileUtility.GetSafeFileName(filename); - - if (filename.Length > 50) - filename = filename.Substring(0, 50); - - if (!string.IsNullOrWhiteSpace(metadataSuffix)) - filename += $" [{metadataSuffix}]"; - - // extension is null when this method is used for directory names - extension = FileUtility.GetStandardizedExtension(extension); - - // ensure uniqueness - var fullfilename = Path.Combine(dirFullPath, filename + extension); - var i = 0; - while (File.Exists(fullfilename)) - fullfilename = Path.Combine(dirFullPath, filename + $" ({++i})" + extension); - - return fullfilename; - } private static string NEW_GetValidFilename_FileNamingTemplate(string dirFullPath, string filename, string extension, string metadataSuffix) { var template = $"<title> [<id>]"; var fullfilename = Path.Combine(dirFullPath, template + FileUtility.GetStandardizedExtension(extension)); - var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" }; + var fileNamingTemplate = new FileNamingTemplate(fullfilename); fileNamingTemplate.AddParameterReplacement("title", filename); fileNamingTemplate.AddParameterReplacement("id", metadataSuffix); - return fileNamingTemplate.GetFilePath().PathWithoutPrefix; + return fileNamingTemplate.GetFilePath(Replacements).PathWithoutPrefix; } [TestMethod] public void equiv_GetMultipartFileName() { var expected = @"C:\foo\bar\my file - 002 - title.txt"; - var f1 = OLD_GetMultipartFileName(@"C:\foo\bar\my file.txt", 2, 100, "title"); var f2 = NEW_GetMultipartFileName_FileNamingTemplate(@"C:\foo\bar\my file.txt", 2, 100, "title"); - f1.Should().Be(expected); - f1.Should().Be(f2); + f2.Should().Be(expected); } - private static string OLD_GetMultipartFileName(string originalPath, int partsPosition, int partsTotal, string suffix) - { - // 1-9 => 1-9 - // 10-99 => 01-99 - // 100-999 => 001-999 - var chapterCountLeadingZeros = partsPosition.ToString().PadLeft(partsTotal.ToString().Length, '0'); - string extension = Path.GetExtension(originalPath); - - var filenameBase = $"{Path.GetFileNameWithoutExtension(originalPath)} - {chapterCountLeadingZeros}"; - if (!string.IsNullOrWhiteSpace(suffix)) - filenameBase += $" - {suffix}"; - - // Replace illegal path characters with spaces - var fileName = FileUtility.GetSafeFileName(filenameBase, " "); - var path = Path.Combine(Path.GetDirectoryName(originalPath), fileName + extension); - return path; - } private static string NEW_GetMultipartFileName_FileNamingTemplate(string originalPath, int partsPosition, int partsTotal, string suffix) { // 1-9 => 1-9 @@ -103,10 +57,10 @@ namespace FileNamingTemplateTests var t = Path.ChangeExtension(originalPath, null) + " - <chapter> - <title>" + Path.GetExtension(originalPath); - var fileNamingTemplate = new FileNamingTemplate(t) { IllegalCharacterReplacements = " " }; + var fileNamingTemplate = new FileNamingTemplate(t); fileNamingTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros); fileNamingTemplate.AddParameterReplacement("title", suffix); - return fileNamingTemplate.GetFilePath().PathWithoutPrefix; + return fileNamingTemplate.GetFilePath(Replacements).PathWithoutPrefix; } [TestMethod] @@ -114,7 +68,7 @@ namespace FileNamingTemplateTests { var fileNamingTemplate = new FileNamingTemplate(@"\foo\<title>.txt"); fileNamingTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s"); - fileNamingTemplate.GetFilePath().PathWithoutPrefix.Should().Be(@"\foo\slashes.txt"); + fileNamingTemplate.GetFilePath(Replacements).PathWithoutPrefix.Should().Be(@"\foo\sl∕as∕he∕s.txt"); } } } diff --git a/Source/_Tests/FileManager.Tests/FileUtilityTests.cs b/Source/_Tests/FileManager.Tests/FileUtilityTests.cs index fe6a8bd5..15b188cd 100644 --- a/Source/_Tests/FileManager.Tests/FileUtilityTests.cs +++ b/Source/_Tests/FileManager.Tests/FileUtilityTests.cs @@ -12,49 +12,88 @@ namespace FileUtilityTests [TestClass] public class GetSafePath { - [TestMethod] - public void null_path_throws() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetSafePath(null)); + static readonly ReplacementCharacters Default = ReplacementCharacters.Default; + static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault; + static readonly ReplacementCharacters Barebones = ReplacementCharacters.Minimum; - // needs separate method. middle null param not running correctly in TestExplorer when used in DataRow() [TestMethod] - [DataRow("http://test.com/a/b/c", @"http꞉\test.com\a\b\c")] - public void null_replacement(string inStr, string outStr) => Tests(inStr, null, outStr); + public void null_path_throws() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetSafePath(null, Default)); + + [TestMethod] + // non-empty replacement + [DataRow("abc*abc.txt", "abc✱abc.txt")] + // standardize slashes + [DataRow(@"a/b\c/d", @"a\b\c\d")] + // remove illegal chars + [DataRow("a*?:z.txt", "a✱?꞉z.txt")] + // retain drive letter path colon + [DataRow(@"C:\az.txt", @"C:\az.txt")] + // replace all other colons + [DataRow(@"a\b:c\d.txt", @"a\b꞉c\d.txt")] + // remove empty directories + [DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt")] + [DataRow(@"C:\""foo\<id>", @"C:\“foo\<id>")] + public void DefaultTests(string inStr, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, Default).PathWithoutPrefix); + + [TestMethod] + // non-empty replacement + [DataRow("abc*abc.txt", "abc_abc.txt")] + // standardize slashes + [DataRow(@"a/b\c/d", @"a\b\c\d")] + // remove illegal chars + [DataRow("a*?:z.txt", "a__-z.txt")] + // retain drive letter path colon + [DataRow(@"C:\az.txt", @"C:\az.txt")] + // replace all other colons + [DataRow(@"a\b:c\d.txt", @"a\b-c\d.txt")] + // remove empty directories + [DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt")] + [DataRow(@"C:\""foo\<id>", @"C:\'foo\{id}")] + public void LoFiDefaultTests(string inStr, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, LoFiDefault).PathWithoutPrefix); [TestMethod] // empty replacement - [DataRow("abc*abc.txt", "", "abc✱abc.txt")] - // non-empty replacement - [DataRow("abc*abc.txt", "ZZZ", "abc✱abc.txt")] + [DataRow("abc*abc.txt", "abc_abc.txt")] // standardize slashes - [DataRow(@"a/b\c/d", "Z", @"a\b\c\d")] + [DataRow(@"a/b\c/d", @"a\b\c\d")] // remove illegal chars - [DataRow("a*?:z.txt", "Z", "a✱?꞉z.txt")] + [DataRow("a*?:z.txt", "a___z.txt")] // retain drive letter path colon - [DataRow(@"C:\az.txt", "Z", @"C:\az.txt")] + [DataRow(@"C:\az.txt", @"C:\az.txt")] // replace all other colons - [DataRow(@"a\b:c\d.txt", "ZZZ", @"a\b꞉c\d.txt")] + [DataRow(@"a\b:c\d.txt", @"a\b_c\d.txt")] // remove empty directories - [DataRow(@"C:\a\\\b\c\\\d.txt", "ZZZ", @"C:\a\b\c\d.txt")] - [DataRow(@"C:\""foo\<id>", "ZZZ", @"C:\“foo\<id>")] - public void Tests(string inStr, string replacement, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, replacement).PathWithoutPrefix); + [DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt")] + [DataRow(@"C:\""foo\<id>", @"C:\_foo\_id_")] + public void BarebonesDefaultTests(string inStr, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, Barebones).PathWithoutPrefix); } [TestClass] public class GetSafeFileName { + static readonly ReplacementCharacters Default = ReplacementCharacters.Default; + static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault; + static readonly ReplacementCharacters Barebones = ReplacementCharacters.Minimum; + // needs separate method. middle null param not running correctly in TestExplorer when used in DataRow() [TestMethod] - [DataRow("http://test.com/a/b/c", "httptest.comabc")] - public void url_null_replacement(string inStr, string outStr) => ReplacementTests(inStr, null, outStr); + [DataRow("http://test.com/a/b/c", "http꞉∕∕test.com∕a∕b∕c")] + public void url_null_replacement(string inStr, string outStr) => DefaultReplacementTest(inStr, outStr); [TestMethod] // empty replacement - [DataRow("http://test.com/a/b/c", "", "httptest.comabc")] - // single char replace - [DataRow("http://test.com/a/b/c", "_", "http___test.com_a_b_c")] - // multi char replace - [DataRow("http://test.com/a/b/c", "!!!", "http!!!!!!!!!test.com!!!a!!!b!!!c")] - public void ReplacementTests(string inStr, string replacement, string outStr) => FileUtility.GetSafeFileName(inStr, replacement).Should().Be(outStr); + [DataRow("http://test.com/a/b/c", "http꞉∕∕test.com∕a∕b∕c")] + public void DefaultReplacementTest(string inStr, string outStr) => Default.ReplaceInvalidFilenameChars(inStr).Should().Be(outStr); + + [TestMethod] + // empty replacement + [DataRow("http://test.com/a/b/c", "http-__test.com_a_b_c")] + public void LoFiDefaultReplacementTest(string inStr, string outStr) => LoFiDefault.ReplaceInvalidFilenameChars(inStr).Should().Be(outStr); + + [TestMethod] + // empty replacement + [DataRow("http://test.com/a/b/c", "http___test.com_a_b_c")] + public void BarebonesDefaultReplacementTest(string inStr, string outStr) => Barebones.ReplaceInvalidFilenameChars(inStr).Should().Be(outStr); } [TestClass] @@ -117,6 +156,8 @@ namespace FileUtilityTests [TestClass] public class GetValidFilename { + static ReplacementCharacters Replacements = ReplacementCharacters.Default; + [TestMethod] // dot-files [DataRow(@"C:\a bc\x y z\.f i l e.txt")] @@ -134,7 +175,7 @@ namespace FileUtilityTests // file end dots [DataRow(@"C:\a bc\x y z\f i l e.txt . . .", @"C:\a bc\x y z\f i l e.txt")] public void Tests(string input, string expected) - => FileUtility.GetValidFilename(input).PathWithoutPrefix.Should().Be(expected); + => FileUtility.GetValidFilename(input, Replacements).PathWithoutPrefix.Should().Be(expected); } [TestClass] diff --git a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs index b0b417ff..f598c8cc 100644 --- a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs +++ b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dinah.Core; +using FileManager; using FluentAssertions; using LibationFileManager; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -51,6 +52,9 @@ namespace TemplatesTests [TestClass] public class getFileNamingTemplate { + static ReplacementCharacters Replacements = ReplacementCharacters.Default; + + [TestMethod] [DataRow(null, "asin", @"C:\", "ext")] [ExpectedException(typeof(ArgumentNullException))] @@ -73,28 +77,28 @@ namespace TemplatesTests [DataRow("<id>", "asin", @"C:\foo\bar", "ext", @"C:\foo\bar\asin.ext")] public void Tests(string template, string asin, string dirFullPath, string extension, string expected) => Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension) - .GetFilePath() + .GetFilePath(Replacements) .PathWithoutPrefix .Should().Be(expected); [TestMethod] public void IfSeries_empty() => Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series-><-if series>bar", @"C:\a\b", "ext") - .GetFilePath() + .GetFilePath(Replacements) .PathWithoutPrefix .Should().Be(@"C:\a\b\foobar.ext"); [TestMethod] public void IfSeries_no_series() => Templates.getFileNamingTemplate(GetLibraryBook("asin", ""), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext") - .GetFilePath() + .GetFilePath(Replacements) .PathWithoutPrefix .Should().Be(@"C:\a\b\foobar.ext"); [TestMethod] public void IfSeries_with_series() => Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext") - .GetFilePath() + .GetFilePath(Replacements) .PathWithoutPrefix .Should().Be(@"C:\a\b\foo-Sherlock Holmes-asin-bar.ext"); } @@ -387,11 +391,13 @@ namespace Templates_ChapterFile_Tests [TestClass] public class GetPortionFilename { + static readonly ReplacementCharacters Default = ReplacementCharacters.Default; + [TestMethod] [DataRow("asin", "[<id>] <ch# 0> of <ch count> - <ch title>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\[asin] 06 of 10 - chap.txt")] [DataRow("asin", "<ch#>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\6.txt")] public void Tests(string asin, string template, string dir, string ext, int pos, int total, string chapter, string expected) - => Templates.ChapterFile.GetPortionFilename(GetLibraryBook(asin), template, new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir) + => Templates.ChapterFile.GetPortionFilename(GetLibraryBook(asin), template, new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir, Default) .Should().Be(expected); } } From 9ff2a83ba374477df1e7dd989ceee38ccaf2f117 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 13:11:35 -0600 Subject: [PATCH 14/20] Rename Minimum to Barebones --- Source/FileManager/ReplacementCharacters.cs | 2 +- Source/LibationWinForms/Dialogs/EditReplacementChars.cs | 2 +- Source/_Tests/FileManager.Tests/FileUtilityTests.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/FileManager/ReplacementCharacters.cs b/Source/FileManager/ReplacementCharacters.cs index ffdd493b..4ca79e69 100644 --- a/Source/FileManager/ReplacementCharacters.cs +++ b/Source/FileManager/ReplacementCharacters.cs @@ -93,7 +93,7 @@ namespace FileManager } }; - public static readonly ReplacementCharacters Minimum = new() + public static readonly ReplacementCharacters Barebones = new() { Replacements = new List<Replacement>() { diff --git a/Source/LibationWinForms/Dialogs/EditReplacementChars.cs b/Source/LibationWinForms/Dialogs/EditReplacementChars.cs index 11a0bf6c..5e16550f 100644 --- a/Source/LibationWinForms/Dialogs/EditReplacementChars.cs +++ b/Source/LibationWinForms/Dialogs/EditReplacementChars.cs @@ -56,7 +56,7 @@ namespace LibationWinForms.Dialogs => LoadTable(ReplacementCharacters.Default.Replacements); private void minDefaultBtn_Click(object sender, EventArgs e) - => LoadTable(ReplacementCharacters.Minimum.Replacements); + => LoadTable(ReplacementCharacters.Barebones.Replacements); private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e) diff --git a/Source/_Tests/FileManager.Tests/FileUtilityTests.cs b/Source/_Tests/FileManager.Tests/FileUtilityTests.cs index 15b188cd..2c060aa2 100644 --- a/Source/_Tests/FileManager.Tests/FileUtilityTests.cs +++ b/Source/_Tests/FileManager.Tests/FileUtilityTests.cs @@ -14,7 +14,7 @@ namespace FileUtilityTests { static readonly ReplacementCharacters Default = ReplacementCharacters.Default; static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault; - static readonly ReplacementCharacters Barebones = ReplacementCharacters.Minimum; + static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones; [TestMethod] public void null_path_throws() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetSafePath(null, Default)); @@ -73,7 +73,7 @@ namespace FileUtilityTests { static readonly ReplacementCharacters Default = ReplacementCharacters.Default; static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault; - static readonly ReplacementCharacters Barebones = ReplacementCharacters.Minimum; + static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones; // needs separate method. middle null param not running correctly in TestExplorer when used in DataRow() [TestMethod] From 314f4850bcb9165199aa708ec0b4aa8851a0767d Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 15:38:39 -0600 Subject: [PATCH 15/20] Add logging and error handling to Process Queue. and Processables --- .../AaxcDownloadMultiConverter.cs | 72 +++++-- .../AaxcDownloadSingleConverter.cs | 75 ++++++-- Source/AaxDecrypter/AudiobookDownloadBase.cs | 12 +- .../UnencryptedAudiobookDownloader.cs | 58 ++++-- Source/FileLiberator/ConvertToMp3.cs | 4 +- Source/FileLiberator/DownloadDecryptBook.cs | 2 +- .../ProcessQueue/ProcessQueueControl.cs | 178 ++++++++++-------- 7 files changed, 264 insertions(+), 137 deletions(-) diff --git a/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs b/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs index ea11c319..8f4f31f2 100644 --- a/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs +++ b/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs @@ -2,31 +2,65 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using AAXClean; using AAXClean.Codecs; -using Dinah.Core; -using Dinah.Core.StepRunner; using FileManager; namespace AaxDecrypter { public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase { - protected override StepSequence Steps { get; } private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3); private List<string> multiPartFilePaths { get; } = new List<string>(); public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions) - : base(outFileName, cacheDirectory, dlOptions) - { - Steps = new StepSequence - { - Name = "Download and Convert Aaxc To " + DownloadOptions.OutputFormat, + : base(outFileName, cacheDirectory, dlOptions) { } - ["Step 1: Get Aaxc Metadata"] = Step_GetMetadata, - ["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsMultipleFilesPerChapter, - ["Step 3: Cleanup"] = Step_Cleanup, - }; + public override async Task<bool> RunAsync() + { + try + { + Serilog.Log.Information("Begin download and convert Aaxc To {format}", DownloadOptions.OutputFormat); + + //Step 1 + Serilog.Log.Information("Begin Get Aaxc Metadata"); + if (await Task.Run(Step_GetMetadata)) + Serilog.Log.Information("Completed Get Aaxc Metadata"); + else + { + Serilog.Log.Information("Failed to Complete Get Aaxc Metadata"); + return false; + } + + //Step 2 + Serilog.Log.Information("Begin Download Decrypted Audiobook"); + if (await Step_DownloadAudiobookAsMultipleFilesPerChapter()) + Serilog.Log.Information("Completed Download Decrypted Audiobook"); + else + { + Serilog.Log.Information("Failed to Complete Download Decrypted Audiobook"); + return false; + } + + //Step 3 + Serilog.Log.Information("Begin Cleanup"); + if (await Task.Run(Step_Cleanup)) + Serilog.Log.Information("Completed Cleanup"); + else + { + Serilog.Log.Information("Failed to Complete Cleanup"); + return false; + } + + Serilog.Log.Information("Completed download and convert Aaxc To {format}", DownloadOptions.OutputFormat); + return true; + } + catch (Exception ex) + { + Serilog.Log.Error(ex, "Error encountered in download and convert Aaxc To {format}", DownloadOptions.OutputFormat); + return false; + } } /* @@ -53,7 +87,7 @@ The book will be split into the following files: That naming may not be desirable for everyone, but it's an easy change to instead use the last of the combined chapter's title in the file name. */ - private bool Step_DownloadAudiobookAsMultipleFilesPerChapter() + private async Task<bool> Step_DownloadAudiobookAsMultipleFilesPerChapter() { var zeroProgress = Step_DownloadAudiobook_Start(); @@ -86,9 +120,9 @@ That naming may not be desirable for everyone, but it's an easy change to instea AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate; if (DownloadOptions.OutputFormat == OutputFormat.M4b) - result = ConvertToMultiMp4a(splitChapters); + result = await ConvertToMultiMp4a(splitChapters); else - result = ConvertToMultiMp3(splitChapters); + result = await ConvertToMultiMp3(splitChapters); AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate; Step_DownloadAudiobook_End(zeroProgress); @@ -96,10 +130,10 @@ That naming may not be desirable for everyone, but it's an easy change to instea return result == ConversionResult.NoErrorsDetected; } - private ConversionResult ConvertToMultiMp4a(ChapterInfo splitChapters) + private Task<ConversionResult> ConvertToMultiMp4a(ChapterInfo splitChapters) { var chapterCount = 0; - return AaxFile.ConvertToMultiMp4a + return AaxFile.ConvertToMultiMp4aAsync ( splitChapters, newSplitCallback => Callback(++chapterCount, splitChapters, newSplitCallback), @@ -107,10 +141,10 @@ That naming may not be desirable for everyone, but it's an easy change to instea ); } - private ConversionResult ConvertToMultiMp3(ChapterInfo splitChapters) + private Task<ConversionResult> ConvertToMultiMp3(ChapterInfo splitChapters) { var chapterCount = 0; - return AaxFile.ConvertToMultiMp3 + return AaxFile.ConvertToMultiMp3Async ( splitChapters, newSplitCallback => Callback(++chapterCount, splitChapters, newSplitCallback), diff --git a/Source/AaxDecrypter/AaxcDownloadSingleConverter.cs b/Source/AaxDecrypter/AaxcDownloadSingleConverter.cs index fbed8894..fcb73ecc 100644 --- a/Source/AaxDecrypter/AaxcDownloadSingleConverter.cs +++ b/Source/AaxDecrypter/AaxcDownloadSingleConverter.cs @@ -1,31 +1,74 @@ using System; using System.IO; +using System.Threading.Tasks; using AAXClean; using AAXClean.Codecs; -using Dinah.Core.StepRunner; using FileManager; namespace AaxDecrypter { public class AaxcDownloadSingleConverter : AaxcDownloadConvertBase { - protected override StepSequence Steps { get; } - public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions) - : base(outFileName, cacheDirectory, dlOptions) - { - Steps = new StepSequence - { - Name = "Download and Convert Aaxc To " + DownloadOptions.OutputFormat, + : base(outFileName, cacheDirectory, dlOptions) { } - ["Step 1: Get Aaxc Metadata"] = Step_GetMetadata, - ["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsSingleFile, - ["Step 3: Create Cue"] = Step_CreateCue, - ["Step 4: Cleanup"] = Step_Cleanup, - }; + public override async Task<bool> RunAsync() + { + try + { + Serilog.Log.Information("Begin download and convert Aaxc To {format}", DownloadOptions.OutputFormat); + + //Step 1 + Serilog.Log.Information("Begin Step 1: Get Aaxc Metadata"); + if (await Task.Run(Step_GetMetadata)) + Serilog.Log.Information("Completed Step 1: Get Aaxc Metadata"); + else + { + Serilog.Log.Information("Failed to Complete Step 1: Get Aaxc Metadata"); + return false; + } + + //Step 2 + Serilog.Log.Information("Begin Step 2: Download Decrypted Audiobook"); + if (await Step_DownloadAudiobookAsSingleFile()) + Serilog.Log.Information("Completed Step 2: Download Decrypted Audiobook"); + else + { + Serilog.Log.Information("Failed to Complete Step 2: Download Decrypted Audiobook"); + return false; + } + + //Step 3 + Serilog.Log.Information("Begin Step 3: Create Cue"); + if (await Task.Run(Step_CreateCue)) + Serilog.Log.Information("Completed Step 3: Create Cue"); + else + { + Serilog.Log.Information("Failed to Complete Step 3: Create Cue"); + return false; + } + + //Step 4 + Serilog.Log.Information("Begin Step 4: Cleanup"); + if (await Task.Run(Step_Cleanup)) + Serilog.Log.Information("Completed Step 4: Cleanup"); + else + { + Serilog.Log.Information("Failed to Complete Step 4: Cleanup"); + return false; + } + + Serilog.Log.Information("Completed download and convert Aaxc To {format}", DownloadOptions.OutputFormat); + return true; + } + catch (Exception ex) + { + Serilog.Log.Error(ex, "Error encountered in download and convert Aaxc To {format}", DownloadOptions.OutputFormat); + return false; + } } - private bool Step_DownloadAudiobookAsSingleFile() + private async Task<bool> Step_DownloadAudiobookAsSingleFile() { var zeroProgress = Step_DownloadAudiobook_Start(); @@ -37,8 +80,8 @@ namespace AaxDecrypter AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate; var decryptionResult = DownloadOptions.OutputFormat == OutputFormat.M4b - ? AaxFile.ConvertToMp4a(outputFile, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength) - : AaxFile.ConvertToMp3(outputFile, DownloadOptions.LameConfig, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength); + ? await AaxFile.ConvertToMp4aAsync(outputFile, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength) + : await AaxFile.ConvertToMp3Async(outputFile, DownloadOptions.LameConfig, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength); AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate; DownloadOptions.ChapterInfo = AaxFile.Chapters; diff --git a/Source/AaxDecrypter/AudiobookDownloadBase.cs b/Source/AaxDecrypter/AudiobookDownloadBase.cs index 08d4f3d1..d6529b39 100644 --- a/Source/AaxDecrypter/AudiobookDownloadBase.cs +++ b/Source/AaxDecrypter/AudiobookDownloadBase.cs @@ -4,7 +4,6 @@ using System.IO; using System.Threading.Tasks; using Dinah.Core; using Dinah.Core.Net.Http; -using Dinah.Core.StepRunner; using FileManager; namespace AaxDecrypter @@ -31,7 +30,6 @@ namespace AaxDecrypter // Don't give the property a 'set'. This should have to be an obvious choice; not accidental protected void SetOutputFileName(string newOutputFileName) => OutputFileName = newOutputFileName; - protected abstract StepSequence Steps { get; } private NetworkFileStreamPersister nfsPersister; private string jsonDownloadState { get; } @@ -64,15 +62,7 @@ namespace AaxDecrypter OnRetrievedCoverArt(coverArt); } - public bool Run() - { - var (IsSuccess, _) = Steps.Run(); - - if (!IsSuccess) - Serilog.Log.Logger.Error("Conversion failed"); - - return IsSuccess; - } + public abstract Task<bool> RunAsync(); protected void OnRetrievedTitle(string title) => RetrievedTitle?.Invoke(this, title); diff --git a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs index 142ccf44..b9ac5c3b 100644 --- a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs +++ b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs @@ -2,29 +2,63 @@ using System.Threading; using System.Threading.Tasks; using Dinah.Core.Net.Http; -using Dinah.Core.StepRunner; using FileManager; namespace AaxDecrypter { public class UnencryptedAudiobookDownloader : AudiobookDownloadBase { - protected override StepSequence Steps { get; } public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, IDownloadOptions dlLic) - : base(outFileName, cacheDirectory, dlLic) - { - Steps = new StepSequence - { - Name = "Download Mp3 Audiobook", + : base(outFileName, cacheDirectory, dlLic) { } - ["Step 1: Get Mp3 Metadata"] = Step_GetMetadata, - ["Step 2: Download Audiobook"] = Step_DownloadAudiobookAsSingleFile, - ["Step 3: Create Cue"] = Step_CreateCue, - ["Step 4: Cleanup"] = Step_Cleanup, - }; + public override async Task<bool> RunAsync() + { + try + { + Serilog.Log.Information("Begin download and convert Aaxc To {format}", DownloadOptions.OutputFormat); + + //Step 1 + Serilog.Log.Information("Begin Step 1: Get Mp3 Metadata"); + if (await Task.Run(Step_GetMetadata)) + Serilog.Log.Information("Completed Step 1: Get Mp3 Metadata"); + else + { + Serilog.Log.Information("Failed to Complete Step 1: Get Mp3 Metadata"); + return false; + } + + //Step 2 + Serilog.Log.Information("Begin Step 2: Download Audiobook"); + if (await Task.Run(Step_DownloadAudiobookAsSingleFile)) + Serilog.Log.Information("Completed Step 2: Download Audiobook"); + else + { + Serilog.Log.Information("Failed to Complete Step 2: Download Audiobook"); + return false; + } + + //Step 3 + Serilog.Log.Information("Begin Step 3: Cleanup"); + if (await Task.Run(Step_Cleanup)) + Serilog.Log.Information("Completed Step 3: Cleanup"); + else + { + Serilog.Log.Information("Failed to Complete Step 3: Cleanup"); + return false; + } + + Serilog.Log.Information("Completed download and convert Aaxc To {format}", DownloadOptions.OutputFormat); + return true; + } + catch (Exception ex) + { + Serilog.Log.Error(ex, "Error encountered in download and convert Aaxc To {format}", DownloadOptions.OutputFormat); + return false; + } } + public override Task CancelAsync() { IsCanceled = true; diff --git a/Source/FileLiberator/ConvertToMp3.cs b/Source/FileLiberator/ConvertToMp3.cs index bb191986..70940d05 100644 --- a/Source/FileLiberator/ConvertToMp3.cs +++ b/Source/FileLiberator/ConvertToMp3.cs @@ -43,7 +43,7 @@ namespace FileLiberator var proposedMp3Path = Mp3FileName(m4bPath); if (File.Exists(proposedMp3Path) || !File.Exists(m4bPath)) continue; - m4bBook = new Mp4File(m4bPath, FileAccess.Read); + m4bBook = await Task.Run(() => new Mp4File(m4bPath, FileAccess.Read)); m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate; fileSize = m4bBook.InputStream.Length; @@ -55,7 +55,7 @@ namespace FileLiberator using var mp3File = File.OpenWrite(Path.GetTempFileName()); var lameConfig = GetLameOptions(Configuration.Instance); - var result = await Task.Run(() => m4bBook.ConvertToMp3(mp3File, lameConfig)); + var result = await m4bBook.ConvertToMp3Async(mp3File, lameConfig); m4bBook.InputStream.Close(); mp3File.Close(); diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index 4bea8538..f3686ca5 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -130,7 +130,7 @@ namespace FileLiberator abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path); // REAL WORK DONE HERE - var success = await Task.Run(abDownloader.Run); + var success = await abDownloader.RunAsync(); return success; } diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs index e34e8ae2..dfae3673 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs @@ -42,8 +42,6 @@ namespace LibationWinForms.ProcessQueue public bool Running => !QueueRunner?.IsCompleted ?? false; public ToolStripButton popoutBtn = new(); - private System.Threading.SynchronizationContext syncContext { get; } = System.Threading.SynchronizationContext.Current; - public ProcessQueueControl() { InitializeComponent(); @@ -103,6 +101,7 @@ namespace LibationWinForms.ProcessQueue procs.Add(pbook); } + Serilog.Log.Logger.Information("Queueing {count} books", procs.Count); AddToQueue(procs); } @@ -121,6 +120,7 @@ namespace LibationWinForms.ProcessQueue procs.Add(pbook); } + Serilog.Log.Logger.Information("Queueing {count} books", procs.Count); AddToQueue(procs); } @@ -138,44 +138,57 @@ namespace LibationWinForms.ProcessQueue procs.Add(pbook); } + Serilog.Log.Logger.Information("Queueing {count} books", procs.Count); AddToQueue(procs); } - private void AddToQueue(IEnumerable<ProcessBook> pbook) { - syncContext.Post(_ => + BeginInvoke(() => { Queue.Enqueue(pbook); if (!Running) QueueRunner = QueueLoop(); - }, - null); + }); } - DateTime StartingTime; private async Task QueueLoop() { - StartingTime = DateTime.Now; - counterTimer.Start(); - - while (Queue.MoveNext()) + try { - var nextBook = Queue.Current; + Serilog.Log.Logger.Information("Begin processing queue"); - var result = await nextBook.ProcessOneAsync(); + StartingTime = DateTime.Now; + counterTimer.Start(); - if (result == ProcessBookResult.ValidationFail) - Queue.ClearCurrent(); - else if (result == ProcessBookResult.FailedAbort) - Queue.ClearQueue(); - else if (result == ProcessBookResult.FailedSkip) - nextBook.LibraryBook.Book.UpdateBookStatus(DataLayer.LiberatedStatus.Error); + while (Queue.MoveNext()) + { + var nextBook = Queue.Current; + + Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook?.LibraryBook); + + var result = await nextBook.ProcessOneAsync(); + + Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook?.LibraryBook, result); + + if (result == ProcessBookResult.ValidationFail) + Queue.ClearCurrent(); + else if (result == ProcessBookResult.FailedAbort) + Queue.ClearQueue(); + else if (result == ProcessBookResult.FailedSkip) + nextBook.LibraryBook.Book.UpdateBookStatus(DataLayer.LiberatedStatus.Error); + } + Serilog.Log.Logger.Information("Completed processing queue"); + + Queue_CompletedCountChanged(this, 0); + counterTimer.Stop(); + virtualFlowControl2.VirtualControlCount = Queue.Count; + UpdateAllControls(); + } + catch (Exception ex) + { + Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items"); } - Queue_CompletedCountChanged(this, 0); - counterTimer.Stop(); - virtualFlowControl2.VirtualControlCount = Queue.Count; - UpdateAllControls(); } public void WriteLine(string text) @@ -282,35 +295,41 @@ namespace LibationWinForms.ProcessQueue /// <param name="propertyName">The nme of the property that needs updating. If null, all properties are updated.</param> private void UpdateControl(int queueIndex, string propertyName = null) { - int i = queueIndex - FirstVisible; - - if (i > NumVisible || i < 0) return; - - var proc = Queue[queueIndex]; - - syncContext.Send(_ => + try { - Panels[i].SuspendLayout(); - if (propertyName is null or nameof(proc.Cover)) - Panels[i].SetCover(proc.Cover); - if (propertyName is null or nameof(proc.BookText)) - Panels[i].SetBookInfo(proc.BookText); + int i = queueIndex - FirstVisible; - if (proc.Result != ProcessBookResult.None) + if (i > NumVisible || i < 0) return; + + var proc = Queue[queueIndex]; + + Invoke(() => { - Panels[i].SetResult(proc.Result); - return; - } + Panels[i].SuspendLayout(); + if (propertyName is null or nameof(proc.Cover)) + Panels[i].SetCover(proc.Cover); + if (propertyName is null or nameof(proc.BookText)) + Panels[i].SetBookInfo(proc.BookText); - if (propertyName is null or nameof(proc.Status)) - Panels[i].SetStatus(proc.Status); - if (propertyName is null or nameof(proc.Progress)) - Panels[i].SetProgrss(proc.Progress); - if (propertyName is null or nameof(proc.TimeRemaining)) - Panels[i].SetRemainingTime(proc.TimeRemaining); - Panels[i].ResumeLayout(); - }, - null); + if (proc.Result != ProcessBookResult.None) + { + Panels[i].SetResult(proc.Result); + return; + } + + if (propertyName is null or nameof(proc.Status)) + Panels[i].SetStatus(proc.Status); + if (propertyName is null or nameof(proc.Progress)) + Panels[i].SetProgrss(proc.Progress); + if (propertyName is null or nameof(proc.TimeRemaining)) + Panels[i].SetRemainingTime(proc.TimeRemaining); + Panels[i].ResumeLayout(); + }); + } + catch (Exception ex) + { + Serilog.Log.Logger.Error(ex, "Error updating the queued item's display."); + } } private void UpdateAllControls() @@ -329,37 +348,44 @@ namespace LibationWinForms.ProcessQueue /// <param name="panelClicked">The clicked control to update</param> private async void VirtualFlowControl2_ButtonClicked(int queueIndex, string buttonName, ProcessBookControl panelClicked) { - ProcessBook item = Queue[queueIndex]; - if (buttonName == nameof(panelClicked.cancelBtn)) + try { - if (item is not null) - await item.CancelAsync(); - Queue.RemoveQueued(item); - virtualFlowControl2.VirtualControlCount = Queue.Count; + ProcessBook item = Queue[queueIndex]; + if (buttonName == nameof(panelClicked.cancelBtn)) + { + if (item is not null) + await item.CancelAsync(); + Queue.RemoveQueued(item); + virtualFlowControl2.VirtualControlCount = Queue.Count; + } + else if (buttonName == nameof(panelClicked.moveFirstBtn)) + { + Queue.MoveQueuePosition(item, QueuePosition.Fisrt); + UpdateAllControls(); + } + else if (buttonName == nameof(panelClicked.moveUpBtn)) + { + Queue.MoveQueuePosition(item, QueuePosition.OneUp); + UpdateControl(queueIndex); + if (queueIndex > 0) + UpdateControl(queueIndex - 1); + } + else if (buttonName == nameof(panelClicked.moveDownBtn)) + { + Queue.MoveQueuePosition(item, QueuePosition.OneDown); + UpdateControl(queueIndex); + if (queueIndex + 1 < Queue.Count) + UpdateControl(queueIndex + 1); + } + else if (buttonName == nameof(panelClicked.moveLastBtn)) + { + Queue.MoveQueuePosition(item, QueuePosition.Last); + UpdateAllControls(); + } } - else if (buttonName == nameof(panelClicked.moveFirstBtn)) + catch(Exception ex) { - Queue.MoveQueuePosition(item, QueuePosition.Fisrt); - UpdateAllControls(); - } - else if (buttonName == nameof(panelClicked.moveUpBtn)) - { - Queue.MoveQueuePosition(item, QueuePosition.OneUp); - UpdateControl(queueIndex); - if (queueIndex > 0) - UpdateControl(queueIndex - 1); - } - else if (buttonName == nameof(panelClicked.moveDownBtn)) - { - Queue.MoveQueuePosition(item, QueuePosition.OneDown); - UpdateControl(queueIndex); - if (queueIndex + 1 < Queue.Count) - UpdateControl(queueIndex + 1); - } - else if (buttonName == nameof(panelClicked.moveLastBtn)) - { - Queue.MoveQueuePosition(item, QueuePosition.Last); - UpdateAllControls(); + Serilog.Log.Logger.Error(ex, "Error handling button click from queued item"); } } From af38750e29d79744f1eaa5cd1b3002f276f7cdcc Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 16:19:00 -0600 Subject: [PATCH 16/20] Fix reverted changes --- Documentation/Advanced.md | 2 +- Source/AppScaffolding/AppScaffolding.csproj | 2 +- Source/FileManager/ReplacementCharacters.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Documentation/Advanced.md b/Documentation/Advanced.md index 66117b29..b7954f7d 100644 --- a/Documentation/Advanced.md +++ b/Documentation/Advanced.md @@ -27,7 +27,7 @@ To make upgrades and reinstalls easier, Libation separates all of its responsibi ### Linux and Mac -Although Libation only currently officially supports Windows, some users have had success with WINE. ([Linux](https://github.com/rmcrackan/Libation/issues/28#issuecomment-890594158), [OSX Crossover and WINE](https://github.com/rmcrackan/Libation/issues/150#issuecomment-1004918592)) +Although Libation only currently officially supports Windows, some users have had success with WINE. ([Linux](https://github.com/rmcrackan/Libation/issues/28#issuecomment-890594158), [OSX Crossover and WINE](https://github.com/rmcrackan/Libation/issues/150#issuecomment-1004918592), [Linux and WINE](https://github.com/rmcrackan/Libation/issues/28#issuecomment-1161111014)) ### Settings diff --git a/Source/AppScaffolding/AppScaffolding.csproj b/Source/AppScaffolding/AppScaffolding.csproj index b0604e58..b7882bd1 100644 --- a/Source/AppScaffolding/AppScaffolding.csproj +++ b/Source/AppScaffolding/AppScaffolding.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <TargetFramework>net6.0-windows</TargetFramework> - <Version>8.1.0.1</Version> + <Version>8.1.1.1</Version> </PropertyGroup> <ItemGroup> diff --git a/Source/FileManager/ReplacementCharacters.cs b/Source/FileManager/ReplacementCharacters.cs index 4ca79e69..690c279e 100644 --- a/Source/FileManager/ReplacementCharacters.cs +++ b/Source/FileManager/ReplacementCharacters.cs @@ -195,7 +195,7 @@ namespace FileManager { var c = pathStr[i]; - if (!invalidChars.Contains(c) || (i <= 2 && Path.IsPathRooted(pathStr))) + if (!invalidChars.Contains(c) || (c == ':' && i == 1 && Path.IsPathRooted(pathStr))) builder.Append(c); else { From 7e00162ef2531177f21bdefec85208697dfcf7ed Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 16:28:21 -0600 Subject: [PATCH 17/20] Code reuse and better naming --- Source/FileManager/FileNamingTemplate.cs | 6 +++++- Source/FileManager/FileUtility.cs | 2 +- Source/FileManager/ReplacementCharacters.cs | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Source/FileManager/FileNamingTemplate.cs b/Source/FileManager/FileNamingTemplate.cs index e1a782ea..c00232a9 100644 --- a/Source/FileManager/FileNamingTemplate.cs +++ b/Source/FileManager/FileNamingTemplate.cs @@ -15,7 +15,11 @@ namespace FileManager /// <summary>Generate a valid path for this file or directory</summary> public LongPath GetFilePath(ReplacementCharacters replacements, bool returnFirstExisting = false) { - string fileName = Template.EndsWith(Path.DirectorySeparatorChar) ? Template[..^1] : Template; + string fileName = + Template.EndsWith(Path.DirectorySeparatorChar) || Template.EndsWith(Path.AltDirectorySeparatorChar) ? + FileUtility.RemoveLastCharacter(Template) : + Template; + List<string> pathParts = new(); var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value, replacements)); diff --git a/Source/FileManager/FileUtility.cs b/Source/FileManager/FileUtility.cs index 6cc42380..4103cb47 100644 --- a/Source/FileManager/FileUtility.cs +++ b/Source/FileManager/FileUtility.cs @@ -84,7 +84,7 @@ namespace FileManager var pathNoPrefix = path.PathWithoutPrefix; - pathNoPrefix = replacements.ReplaceInvalidChars(pathNoPrefix); + pathNoPrefix = replacements.ReplaceInvalidPathChars(pathNoPrefix); pathNoPrefix = removeDoubleSlashes(pathNoPrefix); return pathNoPrefix; diff --git a/Source/FileManager/ReplacementCharacters.cs b/Source/FileManager/ReplacementCharacters.cs index 690c279e..8c839e4c 100644 --- a/Source/FileManager/ReplacementCharacters.cs +++ b/Source/FileManager/ReplacementCharacters.cs @@ -185,7 +185,7 @@ namespace FileManager return builder.ToString(); } - public string ReplaceInvalidChars(string pathStr) + public string ReplaceInvalidPathChars(string pathStr) { if (string.IsNullOrEmpty(pathStr)) return string.Empty; From bc9625fece93068d85ab357d541a09471f01153a Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 16:36:56 -0600 Subject: [PATCH 18/20] Disallow illegal chars in templates --- Source/LibationFileManager/Templates.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Source/LibationFileManager/Templates.cs b/Source/LibationFileManager/Templates.cs index 7a856c73..f9a4d06f 100644 --- a/Source/LibationFileManager/Templates.cs +++ b/Source/LibationFileManager/Templates.cs @@ -54,10 +54,7 @@ namespace LibationFileManager if (template is null) return new[] { ERROR_NULL_IS_INVALID }; - if (template.Contains(':') - || template.Contains(Path.DirectorySeparatorChar) - || template.Contains(Path.AltDirectorySeparatorChar) - ) + if (ReplacementCharacters.ContainsInvalid(template.Replace("<","").Replace(">",""))) return new[] { ERROR_INVALID_FILE_NAME_CHAR }; return Valid; @@ -200,6 +197,10 @@ namespace LibationFileManager if (template.Contains(':')) return new[] { ERROR_FULL_PATH_IS_INVALID }; + // must be relative. no colons. all other path chars are valid enough to pass this check and will be handled on final save. + if (ReplacementCharacters.ContainsInvalid(template.Replace("<", "").Replace(">", ""))) + return new[] { ERROR_INVALID_FILE_NAME_CHAR }; + return Valid; } From 9c53d9bf87875c2739aafd31a8811a783981ceda Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 16:52:13 -0600 Subject: [PATCH 19/20] Better open/close quote detection --- Source/FileManager/ReplacementCharacters.cs | 22 +++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Source/FileManager/ReplacementCharacters.cs b/Source/FileManager/ReplacementCharacters.cs index 8c839e4c..7cbdf322 100644 --- a/Source/FileManager/ReplacementCharacters.cs +++ b/Source/FileManager/ReplacementCharacters.cs @@ -133,18 +133,20 @@ namespace FileManager { if (toReplace == Replacement.QUOTE_MARK) { - if ( - preceding != default - && !char.IsLetter(preceding) - && !char.IsNumber(preceding) - && (char.IsLetter(succeding) || char.IsNumber(succeding)) + if (preceding == default || + (preceding != default + && !char.IsLetter(preceding) + && !char.IsNumber(preceding) + && (char.IsLetter(succeding) || char.IsNumber(succeding)) + ) ) return OpenQuote; - else if ( - succeding != default - && !char.IsLetter(succeding) - && !char.IsNumber(succeding) - && (char.IsLetter(preceding) || char.IsNumber(preceding)) + else if (succeding == default || + (succeding != default + && !char.IsLetter(succeding) + && !char.IsNumber(succeding) + && (char.IsLetter(preceding) || char.IsNumber(preceding)) + ) ) return CloseQuote; else From 13149eff08cb0e7b58ffe193c144a20616541c86 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Thu, 23 Jun 2022 17:29:45 -0600 Subject: [PATCH 20/20] Make better use of heirarch chapters to combine section title audio (which is usually short, eg "Part 1") with the following full-length chapter. --- Source/FileLiberator/DownloadDecryptBook.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index f3686ca5..257a17cf 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -194,19 +194,31 @@ namespace FileLiberator private List<AudibleApi.Common.Chapter> getChapters(IEnumerable<AudibleApi.Common.Chapter> chapters) { - List<AudibleApi.Common.Chapter> chaps = chapters.ToList(); + List<AudibleApi.Common.Chapter> chaps = new(); foreach (var c in chapters) { if (c.Chapters is not null) { - var children = getChapters(c.Chapters); + var firstSub = new AudibleApi.Common.Chapter + { + Title = $"{c.Title}: {c.Chapters[0].Title}", + StartOffsetMs = c.StartOffsetMs, + StartOffsetSec = c.StartOffsetSec, + LengthMs = c.LengthMs + c.Chapters[0].LengthMs + }; + + chaps.Add(firstSub); + + var children = getChapters(c.Chapters[1..]); foreach (var child in children) child.Title = string.IsNullOrEmpty(c.Title) ? child.Title : $"{c.Title}: {child.Title}"; chaps.AddRange(children); } + else + chaps.Add(c); } return chaps; }