Use nullable

This commit is contained in:
Alexey Golub 2019-11-13 19:19:36 +02:00
parent 1bf9d9e2e2
commit e5a2852165
42 changed files with 195 additions and 196 deletions

View file

@ -7,7 +7,6 @@ using CliFx.Utilities;
using DiscordChatExporter.Core.Models; using DiscordChatExporter.Core.Models;
using DiscordChatExporter.Core.Services; using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Core.Services.Helpers; using DiscordChatExporter.Core.Services.Helpers;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Cli.Commands namespace DiscordChatExporter.Cli.Commands
{ {
@ -22,7 +21,7 @@ namespace DiscordChatExporter.Cli.Commands
public ExportFormat ExportFormat { get; set; } = ExportFormat.HtmlDark; public ExportFormat ExportFormat { get; set; } = ExportFormat.HtmlDark;
[CommandOption("output", 'o', Description = "Output file or directory path.")] [CommandOption("output", 'o', Description = "Output file or directory path.")]
public string OutputPath { get; set; } public string? OutputPath { get; set; }
[CommandOption("after",Description = "Limit to messages sent after this date.")] [CommandOption("after",Description = "Limit to messages sent after this date.")]
public DateTimeOffset? After { get; set; } public DateTimeOffset? After { get; set; }
@ -34,7 +33,7 @@ namespace DiscordChatExporter.Cli.Commands
public int? PartitionLimit { get; set; } public int? PartitionLimit { get; set; }
[CommandOption("dateformat", Description = "Date format used in output.")] [CommandOption("dateformat", Description = "Date format used in output.")]
public string DateFormat { get; set; } public string? DateFormat { get; set; }
protected ExportCommandBase(SettingsService settingsService, DataService dataService, ExportService exportService) protected ExportCommandBase(SettingsService settingsService, DataService dataService, ExportService exportService)
: base(dataService) : base(dataService)
@ -46,8 +45,8 @@ namespace DiscordChatExporter.Cli.Commands
protected async Task ExportChannelAsync(IConsole console, Channel channel) protected async Task ExportChannelAsync(IConsole console, Channel channel)
{ {
// Configure settings // Configure settings
if (!DateFormat.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(DateFormat))
SettingsService.DateFormat = DateFormat; SettingsService.DateFormat = DateFormat!;
console.Output.Write($"Exporting channel [{channel.Name}]... "); console.Output.Write($"Exporting channel [{channel.Name}]... ");
var progress = console.CreateProgressTicker(); var progress = console.CreateProgressTicker();
@ -57,7 +56,7 @@ namespace DiscordChatExporter.Cli.Commands
// Generate file path if not set or is a directory // Generate file path if not set or is a directory
var filePath = OutputPath; var filePath = OutputPath;
if (filePath.IsNullOrWhiteSpace() || ExportHelper.IsDirectoryPath(filePath)) if (string.IsNullOrWhiteSpace(filePath) || ExportHelper.IsDirectoryPath(filePath))
{ {
// Generate default file name // Generate default file name
var fileName = ExportHelper.GetDefaultExportFileName(ExportFormat, chatLog.Guild, var fileName = ExportHelper.GetDefaultExportFileName(ExportFormat, chatLog.Guild,

View file

@ -6,13 +6,14 @@
<Version>2.15</Version> <Version>2.15</Version>
<Company>Tyrrrz</Company> <Company>Tyrrrz</Company>
<Copyright>Copyright (c) Alexey Golub</Copyright> <Copyright>Copyright (c) Alexey Golub</Copyright>
<Nullable>enable</Nullable>
<ApplicationIcon>..\favicon.ico</ApplicationIcon> <ApplicationIcon>..\favicon.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CliFx" Version="0.0.7" /> <PackageReference Include="CliFx" Version="0.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" />
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.3" /> <PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -2,10 +2,11 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.3" /> <PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -16,9 +16,9 @@ namespace DiscordChatExporter.Core.Markdown.Internal
{ {
} }
public ParsedMatch<T> Match(StringPart stringPart) public ParsedMatch<T>? Match(StringPart stringPart)
{ {
ParsedMatch<T> earliestMatch = null; ParsedMatch<T>? earliestMatch = null;
// Try to match the input with each matcher and get the match with the lowest start index // Try to match the input with each matcher and get the match with the lowest start index
foreach (var matcher in _matchers) foreach (var matcher in _matchers)

View file

@ -2,6 +2,6 @@
{ {
internal interface IMatcher<T> internal interface IMatcher<T>
{ {
ParsedMatch<T> Match(StringPart stringPart); ParsedMatch<T>? Match(StringPart stringPart);
} }
} }

View file

@ -1,8 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace DiscordChatExporter.Core.Markdown.Internal namespace DiscordChatExporter.Core.Markdown.Internal
@ -23,7 +19,7 @@ namespace DiscordChatExporter.Core.Markdown.Internal
{ {
} }
public ParsedMatch<T> Match(StringPart stringPart) public ParsedMatch<T>? Match(StringPart stringPart)
{ {
var match = _regex.Match(stringPart.Target, stringPart.StartIndex, stringPart.Length); var match = _regex.Match(stringPart.Target, stringPart.StartIndex, stringPart.Length);
if (!match.Success) if (!match.Success)

View file

@ -20,7 +20,7 @@ namespace DiscordChatExporter.Core.Markdown.Internal
{ {
} }
public ParsedMatch<T> Match(StringPart stringPart) public ParsedMatch<T>? Match(StringPart stringPart)
{ {
var index = stringPart.Target.IndexOf(_needle, stringPart.StartIndex, stringPart.Length, _comparison); var index = stringPart.Target.IndexOf(_needle, stringPart.StartIndex, stringPart.Length, _comparison);

View file

@ -3,7 +3,6 @@ using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using DiscordChatExporter.Core.Markdown.Internal; using DiscordChatExporter.Core.Markdown.Internal;
using DiscordChatExporter.Core.Markdown.Nodes; using DiscordChatExporter.Core.Markdown.Nodes;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Core.Markdown namespace DiscordChatExporter.Core.Markdown
{ {
@ -125,7 +124,7 @@ namespace DiscordChatExporter.Core.Markdown
// Capture <:lul:123456> or <a:lul:123456> // Capture <:lul:123456> or <a:lul:123456>
private static readonly IMatcher<Node> CustomEmojiNodeMatcher = new RegexMatcher<Node>( private static readonly IMatcher<Node> CustomEmojiNodeMatcher = new RegexMatcher<Node>(
new Regex("<(a)?:(.+?):(\\d+?)>", DefaultRegexOptions), new Regex("<(a)?:(.+?):(\\d+?)>", DefaultRegexOptions),
m => new EmojiNode(m.Groups[3].Value, m.Groups[2].Value, !m.Groups[1].Value.IsNullOrWhiteSpace())); m => new EmojiNode(m.Groups[3].Value, m.Groups[2].Value, !string.IsNullOrWhiteSpace(m.Groups[1].Value)));
/* Links */ /* Links */

View file

@ -1,18 +1,16 @@
using Tyrrrz.Extensions; namespace DiscordChatExporter.Core.Markdown.Nodes
namespace DiscordChatExporter.Core.Markdown.Nodes
{ {
public class EmojiNode : Node public class EmojiNode : Node
{ {
public string Id { get; } public string? Id { get; }
public string Name { get; } public string Name { get; }
public bool IsAnimated { get; } public bool IsAnimated { get; }
public bool IsCustomEmoji => !Id.IsNullOrWhiteSpace(); public bool IsCustomEmoji => !string.IsNullOrWhiteSpace(Id);
public EmojiNode(string id, string name, bool isAnimated) public EmojiNode(string? id, string name, bool isAnimated)
{ {
Id = id; Id = id;
Name = name; Name = name;

View file

@ -6,17 +6,17 @@
{ {
public string Id { get; } public string Id { get; }
public string ParentId { get; } public string? ParentId { get; }
public string GuildId { get; } public string? GuildId { get; }
public string Name { get; } public string Name { get; }
public string Topic { get; } public string? Topic { get; }
public ChannelType Type { get; } public ChannelType Type { get; }
public Channel(string id, string parentId, string guildId, string name, string topic, ChannelType type) public Channel(string id, string? parentId, string? guildId, string name, string? topic, ChannelType type)
{ {
Id = id; Id = id;
ParentId = parentId; ParentId = parentId;

View file

@ -2,10 +2,11 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.3" /> <PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -8,28 +8,29 @@ namespace DiscordChatExporter.Core.Models
public class Embed public class Embed
{ {
public string Title { get; } public string? Title { get; }
public string Url { get; } public string? Url { get; }
public DateTimeOffset? Timestamp { get; } public DateTimeOffset? Timestamp { get; }
// TODO: this should be nullable and default color should be set in CSS
public Color Color { get; } public Color Color { get; }
public EmbedAuthor Author { get; } public EmbedAuthor? Author { get; }
public string Description { get; } public string? Description { get; }
public IReadOnlyList<EmbedField> Fields { get; } public IReadOnlyList<EmbedField> Fields { get; }
public EmbedImage Thumbnail { get; } public EmbedImage? Thumbnail { get; }
public EmbedImage Image { get; } public EmbedImage? Image { get; }
public EmbedFooter Footer { get; } public EmbedFooter? Footer { get; }
public Embed(string title, string url, DateTimeOffset? timestamp, Color color, EmbedAuthor author, string description, public Embed(string? title, string? url, DateTimeOffset? timestamp, Color color, EmbedAuthor? author, string? description,
IReadOnlyList<EmbedField> fields, EmbedImage thumbnail, EmbedImage image, EmbedFooter footer) IReadOnlyList<EmbedField> fields, EmbedImage? thumbnail, EmbedImage? image, EmbedFooter? footer)
{ {
Title = title; Title = title;
Url = url; Url = url;
@ -43,6 +44,6 @@ namespace DiscordChatExporter.Core.Models
Footer = footer; Footer = footer;
} }
public override string ToString() => Title; public override string ToString() => Title ?? "<untitled embed>";
} }
} }

View file

@ -4,19 +4,19 @@ namespace DiscordChatExporter.Core.Models
public class EmbedAuthor public class EmbedAuthor
{ {
public string Name { get; } public string? Name { get; }
public string Url { get; } public string? Url { get; }
public string IconUrl { get; } public string? IconUrl { get; }
public EmbedAuthor(string name, string url, string iconUrl) public EmbedAuthor(string? name, string? url, string? iconUrl)
{ {
Name = name; Name = name;
Url = url; Url = url;
IconUrl = iconUrl; IconUrl = iconUrl;
} }
public override string ToString() => Name; public override string ToString() => Name ?? "<unnamed author>";
} }
} }

View file

@ -6,9 +6,9 @@ namespace DiscordChatExporter.Core.Models
{ {
public string Text { get; } public string Text { get; }
public string IconUrl { get; } public string? IconUrl { get; }
public EmbedFooter(string text, string iconUrl) public EmbedFooter(string text, string? iconUrl)
{ {
Text = text; Text = text;
IconUrl = iconUrl; IconUrl = iconUrl;

View file

@ -4,13 +4,13 @@ namespace DiscordChatExporter.Core.Models
public class EmbedImage public class EmbedImage
{ {
public string Url { get; } public string? Url { get; }
public int? Width { get; } public int? Width { get; }
public int? Height { get; } public int? Height { get; }
public EmbedImage(string url, int? width, int? height) public EmbedImage(string? url, int? width, int? height)
{ {
Url = url; Url = url;
Height = height; Height = height;

View file

@ -8,7 +8,7 @@ namespace DiscordChatExporter.Core.Models
public partial class Emoji public partial class Emoji
{ {
public string Id { get; } public string? Id { get; }
public string Name { get; } public string Name { get; }
@ -16,7 +16,7 @@ namespace DiscordChatExporter.Core.Models
public string ImageUrl { get; } public string ImageUrl { get; }
public Emoji(string id, string name, bool isAnimated) public Emoji(string? id, string name, bool isAnimated)
{ {
Id = id; Id = id;
Name = name; Name = name;
@ -37,10 +37,10 @@ namespace DiscordChatExporter.Core.Models
private static string GetTwemojiName(string emoji) => private static string GetTwemojiName(string emoji) =>
GetCodePoints(emoji).Select(i => i.ToString("x")).JoinToString("-"); GetCodePoints(emoji).Select(i => i.ToString("x")).JoinToString("-");
public static string GetImageUrl(string id, string name, bool isAnimated) public static string GetImageUrl(string? id, string name, bool isAnimated)
{ {
// Custom emoji // Custom emoji
if (!id.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(id))
{ {
// Animated // Animated
if (isAnimated) if (isAnimated)

View file

@ -1,8 +1,6 @@
using Tyrrrz.Extensions; namespace DiscordChatExporter.Core.Models
namespace DiscordChatExporter.Core.Models
{ {
// https://discordapp.com/developers/docs/resources/guild#guild-object // https://discordapp.string.IsNullOrWhiteSpace(com/developers/docs/resources/guild#guild-object
public partial class Guild public partial class Guild
{ {
@ -10,11 +8,11 @@ namespace DiscordChatExporter.Core.Models
public string Name { get; } public string Name { get; }
public string IconHash { get; } public string? IconHash { get; }
public string IconUrl { get; } public string IconUrl { get; }
public Guild(string id, string name, string iconHash) public Guild(string id, string name, string? iconHash)
{ {
Id = id; Id = id;
Name = name; Name = name;
@ -28,9 +26,9 @@ namespace DiscordChatExporter.Core.Models
public partial class Guild public partial class Guild
{ {
public static string GetIconUrl(string id, string iconHash) public static string GetIconUrl(string id, string? iconHash)
{ {
return !iconHash.IsNullOrWhiteSpace() return !string.IsNullOrWhiteSpace(iconHash)
? $"https://cdn.discordapp.com/icons/{id}/{iconHash}.png" ? $"https://cdn.discordapp.com/icons/{id}/{iconHash}.png"
: "https://cdn.discordapp.com/embed/avatars/0.png"; : "https://cdn.discordapp.com/embed/avatars/0.png";
} }

View file

@ -19,7 +19,7 @@ namespace DiscordChatExporter.Core.Models
public DateTimeOffset? EditedTimestamp { get; } public DateTimeOffset? EditedTimestamp { get; }
public string Content { get; } public string? Content { get; }
public IReadOnlyList<Attachment> Attachments { get; } public IReadOnlyList<Attachment> Attachments { get; }
@ -32,7 +32,7 @@ namespace DiscordChatExporter.Core.Models
public bool IsPinned { get; } public bool IsPinned { get; }
public Message(string id, string channelId, MessageType type, User author, DateTimeOffset timestamp, public Message(string id, string channelId, MessageType type, User author, DateTimeOffset timestamp,
DateTimeOffset? editedTimestamp, string content, IReadOnlyList<Attachment> attachments, DateTimeOffset? editedTimestamp, string? content, IReadOnlyList<Attachment> attachments,
IReadOnlyList<Embed> embeds, IReadOnlyList<Reaction> reactions, IReadOnlyList<User> mentionedUsers, IReadOnlyList<Embed> embeds, IReadOnlyList<Reaction> reactions, IReadOnlyList<User> mentionedUsers,
bool isPinned) bool isPinned)
{ {
@ -50,6 +50,6 @@ namespace DiscordChatExporter.Core.Models
IsPinned = isPinned; IsPinned = isPinned;
} }
public override string ToString() => Content; public override string ToString() => Content ?? "<message without content>";
} }
} }

View file

@ -1,5 +1,4 @@
using System; using System;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Core.Models namespace DiscordChatExporter.Core.Models
{ {
@ -15,13 +14,13 @@ namespace DiscordChatExporter.Core.Models
public string FullName { get; } public string FullName { get; }
public string AvatarHash { get; } public string? AvatarHash { get; }
public string AvatarUrl { get; } public string AvatarUrl { get; }
public bool IsBot { get; } public bool IsBot { get; }
public User(string id, int discriminator, string name, string avatarHash, bool isBot) public User(string id, int discriminator, string name, string? avatarHash, bool isBot)
{ {
Id = id; Id = id;
Discriminator = discriminator; Discriminator = discriminator;
@ -40,10 +39,10 @@ namespace DiscordChatExporter.Core.Models
{ {
public static string GetFullName(string name, int discriminator) => $"{name}#{discriminator:0000}"; public static string GetFullName(string name, int discriminator) => $"{name}#{discriminator:0000}";
public static string GetAvatarUrl(string id, int discriminator, string avatarHash) public static string GetAvatarUrl(string id, int discriminator, string? avatarHash)
{ {
// Custom avatar // Custom avatar
if (!avatarHash.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(avatarHash))
{ {
// Animated // Animated
if (avatarHash.StartsWith("a_", StringComparison.Ordinal)) if (avatarHash.StartsWith("a_", StringComparison.Ordinal))

View file

@ -96,7 +96,7 @@ namespace DiscordChatExporter.Core.Rendering
await RenderFieldAsync(writer, FormatDate(message.Timestamp)); await RenderFieldAsync(writer, FormatDate(message.Timestamp));
// Content // Content
await RenderFieldAsync(writer, FormatMarkdown(message.Content)); await RenderFieldAsync(writer, FormatMarkdown(message.Content ?? ""));
// Attachments // Attachments
var formattedAttachments = message.Attachments.Select(a => a.Url).JoinToString(","); var formattedAttachments = message.Attachments.Select(a => a.Url).JoinToString(",");

View file

@ -2,6 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -97,7 +97,7 @@ namespace DiscordChatExporter.Core.Rendering
if (node is MultiLineCodeBlockNode multilineCodeBlockNode) if (node is MultiLineCodeBlockNode multilineCodeBlockNode)
{ {
// Set CSS class for syntax highlighting // Set CSS class for syntax highlighting
var highlightCssClass = !multilineCodeBlockNode.Language.IsNullOrWhiteSpace() var highlightCssClass = !string.IsNullOrWhiteSpace(multilineCodeBlockNode.Language)
? $"language-{multilineCodeBlockNode.Language}" ? $"language-{multilineCodeBlockNode.Language}"
: "nohighlight"; : "nohighlight";
@ -153,7 +153,7 @@ namespace DiscordChatExporter.Core.Rendering
// Extract message ID if the link points to a Discord message // Extract message ID if the link points to a Discord message
var linkedMessageId = Regex.Match(linkNode.Url, "^https?://discordapp.com/channels/.*?/(\\d+)/?$").Groups[1].Value; var linkedMessageId = Regex.Match(linkNode.Url, "^https?://discordapp.com/channels/.*?/(\\d+)/?$").Groups[1].Value;
return linkedMessageId.IsNullOrWhiteSpace() return string.IsNullOrWhiteSpace(linkedMessageId)
? $"<a href=\"{Uri.EscapeUriString(linkNode.Url)}\">{HtmlEncode(linkNode.Title)}</a>" ? $"<a href=\"{Uri.EscapeUriString(linkNode.Url)}\">{HtmlEncode(linkNode.Title)}</a>"
: $"<a href=\"{Uri.EscapeUriString(linkNode.Url)}\" onclick=\"scrollToMessage(event, '{linkedMessageId}')\">{HtmlEncode(linkNode.Title)}</a>"; : $"<a href=\"{Uri.EscapeUriString(linkNode.Url)}\" onclick=\"scrollToMessage(event, '{linkedMessageId}')\">{HtmlEncode(linkNode.Title)}</a>";
} }
@ -165,7 +165,7 @@ namespace DiscordChatExporter.Core.Rendering
private string FormatMarkdown(IReadOnlyList<Node> nodes, bool isTopLevel) private string FormatMarkdown(IReadOnlyList<Node> nodes, bool isTopLevel)
{ {
// Emojis are jumbo if all top-level nodes are emoji nodes or whitespace text nodes // Emojis are jumbo if all top-level nodes are emoji nodes or whitespace text nodes
var isJumbo = isTopLevel && nodes.All(n => n is EmojiNode || n is TextNode textNode && textNode.Text.IsNullOrWhiteSpace()); var isJumbo = isTopLevel && nodes.All(n => n is EmojiNode || n is TextNode textNode && string.IsNullOrWhiteSpace(textNode.Text));
return nodes.Select(n => FormatMarkdown(n, isJumbo)).JoinToString(""); return nodes.Select(n => FormatMarkdown(n, isJumbo)).JoinToString("");
} }

View file

@ -40,7 +40,7 @@ namespace DiscordChatExporter.Core.Rendering
return $"before {FormatDate(before.Value)}"; return $"before {FormatDate(before.Value)}";
// Neither // Neither
return null; return "";
} }
private string FormatMarkdown(Node node) private string FormatMarkdown(Node node)
@ -131,43 +131,43 @@ namespace DiscordChatExporter.Core.Rendering
await writer.WriteLineAsync("{Embed}"); await writer.WriteLineAsync("{Embed}");
// Author name // Author name
if (!(embed.Author?.Name).IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(embed.Author?.Name))
await writer.WriteLineAsync(embed.Author?.Name); await writer.WriteLineAsync(embed.Author?.Name);
// URL // URL
if (!embed.Url.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(embed.Url))
await writer.WriteLineAsync(embed.Url); await writer.WriteLineAsync(embed.Url);
// Title // Title
if (!embed.Title.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(embed.Title))
await writer.WriteLineAsync(FormatMarkdown(embed.Title)); await writer.WriteLineAsync(FormatMarkdown(embed.Title));
// Description // Description
if (!embed.Description.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(embed.Description))
await writer.WriteLineAsync(FormatMarkdown(embed.Description)); await writer.WriteLineAsync(FormatMarkdown(embed.Description));
// Fields // Fields
foreach (var field in embed.Fields) foreach (var field in embed.Fields)
{ {
// Name // Name
if (!field.Name.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(field.Name))
await writer.WriteLineAsync(field.Name); await writer.WriteLineAsync(field.Name);
// Value // Value
if (!field.Value.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(field.Value))
await writer.WriteLineAsync(field.Value); await writer.WriteLineAsync(field.Value);
} }
// Thumbnail URL // Thumbnail URL
if (!(embed.Thumbnail?.Url).IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(embed.Thumbnail?.Url))
await writer.WriteLineAsync(embed.Thumbnail?.Url); await writer.WriteLineAsync(embed.Thumbnail?.Url);
// Image URL // Image URL
if (!(embed.Image?.Url).IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(embed.Image?.Url))
await writer.WriteLineAsync(embed.Image?.Url); await writer.WriteLineAsync(embed.Image?.Url);
// Footer text // Footer text
if (!(embed.Footer?.Text).IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(embed.Footer?.Text))
await writer.WriteLineAsync(embed.Footer?.Text); await writer.WriteLineAsync(embed.Footer?.Text);
await writer.WriteLineAsync(); await writer.WriteLineAsync();
@ -201,6 +201,7 @@ namespace DiscordChatExporter.Core.Rendering
await RenderMessageHeaderAsync(writer, message); await RenderMessageHeaderAsync(writer, message);
// Content // Content
if (!string.IsNullOrWhiteSpace(message.Content))
await writer.WriteLineAsync(FormatMarkdown(message.Content)); await writer.WriteLineAsync(FormatMarkdown(message.Content));
// Separator // Separator

View file

@ -12,10 +12,10 @@ namespace DiscordChatExporter.Core.Services
{ {
private User ParseUser(JToken json) private User ParseUser(JToken json)
{ {
var id = json["id"].Value<string>(); var id = json["id"]!.Value<string>();
var discriminator = json["discriminator"].Value<int>(); var discriminator = json["discriminator"]!.Value<int>();
var name = json["username"].Value<string>(); var name = json["username"]!.Value<string>();
var avatarHash = json["avatar"].Value<string>(); var avatarHash = json["avatar"]!.Value<string>();
var isBot = json["bot"]?.Value<bool>() ?? false; var isBot = json["bot"]?.Value<bool>() ?? false;
return new User(id, discriminator, name, avatarHash, isBot); return new User(id, discriminator, name, avatarHash, isBot);
@ -23,9 +23,9 @@ namespace DiscordChatExporter.Core.Services
private Guild ParseGuild(JToken json) private Guild ParseGuild(JToken json)
{ {
var id = json["id"].Value<string>(); var id = json["id"]!.Value<string>();
var name = json["name"].Value<string>(); var name = json["name"]!.Value<string>();
var iconHash = json["icon"].Value<string>(); var iconHash = json["icon"]!.Value<string>();
return new Guild(id, name, iconHash); return new Guild(id, name, iconHash);
} }
@ -33,23 +33,23 @@ namespace DiscordChatExporter.Core.Services
private Channel ParseChannel(JToken json) private Channel ParseChannel(JToken json)
{ {
// Get basic data // Get basic data
var id = json["id"].Value<string>(); var id = json["id"]!.Value<string>();
var parentId = json["parent_id"]?.Value<string>(); var parentId = json["parent_id"]?.Value<string>();
var type = (ChannelType) json["type"].Value<int>(); var type = (ChannelType) json["type"]!.Value<int>();
var topic = json["topic"]?.Value<string>(); var topic = json["topic"]?.Value<string>();
// Try to extract guild ID // Try to extract guild ID
var guildId = json["guild_id"]?.Value<string>(); var guildId = json["guild_id"]?.Value<string>();
// If the guild ID is blank, it's direct messages // If the guild ID is blank, it's direct messages
if (guildId.IsNullOrWhiteSpace()) if (string.IsNullOrWhiteSpace(guildId))
guildId = Guild.DirectMessages.Id; guildId = Guild.DirectMessages.Id;
// Try to extract name // Try to extract name
var name = json["name"]?.Value<string>(); var name = json["name"]?.Value<string>();
// If the name is blank, it's direct messages // If the name is blank, it's direct messages
if (name.IsNullOrWhiteSpace()) if (string.IsNullOrWhiteSpace(name))
name = json["recipients"].Select(ParseUser).Select(u => u.Name).JoinToString(", "); name = json["recipients"].Select(ParseUser).Select(u => u.Name).JoinToString(", ");
return new Channel(id, parentId, guildId, name, topic, type); return new Channel(id, parentId, guildId, name, topic, type);
@ -57,20 +57,20 @@ namespace DiscordChatExporter.Core.Services
private Role ParseRole(JToken json) private Role ParseRole(JToken json)
{ {
var id = json["id"].Value<string>(); var id = json["id"]!.Value<string>();
var name = json["name"].Value<string>(); var name = json["name"]!.Value<string>();
return new Role(id, name); return new Role(id, name);
} }
private Attachment ParseAttachment(JToken json) private Attachment ParseAttachment(JToken json)
{ {
var id = json["id"].Value<string>(); var id = json["id"]!.Value<string>();
var url = json["url"].Value<string>(); var url = json["url"]!.Value<string>();
var width = json["width"]?.Value<int>(); var width = json["width"]?.Value<int>();
var height = json["height"]?.Value<int>(); var height = json["height"]?.Value<int>();
var fileName = json["filename"].Value<string>(); var fileName = json["filename"]!.Value<string>();
var fileSizeBytes = json["size"].Value<long>(); var fileSizeBytes = json["size"]!.Value<long>();
var fileSize = new FileSize(fileSizeBytes); var fileSize = new FileSize(fileSizeBytes);
@ -88,8 +88,8 @@ namespace DiscordChatExporter.Core.Services
private EmbedField ParseEmbedField(JToken json) private EmbedField ParseEmbedField(JToken json)
{ {
var name = json["name"].Value<string>(); var name = json["name"]!.Value<string>();
var value = json["value"].Value<string>(); var value = json["value"]!.Value<string>();
var isInline = json["inline"]?.Value<bool>() ?? false; var isInline = json["inline"]?.Value<bool>() ?? false;
return new EmbedField(name, value, isInline); return new EmbedField(name, value, isInline);
@ -106,7 +106,7 @@ namespace DiscordChatExporter.Core.Services
private EmbedFooter ParseEmbedFooter(JToken json) private EmbedFooter ParseEmbedFooter(JToken json)
{ {
var text = json["text"].Value<string>(); var text = json["text"]!.Value<string>();
var iconUrl = json["icon_url"]?.Value<string>(); var iconUrl = json["icon_url"]?.Value<string>();
return new EmbedFooter(text, iconUrl); return new EmbedFooter(text, iconUrl);
@ -122,23 +122,23 @@ namespace DiscordChatExporter.Core.Services
// Get color // Get color
var color = json["color"] != null var color = json["color"] != null
? Color.FromArgb(json["color"].Value<int>()).ResetAlpha() ? Color.FromArgb(json["color"]!.Value<int>()).ResetAlpha()
: Color.FromArgb(79, 84, 92); // default color : Color.FromArgb(79, 84, 92); // default color
// Get author // Get author
var author = json["author"] != null ? ParseEmbedAuthor(json["author"]) : null; var author = json["author"] != null ? ParseEmbedAuthor(json["author"]!) : null;
// Get fields // Get fields
var fields = json["fields"].EmptyIfNull().Select(ParseEmbedField).ToArray(); var fields = (json["fields"] ?? Enumerable.Empty<JToken>()).Select(ParseEmbedField).ToArray();
// Get thumbnail // Get thumbnail
var thumbnail = json["thumbnail"] != null ? ParseEmbedImage(json["thumbnail"]) : null; var thumbnail = json["thumbnail"] != null ? ParseEmbedImage(json["thumbnail"]!) : null;
// Get image // Get image
var image = json["image"] != null ? ParseEmbedImage(json["image"]) : null; var image = json["image"] != null ? ParseEmbedImage(json["image"]!) : null;
// Get footer // Get footer
var footer = json["footer"] != null ? ParseEmbedFooter(json["footer"]) : null; var footer = json["footer"] != null ? ParseEmbedFooter(json["footer"]!) : null;
return new Embed(title, url, timestamp, color, author, description, fields, thumbnail, image, footer); return new Embed(title, url, timestamp, color, author, description, fields, thumbnail, image, footer);
} }
@ -146,7 +146,7 @@ namespace DiscordChatExporter.Core.Services
private Emoji ParseEmoji(JToken json) private Emoji ParseEmoji(JToken json)
{ {
var id = json["id"]?.Value<string>(); var id = json["id"]?.Value<string>();
var name = json["name"]?.Value<string>(); var name = json["name"]!.Value<string>();
var isAnimated = json["animated"]?.Value<bool>() ?? false; var isAnimated = json["animated"]?.Value<bool>() ?? false;
return new Emoji(id, name, isAnimated); return new Emoji(id, name, isAnimated);
@ -154,8 +154,8 @@ namespace DiscordChatExporter.Core.Services
private Reaction ParseReaction(JToken json) private Reaction ParseReaction(JToken json)
{ {
var count = json["count"].Value<int>(); var count = json["count"]!.Value<int>();
var emoji = ParseEmoji(json["emoji"]); var emoji = ParseEmoji(json["emoji"]!);
return new Reaction(count, emoji); return new Reaction(count, emoji);
} }
@ -163,12 +163,12 @@ namespace DiscordChatExporter.Core.Services
private Message ParseMessage(JToken json) private Message ParseMessage(JToken json)
{ {
// Get basic data // Get basic data
var id = json["id"].Value<string>(); var id = json["id"]!.Value<string>();
var channelId = json["channel_id"].Value<string>(); var channelId = json["channel_id"]!.Value<string>();
var timestamp = json["timestamp"].Value<DateTime>().ToDateTimeOffset(); var timestamp = json["timestamp"]!.Value<DateTime>().ToDateTimeOffset();
var editedTimestamp = json["edited_timestamp"]?.Value<DateTime?>()?.ToDateTimeOffset(); var editedTimestamp = json["edited_timestamp"]?.Value<DateTime?>()?.ToDateTimeOffset();
var content = json["content"].Value<string>(); var content = json["content"]!.Value<string>();
var type = (MessageType) json["type"].Value<int>(); var type = (MessageType) json["type"]!.Value<int>();
// Workarounds for non-default types // Workarounds for non-default types
if (type == MessageType.RecipientAdd) if (type == MessageType.RecipientAdd)
@ -187,22 +187,22 @@ namespace DiscordChatExporter.Core.Services
content = "Joined the server."; content = "Joined the server.";
// Get author // Get author
var author = ParseUser(json["author"]); var author = ParseUser(json["author"]!);
// Get attachments // Get attachments
var attachments = json["attachments"].EmptyIfNull().Select(ParseAttachment).ToArray(); var attachments = (json["attachments"] ?? Enumerable.Empty<JToken>()).Select(ParseAttachment).ToArray();
// Get embeds // Get embeds
var embeds = json["embeds"].EmptyIfNull().Select(ParseEmbed).ToArray(); var embeds = (json["embeds"] ?? Enumerable.Empty<JToken>()).Select(ParseEmbed).ToArray();
// Get reactions // Get reactions
var reactions = json["reactions"].EmptyIfNull().Select(ParseReaction).ToArray(); var reactions = (json["reactions"] ?? Enumerable.Empty<JToken>()).Select(ParseReaction).ToArray();
// Get mentioned users // Get mentioned users
var mentionedUsers = json["mentions"].EmptyIfNull().Select(ParseUser).ToArray(); var mentionedUsers = (json["mentions"] ?? Enumerable.Empty<JToken>()).Select(ParseUser).ToArray();
// Get whether this message is pinned // Get whether this message is pinned
var isPinned = json["pinned"].Value<bool>(); var isPinned = json["pinned"]!.Value<bool>();
return new Message(id, channelId, type, author, timestamp, editedTimestamp, content, attachments, embeds, return new Message(id, channelId, type, author, timestamp, editedTimestamp, content, attachments, embeds,
reactions, mentionedUsers, isPinned); reactions, mentionedUsers, isPinned);

View file

@ -45,7 +45,7 @@ namespace DiscordChatExporter.Core.Services
var value = parameter.SubstringAfter("="); var value = parameter.SubstringAfter("=");
// Skip empty values // Skip empty values
if (value.IsNullOrWhiteSpace()) if (string.IsNullOrWhiteSpace(value))
continue; continue;
request.RequestUri = request.RequestUri.SetQueryParameter(key, value); request.RequestUri = request.RequestUri.SetQueryParameter(key, value);
@ -53,6 +53,7 @@ namespace DiscordChatExporter.Core.Services
// Get response // Get response
using var response = await _httpClient.SendAsync(request); using var response = await _httpClient.SendAsync(request);
// Check status code // Check status code
// We throw our own exception here because default one doesn't have status code // We throw our own exception here because default one doesn't have status code
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
@ -119,7 +120,7 @@ namespace DiscordChatExporter.Core.Services
} }
public async Task<IReadOnlyList<Message>> GetChannelMessagesAsync(AuthToken token, string channelId, public async Task<IReadOnlyList<Message>> GetChannelMessagesAsync(AuthToken token, string channelId,
DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double> progress = null) DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double>? progress = null)
{ {
var result = new List<Message>(); var result = new List<Message>();
@ -211,7 +212,7 @@ namespace DiscordChatExporter.Core.Services
} }
public async Task<ChatLog> GetChatLogAsync(AuthToken token, Guild guild, Channel channel, public async Task<ChatLog> GetChatLogAsync(AuthToken token, Guild guild, Channel channel,
DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double> progress = null) DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double>? progress = null)
{ {
// Get messages // Get messages
var messages = await GetChannelMessagesAsync(token, channel.Id, after, before, progress); var messages = await GetChannelMessagesAsync(token, channel.Id, after, before, progress);
@ -223,19 +224,19 @@ namespace DiscordChatExporter.Core.Services
} }
public async Task<ChatLog> GetChatLogAsync(AuthToken token, Channel channel, public async Task<ChatLog> GetChatLogAsync(AuthToken token, Channel channel,
DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double> progress = null) DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double>? progress = null)
{ {
// Get guild // Get guild
var guild = channel.GuildId == Guild.DirectMessages.Id var guild = !string.IsNullOrWhiteSpace(channel.GuildId)
? Guild.DirectMessages ? await GetGuildAsync(token, channel.GuildId)
: await GetGuildAsync(token, channel.GuildId); : Guild.DirectMessages;
// Get the chat log // Get the chat log
return await GetChatLogAsync(token, guild, channel, after, before, progress); return await GetChatLogAsync(token, guild, channel, after, before, progress);
} }
public async Task<ChatLog> GetChatLogAsync(AuthToken token, string channelId, public async Task<ChatLog> GetChatLogAsync(AuthToken token, string channelId,
DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double> progress = null) DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double>? progress = null)
{ {
// Get channel // Get channel
var channel = await GetChannelAsync(token, channelId); var channel = await GetChannelAsync(token, channelId);

View file

@ -2,13 +2,14 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Failsafe" Version="1.1.0" /> <PackageReference Include="Failsafe" Version="1.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Onova" Version="2.4.5" /> <PackageReference Include="Onova" Version="2.5.1" />
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.3" /> <PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
<PackageReference Include="Tyrrrz.Settings" Version="1.3.4" /> <PackageReference Include="Tyrrrz.Settings" Version="1.3.4" />
</ItemGroup> </ItemGroup>

View file

@ -38,7 +38,7 @@ namespace DiscordChatExporter.Core.Services
{ {
// Create output directory // Create output directory
var dirPath = Path.GetDirectoryName(filePath); var dirPath = Path.GetDirectoryName(filePath);
if (!dirPath.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(dirPath))
Directory.CreateDirectory(dirPath); Directory.CreateDirectory(dirPath);
// Render chat log to output file // Render chat log to output file
@ -74,7 +74,7 @@ namespace DiscordChatExporter.Core.Services
var partitionFilePath = $"{fileNameWithoutExt} [{partitionNumber} of {partitions.Length}]{fileExt}"; var partitionFilePath = $"{fileNameWithoutExt} [{partitionNumber} of {partitions.Length}]{fileExt}";
// Compose full file path // Compose full file path
if (!dirPath.IsNullOrWhiteSpace()) if (!string.IsNullOrWhiteSpace(dirPath))
partitionFilePath = Path.Combine(dirPath, partitionFilePath); partitionFilePath = Path.Combine(dirPath, partitionFilePath);
// Export // Export

View file

@ -3,7 +3,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using DiscordChatExporter.Core.Models; using DiscordChatExporter.Core.Models;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Core.Services.Helpers namespace DiscordChatExporter.Core.Services.Helpers
{ {
@ -12,7 +11,7 @@ namespace DiscordChatExporter.Core.Services.Helpers
public static bool IsDirectoryPath(string path) => public static bool IsDirectoryPath(string path) =>
path.Last() == Path.DirectorySeparatorChar || path.Last() == Path.DirectorySeparatorChar ||
path.Last() == Path.AltDirectorySeparatorChar || path.Last() == Path.AltDirectorySeparatorChar ||
Path.GetExtension(path).IsNullOrWhiteSpace() && !File.Exists(path); string.IsNullOrWhiteSpace(Path.GetExtension(path)) && !File.Exists(path);
public static string GetDefaultExportFileName(ExportFormat format, Guild guild, Channel channel, public static string GetDefaultExportFileName(ExportFormat format, Guild guild, Channel channel,
DateTimeOffset? after = null, DateTimeOffset? before = null) DateTimeOffset? after = null, DateTimeOffset? before = null)

View file

@ -9,8 +9,10 @@ namespace DiscordChatExporter.Core.Services
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt"; public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt";
public AuthToken LastToken { get; set; } public AuthToken? LastToken { get; set; }
public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark; public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
public int? LastPartitionLimit { get; set; } public int? LastPartitionLimit { get; set; }
public SettingsService() public SettingsService()

View file

@ -1,6 +1,16 @@
namespace DiscordChatExporter.Gui using System;
using System.Reflection;
namespace DiscordChatExporter.Gui
{ {
public partial class App public partial class App
{ {
private static readonly Assembly Assembly = typeof(App).Assembly;
public static string Name => Assembly.GetName().Name!;
public static Version Version => Assembly.GetName().Version!;
public static string VersionString => Version.ToString(3);
} }
} }

View file

@ -1,11 +1,14 @@
using System.Windows; using DiscordChatExporter.Core.Services;
using System.Windows.Threading;
using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Gui.ViewModels; using DiscordChatExporter.Gui.ViewModels;
using DiscordChatExporter.Gui.ViewModels.Framework; using DiscordChatExporter.Gui.ViewModels.Framework;
using Stylet; using Stylet;
using StyletIoC; using StyletIoC;
#if !DEBUG
using System.Windows;
using System.Windows.Threading;
#endif
namespace DiscordChatExporter.Gui namespace DiscordChatExporter.Gui
{ {
public class Bootstrapper : Bootstrapper<RootViewModel> public class Bootstrapper : Bootstrapper<RootViewModel>

View file

@ -14,7 +14,7 @@ namespace DiscordChatExporter.Gui.Converters
if (value is DateTimeOffset dateTimeOffsetValue) if (value is DateTimeOffset dateTimeOffsetValue)
return dateTimeOffsetValue.DateTime; return dateTimeOffsetValue.DateTime;
return default; return default(DateTime);
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
@ -22,7 +22,7 @@ namespace DiscordChatExporter.Gui.Converters
if (value is DateTime dateTimeValue) if (value is DateTime dateTimeValue)
return new DateTimeOffset(dateTimeValue); return new DateTimeOffset(dateTimeValue);
return default; return default(DateTimeOffset);
} }
} }
} }

View file

@ -10,12 +10,12 @@ namespace DiscordChatExporter.Gui.Converters
{ {
public static ExportFormatToStringConverter Instance { get; } = new ExportFormatToStringConverter(); public static ExportFormatToStringConverter Instance { get; } = new ExportFormatToStringConverter();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {
if (value is ExportFormat exportFormatValue) if (value is ExportFormat exportFormatValue)
return exportFormatValue.GetDisplayName(); return exportFormatValue.GetDisplayName();
return default; return default(string);
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

View file

@ -14,7 +14,7 @@ namespace DiscordChatExporter.Gui.Converters
if (value is bool boolValue) if (value is bool boolValue)
return !boolValue; return !boolValue;
return default; return default(bool);
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
@ -22,7 +22,7 @@ namespace DiscordChatExporter.Gui.Converters
if (value is bool boolValue) if (value is bool boolValue)
return !boolValue; return !boolValue;
return default; return default(bool);
} }
} }
} }

View file

@ -7,6 +7,7 @@
<Version>2.15</Version> <Version>2.15</Version>
<Company>Tyrrrz</Company> <Company>Tyrrrz</Company>
<Copyright>Copyright (c) Alexey Golub</Copyright> <Copyright>Copyright (c) Alexey Golub</Copyright>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<ApplicationIcon>../favicon.ico</ApplicationIcon> <ApplicationIcon>../favicon.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
@ -19,11 +20,11 @@
<PackageReference Include="Gress" Version="1.1.1" /> <PackageReference Include="Gress" Version="1.1.1" />
<PackageReference Include="MaterialDesignColors" Version="1.2.0" /> <PackageReference Include="MaterialDesignColors" Version="1.2.0" />
<PackageReference Include="MaterialDesignThemes" Version="2.6.0" /> <PackageReference Include="MaterialDesignThemes" Version="2.6.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.0.1" /> <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.3" />
<PackageReference Include="Ookii.Dialogs.Wpf" Version="1.1.0" /> <PackageReference Include="Ookii.Dialogs.Wpf" Version="1.1.0" />
<PackageReference Include="PropertyChanged.Fody" Version="3.1.3" /> <PackageReference Include="PropertyChanged.Fody" Version="3.1.3" />
<PackageReference Include="Stylet" Version="1.3.0" /> <PackageReference Include="Stylet" Version="1.3.0" />
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.3" /> <PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using DiscordChatExporter.Core.Services;
using Onova; using Onova;
using Onova.Exceptions; using Onova.Exceptions;
using Onova.Services; using Onova.Services;
@ -13,10 +12,10 @@ namespace DiscordChatExporter.Gui.Services
new GithubPackageResolver("Tyrrrz", "DiscordChatExporter", "DiscordChatExporter.zip"), new GithubPackageResolver("Tyrrrz", "DiscordChatExporter", "DiscordChatExporter.zip"),
new ZipPackageExtractor()); new ZipPackageExtractor());
private Version _updateVersion; private Version? _updateVersion;
private bool _updaterLaunched; private bool _updaterLaunched;
public async Task<Version> CheckForUpdatesAsync() public async Task<Version?> CheckForUpdatesAsync()
{ {
var check = await _updateManager.CheckForUpdatesAsync(); var check = await _updateManager.CheckForUpdatesAsync();
return check.CanUpdate ? check.LastVersion : null; return check.CanUpdate ? check.LastVersion : null;

View file

@ -7,7 +7,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Components
{ {
public Channel Model { get; set; } public Channel Model { get; set; }
public string Category { get; set; } public string? Category { get; set; }
} }
public partial class ChannelViewModel public partial class ChannelViewModel

View file

@ -6,7 +6,6 @@ using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Core.Services.Helpers; using DiscordChatExporter.Core.Services.Helpers;
using DiscordChatExporter.Gui.ViewModels.Components; using DiscordChatExporter.Gui.ViewModels.Components;
using DiscordChatExporter.Gui.ViewModels.Framework; using DiscordChatExporter.Gui.ViewModels.Framework;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Gui.ViewModels.Dialogs namespace DiscordChatExporter.Gui.ViewModels.Dialogs
{ {
@ -21,12 +20,12 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
public bool IsSingleChannel => Channels.Count == 1; public bool IsSingleChannel => Channels.Count == 1;
public string OutputPath { get; set; } public string? OutputPath { get; set; }
public IReadOnlyList<ExportFormat> AvailableFormats => public IReadOnlyList<ExportFormat> AvailableFormats =>
Enum.GetValues(typeof(ExportFormat)).Cast<ExportFormat>().ToArray(); Enum.GetValues(typeof(ExportFormat)).Cast<ExportFormat>().ToArray();
public ExportFormat SelectedFormat { get; set; } = ExportFormat.HtmlDark; public ExportFormat SelectedFormat { get; set; }
public DateTimeOffset? After { get; set; } public DateTimeOffset? After { get; set; }
@ -38,11 +37,6 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
{ {
_dialogManager = dialogManager; _dialogManager = dialogManager;
_settingsService = settingsService; _settingsService = settingsService;
}
protected override void OnViewLoaded()
{
base.OnViewLoaded();
// Persist preferences // Persist preferences
SelectedFormat = _settingsService.LastExportFormat; SelectedFormat = _settingsService.LastExportFormat;
@ -85,7 +79,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
} }
// If canceled - return // If canceled - return
if (OutputPath.IsNullOrWhiteSpace()) if (string.IsNullOrWhiteSpace(OutputPath))
return; return;
// Close dialog // Close dialog

View file

@ -1,4 +1,5 @@
using System.IO; using System;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using MaterialDesignThemes.Wpf; using MaterialDesignThemes.Wpf;
using Microsoft.Win32; using Microsoft.Win32;
@ -22,10 +23,10 @@ namespace DiscordChatExporter.Gui.ViewModels.Framework
var view = _viewManager.CreateAndBindViewForModelIfNecessary(dialogScreen); var view = _viewManager.CreateAndBindViewForModelIfNecessary(dialogScreen);
// Set up event routing that will close the view when called from viewmodel // Set up event routing that will close the view when called from viewmodel
void OnDialogOpened(object sender, DialogOpenedEventArgs openArgs) void OnDialogOpened(object? sender, DialogOpenedEventArgs openArgs)
{ {
// Delegate to close the dialog and unregister event handler // Delegate to close the dialog and unregister event handler
void OnScreenClosed(object o, CloseEventArgs closeArgs) void OnScreenClosed(object? o, EventArgs closeArgs)
{ {
openArgs.Session.Close(); openArgs.Session.Close();
dialogScreen.Closed -= OnScreenClosed; dialogScreen.Closed -= OnScreenClosed;
@ -41,7 +42,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Framework
return dialogScreen.DialogResult; return dialogScreen.DialogResult;
} }
public string PromptSaveFilePath(string filter = "All files|*.*", string defaultFilePath = "") public string? PromptSaveFilePath(string filter = "All files|*.*", string defaultFilePath = "")
{ {
// Create dialog // Create dialog
var dialog = new SaveFileDialog var dialog = new SaveFileDialog
@ -56,7 +57,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Framework
return dialog.ShowDialog() == true ? dialog.FileName : null; return dialog.ShowDialog() == true ? dialog.FileName : null;
} }
public string PromptDirectoryPath(string defaultDirPath = "") public string? PromptDirectoryPath(string defaultDirPath = "")
{ {
// Create dialog // Create dialog
var dialog = new VistaFolderBrowserDialog var dialog = new VistaFolderBrowserDialog

View file

@ -1,22 +1,18 @@
using Stylet; using System;
using Stylet;
namespace DiscordChatExporter.Gui.ViewModels.Framework namespace DiscordChatExporter.Gui.ViewModels.Framework
{ {
public abstract class DialogScreen<T> : Screen public abstract class DialogScreen<T> : PropertyChangedBase
{ {
public T DialogResult { get; private set; } public T DialogResult { get; private set; }
public event EventHandler? Closed;
public void Close(T dialogResult = default) public void Close(T dialogResult = default)
{ {
// Set the result
DialogResult = dialogResult; DialogResult = dialogResult;
Closed?.Invoke(this, EventArgs.Empty);
// If there is a parent - ask them to close this dialog
if (Parent != null)
RequestClose(Equals(dialogResult, default(T)));
// Otherwise close ourselves
else
((IScreenState) this).Close();
} }
} }

View file

@ -7,8 +7,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Framework
{ {
public static class Extensions public static class Extensions
{ {
public static ChannelViewModel CreateChannelViewModel(this IViewModelFactory factory, Channel model, public static ChannelViewModel CreateChannelViewModel(this IViewModelFactory factory, Channel model, string? category = null)
string category = null)
{ {
var viewModel = factory.CreateChannelViewModel(); var viewModel = factory.CreateChannelViewModel();
viewModel.Model = model; viewModel.Model = model;

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using DiscordChatExporter.Core.Models; using DiscordChatExporter.Core.Models;
using DiscordChatExporter.Core.Services; using DiscordChatExporter.Core.Services;
@ -38,13 +37,13 @@ namespace DiscordChatExporter.Gui.ViewModels
public bool IsBotToken { get; set; } public bool IsBotToken { get; set; }
public string TokenValue { get; set; } public string? TokenValue { get; set; }
public IReadOnlyList<GuildViewModel> AvailableGuilds { get; private set; } public IReadOnlyList<GuildViewModel>? AvailableGuilds { get; private set; }
public GuildViewModel SelectedGuild { get; set; } public GuildViewModel? SelectedGuild { get; set; }
public IReadOnlyList<ChannelViewModel> SelectedChannels { get; set; } public IReadOnlyList<ChannelViewModel>? SelectedChannels { get; set; }
public RootViewModel(IViewModelFactory viewModelFactory, DialogManager dialogManager, public RootViewModel(IViewModelFactory viewModelFactory, DialogManager dialogManager,
SettingsService settingsService, UpdateService updateService, DataService dataService, SettingsService settingsService, UpdateService updateService, DataService dataService,
@ -58,8 +57,7 @@ namespace DiscordChatExporter.Gui.ViewModels
_exportService = exportService; _exportService = exportService;
// Set title // Set title
var version = Assembly.GetExecutingAssembly().GetName().Version.ToString(3); DisplayName = $"{App.Name} v{App.VersionString}";
DisplayName = $"DiscordChatExporter v{version}";
// Update busy state when progress manager changes // Update busy state when progress manager changes
ProgressManager.Bind(o => o.IsActive, (sender, args) => IsBusy = ProgressManager.IsActive); ProgressManager.Bind(o => o.IsActive, (sender, args) => IsBusy = ProgressManager.IsActive);
@ -83,7 +81,7 @@ namespace DiscordChatExporter.Gui.ViewModels
return; return;
// Notify user of an update and prepare it // Notify user of an update and prepare it
Notifications.Enqueue($"Downloading update to DiscordChatExporter v{updateVersion}..."); Notifications.Enqueue($"Downloading update to {App.Name} v{updateVersion}...");
await _updateService.PrepareUpdateAsync(updateVersion); await _updateService.PrepareUpdateAsync(updateVersion);
// Prompt user to install update (otherwise install it when application exits) // Prompt user to install update (otherwise install it when application exits)
@ -140,7 +138,7 @@ namespace DiscordChatExporter.Gui.ViewModels
await _dialogManager.ShowDialogAsync(dialog); await _dialogManager.ShowDialogAsync(dialog);
} }
public bool CanPopulateGuildsAndChannels => !IsBusy && !TokenValue.IsNullOrWhiteSpace(); public bool CanPopulateGuildsAndChannels => !IsBusy && !string.IsNullOrWhiteSpace(TokenValue);
public async void PopulateGuildsAndChannels() public async void PopulateGuildsAndChannels()
{ {
@ -150,7 +148,7 @@ namespace DiscordChatExporter.Gui.ViewModels
try try
{ {
// Sanitize token // Sanitize token
TokenValue = TokenValue.Trim('"'); TokenValue = TokenValue!.Trim('"');
// Create token // Create token
var token = new AuthToken( var token = new AuthToken(
@ -253,15 +251,15 @@ namespace DiscordChatExporter.Gui.ViewModels
} }
} }
public bool CanExportChannels => !IsBusy && !SelectedChannels.IsNullOrEmpty(); public bool CanExportChannels => !IsBusy && SelectedGuild != null && SelectedChannels != null && SelectedChannels.Any();
public async void ExportChannels() public async void ExportChannels()
{ {
// Get last used token // Get last used token
var token = _settingsService.LastToken; var token = _settingsService.LastToken!;
// Create dialog // Create dialog
var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild, SelectedChannels); var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild!, SelectedChannels!);
// Show dialog, if canceled - return // Show dialog, if canceled - return
if (await _dialogManager.ShowDialogAsync(dialog) != true) if (await _dialogManager.ShowDialogAsync(dialog) != true)
@ -281,7 +279,7 @@ namespace DiscordChatExporter.Gui.ViewModels
try try
{ {
// Generate file path if necessary // Generate file path if necessary
var filePath = dialog.OutputPath; var filePath = dialog.OutputPath!;
if (ExportHelper.IsDirectoryPath(filePath)) if (ExportHelper.IsDirectoryPath(filePath))
{ {
// Generate default file name // Generate default file name