From fbbac2afaad22308d6f56fc5014765ef69871f82 Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Sun, 16 Nov 2025 20:29:39 +0200 Subject: [PATCH] Make use of C# 14 features --- .../Utils/Extensions/StringExtensions.cs | 13 ++-- .../Utils/Extensions/ConsoleExtensions.cs | 51 +++++++------- .../Discord/RateLimitPreference.cs | 40 ++++++----- .../Exporting/ExportAssetDownloader.cs | 2 +- .../Exporting/ExportFormat.cs | 43 ++++++------ .../Exporting/ExportRequest.cs | 5 +- .../Extensions/AsyncCollectionExtensions.cs | 22 +++--- .../Utils/Extensions/BinaryExtensions.cs | 17 +++-- .../Utils/Extensions/CollectionExtensions.cs | 29 +++++--- .../Utils/Extensions/ColorExtensions.cs | 11 +-- .../Utils/Extensions/ExceptionExtensions.cs | 33 +++++---- .../Utils/Extensions/GenericExtensions.cs | 17 +++-- .../Utils/Extensions/HttpExtensions.cs | 7 +- .../Utils/Extensions/PathExtensions.cs | 29 ++++++++ .../Utils/Extensions/StringExtensions.cs | 41 ++++++----- .../Utils/Extensions/SuperpowerExtensions.cs | 29 ++++---- .../Utils/Extensions/TimeSpanExtensions.cs | 15 ++-- DiscordChatExporter.Core/Utils/PathEx.cs | 32 --------- .../Utils/Extensions/AvaloniaExtensions.cs | 43 ++++++------ .../Utils/Extensions/DisposableExtensions.cs | 33 +++++---- .../NotifyPropertyChangedExtensions.cs | 69 +++++++++---------- .../Utils/Extensions/ProcessExtensions.cs | 17 +++++ DiscordChatExporter.Gui/Utils/ProcessEx.cs | 14 ---- .../Components/DashboardViewModel.cs | 7 +- .../ViewModels/MainViewModel.cs | 5 +- 25 files changed, 337 insertions(+), 287 deletions(-) create mode 100644 DiscordChatExporter.Core/Utils/Extensions/PathExtensions.cs delete mode 100644 DiscordChatExporter.Core/Utils/PathEx.cs create mode 100644 DiscordChatExporter.Gui/Utils/Extensions/ProcessExtensions.cs delete mode 100644 DiscordChatExporter.Gui/Utils/ProcessEx.cs diff --git a/DiscordChatExporter.Cli.Tests/Utils/Extensions/StringExtensions.cs b/DiscordChatExporter.Cli.Tests/Utils/Extensions/StringExtensions.cs index db61fc14..4db07483 100644 --- a/DiscordChatExporter.Cli.Tests/Utils/Extensions/StringExtensions.cs +++ b/DiscordChatExporter.Cli.Tests/Utils/Extensions/StringExtensions.cs @@ -4,13 +4,16 @@ namespace DiscordChatExporter.Cli.Tests.Utils.Extensions; internal static class StringExtensions { - public static string ReplaceWhiteSpace(this string str, string replacement = " ") + extension(string str) { - var buffer = new StringBuilder(str.Length); + public string ReplaceWhiteSpace(string replacement = " ") + { + var buffer = new StringBuilder(str.Length); - foreach (var ch in str) - buffer.Append(char.IsWhiteSpace(ch) ? replacement : ch); + foreach (var ch in str) + buffer.Append(char.IsWhiteSpace(ch) ? replacement : ch); - return buffer.ToString(); + return buffer.ToString(); + } } } diff --git a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs index 4509026b..d7d47c8c 100644 --- a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs +++ b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs @@ -7,32 +7,35 @@ namespace DiscordChatExporter.Cli.Utils.Extensions; internal static class ConsoleExtensions { - public static IAnsiConsole CreateAnsiConsole(this IConsole console) => - AnsiConsole.Create( - new AnsiConsoleSettings - { - Ansi = AnsiSupport.Detect, - ColorSystem = ColorSystemSupport.Detect, - Out = new AnsiConsoleOutput(console.Output), - } - ); - - public static Status CreateStatusTicker(this IConsole console) => - console.CreateAnsiConsole().Status().AutoRefresh(true); - - public static Progress CreateProgressTicker(this IConsole console) => - console - .CreateAnsiConsole() - .Progress() - .AutoClear(false) - .AutoRefresh(true) - .HideCompleted(false) - .Columns( - new TaskDescriptionColumn { Alignment = Justify.Left }, - new ProgressBarColumn(), - new PercentageColumn() + extension(IConsole console) + { + public IAnsiConsole CreateAnsiConsole() => + AnsiConsole.Create( + new AnsiConsoleSettings + { + Ansi = AnsiSupport.Detect, + ColorSystem = ColorSystemSupport.Detect, + Out = new AnsiConsoleOutput(console.Output), + } ); + public Status CreateStatusTicker() => + console.CreateAnsiConsole().Status().AutoRefresh(true); + + public Progress CreateProgressTicker() => + console + .CreateAnsiConsole() + .Progress() + .AutoClear(false) + .AutoRefresh(true) + .HideCompleted(false) + .Columns( + new TaskDescriptionColumn { Alignment = Justify.Left }, + new ProgressBarColumn(), + new PercentageColumn() + ); + } + public static async ValueTask StartTaskAsync( this ProgressContext context, string description, diff --git a/DiscordChatExporter.Core/Discord/RateLimitPreference.cs b/DiscordChatExporter.Core/Discord/RateLimitPreference.cs index 62d9acbc..acd95b6a 100644 --- a/DiscordChatExporter.Core/Discord/RateLimitPreference.cs +++ b/DiscordChatExporter.Core/Discord/RateLimitPreference.cs @@ -13,24 +13,26 @@ public enum RateLimitPreference public static class RateLimitPreferenceExtensions { - internal static bool IsRespectedFor( - this RateLimitPreference rateLimitPreference, - TokenKind tokenKind - ) => - tokenKind switch - { - TokenKind.User => (rateLimitPreference & RateLimitPreference.RespectForUserTokens) != 0, - TokenKind.Bot => (rateLimitPreference & RateLimitPreference.RespectForBotTokens) != 0, - _ => throw new ArgumentOutOfRangeException(nameof(tokenKind)), - }; + extension(RateLimitPreference rateLimitPreference) + { + internal bool IsRespectedFor(TokenKind tokenKind) => + tokenKind switch + { + TokenKind.User => (rateLimitPreference & RateLimitPreference.RespectForUserTokens) + != 0, + TokenKind.Bot => (rateLimitPreference & RateLimitPreference.RespectForBotTokens) + != 0, + _ => throw new ArgumentOutOfRangeException(nameof(tokenKind)), + }; - public static string GetDisplayName(this RateLimitPreference rateLimitPreference) => - rateLimitPreference switch - { - RateLimitPreference.IgnoreAll => "Always ignore", - RateLimitPreference.RespectForUserTokens => "Respect for user tokens", - RateLimitPreference.RespectForBotTokens => "Respect for bot tokens", - RateLimitPreference.RespectAll => "Always respect", - _ => throw new ArgumentOutOfRangeException(nameof(rateLimitPreference)), - }; + public string GetDisplayName() => + rateLimitPreference switch + { + RateLimitPreference.IgnoreAll => "Always ignore", + RateLimitPreference.RespectForUserTokens => "Respect for user tokens", + RateLimitPreference.RespectForBotTokens => "Respect for bot tokens", + RateLimitPreference.RespectAll => "Always respect", + _ => throw new ArgumentOutOfRangeException(nameof(rateLimitPreference)), + }; + } } diff --git a/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs b/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs index 13696b0c..4c74cf6c 100644 --- a/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs +++ b/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs @@ -103,7 +103,7 @@ internal partial class ExportAssetDownloader fileExtension = ""; } - return PathEx.EscapeFileName( + return Path.EscapeFileName( fileNameWithoutExtension.Truncate(42) + '-' + urlHash + fileExtension ); } diff --git a/DiscordChatExporter.Core/Exporting/ExportFormat.cs b/DiscordChatExporter.Core/Exporting/ExportFormat.cs index 233a5a82..4d0e8e9c 100644 --- a/DiscordChatExporter.Core/Exporting/ExportFormat.cs +++ b/DiscordChatExporter.Core/Exporting/ExportFormat.cs @@ -13,25 +13,28 @@ public enum ExportFormat public static class ExportFormatExtensions { - public static string GetFileExtension(this ExportFormat format) => - format switch - { - ExportFormat.PlainText => "txt", - ExportFormat.HtmlDark => "html", - ExportFormat.HtmlLight => "html", - ExportFormat.Csv => "csv", - ExportFormat.Json => "json", - _ => throw new ArgumentOutOfRangeException(nameof(format)), - }; + extension(ExportFormat format) + { + public string GetFileExtension() => + format switch + { + ExportFormat.PlainText => "txt", + ExportFormat.HtmlDark => "html", + ExportFormat.HtmlLight => "html", + ExportFormat.Csv => "csv", + ExportFormat.Json => "json", + _ => throw new ArgumentOutOfRangeException(nameof(format)), + }; - public static string GetDisplayName(this ExportFormat format) => - format switch - { - ExportFormat.PlainText => "TXT", - ExportFormat.HtmlDark => "HTML (Dark)", - ExportFormat.HtmlLight => "HTML (Light)", - ExportFormat.Csv => "CSV", - ExportFormat.Json => "JSON", - _ => throw new ArgumentOutOfRangeException(nameof(format)), - }; + public string GetDisplayName() => + format switch + { + ExportFormat.PlainText => "TXT", + ExportFormat.HtmlDark => "HTML (Dark)", + ExportFormat.HtmlLight => "HTML (Light)", + ExportFormat.Csv => "CSV", + ExportFormat.Json => "JSON", + _ => throw new ArgumentOutOfRangeException(nameof(format)), + }; + } } diff --git a/DiscordChatExporter.Core/Exporting/ExportRequest.cs b/DiscordChatExporter.Core/Exporting/ExportRequest.cs index 2804e6c2..a513e637 100644 --- a/DiscordChatExporter.Core/Exporting/ExportRequest.cs +++ b/DiscordChatExporter.Core/Exporting/ExportRequest.cs @@ -7,7 +7,6 @@ using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Exporting.Filtering; using DiscordChatExporter.Core.Exporting.Partitioning; -using DiscordChatExporter.Core.Utils; using DiscordChatExporter.Core.Utils.Extensions; namespace DiscordChatExporter.Core.Exporting; @@ -145,7 +144,7 @@ public partial class ExportRequest // File extension buffer.Append('.').Append(format.GetFileExtension()); - return PathEx.EscapeFileName(buffer.ToString()); + return Path.EscapeFileName(buffer.ToString()); } private static string FormatPath( @@ -159,7 +158,7 @@ public partial class ExportRequest path, "%.", m => - PathEx.EscapeFileName( + Path.EscapeFileName( m.Value switch { "%g" => guild.Id.ToString(), diff --git a/DiscordChatExporter.Core/Utils/Extensions/AsyncCollectionExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/AsyncCollectionExtensions.cs index 1016a973..2af5e685 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/AsyncCollectionExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/AsyncCollectionExtensions.cs @@ -6,19 +6,19 @@ namespace DiscordChatExporter.Core.Utils.Extensions; public static class AsyncCollectionExtensions { - private static async ValueTask> CollectAsync( - this IAsyncEnumerable asyncEnumerable - ) + extension(IAsyncEnumerable asyncEnumerable) { - var list = new List(); + private async ValueTask> CollectAsync() + { + var list = new List(); - await foreach (var i in asyncEnumerable) - list.Add(i); + await foreach (var i in asyncEnumerable) + list.Add(i); - return list; + return list; + } + + public ValueTaskAwaiter> GetAwaiter() => + asyncEnumerable.CollectAsync().GetAwaiter(); } - - public static ValueTaskAwaiter> GetAwaiter( - this IAsyncEnumerable asyncEnumerable - ) => asyncEnumerable.CollectAsync().GetAwaiter(); } diff --git a/DiscordChatExporter.Core/Utils/Extensions/BinaryExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/BinaryExtensions.cs index a585f258..125b2da3 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/BinaryExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/BinaryExtensions.cs @@ -5,15 +5,18 @@ namespace DiscordChatExporter.Core.Utils.Extensions; public static class BinaryExtensions { - public static string ToHex(this byte[] data, bool isUpperCase = true) + extension(byte[] data) { - var buffer = new StringBuilder(2 * data.Length); - - foreach (var b in data) + public string ToHex(bool isUpperCase = true) { - buffer.Append(b.ToString(isUpperCase ? "X2" : "x2", CultureInfo.InvariantCulture)); - } + var buffer = new StringBuilder(2 * data.Length); - return buffer.ToString(); + foreach (var b in data) + { + buffer.Append(b.ToString(isUpperCase ? "X2" : "x2", CultureInfo.InvariantCulture)); + } + + return buffer.ToString(); + } } } diff --git a/DiscordChatExporter.Core/Utils/Extensions/CollectionExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/CollectionExtensions.cs index b999ed86..99ee2766 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/CollectionExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/CollectionExtensions.cs @@ -4,25 +4,34 @@ namespace DiscordChatExporter.Core.Utils.Extensions; public static class CollectionExtensions { - public static IEnumerable ToSingletonEnumerable(this T obj) + extension(T obj) { - yield return obj; + public IEnumerable ToSingletonEnumerable() + { + yield return obj; + } } - public static IEnumerable<(T value, int index)> WithIndex(this IEnumerable source) + extension(IEnumerable source) { - var i = 0; - foreach (var o in source) - yield return (o, i++); + public IEnumerable<(T value, int index)> WithIndex() + { + var i = 0; + foreach (var o in source) + yield return (o, i++); + } } - public static IEnumerable WhereNotNull(this IEnumerable source) + extension(IEnumerable source) where T : class { - foreach (var o in source) + public IEnumerable WhereNotNull() { - if (o is not null) - yield return o; + foreach (var o in source) + { + if (o is not null) + yield return o; + } } } } diff --git a/DiscordChatExporter.Core/Utils/Extensions/ColorExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/ColorExtensions.cs index 91b7dd02..a3ec6486 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/ColorExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/ColorExtensions.cs @@ -4,11 +4,14 @@ namespace DiscordChatExporter.Core.Utils.Extensions; public static class ColorExtensions { - public static Color WithAlpha(this Color color, int alpha) => Color.FromArgb(alpha, color); + extension(Color color) + { + public Color WithAlpha(int alpha) => Color.FromArgb(alpha, color); - public static Color ResetAlpha(this Color color) => color.WithAlpha(255); + public Color ResetAlpha() => color.WithAlpha(255); - public static int ToRgb(this Color color) => color.ToArgb() & 0xffffff; + public int ToRgb() => color.ToArgb() & 0xffffff; - public static string ToHex(this Color color) => $"#{color.R:X2}{color.G:X2}{color.B:X2}"; + public string ToHex() => $"#{color.R:X2}{color.G:X2}{color.B:X2}"; + } } diff --git a/DiscordChatExporter.Core/Utils/Extensions/ExceptionExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/ExceptionExtensions.cs index 8c7f3c40..e3d67f1f 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/ExceptionExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/ExceptionExtensions.cs @@ -5,27 +5,30 @@ namespace DiscordChatExporter.Core.Utils.Extensions; public static class ExceptionExtensions { - private static void PopulateChildren(this Exception exception, ICollection children) + extension(Exception exception) { - if (exception is AggregateException aggregateException) + private void PopulateChildren(ICollection children) { - foreach (var innerException in aggregateException.InnerExceptions) + if (exception is AggregateException aggregateException) { - children.Add(innerException); - PopulateChildren(innerException, children); + foreach (var innerException in aggregateException.InnerExceptions) + { + children.Add(innerException); + PopulateChildren(innerException, children); + } + } + else if (exception.InnerException is not null) + { + children.Add(exception.InnerException); + PopulateChildren(exception.InnerException, children); } } - else if (exception.InnerException is not null) + + public IReadOnlyList GetSelfAndChildren() { - children.Add(exception.InnerException); - PopulateChildren(exception.InnerException, children); + var children = new List { exception }; + PopulateChildren(exception, children); + return children; } } - - public static IReadOnlyList GetSelfAndChildren(this Exception exception) - { - var children = new List { exception }; - PopulateChildren(exception, children); - return children; - } } diff --git a/DiscordChatExporter.Core/Utils/Extensions/GenericExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/GenericExtensions.cs index c91b720a..2c47995d 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/GenericExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/GenericExtensions.cs @@ -5,12 +5,17 @@ namespace DiscordChatExporter.Core.Utils.Extensions; public static class GenericExtensions { - public static TOut Pipe(this TIn input, Func transform) => - transform(input); + extension(TIn input) + { + public TOut Pipe(Func transform) => transform(input); + } - public static T? NullIf(this T value, Func predicate) - where T : struct => !predicate(value) ? value : null; + extension(T value) + where T : struct + { + public T? NullIf(Func predicate) => !predicate(value) ? value : null; - public static T? NullIfDefault(this T value) - where T : struct => value.NullIf(v => EqualityComparer.Default.Equals(v, default)); + public T? NullIfDefault() => + value.NullIf(v => EqualityComparer.Default.Equals(v, default)); + } } diff --git a/DiscordChatExporter.Core/Utils/Extensions/HttpExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/HttpExtensions.cs index 06626eaf..19841afc 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/HttpExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/HttpExtensions.cs @@ -4,6 +4,9 @@ namespace DiscordChatExporter.Core.Utils.Extensions; public static class HttpExtensions { - public static string? TryGetValue(this HttpHeaders headers, string name) => - headers.TryGetValues(name, out var values) ? string.Concat(values) : null; + extension(HttpHeaders headers) + { + public string? TryGetValue(string name) => + headers.TryGetValues(name, out var values) ? string.Concat(values) : null; + } } diff --git a/DiscordChatExporter.Core/Utils/Extensions/PathExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/PathExtensions.cs new file mode 100644 index 00000000..a7c24048 --- /dev/null +++ b/DiscordChatExporter.Core/Utils/Extensions/PathExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; +using System.Text; + +namespace DiscordChatExporter.Core.Utils.Extensions; + +public static class PathExtensions +{ + extension(Path) + { + public static string EscapeFileName(string path) + { + var buffer = new StringBuilder(path.Length); + + foreach (var c in path) + buffer.Append(!Path.GetInvalidFileNameChars().Contains(c) ? c : '_'); + + // File names cannot end with a dot on Windows + // https://github.com/Tyrrrz/DiscordChatExporter/issues/977 + if (OperatingSystem.IsWindows()) + { + while (buffer.Length > 0 && buffer[^1] == '.') + buffer.Remove(buffer.Length - 1, 1); + } + + return buffer.ToString(); + } + } +} diff --git a/DiscordChatExporter.Core/Utils/Extensions/StringExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/StringExtensions.cs index de035fbe..46dfc0bf 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/StringExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/StringExtensions.cs @@ -5,30 +5,35 @@ namespace DiscordChatExporter.Core.Utils.Extensions; public static class StringExtensions { - public static string? NullIfWhiteSpace(this string str) => - !string.IsNullOrWhiteSpace(str) ? str : null; - - public static string Truncate(this string str, int charCount) => - str.Length > charCount ? str[..charCount] : str; - - public static string ToSpaceSeparatedWords(this string str) + extension(string str) { - var builder = new StringBuilder(str.Length * 2); + public string? NullIfWhiteSpace() => !string.IsNullOrWhiteSpace(str) ? str : null; - foreach (var c in str) + public string Truncate(int charCount) => str.Length > charCount ? str[..charCount] : str; + + public string ToSpaceSeparatedWords() { - if (char.IsUpper(c) && builder.Length > 0) - builder.Append(' '); + var builder = new StringBuilder(str.Length * 2); - builder.Append(c); + foreach (var c in str) + { + if (char.IsUpper(c) && builder.Length > 0) + builder.Append(' '); + + builder.Append(c); + } + + return builder.ToString(); } - return builder.ToString(); + public T? ParseEnumOrNull(bool ignoreCase = true) + where T : struct, Enum => + Enum.TryParse(str, ignoreCase, out var result) ? result : null; } - public static T? ParseEnumOrNull(this string str, bool ignoreCase = true) - where T : struct, Enum => Enum.TryParse(str, ignoreCase, out var result) ? result : null; - - public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) => - builder.Length > 0 ? builder.Append(value) : builder; + extension(StringBuilder builder) + { + public StringBuilder AppendIfNotEmpty(char value) => + builder.Length > 0 ? builder.Append(value) : builder; + } } diff --git a/DiscordChatExporter.Core/Utils/Extensions/SuperpowerExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/SuperpowerExtensions.cs index 28e9cb91..59aeaaa7 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/SuperpowerExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/SuperpowerExtensions.cs @@ -7,18 +7,21 @@ namespace DiscordChatExporter.Core.Utils.Extensions; public static class SuperpowerExtensions { - public static TextParser Token(this TextParser parser) => - parser.Between(Character.WhiteSpace.IgnoreMany(), Character.WhiteSpace.IgnoreMany()); + extension(TextParser parser) + { + public TextParser Token() => + parser.Between(Character.WhiteSpace.IgnoreMany(), Character.WhiteSpace.IgnoreMany()); - // Only used for debugging while writing Superpower parsers. - // From https://twitter.com/nblumhardt/status/1389349059786264578 - [ExcludeFromCodeCoverage] - public static TextParser Log(this TextParser parser, string description) => - i => - { - Console.WriteLine($"Trying {description} ->"); - var r = parser(i); - Console.WriteLine($"Result was {r}"); - return r; - }; + // Only used for debugging while writing Superpower parsers. + // From https://twitter.com/nblumhardt/status/1389349059786264578 + [ExcludeFromCodeCoverage] + public TextParser Log(string description) => + i => + { + Console.WriteLine($"Trying {description} ->"); + var r = parser(i); + Console.WriteLine($"Result was {r}"); + return r; + }; + } } diff --git a/DiscordChatExporter.Core/Utils/Extensions/TimeSpanExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/TimeSpanExtensions.cs index 11a0192a..9d838b09 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/TimeSpanExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/TimeSpanExtensions.cs @@ -4,14 +4,17 @@ namespace DiscordChatExporter.Core.Utils.Extensions; public static class TimeSpanExtensions { - public static TimeSpan Clamp(this TimeSpan value, TimeSpan min, TimeSpan max) + extension(TimeSpan value) { - if (value < min) - return min; + public TimeSpan Clamp(TimeSpan min, TimeSpan max) + { + if (value < min) + return min; - if (value > max) - return max; + if (value > max) + return max; - return value; + return value; + } } } diff --git a/DiscordChatExporter.Core/Utils/PathEx.cs b/DiscordChatExporter.Core/Utils/PathEx.cs deleted file mode 100644 index d8ee8a65..00000000 --- a/DiscordChatExporter.Core/Utils/PathEx.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Frozen; -using System.IO; -using System.Text; - -namespace DiscordChatExporter.Core.Utils; - -public static class PathEx -{ - private static readonly FrozenSet InvalidFileNameChars = - [ - .. Path.GetInvalidFileNameChars(), - ]; - - public static string EscapeFileName(string path) - { - var buffer = new StringBuilder(path.Length); - - foreach (var c in path) - buffer.Append(!InvalidFileNameChars.Contains(c) ? c : '_'); - - // File names cannot end with a dot on Windows - // https://github.com/Tyrrrz/DiscordChatExporter/issues/977 - if (OperatingSystem.IsWindows()) - { - while (buffer.Length > 0 && buffer[^1] == '.') - buffer.Remove(buffer.Length - 1, 1); - } - - return buffer.ToString(); - } -} diff --git a/DiscordChatExporter.Gui/Utils/Extensions/AvaloniaExtensions.cs b/DiscordChatExporter.Gui/Utils/Extensions/AvaloniaExtensions.cs index 9d339a15..33749284 100644 --- a/DiscordChatExporter.Gui/Utils/Extensions/AvaloniaExtensions.cs +++ b/DiscordChatExporter.Gui/Utils/Extensions/AvaloniaExtensions.cs @@ -6,28 +6,31 @@ namespace DiscordChatExporter.Gui.Utils.Extensions; internal static class AvaloniaExtensions { - public static Window? TryGetMainWindow(this IApplicationLifetime lifetime) => - lifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime - ? desktopLifetime.MainWindow - : null; - - public static TopLevel? TryGetTopLevel(this IApplicationLifetime lifetime) => - lifetime.TryGetMainWindow() - ?? (lifetime as ISingleViewApplicationLifetime)?.MainView?.GetVisualRoot() as TopLevel; - - public static bool TryShutdown(this IApplicationLifetime lifetime, int exitCode = 0) + extension(IApplicationLifetime lifetime) { - if (lifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) - { - return desktopLifetime.TryShutdown(exitCode); - } + public Window? TryGetMainWindow() => + lifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime + ? desktopLifetime.MainWindow + : null; - if (lifetime is IControlledApplicationLifetime controlledLifetime) - { - controlledLifetime.Shutdown(exitCode); - return true; - } + public TopLevel? TryGetTopLevel() => + lifetime.TryGetMainWindow() + ?? (lifetime as ISingleViewApplicationLifetime)?.MainView?.GetVisualRoot() as TopLevel; - return false; + public bool TryShutdown(int exitCode = 0) + { + if (lifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + { + return desktopLifetime.TryShutdown(exitCode); + } + + if (lifetime is IControlledApplicationLifetime controlledLifetime) + { + controlledLifetime.Shutdown(exitCode); + return true; + } + + return false; + } } } diff --git a/DiscordChatExporter.Gui/Utils/Extensions/DisposableExtensions.cs b/DiscordChatExporter.Gui/Utils/Extensions/DisposableExtensions.cs index de8651d0..553526bd 100644 --- a/DiscordChatExporter.Gui/Utils/Extensions/DisposableExtensions.cs +++ b/DiscordChatExporter.Gui/Utils/Extensions/DisposableExtensions.cs @@ -6,23 +6,26 @@ namespace DiscordChatExporter.Gui.Utils.Extensions; internal static class DisposableExtensions { - public static void DisposeAll(this IEnumerable disposables) + extension(IEnumerable disposables) { - var exceptions = default(List); - - foreach (var disposable in disposables) + public void DisposeAll() { - try - { - disposable.Dispose(); - } - catch (Exception ex) - { - (exceptions ??= []).Add(ex); - } - } + var exceptions = default(List); - if (exceptions?.Any() == true) - throw new AggregateException(exceptions); + foreach (var disposable in disposables) + { + try + { + disposable.Dispose(); + } + catch (Exception ex) + { + (exceptions ??= []).Add(ex); + } + } + + if (exceptions?.Any() == true) + throw new AggregateException(exceptions); + } } } diff --git a/DiscordChatExporter.Gui/Utils/Extensions/NotifyPropertyChangedExtensions.cs b/DiscordChatExporter.Gui/Utils/Extensions/NotifyPropertyChangedExtensions.cs index 511added..027bccef 100644 --- a/DiscordChatExporter.Gui/Utils/Extensions/NotifyPropertyChangedExtensions.cs +++ b/DiscordChatExporter.Gui/Utils/Extensions/NotifyPropertyChangedExtensions.cs @@ -7,50 +7,47 @@ namespace DiscordChatExporter.Gui.Utils.Extensions; internal static class NotifyPropertyChangedExtensions { - public static IDisposable WatchProperty( - this TOwner owner, - Expression> propertyExpression, - Action callback, - bool watchInitialValue = false - ) + extension(TOwner owner) where TOwner : INotifyPropertyChanged { - var memberExpression = propertyExpression.Body as MemberExpression; - if (memberExpression?.Member is not PropertyInfo property) - throw new ArgumentException("Provided expression must reference a property."); - - void OnPropertyChanged(object? sender, PropertyChangedEventArgs args) + public IDisposable WatchProperty( + Expression> propertyExpression, + Action callback, + bool watchInitialValue = false + ) { - if ( - string.IsNullOrWhiteSpace(args.PropertyName) - || string.Equals(args.PropertyName, property.Name, StringComparison.Ordinal) - ) + var memberExpression = propertyExpression.Body as MemberExpression; + if (memberExpression?.Member is not PropertyInfo property) + throw new ArgumentException("Provided expression must reference a property."); + + void OnPropertyChanged(object? sender, PropertyChangedEventArgs args) { - callback(); + if ( + string.IsNullOrWhiteSpace(args.PropertyName) + || string.Equals(args.PropertyName, property.Name, StringComparison.Ordinal) + ) + { + callback(); + } } + + owner.PropertyChanged += OnPropertyChanged; + + if (watchInitialValue) + callback(); + + return Disposable.Create(() => owner.PropertyChanged -= OnPropertyChanged); } - owner.PropertyChanged += OnPropertyChanged; + public IDisposable WatchAllProperties(Action callback, bool watchInitialValues = false) + { + void OnPropertyChanged(object? sender, PropertyChangedEventArgs args) => callback(); + owner.PropertyChanged += OnPropertyChanged; - if (watchInitialValue) - callback(); + if (watchInitialValues) + callback(); - return Disposable.Create(() => owner.PropertyChanged -= OnPropertyChanged); - } - - public static IDisposable WatchAllProperties( - this TOwner owner, - Action callback, - bool watchInitialValues = false - ) - where TOwner : INotifyPropertyChanged - { - void OnPropertyChanged(object? sender, PropertyChangedEventArgs args) => callback(); - owner.PropertyChanged += OnPropertyChanged; - - if (watchInitialValues) - callback(); - - return Disposable.Create(() => owner.PropertyChanged -= OnPropertyChanged); + return Disposable.Create(() => owner.PropertyChanged -= OnPropertyChanged); + } } } diff --git a/DiscordChatExporter.Gui/Utils/Extensions/ProcessExtensions.cs b/DiscordChatExporter.Gui/Utils/Extensions/ProcessExtensions.cs new file mode 100644 index 00000000..8c4915c9 --- /dev/null +++ b/DiscordChatExporter.Gui/Utils/Extensions/ProcessExtensions.cs @@ -0,0 +1,17 @@ +using System.Diagnostics; + +namespace DiscordChatExporter.Gui.Utils.Extensions; + +internal static class ProcessExtensions +{ + extension(Process) + { + public static void StartShellExecute(string path) + { + using var process = new Process(); + process.StartInfo = new ProcessStartInfo { FileName = path, UseShellExecute = true }; + + process.Start(); + } + } +} diff --git a/DiscordChatExporter.Gui/Utils/ProcessEx.cs b/DiscordChatExporter.Gui/Utils/ProcessEx.cs deleted file mode 100644 index 82ef9eba..00000000 --- a/DiscordChatExporter.Gui/Utils/ProcessEx.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Diagnostics; - -namespace DiscordChatExporter.Gui.Utils; - -internal static class ProcessEx -{ - public static void StartShellExecute(string path) - { - using var process = new Process(); - process.StartInfo = new ProcessStartInfo { FileName = path, UseShellExecute = true }; - - process.Start(); - } -} diff --git a/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs index fd754e70..ebd416c2 100644 --- a/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -102,7 +103,7 @@ public partial class DashboardViewModel : ViewModelBase await _dialogManager.ShowDialogAsync(_viewModelManager.CreateSettingsViewModel()); [RelayCommand] - private void ShowHelp() => ProcessEx.StartShellExecute(Program.ProjectDocumentationUrl); + private void ShowHelp() => Process.StartShellExecute(Program.ProjectDocumentationUrl); private bool CanPullGuilds() => !IsBusy && !string.IsNullOrWhiteSpace(Token); @@ -322,11 +323,11 @@ public partial class DashboardViewModel : ViewModelBase } [RelayCommand] - private void OpenDiscord() => ProcessEx.StartShellExecute("https://discord.com/app"); + private void OpenDiscord() => Process.StartShellExecute("https://discord.com/app"); [RelayCommand] private void OpenDiscordDeveloperPortal() => - ProcessEx.StartShellExecute("https://discord.com/developers/applications"); + Process.StartShellExecute("https://discord.com/developers/applications"); protected override void Dispose(bool disposing) { diff --git a/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs b/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs index 0e14a28a..a40ca6fb 100644 --- a/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs @@ -5,7 +5,6 @@ using Avalonia; using CommunityToolkit.Mvvm.Input; using DiscordChatExporter.Gui.Framework; using DiscordChatExporter.Gui.Services; -using DiscordChatExporter.Gui.Utils; using DiscordChatExporter.Gui.Utils.Extensions; using DiscordChatExporter.Gui.ViewModels.Components; @@ -44,7 +43,7 @@ public partial class MainViewModel( settingsService.Save(); if (await dialogManager.ShowDialogAsync(dialog) == true) - ProcessEx.StartShellExecute("https://tyrrrz.me/ukraine?source=discordchatexporter"); + Process.StartShellExecute("https://tyrrrz.me/ukraine?source=discordchatexporter"); } private async Task ShowDevelopmentBuildMessageAsync() @@ -70,7 +69,7 @@ public partial class MainViewModel( ); if (await dialogManager.ShowDialogAsync(dialog) == true) - ProcessEx.StartShellExecute(Program.ProjectReleasesUrl); + Process.StartShellExecute(Program.ProjectReleasesUrl); } private async Task CheckForUpdatesAsync()