Add character replacement

This commit is contained in:
Michael Bucari-Tovo 2022-06-21 18:34:47 -06:00
parent b7fd87b09c
commit 1470aefd42
19 changed files with 654 additions and 115 deletions

View File

@ -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);

View File

@ -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);
}

View File

@ -4,6 +4,7 @@ namespace AaxDecrypter
{
public interface IDownloadOptions
{
FileManager.ReplacementCharacters ReplacementCharacters { get; }
string DownloadUrl { get; }
string UserAgent { get; }
string AudibleKey { get; }

View File

@ -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();
}
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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();

View File

@ -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)

View File

@ -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);

View File

@ -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)

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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
{

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();