Compare commits

...

16 Commits

Author SHA1 Message Date
Alexey Golub 921f348769 Update version 2020-09-14 15:37:36 +03:00
Alexey Golub a25e809671 Extend try/catch in media downloading to OperationCanceledException
Closes #372
2020-09-14 15:09:57 +03:00
Alexey Golub fa80c82468 Cleanup 2020-09-05 17:10:15 +03:00
Alexey Golub 91f4f02a35 Refactor last commit 2020-09-05 17:05:18 +03:00
wyattscarpenter 6d2880ce26 [CLI] Add command to export all channels across all guilds (#373) 2020-09-05 00:23:06 +03:00
Ahmed Massoud 355b8cb8cf Add call time for call status messages (#365) 2020-08-22 22:14:13 +03:00
Alexey Golub 82945ac3cf [HTML] Use CDN for Whitney fonts
Related to #322
2020-08-21 20:36:59 +03:00
Alexey Golub 5009b90a3e Update screenshots 2020-08-15 13:47:47 +03:00
Alexey Golub 1495ed9b90 Update version 2020-08-12 19:21:24 +03:00
Alexey Golub be66c74c08 Update nuget packages 2020-08-12 19:11:18 +03:00
Alexey Golub 6f54c09d96 [GUI] Refactor sorting 2020-08-12 19:09:12 +03:00
Alexey Golub 45926b0990 Refactor last commit 2020-08-07 21:41:10 +03:00
CarJem Generations 25d29dc079 [GUI] Add controls to configure export time limits in addition to dates (#355) 2020-08-07 21:06:49 +03:00
Alexey Golub efde9931c9 Refactor & optimize changes in last commit 2020-08-07 21:00:43 +03:00
CarJem Generations 2c3b461d49 [GUI] Make categories collapsible in channel list (#354) 2020-08-07 19:42:59 +03:00
CarJem Generations b405052fd6 [HTML] Workaround for Tupperbox nicknames (#352) 2020-08-05 21:58:17 +03:00
23 changed files with 299 additions and 55 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 27 KiB

+13
View File
@@ -1,3 +1,16 @@
### v2.23 (14-Sep-2020)
- [CLI] Added a command to export all channels across all servers. Use `exportall` to do it. (Thanks [@wyattscarpenter](https://github.com/wyattscarpenter))
- [HTML] Fixed an issue where Whitney fonts were not being loaded properly, causing the browser to fall back to Helvetica.
- Fixed an issue where self-contained export crashed occasionally. This usually happened when the server hosting the file did not serve the stream properly. Such files are now ignored after the first failed attempt.
### v2.22 (12-Aug-2020)
- [GUI] Improved the channel list by adding collapsible category groups. (Thanks [@CarJem Generations](https://github.com/CarJem))
- [GUI] Improved exporting options by adding a set of controls that can be used to limit the date range of the export down to minutes. Previously it was only possible to configure the date range without the time component. (Thanks [@CarJem Generations](https://github.com/CarJem))
- [HTML] Fixed an issue where the export didn't reflect changes in nicknames between messages sent by bots. This affected chat logs that contained interactions with the Tupperbox bot. (Thanks [@CarJem Generations](https://github.com/CarJem))
- [CLI] Fixed an issue where the application crashed if there were two environment variables defined that had the same name but in different case.
### v2.21.2 (30-Jul-2020) ### v2.21.2 (30-Jul-2020)
- [GUI] When copy-pasting token, any surrounding spaces are now discarded, in addition to double quotes. - [GUI] When copy-pasting token, any surrounding spaces are now discarded, in addition to double quotes.
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using DiscordChatExporter.Cli.Commands.Base;
using DiscordChatExporter.Domain.Discord.Models;
namespace DiscordChatExporter.Cli.Commands
{
[Command("exportall", Description = "Export all accessible channels.")]
public class ExportAllCommand : ExportMultipleCommandBase
{
[CommandOption("include-dm", Description = "Whether to also export direct message channels.")]
public bool IncludeDirectMessages { get; set; } = true;
public override async ValueTask ExecuteAsync(IConsole console)
{
var channels = new List<Channel>();
// Aggregate channels from all guilds
await foreach (var guild in GetDiscordClient().GetUserGuildsAsync())
{
// Skip DMs if instructed to
if (!IncludeDirectMessages && guild.Id == Guild.DirectMessages.Id)
continue;
await foreach (var channel in GetDiscordClient().GetGuildChannelsAsync(guild.Id))
{
channels.Add(channel);
}
}
await ExportMultipleAsync(console, channels);
}
}
}
@@ -12,8 +12,8 @@ namespace DiscordChatExporter.Cli.Commands
{ {
public override async ValueTask ExecuteAsync(IConsole console) public override async ValueTask ExecuteAsync(IConsole console)
{ {
var dmChannels = await GetDiscordClient().GetGuildChannelsAsync(Guild.DirectMessages.Id); var channels = await GetDiscordClient().GetGuildChannelsAsync(Guild.DirectMessages.Id);
await ExportMultipleAsync(console, dmChannels); await ExportMultipleAsync(console, channels);
} }
} }
} }
@@ -14,8 +14,8 @@ namespace DiscordChatExporter.Cli.Commands
public override async ValueTask ExecuteAsync(IConsole console) public override async ValueTask ExecuteAsync(IConsole console)
{ {
var guildChannels = await GetDiscordClient().GetGuildChannelsAsync(GuildId); var channels = await GetDiscordClient().GetGuildChannelsAsync(GuildId);
await ExportMultipleAsync(console, guildChannels); await ExportMultipleAsync(console, channels);
} }
} }
} }
@@ -7,7 +7,7 @@ using DiscordChatExporter.Domain.Utilities;
namespace DiscordChatExporter.Cli.Commands namespace DiscordChatExporter.Cli.Commands
{ {
[Command("channels", Description = "Get the list of channels in specified guild.")] [Command("channels", Description = "Get the list of channels in a guild.")]
public class GetChannelsCommand : TokenCommandBase public class GetChannelsCommand : TokenCommandBase
{ {
[CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")] [CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")]
@@ -15,9 +15,9 @@ namespace DiscordChatExporter.Cli.Commands
public override async ValueTask ExecuteAsync(IConsole console) public override async ValueTask ExecuteAsync(IConsole console)
{ {
var guildChannels = await GetDiscordClient().GetGuildChannelsAsync(GuildId); var channels = await GetDiscordClient().GetGuildChannelsAsync(GuildId);
foreach (var channel in guildChannels.OrderBy(c => c.Category).ThenBy(c => c.Name)) foreach (var channel in channels.OrderBy(c => c.Category).ThenBy(c => c.Name))
console.Output.WriteLine($"{channel.Id} | {channel.Category} / {channel.Name}"); console.Output.WriteLine($"{channel.Id} | {channel.Category} / {channel.Name}");
} }
} }
@@ -13,9 +13,9 @@ namespace DiscordChatExporter.Cli.Commands
{ {
public override async ValueTask ExecuteAsync(IConsole console) public override async ValueTask ExecuteAsync(IConsole console)
{ {
var dmChannels = await GetDiscordClient().GetGuildChannelsAsync(Guild.DirectMessages.Id); var channels = await GetDiscordClient().GetGuildChannelsAsync(Guild.DirectMessages.Id);
foreach (var channel in dmChannels.OrderBy(c => c.Category).ThenBy(c => c.Name)) foreach (var channel in channels.OrderBy(c => c.Category).ThenBy(c => c.Name))
console.Output.WriteLine($"{channel.Id} | {channel.Category} / {channel.Name}"); console.Output.WriteLine($"{channel.Id} | {channel.Category} / {channel.Name}");
} }
} }
@@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CliFx" Version="1.3.1" /> <PackageReference Include="CliFx" Version="1.3.2" />
<PackageReference Include="Gress" Version="1.2.0" /> <PackageReference Include="Gress" Version="1.2.0" />
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" /> <PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
</ItemGroup> </ItemGroup>
@@ -33,6 +33,8 @@ namespace DiscordChatExporter.Domain.Discord.Models
public DateTimeOffset? EditedTimestamp { get; } public DateTimeOffset? EditedTimestamp { get; }
public DateTimeOffset? CallEndedTimestamp { get; }
public bool IsPinned { get; } public bool IsPinned { get; }
public string Content { get; } public string Content { get; }
@@ -51,6 +53,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
User author, User author,
DateTimeOffset timestamp, DateTimeOffset timestamp,
DateTimeOffset? editedTimestamp, DateTimeOffset? editedTimestamp,
DateTimeOffset? callEndedTimestamp,
bool isPinned, bool isPinned,
string content, string content,
IReadOnlyList<Attachment> attachments, IReadOnlyList<Attachment> attachments,
@@ -63,6 +66,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
Author = author; Author = author;
Timestamp = timestamp; Timestamp = timestamp;
EditedTimestamp = editedTimestamp; EditedTimestamp = editedTimestamp;
CallEndedTimestamp = callEndedTimestamp;
IsPinned = isPinned; IsPinned = isPinned;
Content = content; Content = content;
Attachments = attachments; Attachments = attachments;
@@ -82,6 +86,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
var author = json.GetProperty("author").Pipe(User.Parse); var author = json.GetProperty("author").Pipe(User.Parse);
var timestamp = json.GetProperty("timestamp").GetDateTimeOffset(); var timestamp = json.GetProperty("timestamp").GetDateTimeOffset();
var editedTimestamp = json.GetPropertyOrNull("edited_timestamp")?.GetDateTimeOffset(); var editedTimestamp = json.GetPropertyOrNull("edited_timestamp")?.GetDateTimeOffset();
var callEndedTimestamp = json.GetPropertyOrNull("call")?.GetPropertyOrNull("ended_timestamp")?.GetDateTimeOffset();
var type = (MessageType) json.GetProperty("type").GetInt32(); var type = (MessageType) json.GetProperty("type").GetInt32();
var isPinned = json.GetPropertyOrNull("pinned")?.GetBoolean() ?? false; var isPinned = json.GetPropertyOrNull("pinned")?.GetBoolean() ?? false;
@@ -89,7 +94,8 @@ namespace DiscordChatExporter.Domain.Discord.Models
{ {
MessageType.RecipientAdd => "Added a recipient.", MessageType.RecipientAdd => "Added a recipient.",
MessageType.RecipientRemove => "Removed a recipient.", MessageType.RecipientRemove => "Removed a recipient.",
MessageType.Call => "Started a call.", MessageType.Call =>
$"Started a call that lasted {callEndedTimestamp?.Pipe(t => t - timestamp).Pipe(t => (int) t.TotalMinutes) ?? 0} minutes.",
MessageType.ChannelNameChange => "Changed the channel name.", MessageType.ChannelNameChange => "Changed the channel name.",
MessageType.ChannelIconChange => "Changed the channel icon.", MessageType.ChannelIconChange => "Changed the channel icon.",
MessageType.ChannelPinnedMessage => "Pinned a message.", MessageType.ChannelPinnedMessage => "Pinned a message.",
@@ -119,6 +125,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
author, author,
timestamp, timestamp,
editedTimestamp, editedTimestamp,
callEndedTimestamp,
isPinned, isPinned,
content, content,
attachments, attachments,
@@ -2,7 +2,7 @@
<Import Project="../DiscordChatExporter.props" /> <Import Project="../DiscordChatExporter.props" />
<ItemGroup> <ItemGroup>
<PackageReference Include="MiniRazor" Version="1.0.1" /> <PackageReference Include="MiniRazor" Version="1.1.0" />
<PackageReference Include="Polly" Version="7.2.1" /> <PackageReference Include="Polly" Version="7.2.1" />
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" /> <PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
</ItemGroup> </ItemGroup>
@@ -84,7 +84,10 @@ namespace DiscordChatExporter.Domain.Exporting
return relativeFilePath; return relativeFilePath;
} }
catch (HttpRequestException) // Try to catch only exceptions related to failed HTTP requests
// https://github.com/Tyrrrz/DiscordChatExporter/issues/332
// https://github.com/Tyrrrz/DiscordChatExporter/issues/372
catch (Exception ex) when (ex is HttpRequestException || ex is OperationCanceledException)
{ {
// We don't want this to crash the exporting process in case of failure // We don't want this to crash the exporting process in case of failure
return url; return url;
@@ -2,31 +2,31 @@
@font-face { @font-face {
font-family: Whitney; font-family: Whitney;
src: url(https://discord.com/assets/6c6374bad0b0b6d204d8d6dc4a18d820.woff); src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-300.woff);
font-weight: 300; font-weight: 300;
} }
@font-face { @font-face {
font-family: Whitney; font-family: Whitney;
src: url(https://discord.com/assets/e8acd7d9bf6207f99350ca9f9e23b168.woff); src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-400.woff);
font-weight: 400; font-weight: 400;
} }
@font-face { @font-face {
font-family: Whitney; font-family: Whitney;
src: url(https://discord.com/assets/3bdef1251a424500c1b3a78dea9b7e57.woff); src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-500.woff);
font-weight: 500; font-weight: 500;
} }
@font-face { @font-face {
font-family: Whitney; font-family: Whitney;
src: url(https://discord.com/assets/be0060dafb7a0e31d2a1ca17c0708636.woff); src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-600.woff);
font-weight: 600; font-weight: 600;
} }
@font-face { @font-face {
font-family: Whitney; font-family: Whitney;
src: url(https://discord.com/assets/8e12fb4f14d9c4592eb8ec9f22337b04.woff); src: url(https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-700.woff);
font-weight: 700; font-weight: 700;
} }
@@ -14,7 +14,7 @@
var userMember = Model.ExportContext.TryGetMember(Model.MessageGroup.Author.Id); var userMember = Model.ExportContext.TryGetMember(Model.MessageGroup.Author.Id);
var userColor = Model.ExportContext.TryGetUserColor(Model.MessageGroup.Author.Id); var userColor = Model.ExportContext.TryGetUserColor(Model.MessageGroup.Author.Id);
var userNick = userMember?.Nick ?? Model.MessageGroup.Author.Name; var userNick = (Model.MessageGroup.Author.IsBot ? Model.MessageGroup.Author.Name : (userMember?.Nick ?? Model.MessageGroup.Author.Name));
var userColorStyle = userColor != null var userColorStyle = userColor != null
? $"color: rgb({userColor?.R},{userColor?.G},{userColor?.B})" ? $"color: rgb({userColor?.R},{userColor?.G},{userColor?.B})"
@@ -3,12 +3,18 @@ using System.Threading.Tasks;
using MiniRazor; using MiniRazor;
using Tyrrrz.Extensions; using Tyrrrz.Extensions;
[assembly: InternalsVisibleTo("RazorAssembly")] [assembly: InternalsVisibleTo(DiscordChatExporter.Domain.Exporting.Writers.Html.TemplateBundle.PreambleTemplateAssemblyName)]
[assembly: InternalsVisibleTo(DiscordChatExporter.Domain.Exporting.Writers.Html.TemplateBundle.MessageGroupTemplateAssemblyName)]
[assembly: InternalsVisibleTo(DiscordChatExporter.Domain.Exporting.Writers.Html.TemplateBundle.PostambleTemplateAssemblyName)]
namespace DiscordChatExporter.Domain.Exporting.Writers.Html namespace DiscordChatExporter.Domain.Exporting.Writers.Html
{ {
internal partial class TemplateBundle internal partial class TemplateBundle
{ {
public const string PreambleTemplateAssemblyName = "RazorAssembly_Preamble";
public const string MessageGroupTemplateAssemblyName = "RazorAssembly_MessageGroup";
public const string PostambleTemplateAssemblyName = "RazorAssembly_Postamble";
public MiniRazorTemplateDescriptor PreambleTemplate { get; } public MiniRazorTemplateDescriptor PreambleTemplate { get; }
public MiniRazorTemplateDescriptor MessageGroupTemplate { get; } public MiniRazorTemplateDescriptor MessageGroupTemplate { get; }
@@ -44,11 +50,11 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.Html
var postambleTemplateSource = typeof(HtmlMessageWriter).Assembly var postambleTemplateSource = typeof(HtmlMessageWriter).Assembly
.GetManifestResourceString($"{ns}.PostambleTemplate.cshtml"); .GetManifestResourceString($"{ns}.PostambleTemplate.cshtml");
var engine = new MiniRazorTemplateEngine("RazorAssembly", ns); var engine = new MiniRazorTemplateEngine();
var preambleTemplate = engine.Compile(preambleTemplateSource); var preambleTemplate = engine.Compile(preambleTemplateSource, PreambleTemplateAssemblyName, ns);
var messageGroupTemplate = engine.Compile(messageGroupTemplateSource); var messageGroupTemplate = engine.Compile(messageGroupTemplateSource, MessageGroupTemplateAssemblyName, ns);
var postambleTemplate = engine.Compile(postambleTemplateSource); var postambleTemplate = engine.Compile(postambleTemplateSource, PostambleTemplateAssemblyName, ns);
return new TemplateBundle(preambleTemplate, messageGroupTemplate, postambleTemplate); return new TemplateBundle(preambleTemplate, messageGroupTemplate, postambleTemplate);
}); });
@@ -197,6 +197,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
_writer.WriteString("type", message.Type.ToString()); _writer.WriteString("type", message.Type.ToString());
_writer.WriteString("timestamp", message.Timestamp); _writer.WriteString("timestamp", message.Timestamp);
_writer.WriteString("timestampEdited", message.EditedTimestamp); _writer.WriteString("timestampEdited", message.EditedTimestamp);
_writer.WriteString("callEndedTimestamp", message.CallEndedTimestamp);
_writer.WriteBoolean("isPinned", message.IsPinned); _writer.WriteBoolean("isPinned", message.IsPinned);
// Content // Content
+88
View File
@@ -3,6 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DiscordChatExporter.Gui" xmlns:local="clr-namespace:DiscordChatExporter.Gui"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"> xmlns:s="https://github.com/canton7/Stylet">
<Application.Resources> <Application.Resources>
<s:ApplicationLoader> <s:ApplicationLoader>
@@ -111,6 +112,10 @@
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}" /> <Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}" />
</Style> </Style>
<Style BasedOn="{StaticResource MaterialDesignTimePicker}" TargetType="{x:Type materialDesign:TimePicker}">
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}" />
</Style>
<Style <Style
x:Key="MaterialDesignFlatDarkButton" x:Key="MaterialDesignFlatDarkButton"
BasedOn="{StaticResource MaterialDesignFlatButton}" BasedOn="{StaticResource MaterialDesignFlatButton}"
@@ -136,6 +141,89 @@
</Trigger> </Trigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
<!-- Default MD Expander is incredibly slow (https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/issues/1307) -->
<Style BasedOn="{StaticResource MaterialDesignExpander}" TargetType="{x:Type Expander}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Expander">
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<DockPanel Background="{TemplateBinding Background}">
<ToggleButton
Name="HeaderSite"
BorderThickness="0"
Content="{TemplateBinding Header}"
ContentStringFormat="{TemplateBinding HeaderStringFormat}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}"
Cursor="Hand"
DockPanel.Dock="Top"
Focusable="False"
Foreground="{TemplateBinding Foreground}"
IsChecked="{Binding Path=IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
IsTabStop="False"
Opacity=".87"
Style="{StaticResource MaterialDesignExpanderDownHeaderStyle}"
TextElement.FontSize="15" />
<Border
Name="ContentSite"
DockPanel.Dock="Bottom"
Visibility="Collapsed">
<StackPanel
Name="ContentPanel"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<ContentPresenter
Name="PART_Content"
ContentStringFormat="{TemplateBinding ContentStringFormat}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}"
Focusable="False" />
</StackPanel>
</Border>
</DockPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="true">
<Setter TargetName="ContentSite" Property="Visibility" Value="Visible" />
</Trigger>
<Trigger Property="ExpandDirection" Value="Right">
<Setter TargetName="HeaderSite" Property="DockPanel.Dock" Value="Left" />
<Setter TargetName="ContentSite" Property="DockPanel.Dock" Value="Right" />
<Setter TargetName="ContentPanel" Property="Orientation" Value="Horizontal" />
<Setter TargetName="ContentPanel" Property="Height" Value="Auto" />
<Setter TargetName="HeaderSite" Property="Style" Value="{StaticResource MaterialDesignExpanderRightHeaderStyle}" />
</Trigger>
<Trigger Property="ExpandDirection" Value="Left">
<Setter TargetName="HeaderSite" Property="DockPanel.Dock" Value="Right" />
<Setter TargetName="ContentSite" Property="DockPanel.Dock" Value="Left" />
<Setter TargetName="ContentPanel" Property="Orientation" Value="Horizontal" />
<Setter TargetName="ContentPanel" Property="Height" Value="Auto" />
<Setter TargetName="HeaderSite" Property="Style" Value="{StaticResource MaterialDesignExpanderLeftHeaderStyle}" />
</Trigger>
<Trigger Property="ExpandDirection" Value="Up">
<Setter TargetName="HeaderSite" Property="DockPanel.Dock" Value="Bottom" />
<Setter TargetName="ContentSite" Property="DockPanel.Dock" Value="Top" />
<Setter TargetName="ContentPanel" Property="Orientation" Value="Vertical" />
<Setter TargetName="ContentPanel" Property="Width" Value="Auto" />
<Setter TargetName="HeaderSite" Property="Style" Value="{StaticResource MaterialDesignExpanderUpHeaderStyle}" />
</Trigger>
<Trigger Property="ExpandDirection" Value="Down">
<Setter TargetName="HeaderSite" Property="DockPanel.Dock" Value="Top" />
<Setter TargetName="ContentSite" Property="DockPanel.Dock" Value="Bottom" />
<Setter TargetName="ContentPanel" Property="Orientation" Value="Vertical" />
<Setter TargetName="ContentPanel" Property="Width" Value="Auto" />
<Setter TargetName="HeaderSite" Property="Style" Value="{StaticResource MaterialDesignExpanderDownHeaderStyle}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</s:ApplicationLoader> </s:ApplicationLoader>
</Application.Resources> </Application.Resources>
</Application> </Application>
@@ -0,0 +1,28 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace DiscordChatExporter.Gui.Converters
{
[ValueConversion(typeof(TimeSpan?), typeof(DateTime?))]
public class TimeSpanToDateTimeConverter : IValueConverter
{
public static TimeSpanToDateTimeConverter Instance { get; } = new TimeSpanToDateTimeConverter();
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is TimeSpan timeSpanValue)
return DateTime.Today.Add(timeSpanValue);
return default(DateTime?);
}
public object? ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is DateTime dateTimeValue)
return dateTimeValue.TimeOfDay;
return default(TimeSpan?);
}
}
}
@@ -26,9 +26,23 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
public ExportFormat SelectedFormat { get; set; } public ExportFormat SelectedFormat { get; set; }
public DateTimeOffset? After { get; set; } // This date/time abomination is required because we use separate controls to set these
public DateTimeOffset? Before { get; set; } public DateTimeOffset? AfterDate { get; set; }
public bool IsAfterDateSet => AfterDate != null;
public TimeSpan? AfterTime { get; set; }
public DateTimeOffset? After => AfterDate?.Add(AfterTime ?? TimeSpan.Zero);
public DateTimeOffset? BeforeDate { get; set; }
public bool IsBeforeDateSet => BeforeDate != null;
public TimeSpan? BeforeTime { get; set; }
public DateTimeOffset? Before => BeforeDate?.Add(BeforeTime ?? TimeSpan.Zero);
public int? PartitionLimit { get; set; } public int? PartitionLimit { get; set; }
@@ -60,12 +74,6 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
_settingsService.LastPartitionLimit = PartitionLimit; _settingsService.LastPartitionLimit = PartitionLimit;
_settingsService.LastShouldDownloadMedia = ShouldDownloadMedia; _settingsService.LastShouldDownloadMedia = ShouldDownloadMedia;
// Clamp 'after' and 'before' values
if (After > Before)
After = Before;
if (Before < After)
Before = After;
// If single channel - prompt file path // If single channel - prompt file path
if (IsSingleChannel) if (IsSingleChannel)
{ {
@@ -150,11 +150,7 @@ namespace DiscordChatExporter.Gui.ViewModels
var guildChannelMap = new Dictionary<Guild, IReadOnlyList<Channel>>(); var guildChannelMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
await foreach (var guild in discord.GetUserGuildsAsync()) await foreach (var guild in discord.GetUserGuildsAsync())
{ {
var channels = await discord.GetGuildChannelsAsync(guild.Id); guildChannelMap[guild] = await discord.GetGuildChannelsAsync(guild.Id);
guildChannelMap[guild] = channels
.OrderBy(c => c.Category)
.ThenBy(c => c.Name)
.ToArray();
} }
GuildChannelMap = guildChannelMap; GuildChannelMap = guildChannelMap;
@@ -82,30 +82,52 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DatePicker <DatePicker
Grid.Row="0" Grid.Row="0"
Grid.Column="0" Grid.Column="0"
Margin="16,8" Margin="16,8,16,4"
materialDesign:HintAssist.Hint="From (optional)" materialDesign:HintAssist.Hint="After (date)"
materialDesign:HintAssist.IsFloating="True" materialDesign:HintAssist.IsFloating="True"
DisplayDateEnd="{Binding Before, Converter={x:Static converters:DateTimeOffsetToDateTimeConverter.Instance}}" DisplayDateEnd="{Binding BeforeDate, Converter={x:Static converters:DateTimeOffsetToDateTimeConverter.Instance}}"
SelectedDate="{Binding After, Converter={x:Static converters:DateTimeOffsetToDateTimeConverter.Instance}}" SelectedDate="{Binding AfterDate, Converter={x:Static converters:DateTimeOffsetToDateTimeConverter.Instance}}"
ToolTip="If this is set, only messages sent after this date will be exported" /> ToolTip="If this is set, only messages sent after this date will be exported" />
<DatePicker <DatePicker
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
Margin="16,8" Margin="16,8,16,4"
materialDesign:HintAssist.Hint="To (optional)" materialDesign:HintAssist.Hint="Before (date)"
materialDesign:HintAssist.IsFloating="True" materialDesign:HintAssist.IsFloating="True"
DisplayDateStart="{Binding After, Converter={x:Static converters:DateTimeOffsetToDateTimeConverter.Instance}}" DisplayDateStart="{Binding AfterDate, Converter={x:Static converters:DateTimeOffsetToDateTimeConverter.Instance}}"
SelectedDate="{Binding Before, Converter={x:Static converters:DateTimeOffsetToDateTimeConverter.Instance}}" SelectedDate="{Binding BeforeDate, Converter={x:Static converters:DateTimeOffsetToDateTimeConverter.Instance}}"
ToolTip="If this is set, only messages sent before this date will be exported" /> ToolTip="If this is set, only messages sent before this date will be exported" />
<materialDesign:TimePicker
Grid.Row="1"
Grid.Column="0"
Margin="16,4,16,8"
materialDesign:HintAssist.Hint="After (time)"
materialDesign:HintAssist.IsFloating="True"
IsEnabled="{Binding IsAfterDateSet}"
SelectedTime="{Binding AfterTime, Converter={x:Static converters:TimeSpanToDateTimeConverter.Instance}}"
ToolTip="If this is set, only messages sent after this time will be exported" />
<materialDesign:TimePicker
Grid.Row="1"
Grid.Column="1"
Margin="16,4,16,8"
materialDesign:HintAssist.Hint="Before (time)"
materialDesign:HintAssist.IsFloating="True"
IsEnabled="{Binding IsBeforeDateSet}"
SelectedTime="{Binding BeforeTime, Converter={x:Static converters:TimeSpanToDateTimeConverter.Instance}}"
ToolTip="If this is set, only messages sent before this time will be exported" />
</Grid> </Grid>
<!-- Partitioning --> <!-- Partitioning -->
<TextBox <TextBox
Margin="16,8" Margin="16,8"
materialDesign:HintAssist.Hint="Messages per partition (optional)" materialDesign:HintAssist.Hint="Messages per partition"
materialDesign:HintAssist.IsFloating="True" materialDesign:HintAssist.IsFloating="True"
Text="{Binding PartitionLimit, TargetNullValue=''}" Text="{Binding PartitionLimit, TargetNullValue=''}"
ToolTip="If this is set, the exported file will be split into multiple partitions, each containing no more than specified number of messages" /> ToolTip="If this is set, the exported file will be split into multiple partitions, each containing no more than specified number of messages" />
+44 -8
View File
@@ -3,6 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="clr-namespace:DiscordChatExporter.Gui.Behaviors" xmlns:behaviors="clr-namespace:DiscordChatExporter.Gui.Behaviors"
xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters" xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
@@ -23,6 +24,18 @@
<Window.TaskbarItemInfo> <Window.TaskbarItemInfo>
<TaskbarItemInfo ProgressState="Normal" ProgressValue="{Binding ProgressManager.Progress}" /> <TaskbarItemInfo ProgressState="Normal" ProgressValue="{Binding ProgressManager.Progress}" />
</Window.TaskbarItemInfo> </Window.TaskbarItemInfo>
<Window.Resources>
<CollectionViewSource x:Key="AvailableChannelsViewSource" Source="{Binding AvailableChannels, Mode=OneWay}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Category" />
</CollectionViewSource.GroupDescriptions>
<CollectionViewSource.SortDescriptions>
<componentModel:SortDescription Direction="Ascending" PropertyName="Category" />
<componentModel:SortDescription Direction="Ascending" PropertyName="Name" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Window.Resources>
<materialDesign:DialogHost SnackbarMessageQueue="{Binding Notifications}" Style="{DynamicResource MaterialDesignEmbeddedDialogHost}"> <materialDesign:DialogHost SnackbarMessageQueue="{Binding Notifications}" Style="{DynamicResource MaterialDesignEmbeddedDialogHost}">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -253,12 +266,37 @@
<Border Grid.Column="1"> <Border Grid.Column="1">
<ListBox <ListBox
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
ItemsSource="{Binding AvailableChannels}" ItemsSource="{Binding Source={StaticResource AvailableChannelsViewSource}}"
SelectionMode="Extended" SelectionMode="Extended"
TextSearch.TextPath="Model.Name"> TextSearch.TextPath="Model.Name"
VirtualizingPanel.IsVirtualizingWhenGrouping="True">
<i:Interaction.Behaviors> <i:Interaction.Behaviors>
<behaviors:ChannelMultiSelectionListBoxBehavior SelectedItems="{Binding SelectedChannels}" /> <behaviors:ChannelMultiSelectionListBoxBehavior SelectedItems="{Binding SelectedChannels}" />
</i:Interaction.Behaviors> </i:Interaction.Behaviors>
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate d:DataContext="{x:Type CollectionViewGroup}">
<Expander
Margin="0"
Padding="0"
Background="Transparent"
BorderBrush="{DynamicResource DividerBrush}"
BorderThickness="0,1,0,0"
Header="{Binding Name}"
IsExpanded="False">
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListBox.GroupStyle>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<Grid Margin="-8" Background="Transparent"> <Grid Margin="-8" Background="Transparent">
@@ -278,16 +316,14 @@
VerticalAlignment="Center" VerticalAlignment="Center"
Kind="Pound" /> Kind="Pound" />
<!-- Channel category / name --> <!-- Channel name -->
<TextBlock <TextBlock
Grid.Column="1" Grid.Column="1"
Margin="3,8,8,8" Margin="3,8,8,8"
VerticalAlignment="Center" VerticalAlignment="Center"
FontSize="14"> FontSize="14"
<Run Foreground="{DynamicResource SecondaryTextBrush}" Text="{Binding Category, Mode=OneWay}" /> Foreground="{DynamicResource PrimaryTextBrush}"
<Run Text="/" /> Text="{Binding Name, Mode=OneWay}" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="{Binding Name, Mode=OneWay}" />
</TextBlock>
<!-- Is selected checkmark --> <!-- Is selected checkmark -->
<materialDesign:PackIcon <materialDesign:PackIcon
+1 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<Version>2.21.2</Version> <Version>2.23</Version>
<Company>Tyrrrz</Company> <Company>Tyrrrz</Company>
<Copyright>Copyright (c) Alexey Golub</Copyright> <Copyright>Copyright (c) Alexey Golub</Copyright>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>