diff --git a/AppScaffolding/AppScaffolding.csproj b/AppScaffolding/AppScaffolding.csproj index 8f7f0fae..444aabf4 100644 --- a/AppScaffolding/AppScaffolding.csproj +++ b/AppScaffolding/AppScaffolding.csproj @@ -3,7 +3,7 @@ net5.0 - 6.3.3.1 + 6.3.4.1 diff --git a/FileManager/FileTemplate.cs b/FileManager/FileTemplate.cs index 8fab2de0..6848aba0 100644 --- a/FileManager/FileTemplate.cs +++ b/FileManager/FileTemplate.cs @@ -45,8 +45,20 @@ namespace FileManager .Replace(">", ""); private string formatValue(object value) - => ParameterMaxSize.HasValue && ParameterMaxSize.Value > 0 - ? value?.ToString().Truncate(ParameterMaxSize.Value) - : value?.ToString(); + { + 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. + var val = value + .ToString() + .Replace($"{System.IO.Path.DirectorySeparatorChar}", IllegalCharacterReplacements) + .Replace($"{System.IO.Path.AltDirectorySeparatorChar}", IllegalCharacterReplacements); + return + ParameterMaxSize.HasValue && ParameterMaxSize.Value > 0 + ? val.Truncate(ParameterMaxSize.Value) + : val; + } } } diff --git a/LibationFileManager/Templates.cs b/LibationFileManager/Templates.cs index d33ae7fc..934a7d08 100644 --- a/LibationFileManager/Templates.cs +++ b/LibationFileManager/Templates.cs @@ -16,8 +16,6 @@ namespace LibationFileManager public const string WARNING_NO_TAGS = "Should use tags. Eg: "; public const string WARNING_HAS_CHAPTER_TAGS = "Chapter tags should only be used in the template used for naming files which are split by chapter. Eg: <ch title>"; public const string WARNING_NO_CHAPTER_NUMBER_TAG = "Should include chapter number tag in template used for naming files which are split by chapter. Ie: <ch#> or <ch# 0>"; - // actual possible to se? - public const string WARNING_NO_CHAPTER_TAGS = "Should include chapter tags in template used for naming files which are split by chapter. Eg: <ch title>"; public static Templates Folder { get; } = new FolderTemplate(); public static Templates File { get; } = new FileTemplate(); @@ -26,6 +24,7 @@ namespace LibationFileManager public abstract string Name { get; } public abstract string Description { get; } public abstract string DefaultTemplate { get; } + protected abstract bool IsChapterized { get; } public abstract IEnumerable<string> GetErrors(string template); public bool IsValid(string template) => !GetErrors(template).Any(); @@ -33,7 +32,17 @@ namespace LibationFileManager public abstract IEnumerable<string> GetWarnings(string template); public bool HasWarnings(string template) => GetWarnings(template).Any(); - public abstract int TagCount(string template); + public IEnumerable<TemplateTags> GetTemplateTags() + => TemplateTags.GetAll() + // yeah, this line is a little funky but it works when you think through it. also: trust the unit tests + .Where(t => IsChapterized || !t.IsChapterOnly); + + public int TagCount(string template) + => GetTemplateTags() + // for <id><id> == 1, use: + // .Count(t => template.Contains($"<{t.TagName}>")) + // .Sum() impl: <id><id> == 2 + .Sum(t => template.Split($"<{t.TagName}>").Length - 1); public static bool ContainsChapterOnlyTags(string template) => TemplateTags.GetAll() @@ -59,7 +68,7 @@ namespace LibationFileManager return Valid; } - protected IEnumerable<string> getWarnings(string template, bool isChapter) + protected IEnumerable<string> getWarnings(string template) { var warnings = GetErrors(template).ToList(); if (template is null) @@ -73,28 +82,18 @@ namespace LibationFileManager if (TagCount(template) == 0) warnings.Add(WARNING_NO_TAGS); - var containsChapterOnlyTags = ContainsChapterOnlyTags(template); - if (isChapter && !containsChapterOnlyTags) - warnings.Add(WARNING_NO_CHAPTER_TAGS); - if (!isChapter && containsChapterOnlyTags) + if (!IsChapterized && ContainsChapterOnlyTags(template)) warnings.Add(WARNING_HAS_CHAPTER_TAGS); return warnings; } - protected static int tagCount(string template, Func<TemplateTags, bool> func) - => TemplateTags.GetAll() - .Where(func) - // for <id><id> == 1, use: - // .Count(t => template.Contains($"<{t.TagName}>")) - // .Sum() impl: <id><id> == 2 - .Sum(t => template.Split($"<{t.TagName}>").Length - 1); - private class FolderTemplate : Templates { public override string Name => "Folder Template"; public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate)); public override string DefaultTemplate { get; } = "<title short> [<id>]"; + protected override bool IsChapterized { get; } = false; public override IEnumerable<string> GetErrors(string template) { @@ -109,9 +108,7 @@ namespace LibationFileManager return Valid; } - public override IEnumerable<string> GetWarnings(string template) => getWarnings(template, false); - - public override int TagCount(string template) => tagCount(template, t => !t.IsChapterOnly); + public override IEnumerable<string> GetWarnings(string template) => getWarnings(template); } private class FileTemplate : Templates @@ -119,12 +116,11 @@ namespace LibationFileManager public override string Name => "File Template"; public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate)); public override string DefaultTemplate { get; } = "<title> [<id>]"; + protected override bool IsChapterized { get; } = false; public override IEnumerable<string> GetErrors(string template) => getFileErrors(template); - public override IEnumerable<string> GetWarnings(string template) => getWarnings(template, false); - - public override int TagCount(string template) => tagCount(template, t => !t.IsChapterOnly); + public override IEnumerable<string> GetWarnings(string template) => getWarnings(template); } private class ChapterFileTemplate : Templates @@ -132,26 +128,22 @@ namespace LibationFileManager public override string Name => "Chapter File Template"; public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)); public override string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>"; - + protected override bool IsChapterized { get; } = true; + public override IEnumerable<string> GetErrors(string template) => getFileErrors(template); public override IEnumerable<string> GetWarnings(string template) { - var warnings = getWarnings(template, true).ToList(); + var warnings = getWarnings(template).ToList(); if (template is null) return warnings; // recommended to incl. <ch#> or <ch# 0> if (!ContainsTag(template, TemplateTags.ChNumber.TagName) && !ContainsTag(template, TemplateTags.ChNumber0.TagName)) - { - warnings.Remove(WARNING_NO_CHAPTER_TAGS); warnings.Add(WARNING_NO_CHAPTER_NUMBER_TAG); - } return warnings; } - - public override int TagCount(string template) => tagCount(template, t => true); } } } diff --git a/LibationWinForms/Dialogs/EditTemplateDialog.Designer.cs b/LibationWinForms/Dialogs/EditTemplateDialog.Designer.cs index 79f072d9..7bf11cce 100644 --- a/LibationWinForms/Dialogs/EditTemplateDialog.Designer.cs +++ b/LibationWinForms/Dialogs/EditTemplateDialog.Designer.cs @@ -34,6 +34,9 @@ this.templateLbl = new System.Windows.Forms.Label(); this.resetToDefaultBtn = new System.Windows.Forms.Button(); this.outputTb = new System.Windows.Forms.TextBox(); + this.listView1 = new System.Windows.Forms.ListView(); + this.columnHeader1 = new System.Windows.Forms.ColumnHeader(); + this.columnHeader2 = new System.Windows.Forms.ColumnHeader(); this.SuspendLayout(); // // saveBtn @@ -93,12 +96,41 @@ // // outputTb // - this.outputTb.Location = new System.Drawing.Point(12, 153); + this.outputTb.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.outputTb.Location = new System.Drawing.Point(346, 56); this.outputTb.Multiline = true; this.outputTb.Name = "outputTb"; this.outputTb.ReadOnly = true; - this.outputTb.Size = new System.Drawing.Size(759, 205); - this.outputTb.TabIndex = 100; + this.outputTb.ScrollBars = System.Windows.Forms.ScrollBars.Both; + this.outputTb.Size = new System.Drawing.Size(574, 434); + this.outputTb.TabIndex = 4; + // + // listView1 + // + this.listView1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left))); + this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.columnHeader1, + this.columnHeader2}); + this.listView1.HideSelection = false; + this.listView1.Location = new System.Drawing.Point(12, 56); + this.listView1.Name = "listView1"; + this.listView1.Size = new System.Drawing.Size(328, 434); + this.listView1.TabIndex = 100; + this.listView1.UseCompatibleStateImageBehavior = false; + this.listView1.View = System.Windows.Forms.View.Details; + // + // columnHeader1 + // + this.columnHeader1.Text = "Tag"; + this.columnHeader1.Width = 90; + // + // columnHeader2 + // + this.columnHeader2.Text = "Description"; + this.columnHeader2.Width = 230; // // EditTemplateDialog // @@ -107,6 +139,7 @@ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.cancelBtn; this.ClientSize = new System.Drawing.Size(933, 539); + this.Controls.Add(this.listView1); this.Controls.Add(this.outputTb); this.Controls.Add(this.resetToDefaultBtn); this.Controls.Add(this.templateLbl); @@ -131,5 +164,8 @@ private System.Windows.Forms.Label templateLbl; private System.Windows.Forms.Button resetToDefaultBtn; private System.Windows.Forms.TextBox outputTb; + private System.Windows.Forms.ListView listView1; + private System.Windows.Forms.ColumnHeader columnHeader1; + private System.Windows.Forms.ColumnHeader columnHeader2; } } \ No newline at end of file diff --git a/LibationWinForms/Dialogs/EditTemplateDialog.cs b/LibationWinForms/Dialogs/EditTemplateDialog.cs index aa317cef..03c668a6 100644 --- a/LibationWinForms/Dialogs/EditTemplateDialog.cs +++ b/LibationWinForms/Dialogs/EditTemplateDialog.cs @@ -39,16 +39,32 @@ namespace LibationWinForms.Dialogs this.templateLbl.Text = template.Description; this.templateTb.Text = inputTemplateText; + + // populate list view + foreach (var tag in template.GetTemplateTags()) + listView1.Items.Add(new ListViewItem(new[] { $"<{tag.TagName}>", tag.Description })); } private void resetToDefaultBtn_Click(object sender, EventArgs e) => templateTb.Text = template.DefaultTemplate; private void templateTb_TextChanged(object sender, EventArgs e) { + var t = templateTb.Text; + + var warnings + = !template.HasWarnings(t) + ? "" + : "Warnings:\r\n" + + template + .GetWarnings(t) + .Select(err => $"- {err}") + .Aggregate((a, b) => $"{a}\r\n{b}"); + + var books = config.Books; - var folderTemplate = template == Templates.Folder ? templateTb.Text : config.FolderTemplate; + var folderTemplate = template == Templates.Folder ? t : config.FolderTemplate; folderTemplate = folderTemplate.Trim().Trim(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).Trim(); - var fileTemplate = template == Templates.Folder ? config.FileTemplate : templateTb.Text; + var fileTemplate = template == Templates.Folder ? config.FileTemplate : t; fileTemplate = fileTemplate.Trim(); var ext = config.DecryptToLossy ? "mp3" : "m4b"; @@ -56,13 +72,23 @@ namespace LibationWinForms.Dialogs // this logic should be external path = path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + var sing = $"{Path.DirectorySeparatorChar}"; var dbl = $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}"; while (path.Contains(dbl)) - path = path.Replace(dbl, $"{Path.DirectorySeparatorChar}"); + path = path.Replace(dbl, sing); + + // once path is finalized + const char ZERO_WIDTH_SPACE = '\u200B'; + path = path.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}"); + // result: can wrap long paths. eg: + // |-- LINE WRAP BOUNDARIES --| + // \books\author with a very <= normal line break on space between words + // long name\narrator narrator + // \title <= line break on the zero-with space we added before slashes var book = new DataLayer.Book( new DataLayer.AudibleProductId("123456789"), - "A Study in Scarlet", + "A Study in Scarlet: A Sherlock Holmes Novel", "Fake description", 1234, DataLayer.ContentType.Product, @@ -78,6 +104,9 @@ namespace LibationWinForms.Dialogs var libraryBook = new DataLayer.LibraryBook(book, DateTime.Now, "my account"); outputTb.Text = @$" + +Example: + {books} {folderTemplate} {fileTemplate} @@ -89,14 +118,21 @@ namespace LibationWinForms.Dialogs {book.AuthorNames} {book.NarratorNames} series: {"Sherlock Holmes"} -"; + +{warnings} + +".Trim(); } private void saveBtn_Click(object sender, EventArgs e) { if (!template.IsValid(templateTb.Text)) { - MessageBox.Show("This template text is not valid.", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error); + var errors = template + .GetErrors(templateTb.Text) + .Select(err => $"- {err}") + .Aggregate((a, b) => $"{a}\r\n{b}"); + MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } diff --git a/_Tests/FileManager.Tests/FileTemplateTests.cs b/_Tests/FileManager.Tests/FileTemplateTests.cs index 5567795a..c958e9b1 100644 --- a/_Tests/FileManager.Tests/FileTemplateTests.cs +++ b/_Tests/FileManager.Tests/FileTemplateTests.cs @@ -104,5 +104,13 @@ namespace FileTemplateTests return fileTemplate.GetFilePath(); } + + [TestMethod] + public void remove_slashes() + { + var fileTemplate = new FileTemplate(@"\foo\<title>.txt"); + fileTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s"); + fileTemplate.GetFilePath().Should().Be(@"\foo\slashes.txt"); + } } }