diff --git a/Source/AppScaffolding/AppScaffolding.csproj b/Source/AppScaffolding/AppScaffolding.csproj index 418149e0..51a8046c 100644 --- a/Source/AppScaffolding/AppScaffolding.csproj +++ b/Source/AppScaffolding/AppScaffolding.csproj @@ -5,7 +5,7 @@ 10.6.3.1 - + diff --git a/Source/FileManager/BackgroundFileSystem.cs b/Source/FileManager/BackgroundFileSystem.cs index 3b815c43..877f9160 100644 --- a/Source/FileManager/BackgroundFileSystem.cs +++ b/Source/FileManager/BackgroundFileSystem.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +#nullable enable namespace FileManager { /// @@ -16,9 +17,9 @@ namespace FileManager public string SearchPattern { get; private set; } public SearchOption SearchOption { get; private set; } - private FileSystemWatcher fileSystemWatcher { get; set; } - private BlockingCollection directoryChangesEvents { get; set; } - private Task backgroundScanner { get; set; } + private FileSystemWatcher? fileSystemWatcher { get; set; } + private BlockingCollection? directoryChangesEvents { get; set; } + private Task? backgroundScanner { get; set; } private object fsCacheLocker { get; } = new(); private List fsCache { get; } = new(); @@ -32,7 +33,7 @@ namespace FileManager Init(); } - public LongPath FindFile(System.Text.RegularExpressions.Regex regex) + public LongPath? FindFile(System.Text.RegularExpressions.Regex regex) { lock (fsCacheLocker) return fsCache.FirstOrDefault(s => regex.IsMatch(s)); @@ -105,13 +106,13 @@ namespace FileManager private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e) { - directoryChangesEvents.Add(e); + directoryChangesEvents?.Add(e); } #region Background Thread private void BackgroundScanner() { - while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1)) + while (directoryChangesEvents?.TryTake(out var change, -1) is true) { lock (fsCacheLocker) UpdateLocalCache(change); diff --git a/Source/FileManager/FileUtility.cs b/Source/FileManager/FileUtility.cs index 748d27ec..e6d9074c 100644 --- a/Source/FileManager/FileUtility.cs +++ b/Source/FileManager/FileUtility.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -7,20 +8,20 @@ using Dinah.Core; using Polly; using Polly.Retry; +#nullable enable namespace FileManager { public static class FileUtility { - - /// /// "txt" => ".txt" ///
".txt" => ".txt" ///
null or whitespace => "" ///
- public static string GetStandardizedExtension(string extension) + [return: NotNull] + public static string GetStandardizedExtension(string? extension) => string.IsNullOrWhiteSpace(extension) - ? (extension ?? "")?.Trim() + ? string.Empty : '.' + extension.Trim().Trim('.'); /// @@ -48,18 +49,18 @@ namespace FileManager ///
- ensure uniqueness ///
- enforce max file length ///
- public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, string fileExtension, bool returnFirstExisting = false) + public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, string? fileExtension, bool returnFirstExisting = false) { ArgumentValidator.EnsureNotNull(path, nameof(path)); - ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension)); + ArgumentValidator.EnsureNotNull(replacements, nameof(replacements)); + fileExtension = GetStandardizedExtension(fileExtension); // remove invalid chars path = GetSafePath(path, replacements); // ensure uniqueness and check lengths - var dir = Path.GetDirectoryName(path); - dir = dir?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty; + var dir = Path.GetDirectoryName(path)?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty; var fileName = Path.GetFileName(path); var extIndex = fileName.LastIndexOf(fileExtension, StringComparison.OrdinalIgnoreCase); @@ -84,6 +85,7 @@ namespace FileManager public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements) { ArgumentValidator.EnsureNotNull(path, nameof(path)); + ArgumentValidator.EnsureNotNull(replacements, nameof(replacements)); var pathNoPrefix = path.PathWithoutPrefix; @@ -159,7 +161,7 @@ namespace FileManager LongPath source, LongPath destination, ReplacementCharacters replacements, - string extension = null, + string? extension = null, bool overwrite = false) { extension ??= Path.GetExtension(source); @@ -213,6 +215,9 @@ namespace FileManager SaferDelete(destination); var dir = Path.GetDirectoryName(destination); + if (dir is null) + throw new DirectoryNotFoundException(); + Serilog.Log.Logger.Debug("Attempt to create directory: {@DebugText}", new { dir }); Directory.CreateDirectory(dir); diff --git a/Source/FileManager/LogArchiver.cs b/Source/FileManager/LogArchiver.cs index 248d26eb..04342220 100644 --- a/Source/FileManager/LogArchiver.cs +++ b/Source/FileManager/LogArchiver.cs @@ -8,9 +8,10 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +#nullable enable namespace FileManager { - public sealed class LogArchiver : IAsyncDisposable + public sealed class LogArchiver : IAsyncDisposable, IDisposable { public Encoding Encoding { get; set; } public string FileName { get; } @@ -39,28 +40,28 @@ namespace FileManager e.Delete(); } - public async Task AddFileAsync(string name, JObject contents, string comment = null) + public async Task AddFileAsync(string name, JObject contents, string? comment = null) { ArgumentValidator.EnsureNotNull(contents, nameof(contents)); await AddFileAsync(name, Encoding.GetBytes(contents.ToString(Newtonsoft.Json.Formatting.Indented)), comment); } - public async Task AddFileAsync(string name, string contents, string comment = null) + public async Task AddFileAsync(string name, string contents, string? comment = null) { ArgumentValidator.EnsureNotNull(contents, nameof(contents)); await AddFileAsync(name, Encoding.GetBytes(contents), comment); } - public Task AddFileAsync(string name, ReadOnlyMemory contents, string comment = null) + public Task AddFileAsync(string name, ReadOnlyMemory contents, string? comment = null) { ArgumentValidator.EnsureNotNull(name, nameof(name)); name = ReplacementCharacters.Barebones.ReplaceFilenameChars(name); - return Task.Run(() => AddfileInternal(name, contents.Span, comment)); + return Task.Run(() => AddFileInternal(name, contents.Span, comment)); } private readonly object lockObj = new(); - private void AddfileInternal(string name, ReadOnlySpan contents, string comment) + private void AddFileInternal(string name, ReadOnlySpan contents, string? comment) { lock (lockObj) { @@ -73,5 +74,7 @@ namespace FileManager } public async ValueTask DisposeAsync() => await Task.Run(archive.Dispose); + + public void Dispose() => archive.Dispose(); } } diff --git a/Source/FileManager/LongPath.cs b/Source/FileManager/LongPath.cs index 956f46a4..a6e00e3d 100644 --- a/Source/FileManager/LongPath.cs +++ b/Source/FileManager/LongPath.cs @@ -1,9 +1,11 @@ using Newtonsoft.Json; using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; using System.Text; +#nullable enable namespace FileManager { public class LongPath @@ -15,9 +17,9 @@ namespace FileManager public static readonly int MaxPathLength; private const int WIN_MAX_PATH = 260; private const string WIN_LONG_PATH_PREFIX = @"\\?\"; - internal static readonly bool IsWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - internal static readonly bool IsLinux = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - internal static readonly bool IsOSX = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + internal static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + internal static readonly bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + internal static readonly bool IsOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); public string Path { get; } @@ -60,7 +62,8 @@ namespace FileManager => IsWindows ? filename.Length : Encoding.UTF8.GetByteCount(filename); - public static implicit operator LongPath(string path) + [return: NotNullIfNotNull(nameof(path))] + public static implicit operator LongPath?(string? path) { if (path is null) return null; @@ -93,7 +96,8 @@ namespace FileManager } } - public static implicit operator string(LongPath path) => path?.Path; + [return: NotNullIfNotNull(nameof(path))] + public static implicit operator string?(LongPath? path) => path?.Path; [JsonIgnore] public string ShortPathName @@ -127,8 +131,6 @@ namespace FileManager //for newly-created entries in ther file system. Existing entries made while //8dot3 names were disabled will not be reachable by short paths. - if (Path is null) return null; - StringBuilder shortPathBuffer = new(MaxPathLength); GetShortPathName(Path, shortPathBuffer, MaxPathLength); return shortPathBuffer.ToString(); @@ -141,7 +143,6 @@ namespace FileManager get { if (!IsWindows) return Path; - if (Path is null) return null; StringBuilder longPathBuffer = new(MaxPathLength); GetLongPathName(Path, longPathBuffer, MaxPathLength); @@ -156,17 +157,18 @@ namespace FileManager { if (!IsWindows) return Path; return - Path?.StartsWith(WIN_LONG_PATH_PREFIX) == true ? Path.Remove(0, WIN_LONG_PATH_PREFIX.Length) - :Path; + Path.StartsWith(WIN_LONG_PATH_PREFIX) + ? Path.Remove(0, WIN_LONG_PATH_PREFIX.Length) + : Path; } } public override string ToString() => Path; public override int GetHashCode() => Path.GetHashCode(); - public override bool Equals(object obj) => obj is LongPath other && Path == other.Path; - public static bool operator ==(LongPath path1, LongPath path2) => path1.Equals(path2); - public static bool operator !=(LongPath path1, LongPath path2) => !path1.Equals(path2); + public override bool Equals(object? obj) => obj is LongPath other && Path == other.Path; + public static bool operator ==(LongPath? path1, LongPath? path2) => path1?.Equals(path2) is true; + public static bool operator !=(LongPath? path1, LongPath? path2) => path1 is null || path2 is null || !path1.Equals(path2); [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] diff --git a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs index c2d2a8b1..1cefe52f 100644 --- a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs @@ -1,7 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Text.RegularExpressions; +#nullable enable namespace FileManager.NamingTemplate; internal interface IClosingPropertyTag : IPropertyTag @@ -17,7 +19,7 @@ internal interface IClosingPropertyTag : IPropertyTag /// The substring that was matched. /// The registered /// True if the starts with this tag. - bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag propertyTag); + bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag); } public class ConditionalTagCollection : TagCollection @@ -37,6 +39,7 @@ public class ConditionalTagCollection : TagCollection private class ConditionalTag : TagBase, IClosingPropertyTag { + public override Regex NameMatcher { get; } public Regex NameCloseMatcher { get; } public ConditionalTag(ITemplateTag templateTag, RegexOptions options, Expression conditionExpression) @@ -46,7 +49,7 @@ public class ConditionalTagCollection : TagCollection NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options); } - public bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag propertyTag) + public bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag) { var match = NameCloseMatcher.Match(templateString); if (match.Success) diff --git a/Source/FileManager/NamingTemplate/NamingTemplate.cs b/Source/FileManager/NamingTemplate/NamingTemplate.cs index acd51bb1..0c243398 100644 --- a/Source/FileManager/NamingTemplate/NamingTemplate.cs +++ b/Source/FileManager/NamingTemplate/NamingTemplate.cs @@ -1,19 +1,21 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; +#nullable enable namespace FileManager.NamingTemplate; public class NamingTemplate { - public string TemplateText { get; private set; } + public string TemplateText { get; private set; } = string.Empty; public IEnumerable TagsInUse => _tagsInUse; public IEnumerable TagsRegistered => TagCollections.SelectMany(t => t).DistinctBy(t => t.TagName); public IEnumerable Warnings => errors.Concat(warnings); public IEnumerable Errors => errors; - private Delegate templateToString; + private Delegate? templateToString; private readonly List warnings = new(); private readonly List errors = new(); private readonly IEnumerable TagCollections; @@ -30,6 +32,9 @@ public class NamingTemplate /// Instances of the TClass used in and public TemplatePart Evaluate(params object[] propertyClasses) { + if (templateToString is null) + throw new InvalidOperationException(); + // Match propertyClasses to the arguments required by templateToString.DynamicInvoke(). // First parameter is "this", so ignore it. var delegateArgTypes = templateToString.Method.GetParameters().Skip(1); @@ -39,7 +44,7 @@ public class NamingTemplate if (args.Length != delegateArgTypes.Count()) throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}"); - return ((TemplatePart)templateToString.DynamicInvoke(args)).FirstPart; + return (templateToString.DynamicInvoke(args) as TemplatePart)!.FirstPart; } /// Parse a template string to a @@ -69,7 +74,7 @@ public class NamingTemplate } /// Builds an tree that will evaluate to a - private static Expression GetExpressionTree(BinaryNode node) + private static Expression GetExpressionTree(BinaryNode? node) { if (node is null) return TemplatePart.Blank; else if (node.IsValue) return node.Expression; @@ -81,10 +86,10 @@ public class NamingTemplate } /// Parse a template string into a tree - private BinaryNode IntermediateParse(string templateString) + private BinaryNode IntermediateParse(string? templateString) { if (templateString is null) - throw new NullReferenceException(ERROR_NULL_IS_INVALID); + throw new ArgumentException(ERROR_NULL_IS_INVALID); else if (string.IsNullOrEmpty(templateString)) warnings.Add(WARNING_EMPTY); else if (string.IsNullOrWhiteSpace(templateString)) @@ -93,12 +98,12 @@ public class NamingTemplate TemplateText = templateString; BinaryNode topNode = BinaryNode.CreateRoot(); - BinaryNode currentNode = topNode; + BinaryNode? currentNode = topNode; List literalChars = new(); while (templateString.Length > 0) { - if (StartsWith(templateString, out string exactPropertyName, out var propertyTag, out var valueExpression)) + if (StartsWith(templateString, out var exactPropertyName, out var propertyTag, out var valueExpression)) { checkAndAddLiterals(); @@ -116,7 +121,7 @@ public class NamingTemplate { checkAndAddLiterals(); - BinaryNode lastParenth = currentNode; + BinaryNode? lastParenth = currentNode; while (lastParenth?.IsConditional is false) lastParenth = lastParenth.Parent; @@ -168,7 +173,7 @@ public class NamingTemplate } } - private bool StartsWith(string template, out string exactName, out IPropertyTag propertyTag, out Expression valueExpression) + private bool StartsWith(string template, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? valueExpression) { foreach (var pc in TagCollections) { @@ -182,7 +187,7 @@ public class NamingTemplate return false; } - private bool StartsWithClosing(string template, out string exactName, out IClosingPropertyTag closingPropertyTag) + private bool StartsWithClosing(string template, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? closingPropertyTag) { foreach (var pc in TagCollections) { @@ -198,36 +203,36 @@ public class NamingTemplate private class BinaryNode { public string Name { get; } - public BinaryNode Parent { get; private set; } - public BinaryNode RightChild { get; private set; } - public BinaryNode LeftChild { get; private set; } - public Expression Expression { get; private init; } + public BinaryNode? Parent { get; private set; } + public BinaryNode? RightChild { get; private set; } + public BinaryNode? LeftChild { get; private set; } + public Expression Expression { get; } public bool IsConditional { get; private init; } = false; public bool IsValue { get; private init; } = false; - public static BinaryNode CreateRoot() => new("Root"); + public static BinaryNode CreateRoot() => new("Root", Expression.Empty()); - public static BinaryNode CreateValue(string literal) => new("Literal") - { - IsValue = true, - Expression = TemplatePart.CreateLiteral(literal) - }; + public static BinaryNode CreateValue(string literal) + => new("Literal", TemplatePart.CreateLiteral(literal)) + { + IsValue = true + }; - public static BinaryNode CreateValue(ITemplateTag templateTag, Expression property) => new(templateTag.TagName) - { - IsValue = true, - Expression = TemplatePart.CreateProperty(templateTag, property) - }; + public static BinaryNode CreateValue(ITemplateTag templateTag, Expression property) + => new(templateTag.TagName, TemplatePart.CreateProperty(templateTag, property)) + { + IsValue = true + }; - public static BinaryNode CreateConditional(ITemplateTag templateTag, Expression property) => new(templateTag.TagName) - { - IsConditional = true, - Expression = property - }; + public static BinaryNode CreateConditional(ITemplateTag templateTag, Expression property) + => new(templateTag.TagName, property) + { + IsConditional = true + }; private static BinaryNode CreateConcatenation(BinaryNode left, BinaryNode right) { - var newNode = new BinaryNode("Concatenation") + var newNode = new BinaryNode("Concatenation", Expression.Empty()) { LeftChild = left, RightChild = right @@ -237,7 +242,12 @@ public class NamingTemplate return newNode; } - private BinaryNode(string name) => Name = name; + private BinaryNode(string name, Expression expression) + { + Name = name; + Expression = expression; + } + public override string ToString() => Name; public BinaryNode AddNewNode(BinaryNode newNode) diff --git a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs index 118956a7..9f3acc75 100644 --- a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs @@ -5,6 +5,7 @@ using System.Linq; using System.Linq.Expressions; using System.Text.RegularExpressions; +#nullable enable namespace FileManager.NamingTemplate; public delegate string PropertyFormatter(ITemplateTag templateTag, T value, string formatString); @@ -37,7 +38,7 @@ public class PropertyTagCollection : TagCollection /// Optional formatting function that accepts the property /// and a formatting string and returnes the value the formatted string. If , use the default /// formatter if present, or - public void Add(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter formatter = null) + public void Add(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter? formatter = null) where TProperty : struct => RegisterWithFormatter(templateTag, propertyGetter, formatter); @@ -59,7 +60,7 @@ public class PropertyTagCollection : TagCollection /// Optional formatting function that accepts the property /// and a formatting string and returnes the value formatted to string. If , use the default /// formatter if present, or - public void Add(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter formatter = null) + public void Add(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter? formatter = null) => RegisterWithFormatter(templateTag, propertyGetter, formatter); /// @@ -72,14 +73,15 @@ public class PropertyTagCollection : TagCollection => RegisterWithToString(templateTag, propertyGetter, toString); private void RegisterWithFormatter - (ITemplateTag templateTag, Func propertyGetter, PropertyFormatter formatter) + (ITemplateTag templateTag, Func propertyGetter, PropertyFormatter? formatter) { ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag)); ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter)); var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); + formatter ??= GetDefaultFormatter(); - if ((formatter ??= GetDefaultFormatter()) is null) + if (formatter is null) AddPropertyTag(new PropertyTag(templateTag, Options, expr, ToStringFunc)); else AddPropertyTag(new PropertyTag(templateTag, Options, expr, formatter)); @@ -97,7 +99,7 @@ public class PropertyTagCollection : TagCollection private static string ToStringFunc(T propertyValue) => propertyValue?.ToString() ?? ""; - private PropertyFormatter GetDefaultFormatter() + private PropertyFormatter? GetDefaultFormatter() { try { @@ -109,6 +111,7 @@ public class PropertyTagCollection : TagCollection private class PropertyTag : TagBase { + public override Regex NameMatcher { get; } private Func CreateToStringExpression { get; } public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PropertyFormatter formatter) diff --git a/Source/FileManager/NamingTemplate/TagBase.cs b/Source/FileManager/NamingTemplate/TagBase.cs index a6bf9864..264e48a3 100644 --- a/Source/FileManager/NamingTemplate/TagBase.cs +++ b/Source/FileManager/NamingTemplate/TagBase.cs @@ -1,7 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Text.RegularExpressions; +#nullable enable namespace FileManager.NamingTemplate; internal interface IPropertyTag @@ -22,13 +24,13 @@ internal interface IPropertyTag /// The substring that was matched. /// The that returns the property's value /// True if the starts with this tag. - bool StartsWith(string templateString, out string exactName, out Expression propertyValue); + bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue); } internal abstract class TagBase : IPropertyTag { public ITemplateTag TemplateTag { get; } - public Regex NameMatcher { get; protected init; } + public abstract Regex NameMatcher { get; } public Type ReturnType => ValueExpression.Type; protected Expression ValueExpression { get; } @@ -43,7 +45,7 @@ internal abstract class TagBase : IPropertyTag /// The optional format string in the match inside the square brackets protected abstract Expression GetTagExpression(string exactName, string formatter); - public bool StartsWith(string templateString, out string exactName, out Expression propertyValue) + public bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue) { var match = NameMatcher.Match(templateString); if (match.Success) diff --git a/Source/FileManager/NamingTemplate/TagCollection.cs b/Source/FileManager/NamingTemplate/TagCollection.cs index 8e793571..117c3a33 100644 --- a/Source/FileManager/NamingTemplate/TagCollection.cs +++ b/Source/FileManager/NamingTemplate/TagCollection.cs @@ -1,10 +1,12 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Text.RegularExpressions; +#nullable enable namespace FileManager.NamingTemplate; /// A collection of s registered to a single . @@ -32,7 +34,7 @@ public abstract class TagCollection : IEnumerable /// The substring that was matched. /// The that returns the 's value /// True if the starts with a tag registered in this class. - internal bool StartsWith(string templateString, out string exactName, out IPropertyTag propertyTag, out Expression propertyValue) + internal bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? propertyValue) { foreach (var p in PropertyTags) { @@ -57,7 +59,7 @@ public abstract class TagCollection : IEnumerable /// The substring that was matched. /// The registered /// True if the starts with this tag. - internal bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag closingPropertyTag) + internal bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? closingPropertyTag) { foreach (var cg in PropertyTags.OfType()) { diff --git a/Source/FileManager/NamingTemplate/TemplatePart.cs b/Source/FileManager/NamingTemplate/TemplatePart.cs index 1b7f7630..447a9e0f 100644 --- a/Source/FileManager/NamingTemplate/TemplatePart.cs +++ b/Source/FileManager/NamingTemplate/TemplatePart.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; +#nullable enable namespace FileManager.NamingTemplate; /// Represents one part of an evaluated . @@ -15,13 +16,13 @@ public class TemplatePart : IEnumerable /// The 's if is /// a registered property, otherwise for string literals. - public ITemplateTag TemplateTag { get; } + public ITemplateTag? TemplateTag { get; } /// The evaluated string. public string Value { get; } - private TemplatePart previous; - private TemplatePart next; + private TemplatePart? previous; + private TemplatePart? next; private TemplatePart(string name, string value) { TagName = name; @@ -53,14 +54,33 @@ public class TemplatePart : IEnumerable private static Expression CreateExpression(string name, Expression value) => Expression.New(constructorInfo, Expression.Constant(name), value); - private static readonly ConstructorInfo constructorInfo - = typeof(TemplatePart).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(string), typeof(string) }); + private static readonly ConstructorInfo constructorInfo; + private static readonly ConstructorInfo tagTemplateConstructorInfo; + private static readonly MethodInfo addMethodInfo; + static TemplatePart() + { + var type = typeof(TemplatePart); - private static readonly ConstructorInfo tagTemplateConstructorInfo - = typeof(TemplatePart).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(ITemplateTag), typeof(string) }); + if (type.GetConstructor( + BindingFlags.NonPublic | BindingFlags.Instance, + new Type[] { typeof(string), typeof(string) }) is not ConstructorInfo c1) + throw new MissingMethodException(nameof(TemplatePart)); - private static readonly MethodInfo addMethodInfo - = typeof(TemplatePart).GetMethod(nameof(Concatenate), BindingFlags.NonPublic | BindingFlags.Static, new Type[] { typeof(TemplatePart), typeof(TemplatePart) }); + if (type.GetConstructor( + BindingFlags.NonPublic | BindingFlags.Instance, + new Type[] { typeof(ITemplateTag), typeof(string) }) is not ConstructorInfo c2) + throw new MissingMethodException(nameof(TemplatePart)); + + if (type.GetMethod( + nameof(Concatenate), + BindingFlags.NonPublic | BindingFlags.Static, + new Type[] { typeof(TemplatePart), typeof(TemplatePart) }) is not MethodInfo m1) + throw new MissingMethodException(nameof(Concatenate)); + + constructorInfo = c1; + tagTemplateConstructorInfo = c2; + addMethodInfo = m1; + } public IEnumerator GetEnumerator() { diff --git a/Source/FileManager/PersistentDictionary.cs b/Source/FileManager/PersistentDictionary.cs index 41918e99..3ebbd1cc 100644 --- a/Source/FileManager/PersistentDictionary.cs +++ b/Source/FileManager/PersistentDictionary.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +#nullable enable namespace FileManager { public class PersistentDictionary @@ -13,19 +15,19 @@ namespace FileManager public bool IsReadOnly { get; } // optimize for strings. expectation is most settings will be strings and a rare exception will be something else - private Dictionary stringCache { get; } = new Dictionary(); - private Dictionary objectCache { get; } = new Dictionary(); + private Dictionary stringCache { get; } = new(); + private Dictionary objectCache { get; } = new(); public PersistentDictionary(string filepath, bool isReadOnly = false) { Filepath = filepath; IsReadOnly = isReadOnly; - if (File.Exists(Filepath)) + if (File.Exists(Filepath) || Path.GetDirectoryName(Filepath) is not string dirName) return; // will create any missing directories, incl subdirectories. if all already exist: no action - Directory.CreateDirectory(Path.GetDirectoryName(filepath)); + Directory.CreateDirectory(dirName); if (IsReadOnly) return; @@ -33,13 +35,14 @@ namespace FileManager createNewFile(); } - public string GetString(string propertyName, string defaultValue = null) + [return: NotNullIfNotNull(nameof(defaultValue))] + public string? GetString(string propertyName, string? defaultValue = null) { if (!stringCache.ContainsKey(propertyName)) { var jObject = readFile(); if (jObject.ContainsKey(propertyName)) - stringCache[propertyName] = jObject[propertyName].Value(); + stringCache[propertyName] = jObject[propertyName]?.Value(); else stringCache[propertyName] = defaultValue; } @@ -47,7 +50,8 @@ namespace FileManager return stringCache[propertyName]; } - public T GetNonString(string propertyName, T defaultValue = default) + [return: NotNullIfNotNull(nameof(defaultValue))] + public T? GetNonString(string propertyName, T? defaultValue = default) { var obj = GetObject(propertyName); @@ -72,21 +76,21 @@ namespace FileManager throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}"); } - public object GetObject(string propertyName) + public object? GetObject(string propertyName) { if (!objectCache.ContainsKey(propertyName)) { var jObject = readFile(); if (!jObject.ContainsKey(propertyName)) return null; - objectCache[propertyName] = jObject[propertyName].Value(); + objectCache[propertyName] = jObject[propertyName]?.Value(); } return objectCache[propertyName]; } - public string GetStringFromJsonPath(string jsonPath, string propertyName) => GetStringFromJsonPath($"{jsonPath}.{propertyName}"); - public string GetStringFromJsonPath(string jsonPath) + public string? GetStringFromJsonPath(string jsonPath, string propertyName) => GetStringFromJsonPath($"{jsonPath}.{propertyName}"); + public string? GetStringFromJsonPath(string jsonPath) { if (!stringCache.ContainsKey(jsonPath)) { @@ -96,7 +100,7 @@ namespace FileManager var token = jObject.SelectToken(jsonPath); if (token is null) return null; - stringCache[jsonPath] = (string)token; + stringCache[jsonPath] = token.Value(); } catch { @@ -110,7 +114,7 @@ namespace FileManager public bool Exists(string propertyName) => readFile().ContainsKey(propertyName); private object locker { get; } = new object(); - public void SetString(string propertyName, string newValue) + public void SetString(string propertyName, string? newValue) { // only do this check in string cache, NOT object cache if (stringCache.ContainsKey(propertyName) && stringCache[propertyName] == newValue) @@ -122,7 +126,7 @@ namespace FileManager writeFile(propertyName, newValue); } - public void SetNonString(string propertyName, object newValue) + public void SetNonString(string propertyName, object? newValue) { // set cache objectCache[propertyName] = newValue; @@ -160,7 +164,7 @@ namespace FileManager return success; } - private void writeFile(string propertyName, JToken newValue) + private void writeFile(string propertyName, JToken? newValue) { if (IsReadOnly) return; @@ -190,7 +194,7 @@ namespace FileManager /// WILL ONLY set if already present. WILL NOT create new /// Value was changed - public bool SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false) + public bool SetWithJsonPath(string jsonPath, string propertyName, string? newValue, bool suppressLogging = false) { if (IsReadOnly) return false; @@ -242,7 +246,7 @@ namespace FileManager return true; } - private static string formatValueForLog(string value) + private static string formatValueForLog(string? value) => value is null ? "[null]" : string.IsNullOrEmpty(value) ? "[empty]" : string.IsNullOrWhiteSpace(value) ? $"[whitespace. Length={value.Length}]" @@ -283,7 +287,6 @@ namespace FileManager private void createNewFile() { File.WriteAllText(Filepath, "{}"); - System.Threading.Thread.Sleep(100); } } } diff --git a/Source/FileManager/ReplacementCharacters.cs b/Source/FileManager/ReplacementCharacters.cs index 4457e19f..e83183de 100644 --- a/Source/FileManager/ReplacementCharacters.cs +++ b/Source/FileManager/ReplacementCharacters.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +#nullable enable namespace FileManager { public record Replacement @@ -59,7 +60,7 @@ namespace FileManager [JsonConverter(typeof(ReplacementCharactersConverter))] public class ReplacementCharacters { - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is ReplacementCharacters second && Replacements.Count == second.Replacements.Count) { @@ -173,7 +174,7 @@ namespace FileManager Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).ToArray(); - public IReadOnlyList Replacements { get; init; } + required public IReadOnlyList Replacements { get; init; } private string DefaultReplacement => Replacements[0].ReplacementString; private Replacement ForwardSlash => Replacements[1]; private Replacement BackSlash => Replacements[2]; @@ -298,12 +299,14 @@ namespace FileManager public override bool CanConvert(Type objectType) => objectType == typeof(ReplacementCharacters); - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { var jObj = JObject.Load(reader); var replaceArr = jObj[nameof(Replacement)]; - IReadOnlyList dict = replaceArr - .ToObject().ToList(); + var dict + = replaceArr?.ToObject()?.ToList() + ?? ReplacementCharacters.Default.Replacements; + //Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid. //If not, reset to default. @@ -325,9 +328,10 @@ namespace FileManager return new ReplacementCharacters { Replacements = dict }; } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { - ReplacementCharacters replacements = (ReplacementCharacters)value; + if (value is not ReplacementCharacters replacements) + return; var propertyNames = replacements.Replacements .Select(JObject.FromObject).ToList(); diff --git a/Source/HangoverAvalonia/HangoverAvalonia.csproj b/Source/HangoverAvalonia/HangoverAvalonia.csproj index 1a04d6a2..f668002e 100644 --- a/Source/HangoverAvalonia/HangoverAvalonia.csproj +++ b/Source/HangoverAvalonia/HangoverAvalonia.csproj @@ -67,13 +67,13 @@ - - + + - - - - + + + + diff --git a/Source/LibationAvalonia/LibationAvalonia.csproj b/Source/LibationAvalonia/LibationAvalonia.csproj index 7972f931..c284d70b 100644 --- a/Source/LibationAvalonia/LibationAvalonia.csproj +++ b/Source/LibationAvalonia/LibationAvalonia.csproj @@ -70,13 +70,13 @@ - - - - - - - + + + + + + + diff --git a/Source/LibationFileManager/AudibleFileStorage.cs b/Source/LibationFileManager/AudibleFileStorage.cs index 55157e92..8ca7a356 100644 --- a/Source/LibationFileManager/AudibleFileStorage.cs +++ b/Source/LibationFileManager/AudibleFileStorage.cs @@ -9,11 +9,12 @@ using System.Threading.Tasks; using System.Threading; using FileManager; +#nullable enable namespace LibationFileManager { public abstract class AudibleFileStorage { - protected abstract LongPath GetFilePathCustom(string productId); + protected abstract LongPath? GetFilePathCustom(string productId); protected abstract List GetFilePathsCustom(string productId); #region static @@ -57,7 +58,7 @@ namespace LibationFileManager regexTemplate = $@"{{0}}.*?\.({extAggr})$"; } - protected LongPath GetFilePath(string productId) + protected LongPath? GetFilePath(string productId) { // primary lookup var cachedFile = FilePathCache.GetFirstPath(productId, FileType); @@ -87,7 +88,7 @@ namespace LibationFileManager { internal AaxcFileStorage() : base(FileType.AAXC) { } - protected override LongPath GetFilePathCustom(string productId) + protected override LongPath? GetFilePathCustom(string productId) => GetFilePathsCustom(productId).FirstOrDefault(); protected override List GetFilePathsCustom(string productId) @@ -104,9 +105,9 @@ namespace LibationFileManager public class AudioFileStorage : AudibleFileStorage { internal AudioFileStorage() : base(FileType.Audio) - => BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); + => BookDirectoryFiles ??= newBookDirectoryFiles(); - private static BackgroundFileSystem BookDirectoryFiles { get; set; } + private static BackgroundFileSystem? BookDirectoryFiles { get; set; } private static object bookDirectoryFilesLocker { get; } = new(); private static EnumerationOptions enumerationOptions { get; } = new() { @@ -115,17 +116,20 @@ namespace LibationFileManager MatchCasing = MatchCasing.CaseInsensitive }; - protected override LongPath GetFilePathCustom(string productId) + protected override LongPath? GetFilePathCustom(string productId) => GetFilePathsCustom(productId).FirstOrDefault(); - protected override List GetFilePathsCustom(string productId) + private static BackgroundFileSystem newBookDirectoryFiles() + => new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); + + protected override List GetFilePathsCustom(string productId) { // If user changed the BooksDirectory: reinitialize lock (bookDirectoryFilesLocker) - if (BooksDirectory != BookDirectoryFiles.RootDirectory) - BookDirectoryFiles = new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); + if (BooksDirectory != BookDirectoryFiles?.RootDirectory) + BookDirectoryFiles = newBookDirectoryFiles(); - var regex = GetBookSearchRegex(productId); + var regex = GetBookSearchRegex(productId); //Find all extant files matching the productId //using both the file system and the file path cache @@ -138,9 +142,16 @@ namespace LibationFileManager .ToList(); } - public void Refresh() => BookDirectoryFiles.RefreshFiles(); + public void Refresh() + { + if (BookDirectoryFiles is null) + lock (bookDirectoryFilesLocker) + BookDirectoryFiles = newBookDirectoryFiles(); + else + BookDirectoryFiles?.RefreshFiles(); + } - public LongPath GetPath(string productId) => GetFilePath(productId); + public LongPath? GetPath(string productId) => GetFilePath(productId); public static async IAsyncEnumerable FindAudiobooksAsync(LongPath searchDirectory, [EnumeratorCancellation] CancellationToken cancellationToken) { @@ -151,7 +162,7 @@ namespace LibationFileManager if (cancellationToken.IsCancellationRequested) yield break; - FilePathCache.CacheEntry audioFile = default; + FilePathCache.CacheEntry? audioFile = default; try { diff --git a/Source/LibationFileManager/Configuration.Environment.cs b/Source/LibationFileManager/Configuration.Environment.cs index 55ce4fdc..66ba5302 100644 --- a/Source/LibationFileManager/Configuration.Environment.cs +++ b/Source/LibationFileManager/Configuration.Environment.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +#nullable enable namespace LibationFileManager { [Flags] @@ -20,7 +21,7 @@ namespace LibationFileManager public static bool IsWindows { get; } = OperatingSystem.IsWindows(); public static bool IsLinux { get; } = OperatingSystem.IsLinux(); public static bool IsMacOs { get; } = OperatingSystem.IsMacOS(); - public static Version LibationVersion { get; private set; } + public static Version? LibationVersion { get; private set; } public static void SetLibationVersion(Version version) => LibationVersion = version; public static OS OS { get; } diff --git a/Source/LibationFileManager/Configuration.HelpText.cs b/Source/LibationFileManager/Configuration.HelpText.cs index a3c747bc..8eb7679c 100644 --- a/Source/LibationFileManager/Configuration.HelpText.cs +++ b/Source/LibationFileManager/Configuration.HelpText.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; +#nullable enable namespace LibationFileManager { public partial class Configuration @@ -29,8 +30,7 @@ namespace LibationFileManager } .AsReadOnly(); - public static string GetHelpText(string settingName) + public static string? GetHelpText(string settingName) => HelpText.TryGetValue(settingName, out var value) ? value : null; - } } diff --git a/Source/LibationFileManager/Configuration.KnownDirectories.cs b/Source/LibationFileManager/Configuration.KnownDirectories.cs index d989b616..0549d54d 100644 --- a/Source/LibationFileManager/Configuration.KnownDirectories.cs +++ b/Source/LibationFileManager/Configuration.KnownDirectories.cs @@ -5,11 +5,12 @@ using System.IO; using System.Linq; using Dinah.Core; +#nullable enable namespace LibationFileManager { public partial class Configuration { - public static string ProcessDirectory { get; } = Path.GetDirectoryName(Exe.FileLocationOnDisk); + public static string ProcessDirectory { get; } = Path.GetDirectoryName(Exe.FileLocationOnDisk)!; public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}"; public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY)); public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation")); @@ -36,7 +37,7 @@ namespace LibationFileManager LibationFiles = 5 } // use func calls so we always get the latest value of LibationFiles - private static List<(KnownDirectories directory, Func getPathFunc)> directoryOptionsPaths { get; } = new() + private static List<(KnownDirectories directory, Func getPathFunc)> directoryOptionsPaths { get; } = new() { (KnownDirectories.None, () => null), (KnownDirectories.UserProfile, () => UserProfile), @@ -47,7 +48,7 @@ namespace LibationFileManager // also, keep this at bottom of this list (KnownDirectories.LibationFiles, () => libationFilesPathCache) }; - public static string GetKnownDirectoryPath(KnownDirectories directory) + public static string? GetKnownDirectoryPath(KnownDirectories directory) { var dirFunc = directoryOptionsPaths.SingleOrDefault(dirFunc => dirFunc.directory == directory); return dirFunc == default ? null : dirFunc.getPathFunc(); diff --git a/Source/LibationFileManager/Configuration.LibationFiles.cs b/Source/LibationFileManager/Configuration.LibationFiles.cs index 553c6a5f..00246d99 100644 --- a/Source/LibationFileManager/Configuration.LibationFiles.cs +++ b/Source/LibationFileManager/Configuration.LibationFiles.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using Serilog; using Dinah.Core.Logging; +#nullable enable namespace LibationFileManager { public partial class Configuration @@ -44,7 +45,7 @@ namespace LibationFileManager } } - private static string libationFilesPathCache { get; set; } + private static string? libationFilesPathCache { get; set; } /// /// Try to find appsettings.json in the following locations: @@ -124,7 +125,10 @@ namespace LibationFileManager // do not check whether directory exists. special/meta directory (eg: AppDir) is valid // verify from live file. no try/catch. want failures to be visible var jObjFinal = JObject.Parse(File.ReadAllText(AppsettingsJsonFile)); - var valueFinal = jObjFinal[LIBATION_FILES_KEY].Value(); + + if (jObjFinal[LIBATION_FILES_KEY]?.Value() is not string valueFinal) + throw new InvalidDataException($"{LIBATION_FILES_KEY} not found in {AppsettingsJsonFile}"); + return valueFinal; } diff --git a/Source/LibationFileManager/Configuration.Logging.cs b/Source/LibationFileManager/Configuration.Logging.cs index 3a7dcee4..66ea7cda 100644 --- a/Source/LibationFileManager/Configuration.Logging.cs +++ b/Source/LibationFileManager/Configuration.Logging.cs @@ -1,19 +1,18 @@ using System; -using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using Dinah.Core; using Dinah.Core.Logging; using FileManager; using Microsoft.Extensions.Configuration; using Serilog; using Serilog.Events; +#nullable enable namespace LibationFileManager { public partial class Configuration { - private IConfigurationRoot configuration; + private IConfigurationRoot? configuration; public void ConfigureLogging() { @@ -31,20 +30,20 @@ namespace LibationFileManager { get { - var logLevelStr = persistentDictionary.GetStringFromJsonPath("Serilog", "MinimumLevel"); + var logLevelStr = Settings.GetStringFromJsonPath("Serilog", "MinimumLevel"); return Enum.TryParse(logLevelStr, out var logLevelEnum) ? logLevelEnum : LogEventLevel.Information; } set { OnPropertyChanging(nameof(LogLevel), LogLevel, value); - var valueWasChanged = persistentDictionary.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString()); + var valueWasChanged = Settings.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString()); if (!valueWasChanged) { Log.Logger.Debug("LogLevel.set attempt. No change"); return; } - configuration.Reload(); + configuration?.Reload(); OnPropertyChanged(nameof(LogLevel), value); diff --git a/Source/LibationFileManager/Configuration.PersistentSettings.cs b/Source/LibationFileManager/Configuration.PersistentSettings.cs index 82a91193..bac5e03b 100644 --- a/Source/LibationFileManager/Configuration.PersistentSettings.cs +++ b/Source/LibationFileManager/Configuration.PersistentSettings.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.CompilerServices; @@ -8,6 +9,7 @@ using FileManager; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +#nullable enable namespace LibationFileManager { public partial class Configuration @@ -18,34 +20,52 @@ namespace LibationFileManager // config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation // exceptions: appsettings.json, LibationFiles dir, Settings.json - private PersistentDictionary persistentDictionary; + private PersistentDictionary? persistentDictionary; - public bool RemoveProperty(string propertyName) => persistentDictionary.RemoveProperty(propertyName); + private PersistentDictionary Settings + { + get + { + if (persistentDictionary is null) + throw new InvalidOperationException($"{nameof(persistentDictionary)} must first be set by accessing {nameof(LibationFiles)} or calling {nameof(SettingsFileIsValid)}"); + return persistentDictionary; + } + } - public T GetNonString(T defaultValue, [CallerMemberName] string propertyName = "") => persistentDictionary.GetNonString(propertyName, defaultValue); - public object GetObject([CallerMemberName] string propertyName = "") => persistentDictionary.GetObject(propertyName); - public string GetString(string defaultValue = null, [CallerMemberName] string propertyName = "") => persistentDictionary.GetString(propertyName, defaultValue); - public void SetNonString(object newValue, [CallerMemberName] string propertyName = "") + public bool RemoveProperty(string propertyName) => Settings.RemoveProperty(propertyName); + + [return: NotNullIfNotNull(nameof(defaultValue))] + public T? GetNonString(T defaultValue, [CallerMemberName] string propertyName = "") + => Settings.GetNonString(propertyName, defaultValue); + + + [return: NotNullIfNotNull(nameof(defaultValue))] + public string? GetString(string? defaultValue = null, [CallerMemberName] string propertyName = "") + => Settings.GetString(propertyName, defaultValue); + + public object? GetObject([CallerMemberName] string propertyName = "") => Settings.GetObject(propertyName); + + public void SetNonString(object? newValue, [CallerMemberName] string propertyName = "") { var existing = getExistingValue(propertyName); if (existing?.Equals(newValue) is true) return; OnPropertyChanging(propertyName, existing, newValue); - persistentDictionary.SetNonString(propertyName, newValue); + Settings.SetNonString(propertyName, newValue); OnPropertyChanged(propertyName, newValue); } - public void SetString(string newValue, [CallerMemberName] string propertyName = "") + public void SetString(string? newValue, [CallerMemberName] string propertyName = "") { var existing = getExistingValue(propertyName); if (existing?.Equals(newValue) is true) return; OnPropertyChanging(propertyName, existing, newValue); - persistentDictionary.SetString(propertyName, newValue); + Settings.SetString(propertyName, newValue); OnPropertyChanged(propertyName, newValue); } - private object getExistingValue(string propertyName) + private object? getExistingValue(string propertyName) { var property = GetType().GetProperty(propertyName); if (property is not null) return property.GetValue(this); @@ -53,16 +73,16 @@ namespace LibationFileManager } /// WILL ONLY set if already present. WILL NOT create new - public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false) + public void SetWithJsonPath(string jsonPath, string propertyName, string? newValue, bool suppressLogging = false) { - var settingWasChanged = persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging); + var settingWasChanged = Settings.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging); if (settingWasChanged) configuration?.Reload(); } public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json"); - public static string GetDescription(string propertyName) + public static string? GetDescription(string propertyName) { var attribute = typeof(Configuration) .GetProperty(propertyName) @@ -73,7 +93,7 @@ namespace LibationFileManager return attribute?.Description; } - public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName); + public bool Exists(string propertyName) => Settings.Exists(propertyName); [Description("Set cover art as the folder's icon.")] public bool UseCoverAsFolderIcon { get => GetNonString(defaultValue: false); set => SetNonString(value); } @@ -91,7 +111,7 @@ namespace LibationFileManager public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); } [Description("Location for book storage. Includes destination of newly liberated books")] - public LongPath Books { get => GetString(); set => SetString(value); } + public LongPath? Books { get => GetString(); set => SetString(value); } [Description("Overwrite existing files if they already exist?")] public bool OverwriteExisting { get => GetNonString(defaultValue: false); set => SetNonString(value); } diff --git a/Source/LibationFileManager/Configuration.PropertyChange.cs b/Source/LibationFileManager/Configuration.PropertyChange.cs index f6d70073..e1fbb08c 100644 --- a/Source/LibationFileManager/Configuration.PropertyChange.cs +++ b/Source/LibationFileManager/Configuration.PropertyChange.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; +#nullable enable namespace LibationFileManager { public partial class Configuration @@ -9,17 +10,17 @@ namespace LibationFileManager * and be sure to clone it before returning. This allows Configuration to * accurately detect if any of the Dictionary's elements have changed. */ - private class EquatableDictionary : Dictionary + private class EquatableDictionary : Dictionary where TKey : notnull { public EquatableDictionary() { } public EquatableDictionary(IEnumerable> keyValuePairs) : base(keyValuePairs) { } public EquatableDictionary Clone() => new(this); - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is Dictionary dic && Count == dic.Count) { foreach (var pair in this) - if (!dic.TryGetValue(pair.Key, out var value) || !pair.Value.Equals(value)) + if (!dic.TryGetValue(pair.Key, out var value) || pair.Value?.Equals(value) is not true) return false; return true; diff --git a/Source/LibationFileManager/Configuration.cs b/Source/LibationFileManager/Configuration.cs index 74d95c86..4867ce27 100644 --- a/Source/LibationFileManager/Configuration.cs +++ b/Source/LibationFileManager/Configuration.cs @@ -4,7 +4,7 @@ using System.Linq; using Dinah.Core; using FileManager; - +#nullable enable namespace LibationFileManager { public partial class Configuration : PropertyChangeFilter @@ -24,9 +24,12 @@ namespace LibationFileManager if (!Directory.Exists(booksDir)) { + if (Path.GetDirectoryName(settingsFile) is not string dir) + throw new DirectoryNotFoundException(settingsFile); + //"Books" is not null, so setup has already been run. //Since Books can't be found, try to create it in Libation settings folder - booksDir = Path.Combine(Path.GetDirectoryName(settingsFile), nameof(Books)); + booksDir = Path.Combine(dir, nameof(Books)); try { Directory.CreateDirectory(booksDir); diff --git a/Source/LibationFileManager/FilePathCache.cs b/Source/LibationFileManager/FilePathCache.cs index 9cf34adb..21aee8f6 100644 --- a/Source/LibationFileManager/FilePathCache.cs +++ b/Source/LibationFileManager/FilePathCache.cs @@ -6,6 +6,7 @@ using Dinah.Core.Collections.Immutable; using FileManager; using Newtonsoft.Json; +#nullable enable namespace LibationFileManager { public static class FilePathCache @@ -14,8 +15,8 @@ namespace LibationFileManager private const string FILENAME = "FileLocations.json"; - public static event EventHandler Inserted; - public static event EventHandler Removed; + public static event EventHandler? Inserted; + public static event EventHandler? Removed; private static Cache cache { get; } = new Cache(); @@ -51,7 +52,7 @@ namespace LibationFileManager .Select(entry => (entry.FileType, entry.Path)) .ToList(); - public static LongPath GetFirstPath(string id, FileType type) + public static LongPath? GetFirstPath(string id, FileType type) => getEntries(entry => entry.Id == id && entry.FileType == type) ?.FirstOrDefault() ?.Path; diff --git a/Source/LibationFileManager/IInteropFunctions.cs b/Source/LibationFileManager/IInteropFunctions.cs index a79cc0f3..cc1bcbeb 100644 --- a/Source/LibationFileManager/IInteropFunctions.cs +++ b/Source/LibationFileManager/IInteropFunctions.cs @@ -2,9 +2,9 @@ using System.Diagnostics; using System.Threading.Tasks; +#nullable enable namespace LibationFileManager { -#nullable enable public interface IInteropFunctions { /// diff --git a/Source/LibationFileManager/InteropFactory.cs b/Source/LibationFileManager/InteropFactory.cs index eddc97be..03762099 100644 --- a/Source/LibationFileManager/InteropFactory.cs +++ b/Source/LibationFileManager/InteropFactory.cs @@ -5,11 +5,12 @@ using System.Linq; using System.Reflection; using Dinah.Core; +#nullable enable namespace LibationFileManager { public static class InteropFactory { - public static Type InteropFunctionsType { get; } + public static Type? InteropFunctionsType { get; } public static IInteropFunctions Create() => _create(); @@ -17,13 +18,17 @@ namespace LibationFileManager //public static IInteropFunctions Create(string str, int i) => _create(str, i); //public static IInteropFunctions Create(params object[] values) => _create(values); - private static IInteropFunctions instance { get; set; } + private static IInteropFunctions? instance { get; set; } private static IInteropFunctions _create(params object[] values) { instance ??= InteropFunctionsType is null ? new NullInteropFunctions() : Activator.CreateInstance(InteropFunctionsType, values) as IInteropFunctions; + + if (instance is null) + throw new TypeLoadException(); + return instance; } @@ -66,7 +71,7 @@ namespace LibationFileManager .GetTypes() .FirstOrDefault(type.IsAssignableFrom); } - private static string getOSConfigApp() + private static string? getOSConfigApp() { // find '*ConfigApp.dll' files var appName = @@ -76,8 +81,8 @@ namespace LibationFileManager return appName; } - private static Dictionary lowEffortCache { get; } = new(); - private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + private static Dictionary lowEffortCache { get; } = new(); + private static Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args) { var asmName = new AssemblyName(args.Name); var here = Configuration.ProcessDirectory; @@ -97,7 +102,7 @@ namespace LibationFileManager return assembly; } - private static Assembly CurrentDomain_AssemblyResolve_internal(AssemblyName asmName, string here) + private static Assembly? CurrentDomain_AssemblyResolve_internal(AssemblyName asmName, string here) { /* * Find the requested assembly in the program files directory. diff --git a/Source/LibationFileManager/LibraryBookDto.cs b/Source/LibationFileManager/LibraryBookDto.cs index 0703873c..9059d364 100644 --- a/Source/LibationFileManager/LibraryBookDto.cs +++ b/Source/LibationFileManager/LibraryBookDto.cs @@ -2,26 +2,27 @@ using System.Collections.Generic; using System.Linq; +#nullable enable namespace LibationFileManager { public class BookDto { - public string AudibleProductId { get; set; } - public string Title { get; set; } - public string Subtitle { get; set; } - public string TitleWithSubtitle { get; set; } - public string Locale { get; set; } + public string? AudibleProductId { get; set; } + public string? Title { get; set; } + public string? Subtitle { get; set; } + public string? TitleWithSubtitle { get; set; } + public string? Locale { get; set; } public int? YearPublished { get; set; } - public IEnumerable Authors { get; set; } - public string AuthorNames => string.Join(", ", Authors); - public string FirstAuthor => Authors.FirstOrDefault(); + public IEnumerable? Authors { get; set; } + public string? AuthorNames => Authors is null ? null : string.Join(", ", Authors); + public string? FirstAuthor => Authors?.FirstOrDefault(); - public IEnumerable Narrators { get; set; } - public string NarratorNames => string.Join(", ", Narrators); - public string FirstNarrator => Narrators.FirstOrDefault(); + public IEnumerable? Narrators { get; set; } + public string? NarratorNames => Narrators is null? null: string.Join(", ", Narrators); + public string? FirstNarrator => Narrators?.FirstOrDefault(); - public string SeriesName { get; set; } + public string? SeriesName { get; set; } public float? SeriesNumber { get; set; } public bool IsSeries => !string.IsNullOrEmpty(SeriesName); public bool IsPodcastParent { get; set; } @@ -32,13 +33,13 @@ namespace LibationFileManager public int Channels { get; set; } public DateTime FileDate { get; set; } = DateTime.Now; public DateTime? DatePublished { get; set; } - public string Language { get; set; } + public string? Language { get; set; } } public class LibraryBookDto : BookDto { public DateTime? DateAdded { get; set; } - public string Account { get; set; } - public string AccountNickname { get; set; } + public string? Account { get; set; } + public string? AccountNickname { get; set; } } } diff --git a/Source/LibationFileManager/NameListFormat.cs b/Source/LibationFileManager/NameListFormat.cs index 2ca5f94b..1e906d58 100644 --- a/Source/LibationFileManager/NameListFormat.cs +++ b/Source/LibationFileManager/NameListFormat.cs @@ -5,13 +5,16 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +#nullable enable namespace LibationFileManager { internal partial class NameListFormat { - public static string Formatter(ITemplateTag _, IEnumerable names, string formatString) + public static string Formatter(ITemplateTag _, IEnumerable? names, string formatString) { - var humanNames = names.Select(n => new HumanName(RemoveSuffix(n), Prefer.FirstOverPrefix)); + if (names is null) return ""; + + var humanNames = names.Select(n => new HumanName(RemoveSuffix(n), Prefer.FirstOverPrefix)); var sortedNames = Sort(humanNames, formatString); var nameFormatString = Format(formatString, defaultValue: "{T} {F} {M} {L} {S}"); diff --git a/Source/LibationFileManager/NullInteropFunctions.cs b/Source/LibationFileManager/NullInteropFunctions.cs index b72e354d..19022f94 100644 --- a/Source/LibationFileManager/NullInteropFunctions.cs +++ b/Source/LibationFileManager/NullInteropFunctions.cs @@ -2,7 +2,6 @@ using System.Diagnostics; #nullable enable - namespace LibationFileManager { public class NullInteropFunctions : IInteropFunctions diff --git a/Source/LibationFileManager/PictureStorage.cs b/Source/LibationFileManager/PictureStorage.cs index e3081038..d72c3614 100644 --- a/Source/LibationFileManager/PictureStorage.cs +++ b/Source/LibationFileManager/PictureStorage.cs @@ -6,18 +6,25 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; +#nullable enable namespace LibationFileManager { public enum PictureSize { Native, _80x80 = 80, _300x300 = 300, _500x500 = 500 } public class PictureCachedEventArgs : EventArgs { - public PictureDefinition Definition { get; internal set; } - public byte[] Picture { get; internal set; } + public PictureDefinition Definition { get; } + public byte[] Picture { get; } + + internal PictureCachedEventArgs(PictureDefinition definition, byte[] picture) + { + Definition = definition; + Picture = picture; + } } public struct PictureDefinition : IEquatable { - public string PictureId { get; } - public PictureSize Size { get; } + public string PictureId { get; init; } + public PictureSize Size { get; init; } public PictureDefinition(string pictureId, PictureSize pictureSize) { @@ -45,7 +52,7 @@ namespace LibationFileManager .Start(); } - public static event EventHandler PictureCached; + public static event EventHandler? PictureCached; private static BlockingCollection DownloadQueue { get; } = new BlockingCollection(); private static object cacheLocker { get; } = new object(); @@ -112,7 +119,7 @@ namespace LibationFileManager lock (cacheLocker) cache[def] = bytes; - PictureCached?.Invoke(nameof(PictureStorage), new PictureCachedEventArgs { Definition = def, Picture = bytes }); + PictureCached?.Invoke(nameof(PictureStorage), new PictureCachedEventArgs(def, bytes)); } } diff --git a/Source/LibationFileManager/QuickFilters.cs b/Source/LibationFileManager/QuickFilters.cs index 497f6e58..6b028f2b 100644 --- a/Source/LibationFileManager/QuickFilters.cs +++ b/Source/LibationFileManager/QuickFilters.cs @@ -5,30 +5,30 @@ using System.Linq; using Dinah.Core.Collections.Generic; using Newtonsoft.Json; +#nullable enable namespace LibationFileManager { public static class QuickFilters { - public static event EventHandler Updated; + public static event EventHandler? Updated; - internal class FilterState + public static event EventHandler? UseDefaultChanged; + + internal class FilterState { public bool UseDefault { get; set; } public List Filters { get; set; } = new List(); } - static FilterState inMemoryState { get; } = new FilterState(); - public static string JsonFile => Path.Combine(Configuration.Instance.LibationFiles, "QuickFilters.json"); - static QuickFilters() - { - // load json into memory. if file doesn't exist, nothing to do. save() will create if needed - if (File.Exists(JsonFile)) - inMemoryState = JsonConvert.DeserializeObject(File.ReadAllText(JsonFile)); - } - public static event EventHandler UseDefaultChanged; + // load json into memory. if file doesn't exist, nothing to do. save() will create if needed + static FilterState inMemoryState { get; } + = File.Exists(JsonFile) && JsonConvert.DeserializeObject(File.ReadAllText(JsonFile)) is FilterState inMemState + ? inMemState + : new FilterState(); + public static bool UseDefault { get => inMemoryState.UseDefault; @@ -43,7 +43,7 @@ namespace LibationFileManager save(false); } - UseDefaultChanged?.Invoke(null, null); + UseDefaultChanged?.Invoke(null, EventArgs.Empty); } } @@ -121,7 +121,7 @@ namespace LibationFileManager } if (invokeUpdatedEvent) - Updated?.Invoke(null, null); + Updated?.Invoke(null, EventArgs.Empty); } } } diff --git a/Source/LibationFileManager/TemplateEditor[T].cs b/Source/LibationFileManager/TemplateEditor[T].cs index e3d68dc1..2064876c 100644 --- a/Source/LibationFileManager/TemplateEditor[T].cs +++ b/Source/LibationFileManager/TemplateEditor[T].cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System; using System.IO; +#nullable enable namespace LibationFileManager { public interface ITemplateEditor @@ -14,14 +15,11 @@ namespace LibationFileManager string DefaultTemplate { get; } string TemplateName { get; } string TemplateDescription { get; } - Templates Folder { get; } - Templates File { get; } - Templates Name { get; } Templates EditingTemplate { get; } - void SetTemplateText(string templateText); - string GetFolderName(); - string GetFileName(); - string GetName(); + bool SetTemplateText(string templateText); + string? GetFolderName(); + string? GetFileName(); + string? GetName(); } public class TemplateEditor : ITemplateEditor where T : Templates, ITemplate, new() @@ -32,9 +30,9 @@ namespace LibationFileManager public string DefaultTemplate { get; private init; } public string TemplateName { get; private init; } public string TemplateDescription { get; private init; } - public Templates Folder { get; private set; } - public Templates File { get; private set; } - public Templates Name { get; private set; } + private Templates? Folder { get; set; } + private Templates? File { get; set; } + private Templates? Name { get; set; } public Templates EditingTemplate { get => _editingTemplate; @@ -43,10 +41,14 @@ namespace LibationFileManager private Templates _editingTemplate; - public void SetTemplateText(string templateText) + public bool SetTemplateText(string templateText) { - Templates.TryGetTemplate(templateText, out var template); - EditingTemplate = template; + if (Templates.TryGetTemplate(templateText, out var template)) + { + EditingTemplate = template; + return true; + } + return false; } private static readonly LibraryBookDto libraryBookDto @@ -80,7 +82,7 @@ namespace LibationFileManager Title = "A Flight for Life" }; - public string GetFolderName() + public string? GetFolderName() { /* * Path must be rooted for windows to allow long file paths. This is @@ -88,49 +90,49 @@ namespace LibationFileManager * subdirectories. Without rooting, we won't be allowed to create a * relative path longer than MAX_PATH. */ - var dir = Folder.GetFilename(libraryBookDto, BaseDirectory, ""); + var dir = Folder?.GetFilename(libraryBookDto, BaseDirectory, ""); + if (dir is null) return null; return Path.GetRelativePath(BaseDirectory, dir); } - public string GetFileName() - => File.GetFilename(libraryBookDto, partFileProperties, "", ""); - public string GetName() - => Name.GetName(libraryBookDto, partFileProperties); + public string? GetFileName() + => File?.GetFilename(libraryBookDto, partFileProperties, "", ""); + public string? GetName() + => Name?.GetName(libraryBookDto, partFileProperties); + + private TemplateEditor( + Templates editingTemplate, + LongPath baseDirectory, + string defaultTemplate, + string templateName, + string templateDescription) + { + _editingTemplate = editingTemplate; + BaseDirectory = baseDirectory; + DefaultTemplate = defaultTemplate; + TemplateName = templateName; + TemplateDescription = templateDescription; + } public static ITemplateEditor CreateFilenameEditor(LongPath baseDir, string templateText) { - Templates.TryGetTemplate(templateText, out var template); + if (!Templates.TryGetTemplate(templateText, out var template)) + throw new ArgumentException($"Failed to parse {nameof(templateText)}"); - var templateEditor = new TemplateEditor - { - _editingTemplate = template, - BaseDirectory = baseDir, - DefaultTemplate = T.DefaultTemplate, - TemplateName = T.Name, - TemplateDescription = T.Description - - }; + var templateEditor = new TemplateEditor(template, baseDir, T.DefaultTemplate, T.Name, T.Description); if (!templateEditor.IsFolder && !templateEditor.IsFilePath) throw new InvalidOperationException($"This method is only for File and Folder templates. Use {nameof(CreateNameEditor)} for name templates"); - templateEditor.Folder = templateEditor.IsFolder ? template : Templates.Folder; - templateEditor.File = templateEditor.IsFolder ? Templates.File : template; - return templateEditor; } public static ITemplateEditor CreateNameEditor(string templateText) { - Templates.TryGetTemplate(templateText, out var nameTemplate); + if (!Templates.TryGetTemplate(templateText, out var nameTemplate)) + throw new ArgumentException($"Failed to parse {nameof(templateText)}"); - var templateEditor = new TemplateEditor - { - _editingTemplate = nameTemplate, - DefaultTemplate = T.DefaultTemplate, - TemplateName = T.Name, - TemplateDescription = T.Description - }; + var templateEditor = new TemplateEditor(nameTemplate, "", T.DefaultTemplate, T.Name, T.Description); if (templateEditor.IsFolder || templateEditor.IsFilePath) throw new InvalidOperationException($"This method is only for name templates. Use {nameof(CreateFilenameEditor)} for file templates"); diff --git a/Source/LibationFileManager/TemplateTags.cs b/Source/LibationFileManager/TemplateTags.cs index 725eb98d..c4a74aba 100644 --- a/Source/LibationFileManager/TemplateTags.cs +++ b/Source/LibationFileManager/TemplateTags.cs @@ -1,5 +1,6 @@ using FileManager.NamingTemplate; +#nullable enable namespace LibationFileManager { public sealed class TemplateTags : ITemplateTag @@ -10,7 +11,7 @@ namespace LibationFileManager public string Description { get; } public string Display { get; } - private TemplateTags(string tagName, string description, string defaultValue = null, string display = null) + private TemplateTags(string tagName, string description, string? defaultValue = null, string? display = null) { TagName = tagName; Description = description; diff --git a/Source/LibationFileManager/Templates.cs b/Source/LibationFileManager/Templates.cs index 2a24cf22..edcbbedf 100644 --- a/Source/LibationFileManager/Templates.cs +++ b/Source/LibationFileManager/Templates.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using AaxDecrypter; @@ -8,6 +9,7 @@ using FileManager; using FileManager.NamingTemplate; using NameParser; +#nullable enable namespace LibationFileManager { public interface ITemplate @@ -26,10 +28,10 @@ namespace LibationFileManager //Assigning the properties in the static constructor will require all //Templates users to have a valid configuration file. To allow tests //to work without access to Configuration, only load templates on demand. - private static FolderTemplate _folder; - private static FileTemplate _file; - private static ChapterFileTemplate _chapterFile; - private static ChapterTitleTemplate _chapterTitle; + private static FolderTemplate? _folder; + private static FileTemplate? _file; + private static ChapterFileTemplate? _chapterFile; + private static ChapterTitleTemplate? _chapterTitle; public static FolderTemplate Folder => _folder ??= GetTemplate(Configuration.Instance.FolderTemplate); public static FileTemplate File => _file ??= GetTemplate(Configuration.Instance.FileTemplate); @@ -38,10 +40,10 @@ namespace LibationFileManager #region Template Parsing - public static T GetTemplate(string templateText) where T : Templates, ITemplate, new() - => TryGetTemplate(templateText, out var template) ? template : GetDefaultTemplate(); + public static T GetTemplate(string? templateText) where T : Templates, ITemplate, new() + => TryGetTemplate(templateText ?? "", out var template) ? template : GetDefaultTemplate(); - public static bool TryGetTemplate(string templateText, out T template) where T : Templates, ITemplate, new() + public static bool TryGetTemplate(string templateText, [NotNullWhen(true)] out T? template) where T : Templates, ITemplate, new() { var namingTemplate = NamingTemplate.Parse(templateText, T.TagCollections); @@ -56,19 +58,19 @@ namespace LibationFileManager { Configuration.Instance.PropertyChanged += [PropertyChangeFilter(nameof(Configuration.FolderTemplate))] - (_,e) => _folder = GetTemplate((string)e.NewValue); + (_,e) => _folder = GetTemplate(e.NewValue as string); Configuration.Instance.PropertyChanged += [PropertyChangeFilter(nameof(Configuration.FileTemplate))] - (_, e) => _file = GetTemplate((string)e.NewValue); + (_, e) => _file = GetTemplate(e.NewValue as string); Configuration.Instance.PropertyChanged += [PropertyChangeFilter(nameof(Configuration.ChapterFileTemplate))] - (_, e) => _chapterFile = GetTemplate((string)e.NewValue); + (_, e) => _chapterFile = GetTemplate(e.NewValue as string); Configuration.Instance.PropertyChanged += [PropertyChangeFilter(nameof(Configuration.ChapterTitleTemplate))] - (_, e) => _chapterTitle = GetTemplate((string)e.NewValue); + (_, e) => _chapterTitle = GetTemplate(e.NewValue as string); HumanName.Suffixes.Add("ret"); HumanName.Titles.Add("professor"); @@ -78,10 +80,18 @@ namespace LibationFileManager #region Template Properties - public IEnumerable TagsRegistered => NamingTemplate.TagsRegistered.Cast(); - public IEnumerable TagsInUse => NamingTemplate.TagsInUse.Cast(); - public string TemplateText => NamingTemplate.TemplateText; - protected NamingTemplate NamingTemplate { get; private set; } + public IEnumerable TagsRegistered + => NamingTemplate?.TagsRegistered.Cast() ?? Enumerable.Empty(); + public IEnumerable TagsInUse + => NamingTemplate?.TagsInUse.Cast() ?? Enumerable.Empty(); + public string TemplateText => NamingTemplate?.TemplateText ?? ""; + + private readonly NamingTemplate? _namingTemplate; + protected NamingTemplate NamingTemplate + { + get => _namingTemplate ?? throw new NullReferenceException(nameof(_namingTemplate)); + private init => _namingTemplate = value; + } #endregion @@ -104,7 +114,7 @@ namespace LibationFileManager return string.Concat(NamingTemplate.Evaluate(libraryBookDto, multiChapProps).Select(p => p.Value)); } - public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, ReplacementCharacters replacements = null, bool returnFirstExisting = false) + public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false) { ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto)); ArgumentValidator.EnsureNotNull(baseDir, nameof(baseDir)); @@ -114,7 +124,7 @@ namespace LibationFileManager return GetFilename(baseDir, fileExtension,replacements, returnFirstExisting, libraryBookDto); } - public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir, string fileExtension, ReplacementCharacters replacements = null, bool returnFirstExisting = false) + public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false) { ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto)); ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps)); @@ -246,7 +256,7 @@ namespace LibationFileManager new(caseSensative: true, StringFormatter, DateTimeFormatter, IntegerFormatter, FloatFormatter) { //Don't allow formatting of Id - { TemplateTags.Id, lb => lb.AudibleProductId, v => v }, + { TemplateTags.Id, lb => lb.AudibleProductId, v => v ?? "" }, { TemplateTags.Title, lb => lb.TitleWithSubtitle }, { TemplateTags.TitleShort, lb => getTitleShort(lb.Title) }, { TemplateTags.AudibleTitle, lb => lb.Title }, @@ -308,13 +318,13 @@ namespace LibationFileManager #region Tag Formatters - private static string getTitleShort(string title) + private static string? getTitleShort(string? title) => title?.IndexOf(':') > 0 ? title.Substring(0, title.IndexOf(':')) : title; - private static string getLanguageShort(string language) + private static string getLanguageShort(string? language) { if (language is null) - return null; + return ""; language = language.Trim(); if (language.Length <= 3) @@ -324,8 +334,9 @@ namespace LibationFileManager private static string StringFormatter(ITemplateTag templateTag, string value, string formatString) { - if (string.Compare(formatString, "u", ignoreCase: true) == 0) return value?.ToUpper(); - else if (string.Compare(formatString, "l", ignoreCase: true) == 0) return value?.ToLower(); + if (value is null) return ""; + else if (string.Compare(formatString, "u", ignoreCase: true) == 0) return value.ToUpper(); + else if (string.Compare(formatString, "l", ignoreCase: true) == 0) return value.ToLower(); else return value; } @@ -358,7 +369,7 @@ namespace LibationFileManager public class FolderTemplate : Templates, ITemplate { public static string Name { get; }= "Folder Template"; - public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)); + public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? ""; public static string DefaultTemplate { get; } = " [<id>]"; public static IEnumerable<TagCollection> TagCollections => new TagCollection[] { filePropertyTags, conditionalTags, folderConditionalTags }; @@ -378,7 +389,7 @@ namespace LibationFileManager public class FileTemplate : Templates, ITemplate { public static string Name { get; } = "File Template"; - public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate)); + public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate)) ?? ""; public static string DefaultTemplate { get; } = "<title> [<id>]"; public static IEnumerable<TagCollection> TagCollections { get; } = new TagCollection[] { filePropertyTags, conditionalTags }; } @@ -386,7 +397,7 @@ namespace LibationFileManager public class ChapterFileTemplate : Templates, ITemplate { public static string Name { get; } = "Chapter File Template"; - public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)); + public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)) ?? ""; public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>"; public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags); @@ -399,7 +410,7 @@ namespace LibationFileManager public class ChapterTitleTemplate : Templates, ITemplate { public static string Name { get; } = "Chapter Title Template"; - public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)); + public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)) ?? ""; public static string DefaultTemplate => "<ch#> - <title short>: <ch title>"; public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(conditionalTags); diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs index f2c51a5e..92bd78c3 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs @@ -360,7 +360,7 @@ // booksSelectControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; booksSelectControl.AutoSize = true; - booksSelectControl.Location = new System.Drawing.Point(7, 23); + booksSelectControl.Location = new System.Drawing.Point(8, 38); booksSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); booksSelectControl.Name = "booksSelectControl"; booksSelectControl.Size = new System.Drawing.Size(830, 102); @@ -419,7 +419,7 @@ groupBox1.Controls.Add(gridScaleFactorTbar); groupBox1.Controls.Add(gridFontScaleFactorLbl); groupBox1.Controls.Add(gridFontScaleFactorTbar); - groupBox1.Location = new System.Drawing.Point(6, 261); + groupBox1.Location = new System.Drawing.Point(6, 277); groupBox1.Name = "groupBox1"; groupBox1.Size = new System.Drawing.Size(844, 83); groupBox1.TabIndex = 9; @@ -491,7 +491,7 @@ booksGb.Controls.Add(booksLocationDescLbl); booksGb.Location = new System.Drawing.Point(6, 6); booksGb.Name = "booksGb"; - booksGb.Size = new System.Drawing.Size(844, 249); + booksGb.Size = new System.Drawing.Size(844, 265); booksGb.TabIndex = 0; booksGb.TabStop = false; booksGb.Text = "Books location"; @@ -500,7 +500,7 @@ // lastWriteTimeCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; lastWriteTimeCb.FormattingEnabled = true; - lastWriteTimeCb.Location = new System.Drawing.Point(211, 214); + lastWriteTimeCb.Location = new System.Drawing.Point(212, 229); lastWriteTimeCb.Name = "lastWriteTimeCb"; lastWriteTimeCb.Size = new System.Drawing.Size(272, 23); lastWriteTimeCb.TabIndex = 5; @@ -509,7 +509,7 @@ // creationTimeCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; creationTimeCb.FormattingEnabled = true; - creationTimeCb.Location = new System.Drawing.Point(211, 185); + creationTimeCb.Location = new System.Drawing.Point(212, 200); creationTimeCb.Name = "creationTimeCb"; creationTimeCb.Size = new System.Drawing.Size(272, 23); creationTimeCb.TabIndex = 5; @@ -517,7 +517,7 @@ // lastWriteTimeLbl // lastWriteTimeLbl.AutoSize = true; - lastWriteTimeLbl.Location = new System.Drawing.Point(7, 217); + lastWriteTimeLbl.Location = new System.Drawing.Point(8, 232); lastWriteTimeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); lastWriteTimeLbl.Name = "lastWriteTimeLbl"; lastWriteTimeLbl.Size = new System.Drawing.Size(116, 15); @@ -527,7 +527,7 @@ // creationTimeLbl // creationTimeLbl.AutoSize = true; - creationTimeLbl.Location = new System.Drawing.Point(7, 188); + creationTimeLbl.Location = new System.Drawing.Point(8, 203); creationTimeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); creationTimeLbl.Name = "creationTimeLbl"; creationTimeLbl.Size = new System.Drawing.Size(112, 15); @@ -537,7 +537,7 @@ // overwriteExistingCbox // overwriteExistingCbox.AutoSize = true; - overwriteExistingCbox.Location = new System.Drawing.Point(7, 156); + overwriteExistingCbox.Location = new System.Drawing.Point(8, 171); overwriteExistingCbox.Name = "overwriteExistingCbox"; overwriteExistingCbox.Size = new System.Drawing.Size(129, 19); overwriteExistingCbox.TabIndex = 3; @@ -547,7 +547,7 @@ // saveEpisodesToSeriesFolderCbox // saveEpisodesToSeriesFolderCbox.AutoSize = true; - saveEpisodesToSeriesFolderCbox.Location = new System.Drawing.Point(7, 131); + saveEpisodesToSeriesFolderCbox.Location = new System.Drawing.Point(8, 146); saveEpisodesToSeriesFolderCbox.Name = "saveEpisodesToSeriesFolderCbox"; saveEpisodesToSeriesFolderCbox.Size = new System.Drawing.Size(191, 19); saveEpisodesToSeriesFolderCbox.TabIndex = 3; diff --git a/Source/LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj b/Source/LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj index 7ba33656..146a4b64 100644 --- a/Source/LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj +++ b/Source/LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj @@ -26,7 +26,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.1823.32" /> + <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.1901.177" /> </ItemGroup> <ItemGroup>