mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-10 07:59:35 +02:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b7da21c24 | |||
| 1d0a13c613 | |||
| 67036f59ce | |||
| 517542730a | |||
| 19dc2334e2 | |||
| 4e1f783145 | |||
| ff6ef480ab | |||
| e1e8c73613 | |||
| e0284e281c | |||
| 435510baf9 | |||
| cc2583f96b | |||
| de763f8aaa | |||
| 7b67cbc385 | |||
| 7ffb799136 | |||
| 5a84fb25e6 | |||
| 9988300942 | |||
| 4540134a98 | |||
| 656e5a5b0d | |||
| 63e3ba8891 | |||
| d7572d9fd3 | |||
| 63c835df88 | |||
| 7bfd645e8e | |||
| 4e5d3cb4ed | |||
| cff976e2e4 | |||
| 0c69fd6e45 | |||
| 909a58b641 | |||
| 2db4ad46cc | |||
| ffa1023c50 | |||
| f7ef3bbb44 | |||
| bb2b04d2e5 | |||
| 1f36fb608a | |||
| b6e93e5770 | |||
| 8515efe11b | |||
| 7da82f9ef4 | |||
| 6ccba43807 | |||
| 2ce9e2c77e | |||
| a6a54d688f | |||
| e0d2f9c1ba | |||
| 01f8cf0e32 | |||
| ebab80fda1 | |||
| c432a75f62 | |||
| be4989ea34 | |||
| f8dae72411 | |||
| ab46361489 | |||
| 4a986fe5df | |||
| e3d2afac87 | |||
| 0cfcaca6b2 | |||
| 107c9de265 |
@@ -259,9 +259,3 @@ paket-files/
|
|||||||
# Python Tools for Visual Studio (PTVS)
|
# Python Tools for Visual Studio (PTVS)
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
# Ammy auto-generated XAML
|
|
||||||
*.g.xaml
|
|
||||||
|
|
||||||
# Deploy output
|
|
||||||
Deploy/Output/
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
### v2.4.1 (15-Apr-2018)
|
||||||
|
|
||||||
|
- Added CSV export format.
|
||||||
|
- Channels are now ordered by name in the UI.
|
||||||
|
- Exported file is now always opened upon completion.
|
||||||
|
|
||||||
|
### v2.4 (08-Apr-2018)
|
||||||
|
|
||||||
|
- Added channel topic to output.
|
||||||
|
- Added a command line interface alternative to the current graphical user interface.
|
||||||
|
- Implemented application auto-update (can be disabled in settings).
|
||||||
|
- Improved some GUI elements and replaced some dialogs with notifications.
|
||||||
|
- Fixed a lot of issues with the markdown processor.
|
||||||
|
- Fixed DM group chats that have a custom name being shown as blank in the list.
|
||||||
|
|
||||||
|
### v2.3 (27-Oct-2017)
|
||||||
|
|
||||||
|
- Improved message date filtering, it's now marginally faster.
|
||||||
|
- Fixed underscores not recognized as italics in markdown.
|
||||||
|
- Added support for custom emojis.
|
||||||
|
- Added support for user and role mentions.
|
||||||
|
- Added support for channel mentions.
|
||||||
|
- Fixed text in pre blocks not being wrapped correctly.
|
||||||
|
- Added workaround for non-default message types.
|
||||||
+9
-20
@@ -1,22 +1,11 @@
|
|||||||
$path = "$PSScriptRoot\..\DiscordChatExporter\bin\Release\*"
|
New-Item "$PSScriptRoot\bin" -ItemType Directory -Force
|
||||||
$include = "*.exe", "*.dll", "*.config"
|
|
||||||
$outputDir = "$PSScriptRoot\Output"
|
|
||||||
$outputFile = "DiscordChatExporter.zip"
|
|
||||||
|
|
||||||
# Create output directory
|
# GUI
|
||||||
if (-Not (Test-Path $outputDir))
|
$files = @()
|
||||||
{
|
$files += Get-ChildItem -Path "$PSScriptRoot\..\DiscordChatExporter.Gui\bin\Release\*" -Include "*.exe", "*.dll", "*.config"
|
||||||
New-Item $outputDir -ItemType Directory
|
$files | Compress-Archive -DestinationPath "$PSScriptRoot\bin\DiscordChatExporter.zip" -Force
|
||||||
}
|
|
||||||
|
|
||||||
# Delete output if already exists
|
# CLI
|
||||||
if (Test-Path("$outputDir\$outputFile"))
|
$files = @()
|
||||||
{
|
$files += Get-ChildItem -Path "$PSScriptRoot\..\DiscordChatExporter.Cli\bin\Release\net461\*" -Include "*.exe", "*.dll", "*.config"
|
||||||
Remove-Item -Path "$outputDir\$outputFile"
|
$files | Compress-Archive -DestinationPath "$PSScriptRoot\bin\DiscordChatExporter.Cli.zip" -Force
|
||||||
}
|
|
||||||
|
|
||||||
# Get files
|
|
||||||
$files = Get-ChildItem -Path $path -Include $include
|
|
||||||
|
|
||||||
# Pack into archive
|
|
||||||
$files | Compress-Archive -DestinationPath "$outputDir\$outputFile"
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli
|
||||||
|
{
|
||||||
|
public class CliOptions
|
||||||
|
{
|
||||||
|
public string Token { get; set; }
|
||||||
|
|
||||||
|
public string ChannelId { get; set; }
|
||||||
|
|
||||||
|
public ExportFormat ExportFormat { get; set; }
|
||||||
|
|
||||||
|
public string FilePath { get; set; }
|
||||||
|
|
||||||
|
public DateTime? From { get; set; }
|
||||||
|
|
||||||
|
public DateTime? To { get; set; }
|
||||||
|
|
||||||
|
public string DateFormat { get; set; }
|
||||||
|
|
||||||
|
public int MessageGroupLimit { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using CommonServiceLocator;
|
||||||
|
using DiscordChatExporter.Cli.ViewModels;
|
||||||
|
using DiscordChatExporter.Core.Services;
|
||||||
|
using GalaSoft.MvvmLight.Ioc;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli
|
||||||
|
{
|
||||||
|
public class Container
|
||||||
|
{
|
||||||
|
public IMainViewModel MainViewModel => Resolve<IMainViewModel>();
|
||||||
|
public ISettingsService SettingsService => Resolve<ISettingsService>();
|
||||||
|
|
||||||
|
private T Resolve<T>(string key = null)
|
||||||
|
{
|
||||||
|
return ServiceLocator.Current.GetInstance<T>(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Init()
|
||||||
|
{
|
||||||
|
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
|
||||||
|
SimpleIoc.Default.Reset();
|
||||||
|
|
||||||
|
// Services
|
||||||
|
SimpleIoc.Default.Register<IDataService, DataService>();
|
||||||
|
SimpleIoc.Default.Register<IExportService, ExportService>();
|
||||||
|
SimpleIoc.Default.Register<IMessageGroupService, MessageGroupService>();
|
||||||
|
SimpleIoc.Default.Register<ISettingsService, SettingsService>();
|
||||||
|
|
||||||
|
// View models
|
||||||
|
SimpleIoc.Default.Register<IMainViewModel, MainViewModel>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net461</TargetFramework>
|
||||||
|
<Version>2.4.1</Version>
|
||||||
|
<Company>Tyrrrz</Company>
|
||||||
|
<Copyright>Copyright (c) 2017-2018 Alexey Golub</Copyright>
|
||||||
|
<ApplicationIcon>..\favicon.ico</ApplicationIcon>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommonServiceLocator" Version="2.0.3" />
|
||||||
|
<PackageReference Include="FluentCommandLineParser" Version="1.4.3" />
|
||||||
|
<PackageReference Include="MvvmLightLibs" Version="5.4.1" />
|
||||||
|
<PackageReference Include="Tyrrrz.Extensions" Version="1.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DiscordChatExporter.Core\DiscordChatExporter.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
using Fclp;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli
|
||||||
|
{
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
private static readonly Container Container = new Container();
|
||||||
|
|
||||||
|
private static void ShowHelp()
|
||||||
|
{
|
||||||
|
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
|
var availableFormats = Enum.GetNames(typeof(ExportFormat));
|
||||||
|
|
||||||
|
Console.WriteLine($"=== Discord Chat Exporter (Command Line Interface) v{version} ===");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("[-t] [--token] Discord authorization token.");
|
||||||
|
Console.WriteLine("[-c] [--channel] Discord channel ID.");
|
||||||
|
Console.WriteLine("[-f] [--format] Export format. Optional.");
|
||||||
|
Console.WriteLine("[-o] [--output] Output file path. Optional.");
|
||||||
|
Console.WriteLine(" [--datefrom] Limit to messages after this date. Optional.");
|
||||||
|
Console.WriteLine(" [--dateto] Limit to messages before this date. Optional.");
|
||||||
|
Console.WriteLine(" [--dateformat] Date format. Optional.");
|
||||||
|
Console.WriteLine(" [--grouplimit] Message group limit. Optional.");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine($"Available export formats: {availableFormats.JoinToString(", ")}");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("# To get authorization token:");
|
||||||
|
Console.WriteLine(" - Open Discord app");
|
||||||
|
Console.WriteLine(" - Log in if you haven't");
|
||||||
|
Console.WriteLine(" - Press Ctrl+Shift+I");
|
||||||
|
Console.WriteLine(" - Navigate to Application tab");
|
||||||
|
Console.WriteLine(" - Expand Storage > Local Storage > https://discordapp.com");
|
||||||
|
Console.WriteLine(" - Find \"token\" under key and copy the value");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("# To get channel ID:");
|
||||||
|
Console.WriteLine(" - Open Discord app");
|
||||||
|
Console.WriteLine(" - Log in if you haven't");
|
||||||
|
Console.WriteLine(" - Go to any channel you want to export");
|
||||||
|
Console.WriteLine(" - Press Ctrl+Shift+I");
|
||||||
|
Console.WriteLine(" - Navigate to Console tab");
|
||||||
|
Console.WriteLine(" - Type \"document.URL\" and press Enter");
|
||||||
|
Console.WriteLine(" - Copy the long sequence of numbers after last slash");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CliOptions ParseOptions(string[] args)
|
||||||
|
{
|
||||||
|
var argsParser = new FluentCommandLineParser<CliOptions>();
|
||||||
|
|
||||||
|
var settings = Container.SettingsService;
|
||||||
|
|
||||||
|
argsParser.Setup(o => o.Token).As('t', "token").Required();
|
||||||
|
argsParser.Setup(o => o.ChannelId).As('c', "channel").Required();
|
||||||
|
argsParser.Setup(o => o.ExportFormat).As('f', "format").SetDefault(ExportFormat.HtmlDark);
|
||||||
|
argsParser.Setup(o => o.FilePath).As('o', "output").SetDefault(null);
|
||||||
|
argsParser.Setup(o => o.From).As("datefrom").SetDefault(null);
|
||||||
|
argsParser.Setup(o => o.To).As("dateto").SetDefault(null);
|
||||||
|
argsParser.Setup(o => o.DateFormat).As("dateformat").SetDefault(settings.DateFormat);
|
||||||
|
argsParser.Setup(o => o.MessageGroupLimit).As("grouplimit").SetDefault(settings.MessageGroupLimit);
|
||||||
|
|
||||||
|
var parsed = argsParser.Parse(args);
|
||||||
|
|
||||||
|
// Show help if no arguments
|
||||||
|
if (parsed.EmptyArgs)
|
||||||
|
{
|
||||||
|
ShowHelp();
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
// Show error if there are any
|
||||||
|
else if (parsed.HasErrors)
|
||||||
|
{
|
||||||
|
Console.Error.Write(parsed.ErrorText);
|
||||||
|
Environment.Exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return argsParser.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
// Init container
|
||||||
|
Container.Init();
|
||||||
|
|
||||||
|
// Parse options
|
||||||
|
var options = ParseOptions(args);
|
||||||
|
|
||||||
|
// Inject some settings
|
||||||
|
var settings = Container.SettingsService;
|
||||||
|
settings.DateFormat = options.DateFormat;
|
||||||
|
settings.MessageGroupLimit = options.MessageGroupLimit;
|
||||||
|
|
||||||
|
// Export
|
||||||
|
var vm = Container.MainViewModel;
|
||||||
|
vm.ExportAsync(
|
||||||
|
options.Token,
|
||||||
|
options.ChannelId,
|
||||||
|
options.FilePath,
|
||||||
|
options.ExportFormat,
|
||||||
|
options.From,
|
||||||
|
options.To).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
// Cleanup container
|
||||||
|
Container.Cleanup();
|
||||||
|
|
||||||
|
Console.WriteLine("Export complete.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.ViewModels
|
||||||
|
{
|
||||||
|
public interface IMainViewModel
|
||||||
|
{
|
||||||
|
Task ExportAsync(string token, string channelId, string filePath, ExportFormat format, DateTime? from,
|
||||||
|
DateTime? to);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
using DiscordChatExporter.Core.Services;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.ViewModels
|
||||||
|
{
|
||||||
|
public class MainViewModel : IMainViewModel
|
||||||
|
{
|
||||||
|
private readonly IDataService _dataService;
|
||||||
|
private readonly IMessageGroupService _messageGroupService;
|
||||||
|
private readonly IExportService _exportService;
|
||||||
|
|
||||||
|
public MainViewModel(IDataService dataService, IMessageGroupService messageGroupService,
|
||||||
|
IExportService exportService)
|
||||||
|
{
|
||||||
|
_dataService = dataService;
|
||||||
|
_messageGroupService = messageGroupService;
|
||||||
|
_exportService = exportService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExportAsync(string token, string channelId, string filePath, ExportFormat format, DateTime? from,
|
||||||
|
DateTime? to)
|
||||||
|
{
|
||||||
|
// Get channel and guild
|
||||||
|
var channel = await _dataService.GetChannelAsync(token, channelId);
|
||||||
|
var guild = channel.GuildId == Guild.DirectMessages.Id
|
||||||
|
? Guild.DirectMessages
|
||||||
|
: await _dataService.GetGuildAsync(token, channel.GuildId);
|
||||||
|
|
||||||
|
// Generate file path if not set
|
||||||
|
if (filePath.IsBlank())
|
||||||
|
{
|
||||||
|
filePath = $"{guild.Name} - {channel.Name}.{format.GetFileExtension()}"
|
||||||
|
.Replace(Path.GetInvalidFileNameChars(), '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get messages
|
||||||
|
var messages = await _dataService.GetChannelMessagesAsync(token, channelId, from, to);
|
||||||
|
|
||||||
|
// Group them
|
||||||
|
var messageGroups = _messageGroupService.GroupMessages(messages);
|
||||||
|
|
||||||
|
// Create log
|
||||||
|
var log = new ChannelChatLog(guild, channel, messageGroups, messages.Count);
|
||||||
|
|
||||||
|
// Export
|
||||||
|
await _exportService.ExportAsync(format, filePath, log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net461</TargetFramework>
|
||||||
|
<Version>2.4.1</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Resources\ExportService\DarkTheme.css" />
|
||||||
|
<EmbeddedResource Include="Resources\ExportService\LightTheme.css" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="System.Net.Http" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CsvHelper" Version="7.1.0" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||||
|
<PackageReference Include="Onova" Version="2.1.0" />
|
||||||
|
<PackageReference Include="Tyrrrz.Extensions" Version="1.5.0" />
|
||||||
|
<PackageReference Include="Tyrrrz.Settings" Version="1.3.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Exceptions
|
namespace DiscordChatExporter.Core.Exceptions
|
||||||
{
|
{
|
||||||
public class HttpErrorStatusCodeException : Exception
|
public class HttpErrorStatusCodeException : Exception
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Resources;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Internal
|
||||||
|
{
|
||||||
|
internal static class Extensions
|
||||||
|
{
|
||||||
|
public static string GetManifestResourceString(this Assembly assembly, string resourceName)
|
||||||
|
{
|
||||||
|
var stream = assembly.GetManifestResourceStream(resourceName);
|
||||||
|
if (stream == null)
|
||||||
|
throw new MissingManifestResourceException($"Could not find resource [{resourceName}].");
|
||||||
|
|
||||||
|
using (stream)
|
||||||
|
using (var reader = new StreamReader(stream))
|
||||||
|
{
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Models
|
namespace DiscordChatExporter.Core.Models
|
||||||
{
|
{
|
||||||
public class Attachment
|
public class Attachment
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Models
|
namespace DiscordChatExporter.Core.Models
|
||||||
{
|
{
|
||||||
public enum AttachmentType
|
public enum AttachmentType
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
namespace DiscordChatExporter.Core.Models
|
||||||
|
{
|
||||||
|
public partial class Channel
|
||||||
|
{
|
||||||
|
public string Id { get; }
|
||||||
|
|
||||||
|
public string GuildId { get; }
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
public string Topic { get; }
|
||||||
|
|
||||||
|
public ChannelType Type { get; }
|
||||||
|
|
||||||
|
public Channel(string id, string guildId, string name, string topic, ChannelType type)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
GuildId = guildId;
|
||||||
|
Name = name;
|
||||||
|
Topic = topic;
|
||||||
|
Type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class Channel
|
||||||
|
{
|
||||||
|
public static Channel CreateDeletedChannel(string id)
|
||||||
|
{
|
||||||
|
return new Channel(id, null, "deleted-channel", null, ChannelType.GuildTextChat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Models
|
namespace DiscordChatExporter.Core.Models
|
||||||
{
|
{
|
||||||
public class ChannelChatLog
|
public class ChannelChatLog
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Models
|
namespace DiscordChatExporter.Core.Models
|
||||||
{
|
{
|
||||||
public enum ChannelType
|
public enum ChannelType
|
||||||
{
|
{
|
||||||
+3
-2
@@ -1,9 +1,10 @@
|
|||||||
namespace DiscordChatExporter.Models
|
namespace DiscordChatExporter.Core.Models
|
||||||
{
|
{
|
||||||
public enum ExportFormat
|
public enum ExportFormat
|
||||||
{
|
{
|
||||||
PlainText,
|
PlainText,
|
||||||
HtmlDark,
|
HtmlDark,
|
||||||
HtmlLight
|
HtmlLight,
|
||||||
|
Csv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+7
-3
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Models
|
namespace DiscordChatExporter.Core.Models
|
||||||
{
|
{
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
@@ -12,8 +12,10 @@ namespace DiscordChatExporter.Models
|
|||||||
return "html";
|
return "html";
|
||||||
if (format == ExportFormat.HtmlLight)
|
if (format == ExportFormat.HtmlLight)
|
||||||
return "html";
|
return "html";
|
||||||
|
if (format == ExportFormat.Csv)
|
||||||
|
return "csv";
|
||||||
|
|
||||||
throw new NotImplementedException();
|
throw new ArgumentOutOfRangeException(nameof(format));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetDisplayName(this ExportFormat format)
|
public static string GetDisplayName(this ExportFormat format)
|
||||||
@@ -24,8 +26,10 @@ namespace DiscordChatExporter.Models
|
|||||||
return "HTML (Dark)";
|
return "HTML (Dark)";
|
||||||
if (format == ExportFormat.HtmlLight)
|
if (format == ExportFormat.HtmlLight)
|
||||||
return "HTML (Light)";
|
return "HTML (Light)";
|
||||||
|
if (format == ExportFormat.Csv)
|
||||||
|
return "Comma Seperated Values (CSV)";
|
||||||
|
|
||||||
throw new NotImplementedException();
|
throw new ArgumentOutOfRangeException(nameof(format));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
using Tyrrrz.Extensions;
|
using System.Collections.Generic;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Models
|
namespace DiscordChatExporter.Core.Models
|
||||||
{
|
{
|
||||||
public class Guild
|
public partial class Guild
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
|
|
||||||
@@ -14,11 +15,14 @@ namespace DiscordChatExporter.Models
|
|||||||
? $"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";
|
||||||
|
|
||||||
public Guild(string id, string name, string iconHash)
|
public IReadOnlyList<Role> Roles { get; }
|
||||||
|
|
||||||
|
public Guild(string id, string name, string iconHash, IReadOnlyList<Role> roles)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Name = name;
|
Name = name;
|
||||||
IconHash = iconHash;
|
IconHash = iconHash;
|
||||||
|
Roles = roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
@@ -26,4 +30,9 @@ namespace DiscordChatExporter.Models
|
|||||||
return Name;
|
return Name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class Guild
|
||||||
|
{
|
||||||
|
public static Guild DirectMessages { get; } = new Guild("@me", "Direct Messages", null, new Role[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Models
|
namespace DiscordChatExporter.Core.Models
|
||||||
{
|
{
|
||||||
public class Message
|
public class Message
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
|
|
||||||
|
public string ChannelId { get; }
|
||||||
|
|
||||||
|
public MessageType Type { get; }
|
||||||
|
|
||||||
public User Author { get; }
|
public User Author { get; }
|
||||||
|
|
||||||
public DateTime TimeStamp { get; }
|
public DateTime TimeStamp { get; }
|
||||||
@@ -23,13 +27,15 @@ namespace DiscordChatExporter.Models
|
|||||||
|
|
||||||
public IReadOnlyList<Channel> MentionedChannels { get; }
|
public IReadOnlyList<Channel> MentionedChannels { get; }
|
||||||
|
|
||||||
public Message(string id, User author,
|
public Message(string id, string channelId, MessageType type,
|
||||||
DateTime timeStamp, DateTime? editedTimeStamp,
|
User author, DateTime timeStamp,
|
||||||
string content, IReadOnlyList<Attachment> attachments,
|
DateTime? editedTimeStamp, string content,
|
||||||
IReadOnlyList<User> mentionedUsers, IReadOnlyList<Role> mentionedRoles,
|
IReadOnlyList<Attachment> attachments, IReadOnlyList<User> mentionedUsers,
|
||||||
IReadOnlyList<Channel> mentionedChannels)
|
IReadOnlyList<Role> mentionedRoles, IReadOnlyList<Channel> mentionedChannels)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
|
ChannelId = channelId;
|
||||||
|
Type = type;
|
||||||
Author = author;
|
Author = author;
|
||||||
TimeStamp = timeStamp;
|
TimeStamp = timeStamp;
|
||||||
EditedTimeStamp = editedTimeStamp;
|
EditedTimeStamp = editedTimeStamp;
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Models
|
namespace DiscordChatExporter.Core.Models
|
||||||
{
|
{
|
||||||
public class MessageGroup
|
public class MessageGroup
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Models
|
namespace DiscordChatExporter.Core.Models
|
||||||
{
|
{
|
||||||
public enum MessageType
|
public enum MessageType
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace DiscordChatExporter.Models
|
namespace DiscordChatExporter.Core.Models
|
||||||
{
|
{
|
||||||
public class Role
|
public partial class Role
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
|
|
||||||
@@ -17,4 +17,12 @@
|
|||||||
return Name;
|
return Name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class Role
|
||||||
|
{
|
||||||
|
public static Role CreateDeletedRole(string id)
|
||||||
|
{
|
||||||
|
return new Role(id, "deleted-role");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Models
|
namespace DiscordChatExporter.Core.Models
|
||||||
{
|
{
|
||||||
public class User
|
public class User
|
||||||
{
|
{
|
||||||
@@ -10,11 +10,15 @@ namespace DiscordChatExporter.Models
|
|||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
|
public string FullName => $"{Name}#{Discriminator:0000}";
|
||||||
|
|
||||||
public string AvatarHash { get; }
|
public string AvatarHash { get; }
|
||||||
|
|
||||||
|
public string DefaultAvatarHash => $"{Discriminator % 5}";
|
||||||
|
|
||||||
public string AvatarUrl => AvatarHash.IsNotBlank()
|
public string AvatarUrl => AvatarHash.IsNotBlank()
|
||||||
? $"https://cdn.discordapp.com/avatars/{Id}/{AvatarHash}.png"
|
? $"https://cdn.discordapp.com/avatars/{Id}/{AvatarHash}.png"
|
||||||
: $"https://cdn.discordapp.com/embed/avatars/{Discriminator % 5}.png";
|
: $"https://cdn.discordapp.com/embed/avatars/{DefaultAvatarHash}.png";
|
||||||
|
|
||||||
public User(string id, int discriminator, string name, string avatarHash)
|
public User(string id, int discriminator, string name, string avatarHash)
|
||||||
{
|
{
|
||||||
@@ -26,7 +30,7 @@ namespace DiscordChatExporter.Models
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{Name}#{Discriminator}";
|
return FullName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+22
-10
@@ -10,7 +10,9 @@ a {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover { text-decoration: underline; }
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
div.pre {
|
div.pre {
|
||||||
background-color: #2F3136;
|
background-color: #2F3136;
|
||||||
@@ -37,7 +39,9 @@ div#info {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
div#log { max-width: 100%; }
|
div#log {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
img.guild-icon {
|
img.guild-icon {
|
||||||
max-height: 64px;
|
max-height: 64px;
|
||||||
@@ -51,15 +55,22 @@ div.info-right {
|
|||||||
|
|
||||||
div.guild-name {
|
div.guild-name {
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.channel-name {
|
div.channel-name {
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.misc { margin-top: 2px; }
|
div.channel-topic {
|
||||||
|
margin-top: 2px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.channel-messagecount {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
div.msg {
|
div.msg {
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
@@ -84,29 +95,30 @@ img.msg-avatar {
|
|||||||
div.msg-right {
|
div.msg-right {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
|
min-width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.msg-user {
|
span.msg-user {
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.msg-date {
|
span.msg-date {
|
||||||
color: rgba(255, 255, 255, 0.2);
|
color: rgba(255, 255, 255, 0.2);
|
||||||
font-size: .75rem;
|
font-size: .75em;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.msg-edited {
|
span.msg-edited {
|
||||||
color: rgba(255, 255, 255, 0.2);
|
color: rgba(255, 255, 255, 0.2);
|
||||||
font-size: .8rem;
|
font-size: .8em;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.msg-content {
|
div.msg-content {
|
||||||
font-size: .9375rem;
|
font-size: .9375em;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
word-break: break-all;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.msg-attachment {
|
div.msg-attachment {
|
||||||
+22
-10
@@ -10,7 +10,9 @@ a {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover { text-decoration: underline; }
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
div.pre {
|
div.pre {
|
||||||
background-color: #F9F9F9;
|
background-color: #F9F9F9;
|
||||||
@@ -37,7 +39,9 @@ div#info {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
div#log { max-width: 100%; }
|
div#log {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
img.guild-icon {
|
img.guild-icon {
|
||||||
max-height: 64px;
|
max-height: 64px;
|
||||||
@@ -51,15 +55,22 @@ div.info-right {
|
|||||||
|
|
||||||
div.guild-name {
|
div.guild-name {
|
||||||
color: #2F3136;
|
color: #2F3136;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.channel-name {
|
div.channel-name {
|
||||||
color: #2F3136;
|
color: #2F3136;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.misc { margin-top: 2px; }
|
div.channel-topic {
|
||||||
|
margin-top: 2px;
|
||||||
|
color: #2F3136;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.channel-messagecount {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
div.msg {
|
div.msg {
|
||||||
border-top: 1px solid #ECEEEF;
|
border-top: 1px solid #ECEEEF;
|
||||||
@@ -84,29 +95,30 @@ img.msg-avatar {
|
|||||||
div.msg-right {
|
div.msg-right {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
|
min-width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.msg-user {
|
span.msg-user {
|
||||||
color: #2F3136;
|
color: #2F3136;
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.msg-date {
|
span.msg-date {
|
||||||
color: #99AAB5;
|
color: #99AAB5;
|
||||||
font-size: .75rem;
|
font-size: .75em;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.msg-edited {
|
span.msg-edited {
|
||||||
color: #99AAB5;
|
color: #99AAB5;
|
||||||
font-size: .8rem;
|
font-size: .8em;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.msg-content {
|
div.msg-content {
|
||||||
font-size: .9375rem;
|
font-size: .9375em;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
word-break: break-all;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.msg-attachment {
|
div.msg-attachment {
|
||||||
+179
-149
@@ -4,12 +4,12 @@ using System.Linq;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Exceptions;
|
using DiscordChatExporter.Core.Exceptions;
|
||||||
using DiscordChatExporter.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Services
|
namespace DiscordChatExporter.Core.Services
|
||||||
{
|
{
|
||||||
public partial class DataService : IDataService, IDisposable
|
public partial class DataService : IDataService, IDisposable
|
||||||
{
|
{
|
||||||
@@ -19,6 +19,131 @@ namespace DiscordChatExporter.Services
|
|||||||
private readonly Dictionary<string, Role> _roleCache = new Dictionary<string, Role>();
|
private readonly Dictionary<string, Role> _roleCache = new Dictionary<string, Role>();
|
||||||
private readonly Dictionary<string, Channel> _channelCache = new Dictionary<string, Channel>();
|
private readonly Dictionary<string, Channel> _channelCache = new Dictionary<string, Channel>();
|
||||||
|
|
||||||
|
private User ParseUser(JToken token)
|
||||||
|
{
|
||||||
|
var id = token["id"].Value<string>();
|
||||||
|
var discriminator = token["discriminator"].Value<int>();
|
||||||
|
var name = token["username"].Value<string>();
|
||||||
|
var avatarHash = token["avatar"].Value<string>();
|
||||||
|
|
||||||
|
return new User(id, discriminator, name, avatarHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Role ParseRole(JToken token)
|
||||||
|
{
|
||||||
|
var id = token["id"].Value<string>();
|
||||||
|
var name = token["name"].Value<string>();
|
||||||
|
|
||||||
|
return new Role(id, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guild ParseGuild(JToken token)
|
||||||
|
{
|
||||||
|
var id = token["id"].Value<string>();
|
||||||
|
var name = token["name"].Value<string>();
|
||||||
|
var iconHash = token["icon"].Value<string>();
|
||||||
|
var roles = token["roles"].Select(ParseRole).ToArray();
|
||||||
|
|
||||||
|
return new Guild(id, name, iconHash, roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Channel ParseChannel(JToken token)
|
||||||
|
{
|
||||||
|
// Get basic data
|
||||||
|
var id = token["id"].Value<string>();
|
||||||
|
var guildId = token["guild_id"]?.Value<string>();
|
||||||
|
var type = (ChannelType) token["type"].Value<int>();
|
||||||
|
var topic = token["topic"]?.Value<string>();
|
||||||
|
|
||||||
|
// Extract name based on type
|
||||||
|
string name;
|
||||||
|
if (type.IsEither(ChannelType.DirectTextChat, ChannelType.DirectGroupTextChat))
|
||||||
|
{
|
||||||
|
guildId = Guild.DirectMessages.Id;
|
||||||
|
|
||||||
|
// Try to get name if it's set
|
||||||
|
name = token["name"]?.Value<string>();
|
||||||
|
|
||||||
|
// Otherwise use recipients as the name
|
||||||
|
if (name.IsBlank())
|
||||||
|
name = token["recipients"].Select(ParseUser).Select(u => u.Name).JoinToString(", ");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
name = token["name"].Value<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Channel(id, guildId, name, topic, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Message ParseMessage(JToken token)
|
||||||
|
{
|
||||||
|
// Get basic data
|
||||||
|
var id = token["id"].Value<string>();
|
||||||
|
var channelId = token["channel_id"].Value<string>();
|
||||||
|
var timeStamp = token["timestamp"].Value<DateTime>();
|
||||||
|
var editedTimeStamp = token["edited_timestamp"]?.Value<DateTime?>();
|
||||||
|
var content = token["content"].Value<string>();
|
||||||
|
var type = (MessageType) token["type"].Value<int>();
|
||||||
|
|
||||||
|
// Workarounds for non-default types
|
||||||
|
if (type == MessageType.RecipientAdd)
|
||||||
|
content = "Added a recipient.";
|
||||||
|
else if (type == MessageType.RecipientRemove)
|
||||||
|
content = "Removed a recipient.";
|
||||||
|
else if (type == MessageType.Call)
|
||||||
|
content = "Started a call.";
|
||||||
|
else if (type == MessageType.ChannelNameChange)
|
||||||
|
content = "Changed the channel name.";
|
||||||
|
else if (type == MessageType.ChannelIconChange)
|
||||||
|
content = "Changed the channel icon.";
|
||||||
|
else if (type == MessageType.ChannelPinnedMessage)
|
||||||
|
content = "Pinned a message.";
|
||||||
|
else if (type == MessageType.GuildMemberJoin)
|
||||||
|
content = "Joined the server.";
|
||||||
|
|
||||||
|
// Get author
|
||||||
|
var author = ParseUser(token["author"]);
|
||||||
|
|
||||||
|
// Get attachment
|
||||||
|
var attachments = new List<Attachment>();
|
||||||
|
foreach (var attachmentJson in token["attachments"].EmptyIfNull())
|
||||||
|
{
|
||||||
|
var attachmentId = attachmentJson["id"].Value<string>();
|
||||||
|
var attachmentUrl = attachmentJson["url"].Value<string>();
|
||||||
|
var attachmentType = attachmentJson["width"] != null
|
||||||
|
? AttachmentType.Image
|
||||||
|
: AttachmentType.Other;
|
||||||
|
var attachmentFileName = attachmentJson["filename"].Value<string>();
|
||||||
|
var attachmentFileSize = attachmentJson["size"].Value<long>();
|
||||||
|
|
||||||
|
var attachment = new Attachment(
|
||||||
|
attachmentId, attachmentType, attachmentUrl,
|
||||||
|
attachmentFileName, attachmentFileSize);
|
||||||
|
attachments.Add(attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user mentions
|
||||||
|
var mentionedUsers = token["mentions"].Select(ParseUser).ToArray();
|
||||||
|
|
||||||
|
// Get role mentions
|
||||||
|
var mentionedRoles = token["mention_roles"]
|
||||||
|
.Values<string>()
|
||||||
|
.Select(i => _roleCache.GetOrDefault(i) ?? Role.CreateDeletedRole(id))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// Get channel mentions
|
||||||
|
var mentionedChannels = Regex.Matches(content, "<#(\\d+)>")
|
||||||
|
.Cast<Match>()
|
||||||
|
.Select(m => m.Groups[1].Value)
|
||||||
|
.ExceptBlank()
|
||||||
|
.Select(i => _channelCache.GetOrDefault(i) ?? Channel.CreateDeletedChannel(id))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return new Message(id, channelId, type, author, timeStamp, editedTimeStamp, content, attachments,
|
||||||
|
mentionedUsers, mentionedRoles, mentionedChannels);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<string> GetStringAsync(string url)
|
private async Task<string> GetStringAsync(string url)
|
||||||
{
|
{
|
||||||
using (var response = await _httpClient.GetAsync(url))
|
using (var response = await _httpClient.GetAsync(url))
|
||||||
@@ -33,7 +158,7 @@ namespace DiscordChatExporter.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IReadOnlyList<Role>> GetGuildRolesAsync(string token, string guildId)
|
public async Task<Guild> GetGuildAsync(string token, string guildId)
|
||||||
{
|
{
|
||||||
// Form request url
|
// Form request url
|
||||||
var url = $"{ApiRoot}/guilds/{guildId}?token={token}";
|
var url = $"{ApiRoot}/guilds/{guildId}?token={token}";
|
||||||
@@ -42,12 +167,51 @@ namespace DiscordChatExporter.Services
|
|||||||
var content = await GetStringAsync(url);
|
var content = await GetStringAsync(url);
|
||||||
|
|
||||||
// Parse
|
// Parse
|
||||||
var roles = JToken.Parse(content)["roles"].Select(ParseRole).ToArray();
|
var guild = ParseGuild(JToken.Parse(content));
|
||||||
|
|
||||||
return roles;
|
// Add roles to cache
|
||||||
|
foreach (var role in guild.Roles)
|
||||||
|
_roleCache[role.Id] = role;
|
||||||
|
|
||||||
|
return guild;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Guild>> GetGuildsAsync(string token)
|
public async Task<Channel> GetChannelAsync(string token, string channelId)
|
||||||
|
{
|
||||||
|
// Form request url
|
||||||
|
var url = $"{ApiRoot}/channels/{channelId}?token={token}";
|
||||||
|
|
||||||
|
// Get response
|
||||||
|
var content = await GetStringAsync(url);
|
||||||
|
|
||||||
|
// Parse
|
||||||
|
var channel = ParseChannel(JToken.Parse(content));
|
||||||
|
|
||||||
|
// Add channel to cache
|
||||||
|
_channelCache[channel.Id] = channel;
|
||||||
|
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(string token, string guildId)
|
||||||
|
{
|
||||||
|
// Form request url
|
||||||
|
var url = $"{ApiRoot}/guilds/{guildId}/channels?token={token}";
|
||||||
|
|
||||||
|
// Get response
|
||||||
|
var content = await GetStringAsync(url);
|
||||||
|
|
||||||
|
// Parse
|
||||||
|
var channels = JArray.Parse(content).Select(ParseChannel).ToArray();
|
||||||
|
|
||||||
|
// Add channels to cache
|
||||||
|
foreach (var channel in channels)
|
||||||
|
_channelCache[channel.Id] = channel;
|
||||||
|
|
||||||
|
return channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Guild>> GetUserGuildsAsync(string token)
|
||||||
{
|
{
|
||||||
// Form request url
|
// Form request url
|
||||||
var url = $"{ApiRoot}/users/@me/guilds?token={token}&limit=100";
|
var url = $"{ApiRoot}/users/@me/guilds?token={token}&limit=100";
|
||||||
@@ -55,15 +219,15 @@ namespace DiscordChatExporter.Services
|
|||||||
// Get response
|
// Get response
|
||||||
var content = await GetStringAsync(url);
|
var content = await GetStringAsync(url);
|
||||||
|
|
||||||
// Parse
|
// Parse IDs
|
||||||
var guilds = JArray.Parse(content).Select(ParseGuild).ToArray();
|
var guildIds = JArray.Parse(content).Select(t => t["id"].Value<string>());
|
||||||
|
|
||||||
// HACK: also get roles for all of them
|
// Get full guild infos
|
||||||
foreach (var guild in guilds)
|
var guilds = new List<Guild>();
|
||||||
|
foreach (var guildId in guildIds)
|
||||||
{
|
{
|
||||||
var roles = await GetGuildRolesAsync(token, guild.Id);
|
var guild = await GetGuildAsync(token, guildId);
|
||||||
foreach (var role in roles)
|
guilds.Add(guild);
|
||||||
_roleCache[role.Id] = role;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return guilds;
|
return guilds;
|
||||||
@@ -83,24 +247,6 @@ namespace DiscordChatExporter.Services
|
|||||||
return channels;
|
return channels;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(string token, string guildId)
|
|
||||||
{
|
|
||||||
// Form request url
|
|
||||||
var url = $"{ApiRoot}/guilds/{guildId}/channels?token={token}";
|
|
||||||
|
|
||||||
// Get response
|
|
||||||
var content = await GetStringAsync(url);
|
|
||||||
|
|
||||||
// Parse
|
|
||||||
var channels = JArray.Parse(content).Select(ParseChannel).ToArray();
|
|
||||||
|
|
||||||
// Cache
|
|
||||||
foreach (var channel in channels)
|
|
||||||
_channelCache[channel.Id] = channel;
|
|
||||||
|
|
||||||
return channels;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId,
|
public async Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId,
|
||||||
DateTime? from, DateTime? to)
|
DateTime? from, DateTime? to)
|
||||||
{
|
{
|
||||||
@@ -120,7 +266,7 @@ namespace DiscordChatExporter.Services
|
|||||||
var content = await GetStringAsync(url);
|
var content = await GetStringAsync(url);
|
||||||
|
|
||||||
// Parse
|
// Parse
|
||||||
var messages = JArray.Parse(content).Select(j => ParseMessage(j, _roleCache, _channelCache));
|
var messages = JArray.Parse(content).Select(ParseMessage);
|
||||||
|
|
||||||
// Add messages to list
|
// Add messages to list
|
||||||
string currentMessageId = null;
|
string currentMessageId = null;
|
||||||
@@ -176,121 +322,5 @@ namespace DiscordChatExporter.Services
|
|||||||
var value = ((ulong) unixTime - 1420070400000UL) << 22;
|
var value = ((ulong) unixTime - 1420070400000UL) << 22;
|
||||||
return value.ToString();
|
return value.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Guild ParseGuild(JToken token)
|
|
||||||
{
|
|
||||||
var id = token.Value<string>("id");
|
|
||||||
var name = token.Value<string>("name");
|
|
||||||
var iconHash = token.Value<string>("icon");
|
|
||||||
|
|
||||||
return new Guild(id, name, iconHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static User ParseUser(JToken token)
|
|
||||||
{
|
|
||||||
var id = token.Value<string>("id");
|
|
||||||
var discriminator = token.Value<int>("discriminator");
|
|
||||||
var name = token.Value<string>("username");
|
|
||||||
var avatarHash = token.Value<string>("avatar");
|
|
||||||
|
|
||||||
return new User(id, discriminator, name, avatarHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Role ParseRole(JToken token)
|
|
||||||
{
|
|
||||||
var id = token.Value<string>("id");
|
|
||||||
var name = token.Value<string>("name");
|
|
||||||
|
|
||||||
return new Role(id, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Channel ParseChannel(JToken token)
|
|
||||||
{
|
|
||||||
// Get basic data
|
|
||||||
var id = token.Value<string>("id");
|
|
||||||
var type = (ChannelType) token.Value<int>("type");
|
|
||||||
|
|
||||||
// Extract name based on type
|
|
||||||
string name;
|
|
||||||
if (type.IsEither(ChannelType.DirectTextChat, ChannelType.DirectGroupTextChat))
|
|
||||||
{
|
|
||||||
var recipients = token["recipients"].Select(ParseUser);
|
|
||||||
name = recipients.Select(r => r.Name).JoinToString(", ");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
name = token.Value<string>("name");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Channel(id, name, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Message ParseMessage(JToken token,
|
|
||||||
IDictionary<string, Role> roles, IDictionary<string, Channel> channels)
|
|
||||||
{
|
|
||||||
// Get basic data
|
|
||||||
var id = token.Value<string>("id");
|
|
||||||
var timeStamp = token.Value<DateTime>("timestamp");
|
|
||||||
var editedTimeStamp = token.Value<DateTime?>("edited_timestamp");
|
|
||||||
var content = token.Value<string>("content");
|
|
||||||
var type = (MessageType) token.Value<int>("type");
|
|
||||||
|
|
||||||
// Workarounds for non-default types
|
|
||||||
if (type == MessageType.RecipientAdd)
|
|
||||||
content = "Added a recipient.";
|
|
||||||
else if (type == MessageType.RecipientRemove)
|
|
||||||
content = "Removed a recipient.";
|
|
||||||
else if (type == MessageType.Call)
|
|
||||||
content = "Started a call.";
|
|
||||||
else if (type == MessageType.ChannelNameChange)
|
|
||||||
content = "Changed the channel name.";
|
|
||||||
else if (type == MessageType.ChannelIconChange)
|
|
||||||
content = "Changed the channel icon.";
|
|
||||||
else if (type == MessageType.ChannelPinnedMessage)
|
|
||||||
content = "Pinned a message.";
|
|
||||||
else if (type == MessageType.GuildMemberJoin)
|
|
||||||
content = "Joined the server.";
|
|
||||||
|
|
||||||
// Get author
|
|
||||||
var author = ParseUser(token["author"]);
|
|
||||||
|
|
||||||
// Get attachment
|
|
||||||
var attachments = new List<Attachment>();
|
|
||||||
foreach (var attachmentJson in token["attachments"].EmptyIfNull())
|
|
||||||
{
|
|
||||||
var attachmentId = attachmentJson.Value<string>("id");
|
|
||||||
var attachmentUrl = attachmentJson.Value<string>("url");
|
|
||||||
var attachmentType = attachmentJson["width"] != null
|
|
||||||
? AttachmentType.Image
|
|
||||||
: AttachmentType.Other;
|
|
||||||
var attachmentFileName = attachmentJson.Value<string>("filename");
|
|
||||||
var attachmentFileSize = attachmentJson.Value<long>("size");
|
|
||||||
|
|
||||||
var attachment = new Attachment(
|
|
||||||
attachmentId, attachmentType, attachmentUrl,
|
|
||||||
attachmentFileName, attachmentFileSize);
|
|
||||||
attachments.Add(attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user mentions
|
|
||||||
var mentionedUsers = token["mentions"].Select(ParseUser).ToArray();
|
|
||||||
|
|
||||||
// Get role mentions
|
|
||||||
var mentionedRoles = token["mention_roles"]
|
|
||||||
.Values<string>()
|
|
||||||
.Select(i => roles.GetOrDefault(i) ?? new Role(i, "deleted-role"))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
// Get channel mentions
|
|
||||||
var mentionedChanenls = Regex.Matches(content, "<#(\\d+)>")
|
|
||||||
.Cast<Match>()
|
|
||||||
.Select(m => m.Groups[1].Value)
|
|
||||||
.ExceptBlank()
|
|
||||||
.Select(i => channels.GetOrDefault(i) ?? new Channel(i, "deleted-channel", ChannelType.GuildTextChat))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new Message(id, author, timeStamp, editedTimeStamp, content, attachments,
|
|
||||||
mentionedUsers, mentionedRoles, mentionedChanenls);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CsvHelper;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Services
|
||||||
|
{
|
||||||
|
public partial class ExportService
|
||||||
|
{
|
||||||
|
private string FormatMessageContentCsv(Message message)
|
||||||
|
{
|
||||||
|
var content = message.Content;
|
||||||
|
|
||||||
|
// New lines
|
||||||
|
content = content.Replace("\n", ", ");
|
||||||
|
|
||||||
|
// User mentions (<@id> and <@!id>)
|
||||||
|
foreach (var mentionedUser in message.MentionedUsers)
|
||||||
|
content = Regex.Replace(content, $"<@!?{mentionedUser.Id}>", $"@{mentionedUser}");
|
||||||
|
|
||||||
|
// Role mentions (<@&id>)
|
||||||
|
foreach (var mentionedRole in message.MentionedRoles)
|
||||||
|
content = content.Replace($"<@&{mentionedRole.Id}>", $"@{mentionedRole.Name}");
|
||||||
|
|
||||||
|
// Channel mentions (<#id>)
|
||||||
|
foreach (var mentionedChannel in message.MentionedChannels)
|
||||||
|
content = content.Replace($"<#{mentionedChannel.Id}>", $"#{mentionedChannel.Name}");
|
||||||
|
|
||||||
|
// Custom emojis (<:name:id>)
|
||||||
|
content = Regex.Replace(content, "<(:.*?:)\\d*>", "$1");
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExportAsCsvAsync(ChannelChatLog log, TextWriter output)
|
||||||
|
{
|
||||||
|
using (var writer = new CsvWriter(output))
|
||||||
|
{
|
||||||
|
// Headers
|
||||||
|
writer.WriteField("Author");
|
||||||
|
writer.WriteField("Date");
|
||||||
|
writer.WriteField("Content");
|
||||||
|
writer.WriteField("Attachments");
|
||||||
|
await writer.NextRecordAsync();
|
||||||
|
|
||||||
|
// Chat log
|
||||||
|
foreach (var group in log.MessageGroups)
|
||||||
|
{
|
||||||
|
// Messages
|
||||||
|
foreach (var msg in group.Messages)
|
||||||
|
{
|
||||||
|
// Author
|
||||||
|
writer.WriteField(msg.Author.FullName);
|
||||||
|
|
||||||
|
// Date
|
||||||
|
var timeStampFormatted = msg.TimeStamp.ToString(_settingsService.DateFormat);
|
||||||
|
writer.WriteField(timeStampFormatted);
|
||||||
|
|
||||||
|
// Content
|
||||||
|
var contentFormatted = msg.Content.IsNotBlank() ? FormatMessageContentCsv(msg) : null;
|
||||||
|
writer.WriteField(contentFormatted);
|
||||||
|
|
||||||
|
// Attachments
|
||||||
|
var attachmentsFormatted = msg.Attachments.Select(a => a.Url).JoinToString(",");
|
||||||
|
writer.WriteField(attachmentsFormatted);
|
||||||
|
|
||||||
|
await writer.NextRecordAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Services
|
||||||
|
{
|
||||||
|
public partial class ExportService
|
||||||
|
{
|
||||||
|
private string FormatMessageContentHtml(Message message)
|
||||||
|
{
|
||||||
|
// A lot of these regexes were inspired by or taken from MarkdownSharp
|
||||||
|
|
||||||
|
var content = message.Content;
|
||||||
|
|
||||||
|
// HTML-encode content
|
||||||
|
content = HtmlEncode(content);
|
||||||
|
|
||||||
|
// Encode multiline codeblocks (```text```)
|
||||||
|
content = Regex.Replace(content,
|
||||||
|
@"```+(?:[^`]*?\n)?([^`]+)\n?```+",
|
||||||
|
m => $"\x1AM{Base64Encode(m.Groups[1].Value)}\x1AM");
|
||||||
|
|
||||||
|
// Encode inline codeblocks (`text`)
|
||||||
|
content = Regex.Replace(content,
|
||||||
|
@"`([^`]+)`",
|
||||||
|
m => $"\x1AI{Base64Encode(m.Groups[1].Value)}\x1AI");
|
||||||
|
|
||||||
|
// Encode URLs
|
||||||
|
content = Regex.Replace(content,
|
||||||
|
@"((https?|ftp)://[-a-zA-Z0-9+&@#/%?=~_|!:,\.\[\]\(\);]*[-a-zA-Z0-9+&@#/%=~_|\[\])])(?=$|\W)",
|
||||||
|
m => $"\x1AL{Base64Encode(m.Groups[1].Value)}\x1AL");
|
||||||
|
|
||||||
|
// Process bold (**text**)
|
||||||
|
content = Regex.Replace(content, @"(\*\*)(?=\S)(.+?[*_]*)(?<=\S)\1", "<b>$2</b>");
|
||||||
|
|
||||||
|
// Process underline (__text__)
|
||||||
|
content = Regex.Replace(content, @"(__)(?=\S)(.+?)(?<=\S)\1", "<u>$2</u>");
|
||||||
|
|
||||||
|
// Process italic (*text* or _text_)
|
||||||
|
content = Regex.Replace(content, @"(\*|_)(?=\S)(.+?)(?<=\S)\1", "<i>$2</i>");
|
||||||
|
|
||||||
|
// Process strike through (~~text~~)
|
||||||
|
content = Regex.Replace(content, @"(~~)(?=\S)(.+?)(?<=\S)\1", "<s>$2</s>");
|
||||||
|
|
||||||
|
// Decode and process multiline codeblocks
|
||||||
|
content = Regex.Replace(content, "\x1AM(.*?)\x1AM",
|
||||||
|
m => $"<div class=\"pre\">{Base64Decode(m.Groups[1].Value)}</div>");
|
||||||
|
|
||||||
|
// Decode and process inline codeblocks
|
||||||
|
content = Regex.Replace(content, "\x1AI(.*?)\x1AI",
|
||||||
|
m => $"<span class=\"pre\">{Base64Decode(m.Groups[1].Value)}</span>");
|
||||||
|
|
||||||
|
// Decode and process URLs
|
||||||
|
content = Regex.Replace(content, "\x1AL(.*?)\x1AL",
|
||||||
|
m => $"<a href=\"{Base64Decode(m.Groups[1].Value)}\">{Base64Decode(m.Groups[1].Value)}</a>");
|
||||||
|
|
||||||
|
// New lines
|
||||||
|
content = content.Replace("\n", "<br />");
|
||||||
|
|
||||||
|
// Meta mentions (@everyone)
|
||||||
|
content = content.Replace("@everyone", "<span class=\"mention\">@everyone</span>");
|
||||||
|
|
||||||
|
// Meta mentions (@here)
|
||||||
|
content = content.Replace("@here", "<span class=\"mention\">@here</span>");
|
||||||
|
|
||||||
|
// User mentions (<@id> and <@!id>)
|
||||||
|
foreach (var mentionedUser in message.MentionedUsers)
|
||||||
|
{
|
||||||
|
content = Regex.Replace(content, $"<@!?{mentionedUser.Id}>",
|
||||||
|
$"<span class=\"mention\" title=\"{HtmlEncode(mentionedUser.FullName)}\">" +
|
||||||
|
$"@{HtmlEncode(mentionedUser.Name)}" +
|
||||||
|
"</span>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role mentions (<@&id>)
|
||||||
|
foreach (var mentionedRole in message.MentionedRoles)
|
||||||
|
{
|
||||||
|
content = content.Replace($"<@&{mentionedRole.Id}>",
|
||||||
|
"<span class=\"mention\">" +
|
||||||
|
$"@{HtmlEncode(mentionedRole.Name)}" +
|
||||||
|
"</span>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel mentions (<#id>)
|
||||||
|
foreach (var mentionedChannel in message.MentionedChannels)
|
||||||
|
{
|
||||||
|
content = content.Replace($"<#{mentionedChannel.Id}>",
|
||||||
|
"<span class=\"mention\">" +
|
||||||
|
$"#{HtmlEncode(mentionedChannel.Name)}" +
|
||||||
|
"</span>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom emojis (<:name:id>)
|
||||||
|
content = Regex.Replace(content, "<(:.*?:)(\\d*)>",
|
||||||
|
"<img class=\"emoji\" title=\"$1\" src=\"https://cdn.discordapp.com/emojis/$2.png\" />");
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExportAsHtmlAsync(ChannelChatLog log, TextWriter output, string css)
|
||||||
|
{
|
||||||
|
// Generation info
|
||||||
|
await output.WriteLineAsync("<!-- https://github.com/Tyrrrz/DiscordChatExporter -->");
|
||||||
|
|
||||||
|
// Html start
|
||||||
|
await output.WriteLineAsync("<!DOCTYPE html>");
|
||||||
|
await output.WriteLineAsync("<html lang=\"en\">");
|
||||||
|
|
||||||
|
// HEAD
|
||||||
|
await output.WriteLineAsync("<head>");
|
||||||
|
await output.WriteLineAsync($"<title>{log.Guild.Name} - {log.Channel.Name}</title>");
|
||||||
|
await output.WriteLineAsync("<meta charset=\"utf-8\" />");
|
||||||
|
await output.WriteLineAsync("<meta name=\"viewport\" content=\"width=device-width\" />");
|
||||||
|
await output.WriteLineAsync($"<style>{css}</style>");
|
||||||
|
await output.WriteLineAsync("</head>");
|
||||||
|
|
||||||
|
// Body start
|
||||||
|
await output.WriteLineAsync("<body>");
|
||||||
|
|
||||||
|
// Guild and channel info
|
||||||
|
await output.WriteLineAsync("<div id=\"info\">");
|
||||||
|
await output.WriteLineAsync("<div class=\"info-left\">");
|
||||||
|
await output.WriteLineAsync($"<img class=\"guild-icon\" src=\"{log.Guild.IconUrl}\" />");
|
||||||
|
await output.WriteLineAsync("</div>"); // info-left
|
||||||
|
await output.WriteLineAsync("<div class=\"info-right\">");
|
||||||
|
await output.WriteLineAsync($"<div class=\"guild-name\">{log.Guild.Name}</div>");
|
||||||
|
await output.WriteLineAsync($"<div class=\"channel-name\">{log.Channel.Name}</div>");
|
||||||
|
await output.WriteLineAsync($"<div class=\"channel-topic\">{log.Channel.Topic}</div>");
|
||||||
|
await output.WriteLineAsync(
|
||||||
|
$"<div class=\"channel-messagecount\">{log.TotalMessageCount:N0} messages</div>");
|
||||||
|
await output.WriteLineAsync("</div>"); // info-right
|
||||||
|
await output.WriteLineAsync("</div>"); // info
|
||||||
|
|
||||||
|
// Chat log
|
||||||
|
await output.WriteLineAsync("<div id=\"log\">");
|
||||||
|
foreach (var group in log.MessageGroups)
|
||||||
|
{
|
||||||
|
await output.WriteLineAsync("<div class=\"msg\">");
|
||||||
|
await output.WriteLineAsync("<div class=\"msg-left\">");
|
||||||
|
await output.WriteLineAsync($"<img class=\"msg-avatar\" src=\"{group.Author.AvatarUrl}\" />");
|
||||||
|
await output.WriteLineAsync("</div>");
|
||||||
|
|
||||||
|
await output.WriteLineAsync("<div class=\"msg-right\">");
|
||||||
|
await output.WriteAsync(
|
||||||
|
$"<span class=\"msg-user\" title=\"{HtmlEncode(group.Author.FullName)}\">");
|
||||||
|
await output.WriteAsync(HtmlEncode(group.Author.Name));
|
||||||
|
await output.WriteLineAsync("</span>");
|
||||||
|
var timeStampFormatted = HtmlEncode(group.TimeStamp.ToString(_settingsService.DateFormat));
|
||||||
|
await output.WriteLineAsync($"<span class=\"msg-date\">{timeStampFormatted}</span>");
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
foreach (var message in group.Messages)
|
||||||
|
{
|
||||||
|
// Content
|
||||||
|
if (message.Content.IsNotBlank())
|
||||||
|
{
|
||||||
|
await output.WriteLineAsync("<div class=\"msg-content\">");
|
||||||
|
var contentFormatted = FormatMessageContentHtml(message);
|
||||||
|
await output.WriteAsync(contentFormatted);
|
||||||
|
|
||||||
|
// Edited timestamp
|
||||||
|
if (message.EditedTimeStamp != null)
|
||||||
|
{
|
||||||
|
var editedTimeStampFormatted =
|
||||||
|
HtmlEncode(message.EditedTimeStamp.Value.ToString(_settingsService.DateFormat));
|
||||||
|
await output.WriteAsync(
|
||||||
|
$"<span class=\"msg-edited\" title=\"{editedTimeStampFormatted}\">(edited)</span>");
|
||||||
|
}
|
||||||
|
|
||||||
|
await output.WriteLineAsync("</div>"); // msg-content
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachments
|
||||||
|
foreach (var attachment in message.Attachments)
|
||||||
|
{
|
||||||
|
if (attachment.Type == AttachmentType.Image)
|
||||||
|
{
|
||||||
|
await output.WriteLineAsync("<div class=\"msg-attachment\">");
|
||||||
|
await output.WriteLineAsync($"<a href=\"{attachment.Url}\">");
|
||||||
|
await output.WriteLineAsync($"<img class=\"msg-attachment\" src=\"{attachment.Url}\" />");
|
||||||
|
await output.WriteLineAsync("</a>");
|
||||||
|
await output.WriteLineAsync("</div>");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await output.WriteLineAsync("<div class=\"msg-attachment\">");
|
||||||
|
await output.WriteLineAsync($"<a href=\"{attachment.Url}\">");
|
||||||
|
var fileSizeFormatted = FormatFileSize(attachment.FileSize);
|
||||||
|
await output.WriteLineAsync($"Attachment: {attachment.FileName} ({fileSizeFormatted})");
|
||||||
|
await output.WriteLineAsync("</a>");
|
||||||
|
await output.WriteLineAsync("</div>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await output.WriteLineAsync("</div>"); // msg-right
|
||||||
|
await output.WriteLineAsync("</div>"); // msg
|
||||||
|
}
|
||||||
|
|
||||||
|
await output.WriteLineAsync("</div>"); // log
|
||||||
|
|
||||||
|
await output.WriteLineAsync("</body>");
|
||||||
|
await output.WriteLineAsync("</html>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Services
|
||||||
|
{
|
||||||
|
public partial class ExportService
|
||||||
|
{
|
||||||
|
private string FormatMessageContentPlainText(Message message)
|
||||||
|
{
|
||||||
|
var content = message.Content;
|
||||||
|
|
||||||
|
// New lines
|
||||||
|
content = content.Replace("\n", Environment.NewLine);
|
||||||
|
|
||||||
|
// User mentions (<@id> and <@!id>)
|
||||||
|
foreach (var mentionedUser in message.MentionedUsers)
|
||||||
|
content = Regex.Replace(content, $"<@!?{mentionedUser.Id}>", $"@{mentionedUser}");
|
||||||
|
|
||||||
|
// Role mentions (<@&id>)
|
||||||
|
foreach (var mentionedRole in message.MentionedRoles)
|
||||||
|
content = content.Replace($"<@&{mentionedRole.Id}>", $"@{mentionedRole.Name}");
|
||||||
|
|
||||||
|
// Channel mentions (<#id>)
|
||||||
|
foreach (var mentionedChannel in message.MentionedChannels)
|
||||||
|
content = content.Replace($"<#{mentionedChannel.Id}>", $"#{mentionedChannel.Name}");
|
||||||
|
|
||||||
|
// Custom emojis (<:name:id>)
|
||||||
|
content = Regex.Replace(content, "<(:.*?:)\\d*>", "$1");
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExportAsPlainTextAsync(ChannelChatLog log, TextWriter output)
|
||||||
|
{
|
||||||
|
// Generation info
|
||||||
|
await output.WriteLineAsync("https://github.com/Tyrrrz/DiscordChatExporter");
|
||||||
|
await output.WriteLineAsync();
|
||||||
|
|
||||||
|
// Guild and channel info
|
||||||
|
await output.WriteLineAsync('='.Repeat(48));
|
||||||
|
await output.WriteLineAsync($"Guild: {log.Guild.Name}");
|
||||||
|
await output.WriteLineAsync($"Channel: {log.Channel.Name}");
|
||||||
|
await output.WriteLineAsync($"Topic: {log.Channel.Topic}");
|
||||||
|
await output.WriteLineAsync($"Messages: {log.TotalMessageCount:N0}");
|
||||||
|
await output.WriteLineAsync('='.Repeat(48));
|
||||||
|
await output.WriteLineAsync();
|
||||||
|
|
||||||
|
// Chat log
|
||||||
|
foreach (var group in log.MessageGroups)
|
||||||
|
{
|
||||||
|
var timeStampFormatted = group.TimeStamp.ToString(_settingsService.DateFormat);
|
||||||
|
await output.WriteLineAsync($"{group.Author.FullName} [{timeStampFormatted}]");
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
foreach (var message in group.Messages)
|
||||||
|
{
|
||||||
|
// Content
|
||||||
|
if (message.Content.IsNotBlank())
|
||||||
|
{
|
||||||
|
var contentFormatted = FormatMessageContentPlainText(message);
|
||||||
|
await output.WriteLineAsync(contentFormatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachments
|
||||||
|
foreach (var attachment in message.Attachments)
|
||||||
|
{
|
||||||
|
await output.WriteLineAsync(attachment.Url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await output.WriteLineAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Core.Internal;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Services
|
||||||
|
{
|
||||||
|
public partial class ExportService : IExportService
|
||||||
|
{
|
||||||
|
private readonly ISettingsService _settingsService;
|
||||||
|
|
||||||
|
public ExportService(ISettingsService settingsService)
|
||||||
|
{
|
||||||
|
_settingsService = settingsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExportAsync(ExportFormat format, string filePath, ChannelChatLog log)
|
||||||
|
{
|
||||||
|
using (var output = File.CreateText(filePath))
|
||||||
|
{
|
||||||
|
if (format == ExportFormat.PlainText)
|
||||||
|
{
|
||||||
|
await ExportAsPlainTextAsync(log, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (format == ExportFormat.HtmlDark)
|
||||||
|
{
|
||||||
|
var css = Assembly.GetExecutingAssembly()
|
||||||
|
.GetManifestResourceString("DiscordChatExporter.Core.Resources.ExportService.DarkTheme.css");
|
||||||
|
await ExportAsHtmlAsync(log, output, css);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (format == ExportFormat.HtmlLight)
|
||||||
|
{
|
||||||
|
var css = Assembly.GetExecutingAssembly()
|
||||||
|
.GetManifestResourceString("DiscordChatExporter.Core.Resources.ExportService.LightTheme.css");
|
||||||
|
await ExportAsHtmlAsync(log, output, css);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (format == ExportFormat.Csv)
|
||||||
|
{
|
||||||
|
await ExportAsCsvAsync(log, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
else throw new ArgumentOutOfRangeException(nameof(format));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class ExportService
|
||||||
|
{
|
||||||
|
private static string Base64Encode(string str)
|
||||||
|
{
|
||||||
|
return str.GetBytes().ToBase64();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Base64Decode(string str)
|
||||||
|
{
|
||||||
|
return str.FromBase64().GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string HtmlEncode(string str)
|
||||||
|
{
|
||||||
|
return WebUtility.HtmlEncode(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatFileSize(long fileSize)
|
||||||
|
{
|
||||||
|
string[] units = {"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
|
||||||
|
double size = fileSize;
|
||||||
|
var unit = 0;
|
||||||
|
|
||||||
|
while (size >= 1024)
|
||||||
|
{
|
||||||
|
size /= 1024;
|
||||||
|
++unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{size:0.#} {units[unit]}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-4
@@ -1,18 +1,22 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Services
|
namespace DiscordChatExporter.Core.Services
|
||||||
{
|
{
|
||||||
public interface IDataService
|
public interface IDataService
|
||||||
{
|
{
|
||||||
Task<IReadOnlyList<Guild>> GetGuildsAsync(string token);
|
Task<Guild> GetGuildAsync(string token, string guildId);
|
||||||
|
|
||||||
Task<IReadOnlyList<Channel>> GetDirectMessageChannelsAsync(string token);
|
Task<Channel> GetChannelAsync(string token, string channelId);
|
||||||
|
|
||||||
Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(string token, string guildId);
|
Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(string token, string guildId);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<Guild>> GetUserGuildsAsync(string token);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<Channel>> GetDirectMessageChannelsAsync(string token);
|
||||||
|
|
||||||
Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId,
|
Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId,
|
||||||
DateTime? from, DateTime? to);
|
DateTime? from, DateTime? to);
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Services
|
namespace DiscordChatExporter.Core.Services
|
||||||
{
|
{
|
||||||
public interface IExportService
|
public interface IExportService
|
||||||
{
|
{
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using DiscordChatExporter.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Services
|
namespace DiscordChatExporter.Core.Services
|
||||||
{
|
{
|
||||||
public interface IMessageGroupService
|
public interface IMessageGroupService
|
||||||
{
|
{
|
||||||
+4
-2
@@ -1,9 +1,11 @@
|
|||||||
using DiscordChatExporter.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Services
|
namespace DiscordChatExporter.Core.Services
|
||||||
{
|
{
|
||||||
public interface ISettingsService
|
public interface ISettingsService
|
||||||
{
|
{
|
||||||
|
bool IsAutoUpdateEnabled { get; set; }
|
||||||
|
|
||||||
string DateFormat { get; set; }
|
string DateFormat { get; set; }
|
||||||
int MessageGroupLimit { get; set; }
|
int MessageGroupLimit { get; set; }
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Services
|
||||||
|
{
|
||||||
|
public interface IUpdateService
|
||||||
|
{
|
||||||
|
bool NeedRestart { get; set; }
|
||||||
|
|
||||||
|
Task<Version> CheckPrepareUpdateAsync();
|
||||||
|
|
||||||
|
void FinalizeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using DiscordChatExporter.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Services
|
namespace DiscordChatExporter.Core.Services
|
||||||
{
|
{
|
||||||
public class MessageGroupService : IMessageGroupService
|
public class MessageGroupService : IMessageGroupService
|
||||||
{
|
{
|
||||||
+4
-2
@@ -1,10 +1,12 @@
|
|||||||
using DiscordChatExporter.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
using Tyrrrz.Settings;
|
using Tyrrrz.Settings;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Services
|
namespace DiscordChatExporter.Core.Services
|
||||||
{
|
{
|
||||||
public class SettingsService : SettingsManager, ISettingsService
|
public class SettingsService : SettingsManager, ISettingsService
|
||||||
{
|
{
|
||||||
|
public bool IsAutoUpdateEnabled { get; set; } = true;
|
||||||
|
|
||||||
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt";
|
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt";
|
||||||
public int MessageGroupLimit { get; set; } = 20;
|
public int MessageGroupLimit { get; set; } = 20;
|
||||||
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Onova;
|
||||||
|
using Onova.Services;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Services
|
||||||
|
{
|
||||||
|
public class UpdateService : IUpdateService
|
||||||
|
{
|
||||||
|
private readonly ISettingsService _settingsService;
|
||||||
|
private readonly IUpdateManager _manager;
|
||||||
|
|
||||||
|
private Version _updateVersion;
|
||||||
|
private bool _updateFinalized;
|
||||||
|
|
||||||
|
public bool NeedRestart { get; set; }
|
||||||
|
|
||||||
|
public UpdateService(ISettingsService settingsService)
|
||||||
|
{
|
||||||
|
_settingsService = settingsService;
|
||||||
|
|
||||||
|
_manager = new UpdateManager(
|
||||||
|
new GithubPackageResolver("Tyrrrz", "DiscordChatExporter", "DiscordChatExporter.zip"),
|
||||||
|
new ZipPackageExtractor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Version> CheckPrepareUpdateAsync()
|
||||||
|
{
|
||||||
|
// If auto-update is disabled - don't check for updates
|
||||||
|
if (!_settingsService.IsAutoUpdateEnabled)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Cleanup leftover files
|
||||||
|
_manager.Cleanup();
|
||||||
|
|
||||||
|
// Check for updates
|
||||||
|
var check = await _manager.CheckForUpdatesAsync();
|
||||||
|
if (!check.CanUpdate)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Prepare the update
|
||||||
|
if (!_manager.IsUpdatePrepared(check.LastVersion))
|
||||||
|
await _manager.PrepareUpdateAsync(check.LastVersion);
|
||||||
|
|
||||||
|
return _updateVersion = check.LastVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FinalizeUpdate()
|
||||||
|
{
|
||||||
|
// Check if an update is pending
|
||||||
|
if (_updateVersion == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Check if the update has already been finalized
|
||||||
|
if (_updateFinalized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Launch the updater
|
||||||
|
_manager.LaunchUpdater(_updateVersion, NeedRestart);
|
||||||
|
_updateFinalized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<Application
|
||||||
|
x:Class="DiscordChatExporter.Gui.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
||||||
|
xmlns:local="clr-namespace:DiscordChatExporter.Gui"
|
||||||
|
Exit="App_Exit"
|
||||||
|
Startup="App_Startup"
|
||||||
|
StartupUri="Views/MainWindow.xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml" />
|
||||||
|
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
|
||||||
|
<ResourceDictionary Source="pack://application:,,,/Tyrrrz.WpfExtensions;component/ConvertersDictionary.xaml" />
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
|
<!-- Colors -->
|
||||||
|
<Color x:Key="PrimaryColor">#343838</Color>
|
||||||
|
<Color x:Key="PrimaryLightColor">#5E6262</Color>
|
||||||
|
<Color x:Key="PrimaryDarkColor">#0D1212</Color>
|
||||||
|
<Color x:Key="AccentColor">#F9A825</Color>
|
||||||
|
<Color x:Key="AccentDarkColor">#C17900</Color>
|
||||||
|
<Color x:Key="TextColor">#000000</Color>
|
||||||
|
<Color x:Key="InverseTextColor">#FFFFFF</Color>
|
||||||
|
|
||||||
|
<SolidColorBrush x:Key="PrimaryHueLightBrush" Color="{DynamicResource PrimaryLightColor}" />
|
||||||
|
<SolidColorBrush x:Key="PrimaryHueLightForegroundBrush" Color="{DynamicResource InverseTextColor}" />
|
||||||
|
<SolidColorBrush x:Key="PrimaryHueMidBrush" Color="{DynamicResource PrimaryColor}" />
|
||||||
|
<SolidColorBrush x:Key="PrimaryHueMidForegroundBrush" Color="{DynamicResource InverseTextColor}" />
|
||||||
|
<SolidColorBrush x:Key="PrimaryHueDarkBrush" Color="{DynamicResource PrimaryDarkColor}" />
|
||||||
|
<SolidColorBrush x:Key="PrimaryHueDarkForegroundBrush" Color="{DynamicResource InverseTextColor}" />
|
||||||
|
<SolidColorBrush x:Key="SecondaryAccentBrush" Color="{DynamicResource AccentColor}" />
|
||||||
|
<SolidColorBrush x:Key="SecondaryAccentForegroundBrush" Color="{DynamicResource TextColor}" />
|
||||||
|
|
||||||
|
<SolidColorBrush
|
||||||
|
x:Key="PrimaryTextBrush"
|
||||||
|
Opacity="0.87"
|
||||||
|
Color="{DynamicResource TextColor}" />
|
||||||
|
<SolidColorBrush
|
||||||
|
x:Key="SecondaryTextBrush"
|
||||||
|
Opacity="0.64"
|
||||||
|
Color="{DynamicResource TextColor}" />
|
||||||
|
<SolidColorBrush
|
||||||
|
x:Key="DimTextBrush"
|
||||||
|
Opacity="0.45"
|
||||||
|
Color="{DynamicResource TextColor}" />
|
||||||
|
<SolidColorBrush
|
||||||
|
x:Key="PrimaryInverseTextBrush"
|
||||||
|
Opacity="1"
|
||||||
|
Color="{DynamicResource InverseTextColor}" />
|
||||||
|
<SolidColorBrush
|
||||||
|
x:Key="SecondaryInverseTextBrush"
|
||||||
|
Opacity="0.7"
|
||||||
|
Color="{DynamicResource InverseTextColor}" />
|
||||||
|
<SolidColorBrush
|
||||||
|
x:Key="DimInverseTextBrush"
|
||||||
|
Opacity="0.52"
|
||||||
|
Color="{DynamicResource InverseTextColor}" />
|
||||||
|
<SolidColorBrush
|
||||||
|
x:Key="AccentTextBrush"
|
||||||
|
Opacity="1"
|
||||||
|
Color="{DynamicResource AccentColor}" />
|
||||||
|
<SolidColorBrush
|
||||||
|
x:Key="AccentDarkTextBrush"
|
||||||
|
Opacity="1"
|
||||||
|
Color="{DynamicResource AccentDarkColor}" />
|
||||||
|
<SolidColorBrush
|
||||||
|
x:Key="DividerBrush"
|
||||||
|
Opacity="0.12"
|
||||||
|
Color="{DynamicResource TextColor}" />
|
||||||
|
<SolidColorBrush
|
||||||
|
x:Key="InverseDividerBrush"
|
||||||
|
Opacity="0.12"
|
||||||
|
Color="{DynamicResource InverseTextColor}" />
|
||||||
|
|
||||||
|
<!-- Styles -->
|
||||||
|
<Style TargetType="{x:Type Image}">
|
||||||
|
<Setter Property="RenderOptions.BitmapScalingMode" Value="HighQuality" />
|
||||||
|
<Setter Property="RenderOptions.EdgeMode" Value="Aliased" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style BasedOn="{StaticResource MaterialDesignLinearProgressBar}" TargetType="{x:Type ProgressBar}">
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource SecondaryAccentBrush}" />
|
||||||
|
<Setter Property="Height" Value="2" />
|
||||||
|
<Setter Property="Maximum" Value="1" />
|
||||||
|
<Setter Property="Minimum" Value="0" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style BasedOn="{StaticResource MaterialDesignTextBox}" TargetType="{x:Type TextBox}">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style BasedOn="{StaticResource MaterialDesignComboBox}" TargetType="{x:Type ComboBox}">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Converters -->
|
||||||
|
<converters:ExportFormatToStringConverter x:Key="ExportFormatToStringConverter" />
|
||||||
|
|
||||||
|
<!-- Container -->
|
||||||
|
<local:Container x:Key="Container" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace DiscordChatExporter
|
namespace DiscordChatExporter.Gui
|
||||||
{
|
{
|
||||||
public partial class App
|
public partial class App
|
||||||
{
|
{
|
||||||
|
private Container Container => (Container) Resources["Container"];
|
||||||
|
|
||||||
private void App_Startup(object sender, StartupEventArgs e)
|
private void App_Startup(object sender, StartupEventArgs e)
|
||||||
{
|
{
|
||||||
Container.Init();
|
Container.Init();
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using CommonServiceLocator;
|
||||||
|
using DiscordChatExporter.Core.Services;
|
||||||
|
using DiscordChatExporter.Gui.ViewModels;
|
||||||
|
using GalaSoft.MvvmLight.Ioc;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Gui
|
||||||
|
{
|
||||||
|
public class Container
|
||||||
|
{
|
||||||
|
public IExportSetupViewModel ExportSetupViewModel => Resolve<IExportSetupViewModel>();
|
||||||
|
public IMainViewModel MainViewModel => Resolve<IMainViewModel>();
|
||||||
|
public ISettingsViewModel SettingsViewModel => Resolve<ISettingsViewModel>();
|
||||||
|
|
||||||
|
private T Resolve<T>(string key = null)
|
||||||
|
{
|
||||||
|
return ServiceLocator.Current.GetInstance<T>(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Init()
|
||||||
|
{
|
||||||
|
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
|
||||||
|
SimpleIoc.Default.Reset();
|
||||||
|
|
||||||
|
// Services
|
||||||
|
SimpleIoc.Default.Register<IDataService, DataService>();
|
||||||
|
SimpleIoc.Default.Register<IExportService, ExportService>();
|
||||||
|
SimpleIoc.Default.Register<IMessageGroupService, MessageGroupService>();
|
||||||
|
SimpleIoc.Default.Register<ISettingsService, SettingsService>();
|
||||||
|
SimpleIoc.Default.Register<IUpdateService, UpdateService>();
|
||||||
|
|
||||||
|
// View models
|
||||||
|
SimpleIoc.Default.Register<IExportSetupViewModel, ExportSetupViewModel>(true);
|
||||||
|
SimpleIoc.Default.Register<IMainViewModel, MainViewModel>(true);
|
||||||
|
SimpleIoc.Default.Register<ISettingsViewModel, SettingsViewModel>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Gui.Converters
|
||||||
|
{
|
||||||
|
[ValueConversion(typeof(ExportFormat), typeof(string))]
|
||||||
|
public class ExportFormatToStringConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
var format = (ExportFormat) value;
|
||||||
|
return format.GetDisplayName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{732A67AF-93DE-49DF-B10F-FD74710B7863}</ProjectGuid>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<RootNamespace>DiscordChatExporter.Gui</RootNamespace>
|
||||||
|
<AssemblyName>DiscordChatExporter</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
|
<NuGetPackageImportStamp>
|
||||||
|
</NuGetPackageImportStamp>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<ApplicationIcon>..\favicon.ico</ApplicationIcon>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<StartupObject />
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="CommonServiceLocator, Version=2.0.3.0, Culture=neutral, PublicKeyToken=489b6accfaf20ef0, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\CommonServiceLocator.2.0.3\lib\net45\CommonServiceLocator.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="GalaSoft.MvvmLight, Version=5.4.1.0, Culture=neutral, PublicKeyToken=e7570ab207bcb616, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\MvvmLightLibs.5.4.1\lib\net45\GalaSoft.MvvmLight.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="GalaSoft.MvvmLight.Extras, Version=5.4.1.0, Culture=neutral, PublicKeyToken=669f0b5e8f868abf, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\MvvmLightLibs.5.4.1\lib\net45\GalaSoft.MvvmLight.Extras.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="GalaSoft.MvvmLight.Platform, Version=5.4.1.0, Culture=neutral, PublicKeyToken=5f873c45e98af8a1, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\MvvmLightLibs.5.4.1\lib\net45\GalaSoft.MvvmLight.Platform.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="MaterialDesignColors, Version=1.1.3.0, Culture=neutral, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\MaterialDesignColors.1.1.3\lib\net45\MaterialDesignColors.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="MaterialDesignThemes.Wpf, Version=2.4.0.1044, Culture=neutral, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\MaterialDesignThemes.2.4.0.1044\lib\net45\MaterialDesignThemes.Wpf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Windows.Interactivity, Version=4.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\MvvmLightLibs.5.4.1\lib\net45\System.Windows.Interactivity.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Xaml">
|
||||||
|
<RequiredTargetFramework>4.0</RequiredTargetFramework>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Tyrrrz.Extensions, Version=1.5.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\Tyrrrz.Extensions.1.5.0\lib\net45\Tyrrrz.Extensions.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Tyrrrz.Settings, Version=1.3.2.0, Culture=neutral, PublicKeyToken=null" />
|
||||||
|
<Reference Include="Tyrrrz.WpfExtensions, Version=1.0.6269.37170, Culture=neutral, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\Tyrrrz.WpfExtensions.1.0.5\lib\net45\Tyrrrz.WpfExtensions.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="WindowsBase" />
|
||||||
|
<Reference Include="PresentationCore" />
|
||||||
|
<Reference Include="PresentationFramework" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="App.xaml.cs">
|
||||||
|
<DependentUpon>App.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Converters\ExportFormatToStringConverter.cs" />
|
||||||
|
<Compile Include="Messages\ShowExportSetupMessage.cs" />
|
||||||
|
<Compile Include="Messages\ShowNotificationMessage.cs" />
|
||||||
|
<Compile Include="Messages\ShowSettingsMessage.cs" />
|
||||||
|
<Compile Include="Messages\StartExportMessage.cs" />
|
||||||
|
<Compile Include="ViewModels\ExportSetupViewModel.cs" />
|
||||||
|
<Compile Include="ViewModels\IExportSetupViewModel.cs" />
|
||||||
|
<Compile Include="ViewModels\ISettingsViewModel.cs" />
|
||||||
|
<Compile Include="ViewModels\SettingsViewModel.cs" />
|
||||||
|
<Compile Include="Container.cs" />
|
||||||
|
<Compile Include="ViewModels\IMainViewModel.cs" />
|
||||||
|
<Compile Include="ViewModels\MainViewModel.cs" />
|
||||||
|
<Compile Include="Views\ExportSetupDialog.xaml.cs">
|
||||||
|
<DependentUpon>ExportSetupDialog.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Views\MainWindow.xaml.cs">
|
||||||
|
<DependentUpon>MainWindow.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Views\SettingsDialog.xaml.cs">
|
||||||
|
<DependentUpon>SettingsDialog.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs">
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Properties\Resources.Designer.cs">
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DesignTime>True</DesignTime>
|
||||||
|
<DependentUpon>Resources.resx</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<EmbeddedResource Include="Properties\Resources.resx">
|
||||||
|
<Generator>ResXFileCodeGenerator</Generator>
|
||||||
|
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<None Include="app.config" />
|
||||||
|
<None Include="packages.config">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Resource Include="..\favicon.ico" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DiscordChatExporter.Core\DiscordChatExporter.Core.csproj">
|
||||||
|
<Project>{707c0cd0-a7e0-4cab-8db9-07a45cb87377}</Project>
|
||||||
|
<Name>DiscordChatExporter.Core</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ApplicationDefinition Include="App.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</ApplicationDefinition>
|
||||||
|
<Page Include="Views\ExportSetupDialog.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
<Page Include="Views\MainWindow.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
<Page Include="Views\SettingsDialog.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup />
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
</Project>
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
using DiscordChatExporter.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Messages
|
namespace DiscordChatExporter.Gui.Messages
|
||||||
{
|
{
|
||||||
public class ShowExportSetupMessage
|
public class ShowExportSetupMessage
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Gui.Messages
|
||||||
|
{
|
||||||
|
public class ShowNotificationMessage
|
||||||
|
{
|
||||||
|
public string Message { get; }
|
||||||
|
|
||||||
|
public string CallbackCaption { get; }
|
||||||
|
|
||||||
|
public Action Callback { get; }
|
||||||
|
|
||||||
|
public ShowNotificationMessage(string message)
|
||||||
|
{
|
||||||
|
Message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShowNotificationMessage(string message, string callbackCaption, Action callback)
|
||||||
|
: this(message)
|
||||||
|
{
|
||||||
|
CallbackCaption = callbackCaption;
|
||||||
|
Callback = callback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Messages
|
namespace DiscordChatExporter.Gui.Messages
|
||||||
{
|
{
|
||||||
public class ShowSettingsMessage
|
public class ShowSettingsMessage
|
||||||
{
|
{
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using DiscordChatExporter.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Messages
|
namespace DiscordChatExporter.Gui.Messages
|
||||||
{
|
{
|
||||||
public class StartExportMessage
|
public class StartExportMessage
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
[assembly: AssemblyTitle("DiscordChatExporter")]
|
||||||
|
[assembly: AssemblyCompany("Tyrrrz")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright (c) 2017-2018 Alexey Golub")]
|
||||||
|
[assembly: AssemblyVersion("2.4.1")]
|
||||||
|
[assembly: AssemblyFileVersion("2.4.1")]
|
||||||
+12
-20
@@ -8,8 +8,8 @@
|
|||||||
// </auto-generated>
|
// </auto-generated>
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
namespace DiscordChatExporter.Properties
|
namespace DiscordChatExporter.Gui.Properties {
|
||||||
{
|
using System;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -19,32 +19,27 @@ namespace DiscordChatExporter.Properties
|
|||||||
// class via a tool like ResGen or Visual Studio.
|
// class via a tool like ResGen or Visual Studio.
|
||||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||||
// with the /str option, or rebuild your VS project.
|
// with the /str option, or rebuild your VS project.
|
||||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
internal class Resources
|
internal class Resources {
|
||||||
{
|
|
||||||
|
|
||||||
private static global::System.Resources.ResourceManager resourceMan;
|
private static global::System.Resources.ResourceManager resourceMan;
|
||||||
|
|
||||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||||
|
|
||||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||||
internal Resources()
|
internal Resources() {
|
||||||
{
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the cached ResourceManager instance used by this class.
|
/// Returns the cached ResourceManager instance used by this class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
internal static global::System.Resources.ResourceManager ResourceManager
|
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||||
{
|
get {
|
||||||
get
|
if (object.ReferenceEquals(resourceMan, null)) {
|
||||||
{
|
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DiscordChatExporter.Gui.Properties.Resources", typeof(Resources).Assembly);
|
||||||
if ((resourceMan == null))
|
|
||||||
{
|
|
||||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DiscordChatExporter.Properties.Resources", typeof(Resources).Assembly);
|
|
||||||
resourceMan = temp;
|
resourceMan = temp;
|
||||||
}
|
}
|
||||||
return resourceMan;
|
return resourceMan;
|
||||||
@@ -56,14 +51,11 @@ namespace DiscordChatExporter.Properties
|
|||||||
/// resource lookups using this strongly typed resource class.
|
/// resource lookups using this strongly typed resource class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
internal static global::System.Globalization.CultureInfo Culture
|
internal static global::System.Globalization.CultureInfo Culture {
|
||||||
{
|
get {
|
||||||
get
|
|
||||||
{
|
|
||||||
return resourceCulture;
|
return resourceCulture;
|
||||||
}
|
}
|
||||||
set
|
set {
|
||||||
{
|
|
||||||
resourceCulture = value;
|
resourceCulture = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-5
@@ -2,14 +2,14 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using DiscordChatExporter.Messages;
|
using DiscordChatExporter.Core.Models;
|
||||||
using DiscordChatExporter.Models;
|
using DiscordChatExporter.Core.Services;
|
||||||
using DiscordChatExporter.Services;
|
using DiscordChatExporter.Gui.Messages;
|
||||||
using GalaSoft.MvvmLight;
|
using GalaSoft.MvvmLight;
|
||||||
using GalaSoft.MvvmLight.CommandWpf;
|
using GalaSoft.MvvmLight.CommandWpf;
|
||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.ViewModels
|
namespace DiscordChatExporter.Gui.ViewModels
|
||||||
{
|
{
|
||||||
public class ExportSetupViewModel : ViewModelBase, IExportSetupViewModel
|
public class ExportSetupViewModel : ViewModelBase, IExportSetupViewModel
|
||||||
{
|
{
|
||||||
@@ -81,7 +81,7 @@ namespace DiscordChatExporter.ViewModels
|
|||||||
Guild = m.Guild;
|
Guild = m.Guild;
|
||||||
Channel = m.Channel;
|
Channel = m.Channel;
|
||||||
SelectedFormat = _settingsService.LastExportFormat;
|
SelectedFormat = _settingsService.LastExportFormat;
|
||||||
FilePath = $"{Guild} - {Channel}.{SelectedFormat.GetFileExtension()}"
|
FilePath = $"{Guild.Name} - {Channel.Name}.{SelectedFormat.GetFileExtension()}"
|
||||||
.Replace(Path.GetInvalidFileNameChars(), '_');
|
.Replace(Path.GetInvalidFileNameChars(), '_');
|
||||||
From = null;
|
From = null;
|
||||||
To = null;
|
To = null;
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using DiscordChatExporter.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
using GalaSoft.MvvmLight.CommandWpf;
|
using GalaSoft.MvvmLight.CommandWpf;
|
||||||
|
|
||||||
namespace DiscordChatExporter.ViewModels
|
namespace DiscordChatExporter.Gui.ViewModels
|
||||||
{
|
{
|
||||||
public interface IExportSetupViewModel
|
public interface IExportSetupViewModel
|
||||||
{
|
{
|
||||||
+4
-2
@@ -1,8 +1,8 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using DiscordChatExporter.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
using GalaSoft.MvvmLight.CommandWpf;
|
using GalaSoft.MvvmLight.CommandWpf;
|
||||||
|
|
||||||
namespace DiscordChatExporter.ViewModels
|
namespace DiscordChatExporter.Gui.ViewModels
|
||||||
{
|
{
|
||||||
public interface IMainViewModel
|
public interface IMainViewModel
|
||||||
{
|
{
|
||||||
@@ -15,6 +15,8 @@ namespace DiscordChatExporter.ViewModels
|
|||||||
Guild SelectedGuild { get; set; }
|
Guild SelectedGuild { get; set; }
|
||||||
IReadOnlyList<Channel> AvailableChannels { get; }
|
IReadOnlyList<Channel> AvailableChannels { get; }
|
||||||
|
|
||||||
|
RelayCommand ViewLoadedCommand { get; }
|
||||||
|
RelayCommand ViewClosedCommand { get; }
|
||||||
RelayCommand PullDataCommand { get; }
|
RelayCommand PullDataCommand { get; }
|
||||||
RelayCommand ShowSettingsCommand { get; }
|
RelayCommand ShowSettingsCommand { get; }
|
||||||
RelayCommand ShowAboutCommand { get; }
|
RelayCommand ShowAboutCommand { get; }
|
||||||
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
namespace DiscordChatExporter.ViewModels
|
namespace DiscordChatExporter.Gui.ViewModels
|
||||||
{
|
{
|
||||||
public interface ISettingsViewModel
|
public interface ISettingsViewModel
|
||||||
{
|
{
|
||||||
|
bool IsAutoUpdateEnabled { get; set; }
|
||||||
|
|
||||||
string DateFormat { get; set; }
|
string DateFormat { get; set; }
|
||||||
int MessageGroupLimit { get; set; }
|
int MessageGroupLimit { get; set; }
|
||||||
}
|
}
|
||||||
+65
-23
@@ -3,19 +3,21 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using DiscordChatExporter.Exceptions;
|
using System.Windows;
|
||||||
using DiscordChatExporter.Messages;
|
using DiscordChatExporter.Core.Exceptions;
|
||||||
using DiscordChatExporter.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
using DiscordChatExporter.Services;
|
using DiscordChatExporter.Core.Services;
|
||||||
|
using DiscordChatExporter.Gui.Messages;
|
||||||
using GalaSoft.MvvmLight;
|
using GalaSoft.MvvmLight;
|
||||||
using GalaSoft.MvvmLight.CommandWpf;
|
using GalaSoft.MvvmLight.CommandWpf;
|
||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.ViewModels
|
namespace DiscordChatExporter.Gui.ViewModels
|
||||||
{
|
{
|
||||||
public class MainViewModel : ViewModelBase, IMainViewModel
|
public class MainViewModel : ViewModelBase, IMainViewModel
|
||||||
{
|
{
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
|
private readonly IUpdateService _updateService;
|
||||||
private readonly IDataService _dataService;
|
private readonly IDataService _dataService;
|
||||||
private readonly IMessageGroupService _messageGroupService;
|
private readonly IMessageGroupService _messageGroupService;
|
||||||
private readonly IExportService _exportService;
|
private readonly IExportService _exportService;
|
||||||
@@ -81,15 +83,18 @@ namespace DiscordChatExporter.ViewModels
|
|||||||
private set => Set(ref _availableChannels, value);
|
private set => Set(ref _availableChannels, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RelayCommand ViewLoadedCommand { get; }
|
||||||
|
public RelayCommand ViewClosedCommand { get; }
|
||||||
public RelayCommand PullDataCommand { get; }
|
public RelayCommand PullDataCommand { get; }
|
||||||
public RelayCommand ShowSettingsCommand { get; }
|
public RelayCommand ShowSettingsCommand { get; }
|
||||||
public RelayCommand ShowAboutCommand { get; }
|
public RelayCommand ShowAboutCommand { get; }
|
||||||
public RelayCommand<Channel> ShowExportSetupCommand { get; }
|
public RelayCommand<Channel> ShowExportSetupCommand { get; }
|
||||||
|
|
||||||
public MainViewModel(ISettingsService settingsService, IDataService dataService,
|
public MainViewModel(ISettingsService settingsService, IUpdateService updateService, IDataService dataService,
|
||||||
IMessageGroupService messageGroupService, IExportService exportService)
|
IMessageGroupService messageGroupService, IExportService exportService)
|
||||||
{
|
{
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
|
_updateService = updateService;
|
||||||
_dataService = dataService;
|
_dataService = dataService;
|
||||||
_messageGroupService = messageGroupService;
|
_messageGroupService = messageGroupService;
|
||||||
_exportService = exportService;
|
_exportService = exportService;
|
||||||
@@ -97,19 +102,54 @@ namespace DiscordChatExporter.ViewModels
|
|||||||
_guildChannelsMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
|
_guildChannelsMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
|
||||||
|
|
||||||
// Commands
|
// Commands
|
||||||
|
ViewLoadedCommand = new RelayCommand(ViewLoaded);
|
||||||
|
ViewClosedCommand = new RelayCommand(ViewClosed);
|
||||||
PullDataCommand = new RelayCommand(PullData, () => Token.IsNotBlank() && !IsBusy);
|
PullDataCommand = new RelayCommand(PullData, () => Token.IsNotBlank() && !IsBusy);
|
||||||
ShowSettingsCommand = new RelayCommand(ShowSettings);
|
ShowSettingsCommand = new RelayCommand(ShowSettings);
|
||||||
ShowAboutCommand = new RelayCommand(ShowAbout);
|
ShowAboutCommand = new RelayCommand(ShowAbout);
|
||||||
ShowExportSetupCommand = new RelayCommand<Channel>(ShowExportSetup, _ => !IsBusy);
|
ShowExportSetupCommand = new RelayCommand<Channel>(ShowExportSetup, _ => !IsBusy);
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
MessengerInstance.Register<StartExportMessage>(this, m =>
|
MessengerInstance.Register<StartExportMessage>(this,
|
||||||
{
|
m => Export(m.Channel, m.FilePath, m.Format, m.From, m.To));
|
||||||
Export(m.Channel, m.FilePath, m.Format, m.From, m.To);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Defaults
|
private async void ViewLoaded()
|
||||||
_token = _settingsService.LastToken;
|
{
|
||||||
|
// Load settings
|
||||||
|
_settingsService.Load();
|
||||||
|
|
||||||
|
// Set last token
|
||||||
|
Token = _settingsService.LastToken;
|
||||||
|
|
||||||
|
// Check and prepare update
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var updateVersion = await _updateService.CheckPrepareUpdateAsync();
|
||||||
|
if (updateVersion != null)
|
||||||
|
{
|
||||||
|
MessengerInstance.Send(new ShowNotificationMessage(
|
||||||
|
$"Update to DiscordChatExporter v{updateVersion} will be installed when you exit",
|
||||||
|
"INSTALL NOW", () =>
|
||||||
|
{
|
||||||
|
_updateService.NeedRestart = true;
|
||||||
|
Application.Current.Shutdown();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
MessengerInstance.Send(new ShowNotificationMessage("Failed to perform application auto-update"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ViewClosed()
|
||||||
|
{
|
||||||
|
// Save settings
|
||||||
|
_settingsService.Save();
|
||||||
|
|
||||||
|
// Finalize updates if available
|
||||||
|
_updateService.FinalizeUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void PullData()
|
private async void PullData()
|
||||||
@@ -130,29 +170,29 @@ namespace DiscordChatExporter.ViewModels
|
|||||||
// Get DM channels
|
// Get DM channels
|
||||||
{
|
{
|
||||||
var channels = await _dataService.GetDirectMessageChannelsAsync(token);
|
var channels = await _dataService.GetDirectMessageChannelsAsync(token);
|
||||||
var guild = new Guild("@me", "Direct Messages", null);
|
var guild = Guild.DirectMessages;
|
||||||
_guildChannelsMap[guild] = channels.ToArray();
|
_guildChannelsMap[guild] = channels.OrderBy(c => c.Name).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get guild channels
|
// Get guild channels
|
||||||
{
|
{
|
||||||
var guilds = await _dataService.GetGuildsAsync(token);
|
var guilds = await _dataService.GetUserGuildsAsync(token);
|
||||||
foreach (var guild in guilds)
|
foreach (var guild in guilds)
|
||||||
{
|
{
|
||||||
var channels = await _dataService.GetGuildChannelsAsync(token, guild.Id);
|
var channels = await _dataService.GetGuildChannelsAsync(token, guild.Id);
|
||||||
_guildChannelsMap[guild] = channels.Where(c => c.Type == ChannelType.GuildTextChat).ToArray();
|
_guildChannelsMap[guild] = channels.Where(c => c.Type == ChannelType.GuildTextChat)
|
||||||
|
.OrderBy(c => c.Name)
|
||||||
|
.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
|
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
{
|
{
|
||||||
const string message = "Unauthorized to perform request. Make sure token is valid.";
|
MessengerInstance.Send(new ShowNotificationMessage("Unauthorized – make sure the token is valid"));
|
||||||
MessengerInstance.Send(new ShowErrorMessage(message));
|
|
||||||
}
|
}
|
||||||
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
|
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
|
||||||
{
|
{
|
||||||
const string message = "Forbidden to perform request. The account may be locked by 2FA.";
|
MessengerInstance.Send(new ShowNotificationMessage("Forbidden – account may be locked by 2FA"));
|
||||||
MessengerInstance.Send(new ShowErrorMessage(message));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AvailableGuilds = _guildChannelsMap.Keys.ToArray();
|
AvailableGuilds = _guildChannelsMap.Keys.ToArray();
|
||||||
@@ -196,13 +236,15 @@ namespace DiscordChatExporter.ViewModels
|
|||||||
// Export
|
// Export
|
||||||
await _exportService.ExportAsync(format, filePath, log);
|
await _exportService.ExportAsync(format, filePath, log);
|
||||||
|
|
||||||
|
// Open
|
||||||
|
Process.Start(filePath);
|
||||||
|
|
||||||
// Notify completion
|
// Notify completion
|
||||||
MessengerInstance.Send(new ShowExportDoneMessage(filePath));
|
MessengerInstance.Send(new ShowNotificationMessage("Export complete"));
|
||||||
}
|
}
|
||||||
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
|
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
|
||||||
{
|
{
|
||||||
const string message = "Forbidden to view messages in that channel.";
|
MessengerInstance.Send(new ShowNotificationMessage("You don't have access to this channel"));
|
||||||
MessengerInstance.Send(new ShowErrorMessage(message));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IsBusy = false;
|
IsBusy = false;
|
||||||
+8
-2
@@ -1,13 +1,19 @@
|
|||||||
using DiscordChatExporter.Services;
|
using DiscordChatExporter.Core.Services;
|
||||||
using GalaSoft.MvvmLight;
|
using GalaSoft.MvvmLight;
|
||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.ViewModels
|
namespace DiscordChatExporter.Gui.ViewModels
|
||||||
{
|
{
|
||||||
public class SettingsViewModel : ViewModelBase, ISettingsViewModel
|
public class SettingsViewModel : ViewModelBase, ISettingsViewModel
|
||||||
{
|
{
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
|
|
||||||
|
public bool IsAutoUpdateEnabled
|
||||||
|
{
|
||||||
|
get => _settingsService.IsAutoUpdateEnabled;
|
||||||
|
set => _settingsService.IsAutoUpdateEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
public string DateFormat
|
public string DateFormat
|
||||||
{
|
{
|
||||||
get => _settingsService.DateFormat;
|
get => _settingsService.DateFormat;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="DiscordChatExporter.Gui.Views.ExportSetupDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
Width="325"
|
||||||
|
DataContext="{Binding ExportSetupViewModel, Source={StaticResource Container}}">
|
||||||
|
<StackPanel>
|
||||||
|
<!-- File path -->
|
||||||
|
<TextBox
|
||||||
|
Margin="16,16,16,8"
|
||||||
|
materialDesign:HintAssist.Hint="Output file"
|
||||||
|
materialDesign:HintAssist.IsFloating="True"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Text="{Binding FilePath, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<!-- Format -->
|
||||||
|
<ComboBox
|
||||||
|
Margin="16,8,16,8"
|
||||||
|
materialDesign:HintAssist.Hint="Export format"
|
||||||
|
materialDesign:HintAssist.IsFloating="True"
|
||||||
|
IsReadOnly="True"
|
||||||
|
ItemsSource="{Binding AvailableFormats}"
|
||||||
|
SelectedItem="{Binding SelectedFormat}">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding Converter={StaticResource ExportFormatToStringConverter}}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<!-- Date limits -->
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<DatePicker
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="16,20,8,8"
|
||||||
|
materialDesign:HintAssist.Hint="From (optional)"
|
||||||
|
materialDesign:HintAssist.IsFloating="True"
|
||||||
|
SelectedDate="{Binding From}" />
|
||||||
|
<DatePicker
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="8,20,16,8"
|
||||||
|
materialDesign:HintAssist.Hint="To (optional)"
|
||||||
|
materialDesign:HintAssist.IsFloating="True"
|
||||||
|
SelectedDate="{Binding To}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||||
|
<Button
|
||||||
|
x:Name="BrowseButton"
|
||||||
|
Margin="8"
|
||||||
|
Click="BrowseButton_Click"
|
||||||
|
Content="BROWSE"
|
||||||
|
Style="{DynamicResource MaterialDesignFlatButton}" />
|
||||||
|
<Button
|
||||||
|
x:Name="ExportButton"
|
||||||
|
Margin="8"
|
||||||
|
Click="ExportButton_Click"
|
||||||
|
Command="{Binding ExportCommand}"
|
||||||
|
Content="EXPORT"
|
||||||
|
IsDefault="True"
|
||||||
|
Style="{DynamicResource MaterialDesignFlatButton}" />
|
||||||
|
<Button
|
||||||
|
Margin="8"
|
||||||
|
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||||
|
Content="CANCEL"
|
||||||
|
IsCancel="True"
|
||||||
|
Style="{DynamicResource MaterialDesignFlatButton}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
+4
-4
@@ -1,14 +1,14 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using DiscordChatExporter.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
using DiscordChatExporter.ViewModels;
|
using DiscordChatExporter.Gui.ViewModels;
|
||||||
using MaterialDesignThemes.Wpf;
|
using MaterialDesignThemes.Wpf;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Views
|
namespace DiscordChatExporter.Gui.Views
|
||||||
{
|
{
|
||||||
public partial class ExportSetupDialog
|
public partial class ExportSetupDialog
|
||||||
{
|
{
|
||||||
private IExportSetupViewModel ViewModel => (IExportSetupViewModel) DataContext;
|
private IExportSetupViewModel ViewModel => (IExportSetupViewModel)DataContext;
|
||||||
|
|
||||||
public ExportSetupDialog()
|
public ExportSetupDialog()
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
<Window
|
||||||
|
x:Class="DiscordChatExporter.Gui.Views.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
Title="DiscordChatExporter"
|
||||||
|
Width="600"
|
||||||
|
Height="550"
|
||||||
|
Background="{DynamicResource MaterialDesignPaper}"
|
||||||
|
DataContext="{Binding MainViewModel, Source={StaticResource Container}}"
|
||||||
|
FocusManager.FocusedElement="{Binding ElementName=TokenTextBox}"
|
||||||
|
FontFamily="{DynamicResource MaterialDesignFont}"
|
||||||
|
Icon="/DiscordChatExporter;component/favicon.ico"
|
||||||
|
SnapsToDevicePixels="True"
|
||||||
|
TextElement.FontSize="13"
|
||||||
|
TextElement.FontWeight="Regular"
|
||||||
|
TextElement.Foreground="{DynamicResource SecondaryTextBrush}"
|
||||||
|
TextOptions.TextFormattingMode="Ideal"
|
||||||
|
TextOptions.TextRenderingMode="Auto"
|
||||||
|
UseLayoutRounding="True"
|
||||||
|
WindowStartupLocation="CenterScreen">
|
||||||
|
<i:Interaction.Triggers>
|
||||||
|
<i:EventTrigger EventName="Loaded">
|
||||||
|
<i:InvokeCommandAction Command="{Binding ViewLoadedCommand}" />
|
||||||
|
</i:EventTrigger>
|
||||||
|
<i:EventTrigger EventName="Closed">
|
||||||
|
<i:InvokeCommandAction Command="{Binding ViewClosedCommand}" />
|
||||||
|
</i:EventTrigger>
|
||||||
|
</i:Interaction.Triggers>
|
||||||
|
<materialDesign:DialogHost SnackbarMessageQueue="{Binding ElementName=Snackbar}">
|
||||||
|
<DockPanel>
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<Border
|
||||||
|
Background="{DynamicResource PrimaryHueMidBrush}"
|
||||||
|
DockPanel.Dock="Top"
|
||||||
|
IsEnabled="{Binding IsBusy, Converter={StaticResource InvertBoolConverter}}"
|
||||||
|
TextElement.Foreground="{DynamicResource SecondaryInverseTextBrush}">
|
||||||
|
<StackPanel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<materialDesign:Card
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="6,6,0,6">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Token -->
|
||||||
|
<TextBox
|
||||||
|
x:Name="TokenTextBox"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="6"
|
||||||
|
materialDesign:HintAssist.Hint="Token"
|
||||||
|
materialDesign:TextFieldAssist.DecorationVisibility="Hidden"
|
||||||
|
materialDesign:TextFieldAssist.TextBoxViewMargin="0,0,2,0"
|
||||||
|
BorderThickness="0"
|
||||||
|
FontSize="16"
|
||||||
|
Text="{Binding Token, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<!-- Pull data button -->
|
||||||
|
<Button
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="0,6,6,6"
|
||||||
|
Padding="4"
|
||||||
|
Command="{Binding PullDataCommand}"
|
||||||
|
IsDefault="True"
|
||||||
|
Style="{DynamicResource MaterialDesignFlatButton}">
|
||||||
|
<materialDesign:PackIcon
|
||||||
|
Width="24"
|
||||||
|
Height="24"
|
||||||
|
Kind="ArrowRight" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</materialDesign:Card>
|
||||||
|
|
||||||
|
<!-- Menu -->
|
||||||
|
<materialDesign:PopupBox
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Foreground="{DynamicResource PrimaryHueMidForegroundBrush}"
|
||||||
|
PlacementMode="LeftAndAlignTopEdges">
|
||||||
|
<StackPanel>
|
||||||
|
<Button Command="{Binding ShowSettingsCommand}" Content="Settings" />
|
||||||
|
<Button Command="{Binding ShowAboutCommand}" Content="About" />
|
||||||
|
</StackPanel>
|
||||||
|
</materialDesign:PopupBox>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Progress bar -->
|
||||||
|
<ProgressBar
|
||||||
|
Background="Transparent"
|
||||||
|
IsIndeterminate="True"
|
||||||
|
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<Grid>
|
||||||
|
<DockPanel
|
||||||
|
Background="{DynamicResource MaterialDesignCardBackground}"
|
||||||
|
IsEnabled="{Binding IsBusy, Converter={StaticResource InvertBoolConverter}}"
|
||||||
|
Visibility="{Binding IsDataAvailable, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
|
|
||||||
|
<!-- Guilds -->
|
||||||
|
<Border
|
||||||
|
BorderBrush="{DynamicResource DividerBrush}"
|
||||||
|
BorderThickness="0,0,1,0"
|
||||||
|
DockPanel.Dock="Left">
|
||||||
|
<ListBox
|
||||||
|
ItemsSource="{Binding AvailableGuilds}"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Hidden"
|
||||||
|
SelectedItem="{Binding SelectedGuild}"
|
||||||
|
VirtualizingStackPanel.IsVirtualizing="False">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<materialDesign:TransitioningContent>
|
||||||
|
<materialDesign:TransitioningContent.OpeningEffect>
|
||||||
|
<materialDesign:TransitionEffect Kind="SlideInFromLeft" Duration="0:0:0.3" />
|
||||||
|
</materialDesign:TransitioningContent.OpeningEffect>
|
||||||
|
<Border
|
||||||
|
Margin="-8"
|
||||||
|
Background="Transparent"
|
||||||
|
Cursor="Hand">
|
||||||
|
<Image
|
||||||
|
Width="48"
|
||||||
|
Height="48"
|
||||||
|
Margin="12,4,12,4"
|
||||||
|
Source="{Binding IconUrl}"
|
||||||
|
ToolTip="{Binding Name}">
|
||||||
|
<Image.OpacityMask>
|
||||||
|
<RadialGradientBrush>
|
||||||
|
<RadialGradientBrush.GradientStops>
|
||||||
|
<GradientStop Offset="0" Color="#FF000000" />
|
||||||
|
<GradientStop Offset="0.96" Color="#FF000000" />
|
||||||
|
<GradientStop Offset="1" Color="#00000000" />
|
||||||
|
</RadialGradientBrush.GradientStops>
|
||||||
|
</RadialGradientBrush>
|
||||||
|
</Image.OpacityMask>
|
||||||
|
</Image>
|
||||||
|
</Border>
|
||||||
|
</materialDesign:TransitioningContent>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Channels -->
|
||||||
|
<Border>
|
||||||
|
<ListBox
|
||||||
|
HorizontalContentAlignment="Stretch"
|
||||||
|
ItemsSource="{Binding AvailableChannels}"
|
||||||
|
VirtualizingStackPanel.IsVirtualizing="False">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<materialDesign:TransitioningContent>
|
||||||
|
<materialDesign:TransitioningContent.OpeningEffect>
|
||||||
|
<materialDesign:TransitionEffect Kind="SlideInFromLeft" Duration="0:0:0.3" />
|
||||||
|
</materialDesign:TransitioningContent.OpeningEffect>
|
||||||
|
<StackPanel
|
||||||
|
Margin="-8"
|
||||||
|
Background="Transparent"
|
||||||
|
Cursor="Hand"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<StackPanel.InputBindings>
|
||||||
|
<MouseBinding
|
||||||
|
Command="{Binding DataContext.ShowExportSetupCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
MouseAction="LeftClick" />
|
||||||
|
</StackPanel.InputBindings>
|
||||||
|
<materialDesign:PackIcon
|
||||||
|
Margin="16,7,0,6"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Kind="Pound" />
|
||||||
|
<TextBlock
|
||||||
|
Margin="3,8,8,8"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="14"
|
||||||
|
Text="{Binding Name}" />
|
||||||
|
</StackPanel>
|
||||||
|
</materialDesign:TransitioningContent>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Border>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- Usage instructions -->
|
||||||
|
<StackPanel Margin="32,32,8,8" Visibility="{Binding IsDataAvailable, Converter={StaticResource InvertBoolToVisibilityConverter}}">
|
||||||
|
<TextBlock FontSize="18" Text="DiscordChatExporter needs your authorization token to work." />
|
||||||
|
<TextBlock
|
||||||
|
Margin="0,8,0,0"
|
||||||
|
FontSize="16"
|
||||||
|
Text="To obtain it, follow these steps:" />
|
||||||
|
<TextBlock Margin="8,0,0,0" FontSize="14">
|
||||||
|
<Run Text="1. Open the Discord app" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="2. Log in if you haven't" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="3. Press" />
|
||||||
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Ctrl+Shift+I" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="4. Navigate to" />
|
||||||
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Application" />
|
||||||
|
<Run Text="tab" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="5. Expand" />
|
||||||
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Storage > Local Storage > https://discordapp.com" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="6. Find" />
|
||||||
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text=""token"" />
|
||||||
|
<Run Text="under key and copy the value" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="7. Paste the value in the textbox above" />
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<materialDesign:Snackbar x:Name="Snackbar" />
|
||||||
|
</Grid>
|
||||||
|
</DockPanel>
|
||||||
|
</materialDesign:DialogHost>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using DiscordChatExporter.Gui.Messages;
|
||||||
|
using GalaSoft.MvvmLight.Messaging;
|
||||||
|
using MaterialDesignThemes.Wpf;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Gui.Views
|
||||||
|
{
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Title += $" v{Assembly.GetExecutingAssembly().GetName().Version}";
|
||||||
|
|
||||||
|
Snackbar.MessageQueue = new SnackbarMessageQueue(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
// Notification messages
|
||||||
|
Messenger.Default.Register<ShowNotificationMessage>(this, m =>
|
||||||
|
{
|
||||||
|
if (m.CallbackCaption != null && m.Callback != null)
|
||||||
|
Snackbar.MessageQueue.Enqueue(m.Message, m.CallbackCaption, m.Callback);
|
||||||
|
else
|
||||||
|
Snackbar.MessageQueue.Enqueue(m.Message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dialog messages
|
||||||
|
Messenger.Default.Register<ShowExportSetupMessage>(this,
|
||||||
|
m => DialogHost.Show(new ExportSetupDialog()).Forget());
|
||||||
|
Messenger.Default.Register<ShowSettingsMessage>(this,
|
||||||
|
m => DialogHost.Show(new SettingsDialog()).Forget());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="DiscordChatExporter.Gui.Views.SettingsDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
Width="250"
|
||||||
|
DataContext="{Binding SettingsViewModel, Source={StaticResource Container}}">
|
||||||
|
<StackPanel>
|
||||||
|
<!-- Date format -->
|
||||||
|
<TextBox
|
||||||
|
Margin="16,16,16,8"
|
||||||
|
materialDesign:HintAssist.Hint="Date format"
|
||||||
|
materialDesign:HintAssist.IsFloating="True"
|
||||||
|
Text="{Binding DateFormat}" />
|
||||||
|
|
||||||
|
<!-- Message group limit -->
|
||||||
|
<TextBox
|
||||||
|
Margin="16,8,16,8"
|
||||||
|
materialDesign:HintAssist.Hint="Message group limit"
|
||||||
|
materialDesign:HintAssist.IsFloating="True"
|
||||||
|
Text="{Binding MessageGroupLimit}" />
|
||||||
|
|
||||||
|
<!-- Auto-updates -->
|
||||||
|
<DockPanel LastChildFill="False">
|
||||||
|
<TextBlock
|
||||||
|
Margin="16,8,16,8"
|
||||||
|
DockPanel.Dock="Left"
|
||||||
|
Text="Auto-updates" />
|
||||||
|
<ToggleButton
|
||||||
|
Margin="16,8,16,8"
|
||||||
|
DockPanel.Dock="Right"
|
||||||
|
IsChecked="{Binding IsAutoUpdateEnabled}" />
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- Save button -->
|
||||||
|
<Button
|
||||||
|
Margin="8"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
|
||||||
|
Content="SAVE"
|
||||||
|
IsCancel="True"
|
||||||
|
IsDefault="True"
|
||||||
|
Style="{DynamicResource MaterialDesignFlatButton}" />
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Views
|
namespace DiscordChatExporter.Gui.Views
|
||||||
{
|
{
|
||||||
public partial class SettingsDialog
|
public partial class SettingsDialog
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<runtime>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="CommonServiceLocator" publicKeyToken="489b6accfaf20ef0" culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-2.0.3.0" newVersion="2.0.3.0" />
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
</runtime>
|
||||||
|
</configuration>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="CommonServiceLocator" version="2.0.3" targetFramework="net461" />
|
||||||
|
<package id="MaterialDesignColors" version="1.1.3" targetFramework="net461" />
|
||||||
|
<package id="MaterialDesignThemes" version="2.4.0.1044" targetFramework="net461" />
|
||||||
|
<package id="MvvmLightLibs" version="5.4.1" targetFramework="net461" />
|
||||||
|
<package id="Tyrrrz.Extensions" version="1.5.0" targetFramework="net461" />
|
||||||
|
<package id="Tyrrrz.WpfExtensions" version="1.0.5" targetFramework="net461" />
|
||||||
|
</packages>
|
||||||
+15
-2
@@ -1,15 +1,20 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio 15
|
# Visual Studio 15
|
||||||
VisualStudioVersion = 15.0.26730.15
|
VisualStudioVersion = 15.0.27130.2026
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EA305DD5-1F98-415D-B6C4-65053A58F914}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EA305DD5-1F98-415D-B6C4-65053A58F914}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
Changelog.md = Changelog.md
|
||||||
License.txt = License.txt
|
License.txt = License.txt
|
||||||
Readme.md = Readme.md
|
Readme.md = Readme.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordChatExporter", "DiscordChatExporter\DiscordChatExporter.csproj", "{732A67AF-93DE-49DF-B10F-FD74710B7863}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordChatExporter.Gui", "DiscordChatExporter.Gui\DiscordChatExporter.Gui.csproj", "{732A67AF-93DE-49DF-B10F-FD74710B7863}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordChatExporter.Core", "DiscordChatExporter.Core\DiscordChatExporter.Core.csproj", "{707C0CD0-A7E0-4CAB-8DB9-07A45CB87377}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordChatExporter.Cli", "DiscordChatExporter.Cli\DiscordChatExporter.Cli.csproj", "{D08624B6-3081-4BCB-91F8-E9832FACC6CE}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -21,6 +26,14 @@ Global
|
|||||||
{732A67AF-93DE-49DF-B10F-FD74710B7863}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{732A67AF-93DE-49DF-B10F-FD74710B7863}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{732A67AF-93DE-49DF-B10F-FD74710B7863}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{732A67AF-93DE-49DF-B10F-FD74710B7863}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{732A67AF-93DE-49DF-B10F-FD74710B7863}.Release|Any CPU.Build.0 = Release|Any CPU
|
{732A67AF-93DE-49DF-B10F-FD74710B7863}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{707C0CD0-A7E0-4CAB-8DB9-07A45CB87377}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{707C0CD0-A7E0-4CAB-8DB9-07A45CB87377}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{707C0CD0-A7E0-4CAB-8DB9-07A45CB87377}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{707C0CD0-A7E0-4CAB-8DB9-07A45CB87377}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D08624B6-3081-4BCB-91F8-E9832FACC6CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D08624B6-3081-4BCB-91F8-E9832FACC6CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D08624B6-3081-4BCB-91F8-E9832FACC6CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D08624B6-3081-4BCB-91F8-E9832FACC6CE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
Application "DiscordChatExporter.App" {
|
|
||||||
StartupUri: "Views/MainWindow.g.xaml"
|
|
||||||
Startup: App_Startup
|
|
||||||
Exit: App_Exit
|
|
||||||
|
|
||||||
Resources: ResourceDictionary {
|
|
||||||
// Material Design
|
|
||||||
#MergeDictionary("pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml")
|
|
||||||
#MergeDictionary("pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml")
|
|
||||||
|
|
||||||
// Colors
|
|
||||||
Color Key="PrimaryColor" { "#343838" }
|
|
||||||
Color Key="PrimaryLightColor" { "#5E6262" }
|
|
||||||
Color Key="PrimaryDarkColor" { "#0D1212" }
|
|
||||||
Color Key="AccentColor" { "#F9A825" }
|
|
||||||
Color Key="TextColor" { "#000000" }
|
|
||||||
Color Key="InverseTextColor" { "#FFFFFF" }
|
|
||||||
|
|
||||||
// Brushes
|
|
||||||
SolidColorBrush Key="PrimaryHueLightBrush" { Color: resource dyn "PrimaryLightColor" }
|
|
||||||
SolidColorBrush Key="PrimaryHueLightForegroundBrush" { Color: resource dyn "InverseTextColor" }
|
|
||||||
SolidColorBrush Key="PrimaryHueMidBrush" { Color: resource dyn "PrimaryColor" }
|
|
||||||
SolidColorBrush Key="PrimaryHueMidForegroundBrush" { Color: resource dyn "InverseTextColor" }
|
|
||||||
SolidColorBrush Key="PrimaryHueDarkBrush" { Color: resource dyn "PrimaryDarkColor" }
|
|
||||||
SolidColorBrush Key="PrimaryHueDarkForegroundBrush" { Color: resource dyn "InverseTextColor" }
|
|
||||||
SolidColorBrush Key="SecondaryAccentBrush" { Color: resource dyn "AccentColor" }
|
|
||||||
SolidColorBrush Key="SecondaryAccentForegroundBrush" { Color: resource dyn "TextColor" }
|
|
||||||
SolidColorBrush Key="PrimaryTextBrush" { Color: resource dyn "TextColor", Opacity: 0.87 }
|
|
||||||
SolidColorBrush Key="SecondaryTextBrush" { Color: resource dyn "TextColor", Opacity: 0.64 }
|
|
||||||
SolidColorBrush Key="DimTextBrush" { Color: resource dyn "TextColor", Opacity: 0.45 }
|
|
||||||
SolidColorBrush Key="PrimaryInverseTextBrush" { Color: resource dyn "InverseTextColor", Opacity: 1 }
|
|
||||||
SolidColorBrush Key="SecondaryInverseTextBrush" { Color: resource dyn "InverseTextColor", Opacity: 0.7 }
|
|
||||||
SolidColorBrush Key="DimInverseTextBrush" { Color: resource dyn "InverseTextColor", Opacity: 0.52 }
|
|
||||||
SolidColorBrush Key="AccentTextBrush" { Color: resource dyn "AccentColor", Opacity: 1 }
|
|
||||||
SolidColorBrush Key="DividerBrush" { Color: resource dyn "TextColor", Opacity: 0.12 }
|
|
||||||
SolidColorBrush Key="InverseDividerBrush" { Color: resource dyn "InverseTextColor", Opacity: 0.12 }
|
|
||||||
|
|
||||||
// Styles
|
|
||||||
Style {
|
|
||||||
TargetType: "Image"
|
|
||||||
#Setter("RenderOptions.BitmapScalingMode", "HighQuality")
|
|
||||||
}
|
|
||||||
|
|
||||||
Style {
|
|
||||||
TargetType: "ProgressBar"
|
|
||||||
BasedOn: resource "MaterialDesignLinearProgressBar"
|
|
||||||
#Setter("Foreground", resource dyn "SecondaryAccentBrush")
|
|
||||||
#Setter("Height", 2)
|
|
||||||
#Setter("Minimum", 0)
|
|
||||||
#Setter("Maximum", 1)
|
|
||||||
#Setter("BorderThickness", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
Style {
|
|
||||||
TargetType: "TextBox"
|
|
||||||
BasedOn: resource "MaterialDesignTextBox"
|
|
||||||
#Setter("Foreground", resource dyn "PrimaryTextBrush")
|
|
||||||
}
|
|
||||||
|
|
||||||
Style {
|
|
||||||
TargetType: "ComboBox"
|
|
||||||
BasedOn: resource "MaterialDesignComboBox"
|
|
||||||
#Setter("Foreground", resource dyn "PrimaryTextBrush")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container
|
|
||||||
Container Key="Container" { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<configuration>
|
|
||||||
<startup>
|
|
||||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
|
|
||||||
</startup>
|
|
||||||
</configuration>
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using DiscordChatExporter.Services;
|
|
||||||
using DiscordChatExporter.ViewModels;
|
|
||||||
using GalaSoft.MvvmLight.Ioc;
|
|
||||||
using Microsoft.Practices.ServiceLocation;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter
|
|
||||||
{
|
|
||||||
public class Container
|
|
||||||
{
|
|
||||||
public static void Init()
|
|
||||||
{
|
|
||||||
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
|
|
||||||
|
|
||||||
// Settings
|
|
||||||
SimpleIoc.Default.Register<ISettingsService, SettingsService>();
|
|
||||||
ServiceLocator.Current.GetInstance<ISettingsService>().Load();
|
|
||||||
|
|
||||||
// Services
|
|
||||||
SimpleIoc.Default.Register<IDataService, DataService>();
|
|
||||||
SimpleIoc.Default.Register<IExportService, ExportService>();
|
|
||||||
SimpleIoc.Default.Register<IMessageGroupService, MessageGroupService>();
|
|
||||||
|
|
||||||
// View models
|
|
||||||
SimpleIoc.Default.Register<IErrorViewModel, ErrorViewModel>(true);
|
|
||||||
SimpleIoc.Default.Register<IExportDoneViewModel, ExportDoneViewModel>(true);
|
|
||||||
SimpleIoc.Default.Register<IExportSetupViewModel, ExportSetupViewModel>(true);
|
|
||||||
SimpleIoc.Default.Register<IMainViewModel, MainViewModel>(true);
|
|
||||||
SimpleIoc.Default.Register<ISettingsViewModel, SettingsViewModel>(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Cleanup()
|
|
||||||
{
|
|
||||||
// Settings
|
|
||||||
ServiceLocator.Current.GetInstance<ISettingsService>().Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IErrorViewModel ErrorViewModel => ServiceLocator.Current.GetInstance<IErrorViewModel>();
|
|
||||||
public IExportDoneViewModel ExportDoneViewModel => ServiceLocator.Current.GetInstance<IExportDoneViewModel>();
|
|
||||||
public IExportSetupViewModel ExportSetupViewModel => ServiceLocator.Current.GetInstance<IExportSetupViewModel>();
|
|
||||||
public IMainViewModel MainViewModel => ServiceLocator.Current.GetInstance<IMainViewModel>();
|
|
||||||
public ISettingsViewModel SettingsViewModel => ServiceLocator.Current.GetInstance<ISettingsViewModel>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
|
||||||
<PropertyGroup>
|
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
|
||||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
|
||||||
<ProjectGuid>{732A67AF-93DE-49DF-B10F-FD74710B7863}</ProjectGuid>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<RootNamespace>DiscordChatExporter</RootNamespace>
|
|
||||||
<AssemblyName>DiscordChatExporter</AssemblyName>
|
|
||||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
|
||||||
<FileAlignment>512</FileAlignment>
|
|
||||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
|
||||||
<NuGetPackageImportStamp>
|
|
||||||
</NuGetPackageImportStamp>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
|
||||||
<DebugType>full</DebugType>
|
|
||||||
<Optimize>false</Optimize>
|
|
||||||
<OutputPath>bin\Debug\</OutputPath>
|
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
|
||||||
<DebugType>pdbonly</DebugType>
|
|
||||||
<Optimize>true</Optimize>
|
|
||||||
<OutputPath>bin\Release\</OutputPath>
|
|
||||||
<DefineConstants>TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Reference Include="AmmySidekick, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7c1296d24569a67d, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Ammy.WPF.1.2.87\lib\net40\AmmySidekick.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="GalaSoft.MvvmLight, Version=5.3.0.19026, Culture=neutral, PublicKeyToken=e7570ab207bcb616, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\MvvmLightLibs.5.3.0.0\lib\net45\GalaSoft.MvvmLight.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="GalaSoft.MvvmLight.Extras, Version=5.3.0.19032, Culture=neutral, PublicKeyToken=669f0b5e8f868abf, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\MvvmLightLibs.5.3.0.0\lib\net45\GalaSoft.MvvmLight.Extras.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="GalaSoft.MvvmLight.Platform, Version=5.3.0.19032, Culture=neutral, PublicKeyToken=5f873c45e98af8a1, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\MvvmLightLibs.5.3.0.0\lib\net45\GalaSoft.MvvmLight.Platform.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="MaterialDesignColors, Version=1.1.3.0, Culture=neutral, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\MaterialDesignColors.1.1.3\lib\net45\MaterialDesignColors.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="MaterialDesignThemes.Wpf, Version=2.3.1.953, Culture=neutral, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\MaterialDesignThemes.2.3.1.953\lib\net45\MaterialDesignThemes.Wpf.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Microsoft.Practices.ServiceLocation, Version=1.3.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\CommonServiceLocator.1.3\lib\portable-net4+sl5+netcore45+wpa81+wp8\Microsoft.Practices.ServiceLocation.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System" />
|
|
||||||
<Reference Include="System.Core" />
|
|
||||||
<Reference Include="System.Net.Http" />
|
|
||||||
<Reference Include="System.Windows.Interactivity, Version=4.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\MvvmLightLibs.5.3.0.0\lib\net45\System.Windows.Interactivity.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Xaml">
|
|
||||||
<RequiredTargetFramework>4.0</RequiredTargetFramework>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Xml" />
|
|
||||||
<Reference Include="Tyrrrz.Extensions, Version=1.4.1.0, Culture=neutral, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Tyrrrz.Extensions.1.4.1\lib\net45\Tyrrrz.Extensions.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Tyrrrz.Settings, Version=1.3.0.0, Culture=neutral, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Tyrrrz.Settings.1.3.0\lib\net45\Tyrrrz.Settings.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="WindowsBase" />
|
|
||||||
<Reference Include="PresentationCore" />
|
|
||||||
<Reference Include="PresentationFramework" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="Exceptions\HttpErrorStatusCodeException.cs" />
|
|
||||||
<Compile Include="Messages\ShowErrorMessage.cs" />
|
|
||||||
<Compile Include="Messages\ShowExportDoneMessage.cs" />
|
|
||||||
<Compile Include="Messages\ShowExportSetupMessage.cs" />
|
|
||||||
<Compile Include="Messages\ShowSettingsMessage.cs" />
|
|
||||||
<Compile Include="Messages\StartExportMessage.cs" />
|
|
||||||
<Compile Include="Models\AttachmentType.cs" />
|
|
||||||
<Compile Include="Models\ChannelChatLog.cs" />
|
|
||||||
<Compile Include="Models\ChannelType.cs" />
|
|
||||||
<Compile Include="Models\ExportFormat.cs" />
|
|
||||||
<Compile Include="Models\Extensions.cs" />
|
|
||||||
<Compile Include="Models\Role.cs" />
|
|
||||||
<Compile Include="Models\MessageType.cs" />
|
|
||||||
<Compile Include="Services\IMessageGroupService.cs" />
|
|
||||||
<Compile Include="Services\MessageGroupService.cs" />
|
|
||||||
<Compile Include="ViewModels\ErrorViewModel.cs" />
|
|
||||||
<Compile Include="ViewModels\ExportSetupViewModel.cs" />
|
|
||||||
<Compile Include="ViewModels\IErrorViewModel.cs" />
|
|
||||||
<Compile Include="ViewModels\IExportSetupViewModel.cs" />
|
|
||||||
<Compile Include="ViewModels\ISettingsViewModel.cs" />
|
|
||||||
<Compile Include="ViewModels\IExportDoneViewModel.cs" />
|
|
||||||
<Compile Include="ViewModels\SettingsViewModel.cs" />
|
|
||||||
<Compile Include="ViewModels\ExportDoneViewModel.cs" />
|
|
||||||
<Compile Include="Views\ErrorDialog.ammy.cs">
|
|
||||||
<DependentUpon>ErrorDialog.ammy</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Views\ExportDoneDialog.ammy.cs">
|
|
||||||
<DependentUpon>ExportDoneDialog.ammy</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Views\ExportSetupDialog.ammy.cs">
|
|
||||||
<DependentUpon>ExportSetupDialog.ammy</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Views\SettingsDialog.ammy.cs">
|
|
||||||
<DependentUpon>SettingsDialog.ammy</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Page Include="App.g.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>XamlIntelliSenseFileGenerator</Generator>
|
|
||||||
<DependentUpon>App.ammy</DependentUpon>
|
|
||||||
</Page>
|
|
||||||
<Page Include="Views\ErrorDialog.g.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
<DependentUpon>ErrorDialog.ammy</DependentUpon>
|
|
||||||
</Page>
|
|
||||||
<Page Include="Views\ExportDoneDialog.g.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
<DependentUpon>ExportDoneDialog.ammy</DependentUpon>
|
|
||||||
</Page>
|
|
||||||
<Page Include="Views\ExportSetupDialog.g.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
<DependentUpon>ExportSetupDialog.ammy</DependentUpon>
|
|
||||||
</Page>
|
|
||||||
<Page Include="Views\MainWindow.g.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
<DependentUpon>MainWindow.ammy</DependentUpon>
|
|
||||||
</Page>
|
|
||||||
<Compile Include="App.ammy.cs">
|
|
||||||
<DependentUpon>App.ammy</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Container.cs" />
|
|
||||||
<Compile Include="Models\Attachment.cs" />
|
|
||||||
<Compile Include="Models\Channel.cs" />
|
|
||||||
<Compile Include="Models\Guild.cs" />
|
|
||||||
<Compile Include="Models\Message.cs" />
|
|
||||||
<Compile Include="Models\MessageGroup.cs" />
|
|
||||||
<Compile Include="Models\User.cs" />
|
|
||||||
<Compile Include="Program.cs" />
|
|
||||||
<Compile Include="Services\DataService.cs" />
|
|
||||||
<Compile Include="Services\ExportService.cs" />
|
|
||||||
<Compile Include="Services\IDataService.cs" />
|
|
||||||
<Compile Include="Services\IExportService.cs" />
|
|
||||||
<Compile Include="Services\ISettingsService.cs" />
|
|
||||||
<Compile Include="Services\SettingsService.cs" />
|
|
||||||
<Compile Include="ViewModels\IMainViewModel.cs" />
|
|
||||||
<Compile Include="ViewModels\MainViewModel.cs" />
|
|
||||||
<Compile Include="Views\MainWindow.ammy.cs">
|
|
||||||
<DependentUpon>MainWindow.ammy</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Page Include="Views\SettingsDialog.g.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
<DependentUpon>SettingsDialog.ammy</DependentUpon>
|
|
||||||
</Page>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="Properties\AssemblyInfo.cs">
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Properties\Resources.Designer.cs">
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DesignTime>True</DesignTime>
|
|
||||||
<DependentUpon>Resources.resx</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<EmbeddedResource Include="Properties\Resources.resx">
|
|
||||||
<Generator>ResXFileCodeGenerator</Generator>
|
|
||||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
|
||||||
</EmbeddedResource>
|
|
||||||
<None Include="App.ammy" />
|
|
||||||
<None Include="lib.ammy" />
|
|
||||||
<None Include="packages.config">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</None>
|
|
||||||
<None Include="Views\ErrorDialog.ammy" />
|
|
||||||
<None Include="Views\ExportDoneDialog.ammy" />
|
|
||||||
<None Include="Views\ExportSetupDialog.ammy" />
|
|
||||||
<None Include="Views\MainWindow.ammy" />
|
|
||||||
<None Include="Views\SettingsDialog.ammy" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="App.config" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Include="Resources\ExportService\DarkTheme.css" />
|
|
||||||
<EmbeddedResource Include="Resources\ExportService\LightTheme.css" />
|
|
||||||
</ItemGroup>
|
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
|
||||||
<Import Project="..\packages\Ammy.1.2.87\build\Ammy.targets" Condition="Exists('..\packages\Ammy.1.2.87\build\Ammy.targets')" />
|
|
||||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
|
||||||
<PropertyGroup>
|
|
||||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
|
||||||
</PropertyGroup>
|
|
||||||
<Error Condition="!Exists('..\packages\Ammy.1.2.87\build\Ammy.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Ammy.1.2.87\build\Ammy.targets'))" />
|
|
||||||
</Target>
|
|
||||||
</Project>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace DiscordChatExporter.Messages
|
|
||||||
{
|
|
||||||
public class ShowErrorMessage
|
|
||||||
{
|
|
||||||
public string Message { get; }
|
|
||||||
|
|
||||||
public ShowErrorMessage(string message)
|
|
||||||
{
|
|
||||||
Message = message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace DiscordChatExporter.Messages
|
|
||||||
{
|
|
||||||
public class ShowExportDoneMessage
|
|
||||||
{
|
|
||||||
public string FilePath { get; }
|
|
||||||
|
|
||||||
public ShowExportDoneMessage(string filePath)
|
|
||||||
{
|
|
||||||
FilePath = filePath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
namespace DiscordChatExporter.Models
|
|
||||||
{
|
|
||||||
public class Channel
|
|
||||||
{
|
|
||||||
public string Id { get; }
|
|
||||||
|
|
||||||
public string Name { get; }
|
|
||||||
|
|
||||||
public ChannelType Type { get; }
|
|
||||||
|
|
||||||
public Channel(string id, string name, ChannelType type)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
Name = name;
|
|
||||||
Type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return Name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Resources;
|
|
||||||
using AmmySidekick;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter
|
|
||||||
{
|
|
||||||
public static class Program
|
|
||||||
{
|
|
||||||
[STAThread]
|
|
||||||
public static void Main()
|
|
||||||
{
|
|
||||||
var app = new App();
|
|
||||||
app.InitializeComponent();
|
|
||||||
|
|
||||||
RuntimeUpdateHandler.Register(app, $"/{Ammy.GetAssemblyName(app)};component/App.g.xaml");
|
|
||||||
|
|
||||||
app.Run();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetResourceString(string resourcePath)
|
|
||||||
{
|
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
|
||||||
var stream = assembly.GetManifestResourceStream(resourcePath);
|
|
||||||
if (stream == null)
|
|
||||||
throw new MissingManifestResourceException("Could not find resource");
|
|
||||||
|
|
||||||
using (stream)
|
|
||||||
using (var reader = new StreamReader(stream))
|
|
||||||
{
|
|
||||||
return reader.ReadToEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
|
|
||||||
[assembly: AssemblyTitle("DiscordChatExporter")]
|
|
||||||
[assembly: AssemblyCompany("Tyrrrz")]
|
|
||||||
[assembly: AssemblyCopyright("Copyright (c) 2017 Alexey Golub")]
|
|
||||||
[assembly: AssemblyVersion("2.3")]
|
|
||||||
[assembly: AssemblyFileVersion("2.3")]
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using DiscordChatExporter.Models;
|
|
||||||
using Tyrrrz.Extensions;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Services
|
|
||||||
{
|
|
||||||
public partial class ExportService : IExportService
|
|
||||||
{
|
|
||||||
private readonly ISettingsService _settingsService;
|
|
||||||
|
|
||||||
public ExportService(ISettingsService settingsService)
|
|
||||||
{
|
|
||||||
_settingsService = settingsService;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExportAsTextAsync(string filePath, ChannelChatLog log)
|
|
||||||
{
|
|
||||||
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8, 128 * 1024))
|
|
||||||
{
|
|
||||||
// Generation info
|
|
||||||
await writer.WriteLineAsync("https://github.com/Tyrrrz/DiscordChatExporter");
|
|
||||||
await writer.WriteLineAsync();
|
|
||||||
|
|
||||||
// Guild and channel info
|
|
||||||
await writer.WriteLineAsync('='.Repeat(48));
|
|
||||||
await writer.WriteLineAsync($"Guild: {log.Guild}");
|
|
||||||
await writer.WriteLineAsync($"Channel: {log.Channel}");
|
|
||||||
await writer.WriteLineAsync($"Messages: {log.TotalMessageCount:N0}");
|
|
||||||
await writer.WriteLineAsync('='.Repeat(48));
|
|
||||||
await writer.WriteLineAsync();
|
|
||||||
|
|
||||||
// Chat log
|
|
||||||
foreach (var group in log.MessageGroups)
|
|
||||||
{
|
|
||||||
var timeStampFormatted = group.TimeStamp.ToString(_settingsService.DateFormat);
|
|
||||||
await writer.WriteLineAsync($"{group.Author} [{timeStampFormatted}]");
|
|
||||||
|
|
||||||
// Messages
|
|
||||||
foreach (var message in group.Messages)
|
|
||||||
{
|
|
||||||
// Content
|
|
||||||
if (message.Content.IsNotBlank())
|
|
||||||
{
|
|
||||||
var contentFormatted = FormatMessageContentText(message);
|
|
||||||
await writer.WriteLineAsync(contentFormatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attachments
|
|
||||||
foreach (var attachment in message.Attachments)
|
|
||||||
{
|
|
||||||
await writer.WriteLineAsync(attachment.Url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await writer.WriteLineAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExportAsHtmlAsync(string filePath, ChannelChatLog log, string css)
|
|
||||||
{
|
|
||||||
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8, 128 * 1024))
|
|
||||||
{
|
|
||||||
// Generation info
|
|
||||||
await writer.WriteLineAsync("<!-- https://github.com/Tyrrrz/DiscordChatExporter -->");
|
|
||||||
|
|
||||||
// Html start
|
|
||||||
await writer.WriteLineAsync("<!DOCTYPE html>");
|
|
||||||
await writer.WriteLineAsync("<html lang=\"en\">");
|
|
||||||
|
|
||||||
// HEAD
|
|
||||||
await writer.WriteLineAsync("<head>");
|
|
||||||
await writer.WriteLineAsync($"<title>{log.Guild} - {log.Channel}</title>");
|
|
||||||
await writer.WriteLineAsync("<meta charset=\"utf-8\" />");
|
|
||||||
await writer.WriteLineAsync("<meta name=\"viewport\" content=\"width=device-width\" />");
|
|
||||||
await writer.WriteLineAsync($"<style>{css}</style>");
|
|
||||||
await writer.WriteLineAsync("</head>");
|
|
||||||
|
|
||||||
// Body start
|
|
||||||
await writer.WriteLineAsync("<body>");
|
|
||||||
|
|
||||||
// Guild and channel info
|
|
||||||
await writer.WriteLineAsync("<div id=\"info\">");
|
|
||||||
await writer.WriteLineAsync("<div class=\"info-left\">");
|
|
||||||
await writer.WriteLineAsync($"<img class=\"guild-icon\" src=\"{log.Guild.IconUrl}\" />");
|
|
||||||
await writer.WriteLineAsync("</div>"); // info-left
|
|
||||||
await writer.WriteLineAsync("<div class=\"info-right\">");
|
|
||||||
await writer.WriteLineAsync($"<div class=\"guild-name\">{log.Guild}</div>");
|
|
||||||
await writer.WriteLineAsync($"<div class=\"channel-name\">{log.Channel}</div>");
|
|
||||||
await writer.WriteLineAsync($"<div class=\"misc\">{log.TotalMessageCount:N0} messages</div>");
|
|
||||||
await writer.WriteLineAsync("</div>"); // info-right
|
|
||||||
await writer.WriteLineAsync("</div>"); // info
|
|
||||||
|
|
||||||
// Chat log
|
|
||||||
await writer.WriteLineAsync("<div id=\"log\">");
|
|
||||||
foreach (var group in log.MessageGroups)
|
|
||||||
{
|
|
||||||
await writer.WriteLineAsync("<div class=\"msg\">");
|
|
||||||
await writer.WriteLineAsync("<div class=\"msg-left\">");
|
|
||||||
await writer.WriteLineAsync($"<img class=\"msg-avatar\" src=\"{group.Author.AvatarUrl}\" />");
|
|
||||||
await writer.WriteLineAsync("</div>");
|
|
||||||
|
|
||||||
await writer.WriteLineAsync("<div class=\"msg-right\">");
|
|
||||||
await writer.WriteAsync($"<span class=\"msg-user\" title=\"{HtmlEncode(group.Author)}\">");
|
|
||||||
await writer.WriteAsync(HtmlEncode(group.Author.Name));
|
|
||||||
await writer.WriteLineAsync("</span>");
|
|
||||||
var timeStampFormatted = HtmlEncode(group.TimeStamp.ToString(_settingsService.DateFormat));
|
|
||||||
await writer.WriteLineAsync($"<span class=\"msg-date\">{timeStampFormatted}</span>");
|
|
||||||
|
|
||||||
// Messages
|
|
||||||
foreach (var message in group.Messages)
|
|
||||||
{
|
|
||||||
// Content
|
|
||||||
if (message.Content.IsNotBlank())
|
|
||||||
{
|
|
||||||
await writer.WriteLineAsync("<div class=\"msg-content\">");
|
|
||||||
var contentFormatted = FormatMessageContentHtml(message);
|
|
||||||
await writer.WriteAsync(contentFormatted);
|
|
||||||
|
|
||||||
// Edited timestamp
|
|
||||||
if (message.EditedTimeStamp != null)
|
|
||||||
{
|
|
||||||
var editedTimeStampFormatted =
|
|
||||||
HtmlEncode(message.EditedTimeStamp.Value.ToString(_settingsService.DateFormat));
|
|
||||||
await writer.WriteAsync(
|
|
||||||
$"<span class=\"msg-edited\" title=\"{editedTimeStampFormatted}\">(edited)</span>");
|
|
||||||
}
|
|
||||||
|
|
||||||
await writer.WriteLineAsync("</div>"); // msg-content
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attachments
|
|
||||||
foreach (var attachment in message.Attachments)
|
|
||||||
{
|
|
||||||
if (attachment.Type == AttachmentType.Image)
|
|
||||||
{
|
|
||||||
await writer.WriteLineAsync("<div class=\"msg-attachment\">");
|
|
||||||
await writer.WriteLineAsync($"<a href=\"{attachment.Url}\">");
|
|
||||||
await writer.WriteLineAsync(
|
|
||||||
$"<img class=\"msg-attachment\" src=\"{attachment.Url}\" />");
|
|
||||||
await writer.WriteLineAsync("</a>");
|
|
||||||
await writer.WriteLineAsync("</div>");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await writer.WriteLineAsync("<div class=\"msg-attachment\">");
|
|
||||||
await writer.WriteLineAsync($"<a href=\"{attachment.Url}\">");
|
|
||||||
var fileSizeFormatted = FormatFileSize(attachment.FileSize);
|
|
||||||
await writer.WriteLineAsync($"Attachment: {attachment.FileName} ({fileSizeFormatted})");
|
|
||||||
await writer.WriteLineAsync("</a>");
|
|
||||||
await writer.WriteLineAsync("</div>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await writer.WriteLineAsync("</div>"); // msg-right
|
|
||||||
await writer.WriteLineAsync("</div>"); // msg
|
|
||||||
}
|
|
||||||
await writer.WriteLineAsync("</div>"); // log
|
|
||||||
|
|
||||||
await writer.WriteLineAsync("</body>");
|
|
||||||
await writer.WriteLineAsync("</html>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task ExportAsync(ExportFormat format, string filePath, ChannelChatLog log)
|
|
||||||
{
|
|
||||||
if (format == ExportFormat.PlainText)
|
|
||||||
{
|
|
||||||
return ExportAsTextAsync(filePath, log);
|
|
||||||
}
|
|
||||||
if (format == ExportFormat.HtmlDark)
|
|
||||||
{
|
|
||||||
var css = Program.GetResourceString("DiscordChatExporter.Resources.ExportService.DarkTheme.css");
|
|
||||||
return ExportAsHtmlAsync(filePath, log, css);
|
|
||||||
}
|
|
||||||
if (format == ExportFormat.HtmlLight)
|
|
||||||
{
|
|
||||||
var css = Program.GetResourceString("DiscordChatExporter.Resources.ExportService.LightTheme.css");
|
|
||||||
return ExportAsHtmlAsync(filePath, log, css);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class ExportService
|
|
||||||
{
|
|
||||||
private static string HtmlEncode(string str)
|
|
||||||
{
|
|
||||||
return WebUtility.HtmlEncode(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string HtmlEncode(object obj)
|
|
||||||
{
|
|
||||||
return WebUtility.HtmlEncode(obj.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatFileSize(long fileSize)
|
|
||||||
{
|
|
||||||
string[] units = {"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
|
|
||||||
double size = fileSize;
|
|
||||||
var unit = 0;
|
|
||||||
|
|
||||||
while (size >= 1024)
|
|
||||||
{
|
|
||||||
size /= 1024;
|
|
||||||
++unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"{size:0.#} {units[unit]}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string FormatMessageContentText(Message message)
|
|
||||||
{
|
|
||||||
var content = message.Content;
|
|
||||||
|
|
||||||
// New lines
|
|
||||||
content = content.Replace("\n", Environment.NewLine);
|
|
||||||
|
|
||||||
// User mentions (<@id>)
|
|
||||||
foreach (var mentionedUser in message.MentionedUsers)
|
|
||||||
content = Regex.Replace(content, $"<@!?{mentionedUser.Id}>", $"@{mentionedUser}");
|
|
||||||
|
|
||||||
// Role mentions (<@&id>)
|
|
||||||
foreach (var mentionedRole in message.MentionedRoles)
|
|
||||||
content = content.Replace($"<@&{mentionedRole.Id}>", $"@{mentionedRole.Name}");
|
|
||||||
|
|
||||||
// Channel mentions (<#id>)
|
|
||||||
foreach (var mentionedChannel in message.MentionedChannels)
|
|
||||||
content = content.Replace($"<#{mentionedChannel.Id}>", $"#{mentionedChannel.Name}");
|
|
||||||
|
|
||||||
// Custom emojis (<:name:id>)
|
|
||||||
content = Regex.Replace(content, "<(:.*?:)\\d*>", "$1");
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatMessageContentHtml(Message message)
|
|
||||||
{
|
|
||||||
var content = message.Content;
|
|
||||||
|
|
||||||
// Encode HTML
|
|
||||||
content = HtmlEncode(content);
|
|
||||||
|
|
||||||
// Pre multiline (```text```)
|
|
||||||
content = Regex.Replace(content, "```+(?:[^`]*?\\n)?([^`]+)\\n?```+", "<div class=\"pre\">$1</div>");
|
|
||||||
|
|
||||||
// Pre inline (`text`)
|
|
||||||
content = Regex.Replace(content, "`([^`]+)`", "<span class=\"pre\">$1</span>");
|
|
||||||
|
|
||||||
// Bold (**text**)
|
|
||||||
content = Regex.Replace(content, "\\*\\*([^\\*]*?)\\*\\*", "<b>$1</b>");
|
|
||||||
|
|
||||||
// Italic (*text*)
|
|
||||||
content = Regex.Replace(content, "\\*([^\\*]*?)\\*", "<i>$1</i>");
|
|
||||||
|
|
||||||
// Underline (__text__)
|
|
||||||
content = Regex.Replace(content, "__([^_]*?)__", "<u>$1</u>");
|
|
||||||
|
|
||||||
// Italic (_text_)
|
|
||||||
content = Regex.Replace(content, "_([^_]*?)_", "<i>$1</i>");
|
|
||||||
|
|
||||||
// Strike through (~~text~~)
|
|
||||||
content = Regex.Replace(content, "~~([^~]*?)~~", "<s>$1</s>");
|
|
||||||
|
|
||||||
// New lines
|
|
||||||
content = content.Replace("\n", "<br />");
|
|
||||||
|
|
||||||
// URL links
|
|
||||||
content = Regex.Replace(content, "((https?|ftp)://[^\\s/$.?#].[^\\s<>]*)", "<a href=\"$1\">$1</a>");
|
|
||||||
|
|
||||||
// Meta mentions (@everyone)
|
|
||||||
content = content.Replace("@everyone", "<span class=\"mention\">@everyone</span>");
|
|
||||||
|
|
||||||
// Meta mentions (@here)
|
|
||||||
content = content.Replace("@here", "<span class=\"mention\">@here</span>");
|
|
||||||
|
|
||||||
// User mentions (<@id>)
|
|
||||||
foreach (var mentionedUser in message.MentionedUsers)
|
|
||||||
{
|
|
||||||
content = Regex.Replace(content, $"<@!?{mentionedUser.Id}>",
|
|
||||||
$"<span class=\"mention\" title=\"{HtmlEncode(mentionedUser)}\">" +
|
|
||||||
$"@{HtmlEncode(mentionedUser.Name)}" +
|
|
||||||
"</span>");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Role mentions (<@&id>)
|
|
||||||
foreach (var mentionedRole in message.MentionedRoles)
|
|
||||||
{
|
|
||||||
content = content.Replace($"<@&{mentionedRole.Id}>",
|
|
||||||
"<span class=\"mention\">" +
|
|
||||||
$"@{HtmlEncode(mentionedRole.Name)}" +
|
|
||||||
"</span>");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channel mentions (<#id>)
|
|
||||||
foreach (var mentionedChannel in message.MentionedChannels)
|
|
||||||
{
|
|
||||||
content = content.Replace($"<#{mentionedChannel.Id}>",
|
|
||||||
"<span class=\"mention\">" +
|
|
||||||
$"#{HtmlEncode(mentionedChannel.Name)}" +
|
|
||||||
"</span>");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom emojis (<:name:id>)
|
|
||||||
content = Regex.Replace(content, "<(:.*?:)(\\d*)>",
|
|
||||||
"<img class=\"emoji\" title=\"$1\" src=\"https://cdn.discordapp.com/emojis/$2.png\" />");
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using DiscordChatExporter.Messages;
|
|
||||||
using GalaSoft.MvvmLight;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.ViewModels
|
|
||||||
{
|
|
||||||
public class ErrorViewModel : ViewModelBase, IErrorViewModel
|
|
||||||
{
|
|
||||||
public string Message { get; private set; }
|
|
||||||
|
|
||||||
public ErrorViewModel()
|
|
||||||
{
|
|
||||||
// Messages
|
|
||||||
MessengerInstance.Register<ShowErrorMessage>(this, m =>
|
|
||||||
{
|
|
||||||
Message = m.Message;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using DiscordChatExporter.Messages;
|
|
||||||
using GalaSoft.MvvmLight;
|
|
||||||
using GalaSoft.MvvmLight.CommandWpf;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.ViewModels
|
|
||||||
{
|
|
||||||
public class ExportDoneViewModel : ViewModelBase, IExportDoneViewModel
|
|
||||||
{
|
|
||||||
private string _filePath;
|
|
||||||
|
|
||||||
// Commands
|
|
||||||
public RelayCommand OpenCommand { get; }
|
|
||||||
|
|
||||||
public ExportDoneViewModel()
|
|
||||||
{
|
|
||||||
// Commands
|
|
||||||
OpenCommand = new RelayCommand(Open);
|
|
||||||
|
|
||||||
// Messages
|
|
||||||
MessengerInstance.Register<ShowExportDoneMessage>(this, m =>
|
|
||||||
{
|
|
||||||
_filePath = m.FilePath;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Open()
|
|
||||||
{
|
|
||||||
Process.Start(_filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace DiscordChatExporter.ViewModels
|
|
||||||
{
|
|
||||||
public interface IErrorViewModel
|
|
||||||
{
|
|
||||||
string Message { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using GalaSoft.MvvmLight.CommandWpf;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.ViewModels
|
|
||||||
{
|
|
||||||
public interface IExportDoneViewModel
|
|
||||||
{
|
|
||||||
RelayCommand OpenCommand { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using MaterialDesignThemes.Wpf
|
|
||||||
|
|
||||||
UserControl "DiscordChatExporter.Views.ErrorDialog" {
|
|
||||||
DataContext: bind ErrorViewModel from $resource Container
|
|
||||||
Width: 250
|
|
||||||
|
|
||||||
StackPanel {
|
|
||||||
// Message
|
|
||||||
TextBlock {
|
|
||||||
Margin: 16
|
|
||||||
FontSize: 16
|
|
||||||
TextWrapping: WrapWithOverflow
|
|
||||||
Text: bind Message
|
|
||||||
}
|
|
||||||
|
|
||||||
// OK
|
|
||||||
Button {
|
|
||||||
Margin: 8
|
|
||||||
Command: DialogHost.CloseDialogCommand
|
|
||||||
Content: "OK"
|
|
||||||
HorizontalAlignment: Right
|
|
||||||
Style: resource dyn "MaterialDesignFlatButton"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace DiscordChatExporter.Views
|
|
||||||
{
|
|
||||||
public partial class ErrorDialog
|
|
||||||
{
|
|
||||||
public ErrorDialog()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
using MaterialDesignThemes.Wpf
|
|
||||||
|
|
||||||
UserControl "DiscordChatExporter.Views.ExportDoneDialog" {
|
|
||||||
DataContext: bind ExportDoneViewModel from $resource Container
|
|
||||||
Width: 250
|
|
||||||
|
|
||||||
StackPanel {
|
|
||||||
// Message
|
|
||||||
TextBlock {
|
|
||||||
Margin: 16
|
|
||||||
FontSize: 16
|
|
||||||
TextWrapping: WrapWithOverflow
|
|
||||||
Text: "Finished exporting chat log"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
@StackPanelHorizontal {
|
|
||||||
HorizontalAlignment: Right
|
|
||||||
|
|
||||||
// Open
|
|
||||||
Button "OpenButton" {
|
|
||||||
Margin: 8
|
|
||||||
Click: OpenButton_Click
|
|
||||||
Command: bind OpenCommand
|
|
||||||
Content: "OPEN IT"
|
|
||||||
Style: resource dyn "MaterialDesignFlatButton"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dismiss
|
|
||||||
Button {
|
|
||||||
Margin: 8
|
|
||||||
Command: DialogHost.CloseDialogCommand
|
|
||||||
Content: "DISMISS"
|
|
||||||
Style: resource dyn "MaterialDesignFlatButton"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
using MaterialDesignThemes.Wpf;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Views
|
|
||||||
{
|
|
||||||
public partial class ExportDoneDialog
|
|
||||||
{
|
|
||||||
public ExportDoneDialog()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OpenButton_Click(object sender, RoutedEventArgs args)
|
|
||||||
{
|
|
||||||
DialogHost.CloseDialogCommand.Execute(null, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
using DiscordChatExporter.Models
|
|
||||||
using MaterialDesignThemes.Wpf
|
|
||||||
|
|
||||||
UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
|
|
||||||
DataContext: bind ExportSetupViewModel from $resource Container
|
|
||||||
Width: 325
|
|
||||||
|
|
||||||
StackPanel {
|
|
||||||
// File path
|
|
||||||
TextBox {
|
|
||||||
Margin: [16, 16, 16, 8]
|
|
||||||
HintAssist.Hint: "Output file"
|
|
||||||
HintAssist.IsFloating: true
|
|
||||||
IsReadOnly: true
|
|
||||||
Text: bind FilePath
|
|
||||||
set [ UpdateSourceTrigger: PropertyChanged ]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format
|
|
||||||
ComboBox {
|
|
||||||
Margin: [16, 8, 16, 8]
|
|
||||||
HintAssist.Hint: "Export format"
|
|
||||||
HintAssist.IsFloating: true
|
|
||||||
IsReadOnly: true
|
|
||||||
ItemsSource: bind AvailableFormats
|
|
||||||
SelectedItem: bind SelectedFormat
|
|
||||||
|
|
||||||
ItemTemplate: DataTemplate {
|
|
||||||
TextBlock {
|
|
||||||
Text: bind
|
|
||||||
convert (ExportFormat f) => Extensions.GetDisplayName(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date range
|
|
||||||
Grid {
|
|
||||||
#TwoColumns("*", "*")
|
|
||||||
|
|
||||||
DatePicker {
|
|
||||||
#Cell(0, 0)
|
|
||||||
Margin: [16, 20, 8, 8]
|
|
||||||
HintAssist.Hint: "From (optional)"
|
|
||||||
HintAssist.IsFloating: true
|
|
||||||
SelectedDate: bind From
|
|
||||||
}
|
|
||||||
|
|
||||||
DatePicker {
|
|
||||||
#Cell(0, 1)
|
|
||||||
Margin: [8, 20, 16, 8]
|
|
||||||
HintAssist.Hint: "To (optional)"
|
|
||||||
HintAssist.IsFloating: true
|
|
||||||
SelectedDate: bind To
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
@StackPanelHorizontal {
|
|
||||||
HorizontalAlignment: Right
|
|
||||||
|
|
||||||
// Browse
|
|
||||||
Button "BrowseButton" {
|
|
||||||
Margin: 8
|
|
||||||
Click: BrowseButton_Click
|
|
||||||
Content: "BROWSE"
|
|
||||||
Style: resource dyn "MaterialDesignFlatButton"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export
|
|
||||||
Button "ExportButton" {
|
|
||||||
Margin: 8
|
|
||||||
Click: ExportButton_Click
|
|
||||||
Command: bind ExportCommand
|
|
||||||
Content: "EXPORT"
|
|
||||||
IsDefault: true
|
|
||||||
Style: resource dyn "MaterialDesignFlatButton"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel
|
|
||||||
Button {
|
|
||||||
Margin: 8
|
|
||||||
Command: DialogHost.CloseDialogCommand
|
|
||||||
Content: "CANCEL"
|
|
||||||
Style: resource dyn "MaterialDesignFlatButton"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
using MaterialDesignThemes.Wpf
|
|
||||||
using MaterialDesignThemes.Wpf.Transitions
|
|
||||||
|
|
||||||
Window "DiscordChatExporter.Views.MainWindow" {
|
|
||||||
Title: "DiscordChatExporter"
|
|
||||||
Width: 600
|
|
||||||
Height: 550
|
|
||||||
Background: resource dyn "MaterialDesignPaper"
|
|
||||||
DataContext: bind MainViewModel from $resource Container
|
|
||||||
FocusManager.FocusedElement: bind from "TokenTextBox"
|
|
||||||
FontFamily: resource dyn "MaterialDesignFont"
|
|
||||||
SnapsToDevicePixels: true
|
|
||||||
TextElement.FontSize: 13
|
|
||||||
TextElement.FontWeight: Regular
|
|
||||||
TextElement.Foreground: resource dyn "SecondaryTextBrush"
|
|
||||||
TextOptions.TextFormattingMode: Ideal
|
|
||||||
TextOptions.TextRenderingMode: Auto
|
|
||||||
UseLayoutRounding: true
|
|
||||||
WindowStartupLocation: CenterScreen
|
|
||||||
|
|
||||||
DialogHost {
|
|
||||||
DockPanel {
|
|
||||||
IsEnabled: bind IsBusy
|
|
||||||
convert (bool b) => b ? false : true
|
|
||||||
|
|
||||||
// Toolbar
|
|
||||||
Border {
|
|
||||||
DockPanel.Dock: Top
|
|
||||||
Background: resource dyn "PrimaryHueMidBrush"
|
|
||||||
TextElement.Foreground: resource dyn "SecondaryInverseTextBrush"
|
|
||||||
StackPanel {
|
|
||||||
Grid {
|
|
||||||
#TwoColumns("*", "Auto")
|
|
||||||
|
|
||||||
Card {
|
|
||||||
#Cell(0, 0)
|
|
||||||
Margin: [6, 6, 0, 6]
|
|
||||||
|
|
||||||
Grid {
|
|
||||||
#TwoColumns("*", "Auto")
|
|
||||||
|
|
||||||
// Token
|
|
||||||
TextBox "TokenTextBox" {
|
|
||||||
#Cell(0, 0)
|
|
||||||
Margin: 6
|
|
||||||
BorderThickness: 0
|
|
||||||
HintAssist.Hint: "Token"
|
|
||||||
FontSize: 16
|
|
||||||
Text: bind Token
|
|
||||||
set [ UpdateSourceTrigger: PropertyChanged ]
|
|
||||||
TextFieldAssist.DecorationVisibility: Hidden
|
|
||||||
TextFieldAssist.TextBoxViewMargin: [0, 0, 2, 0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit
|
|
||||||
Button {
|
|
||||||
#Cell(0, 1)
|
|
||||||
Margin: [0, 6, 6, 6]
|
|
||||||
Padding: 4
|
|
||||||
Command: bind PullDataCommand
|
|
||||||
IsDefault: true
|
|
||||||
Style: resource dyn "MaterialDesignFlatButton"
|
|
||||||
|
|
||||||
PackIcon {
|
|
||||||
Width: 24
|
|
||||||
Height: 24
|
|
||||||
Kind: PackIconKind.ArrowRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Popup menu
|
|
||||||
PopupBox {
|
|
||||||
#Cell(0, 1)
|
|
||||||
Foreground: resource dyn "PrimaryHueMidForegroundBrush"
|
|
||||||
PlacementMode: LeftAndAlignTopEdges
|
|
||||||
|
|
||||||
StackPanel {
|
|
||||||
Button {
|
|
||||||
Command: bind ShowSettingsCommand
|
|
||||||
Content: "Settings"
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
Command: bind ShowAboutCommand
|
|
||||||
Content: "About"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Progress
|
|
||||||
ProgressBar {
|
|
||||||
Background: Transparent
|
|
||||||
IsIndeterminate: true
|
|
||||||
Visibility: bind IsBusy
|
|
||||||
convert (bool b) => b ? Visibility.Visible : Visibility.Hidden
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content
|
|
||||||
Grid {
|
|
||||||
DockPanel {
|
|
||||||
Background: resource dyn "MaterialDesignCardBackground"
|
|
||||||
Visibility: bind IsDataAvailable
|
|
||||||
convert (bool b) => b ? Visibility.Visible : Visibility.Hidden
|
|
||||||
|
|
||||||
// Guilds
|
|
||||||
Border {
|
|
||||||
DockPanel.Dock: Left
|
|
||||||
BorderBrush: resource dyn "DividerBrush"
|
|
||||||
BorderThickness: "0 0 1 0"
|
|
||||||
|
|
||||||
ListBox {
|
|
||||||
ItemsSource: bind AvailableGuilds
|
|
||||||
ScrollViewer.VerticalScrollBarVisibility: Hidden
|
|
||||||
SelectedItem: bind SelectedGuild
|
|
||||||
VirtualizingStackPanel.IsVirtualizing: false
|
|
||||||
|
|
||||||
ItemTemplate: DataTemplate {
|
|
||||||
TransitioningContent {
|
|
||||||
OpeningEffect: TransitionEffect {
|
|
||||||
Duration: "0:0:0.3"
|
|
||||||
Kind: SlideInFromLeft
|
|
||||||
}
|
|
||||||
|
|
||||||
Border {
|
|
||||||
Margin: -8
|
|
||||||
Background: Transparent
|
|
||||||
Cursor: CursorType.Hand
|
|
||||||
|
|
||||||
Image {
|
|
||||||
Margin: [12, 4, 12, 4]
|
|
||||||
Width: 48
|
|
||||||
Height: 48
|
|
||||||
Source: bind IconUrl
|
|
||||||
ToolTip: bind Name
|
|
||||||
OpacityMask: RadialGradientBrush {
|
|
||||||
GradientStops: [
|
|
||||||
GradientStop { Color: "#FF000000", Offset: 0 }
|
|
||||||
GradientStop { Color: "#FF000000", Offset: 0.96 }
|
|
||||||
GradientStop { Color: "#00000000", Offset: 1 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channels
|
|
||||||
Border {
|
|
||||||
ListBox {
|
|
||||||
ItemsSource: bind AvailableChannels
|
|
||||||
HorizontalContentAlignment: Stretch
|
|
||||||
VirtualizingStackPanel.IsVirtualizing: false
|
|
||||||
|
|
||||||
ItemTemplate: DataTemplate {
|
|
||||||
TransitioningContent {
|
|
||||||
OpeningEffect: TransitionEffect {
|
|
||||||
Duration: "0:0:0.3"
|
|
||||||
Kind: SlideInFromLeft
|
|
||||||
}
|
|
||||||
|
|
||||||
@StackPanelHorizontal {
|
|
||||||
Margin: -8
|
|
||||||
Background: Transparent
|
|
||||||
Cursor: CursorType.Hand
|
|
||||||
InputBindings: [
|
|
||||||
MouseBinding {
|
|
||||||
Command: bind DataContext.ShowExportSetupCommand from $ancestor<ItemsControl>
|
|
||||||
CommandParameter: bind
|
|
||||||
MouseAction: LeftClick
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
PackIcon {
|
|
||||||
Margin: [16, 7, 0, 6]
|
|
||||||
Kind: PackIconKind.Pound
|
|
||||||
VerticalAlignment: Center
|
|
||||||
}
|
|
||||||
TextBlock {
|
|
||||||
Margin: [3, 8, 8, 8]
|
|
||||||
FontSize: 14
|
|
||||||
Text: bind Name
|
|
||||||
VerticalAlignment: Center
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content placeholder
|
|
||||||
StackPanel {
|
|
||||||
Margin: [32, 32, 8, 8]
|
|
||||||
Visibility: bind IsDataAvailable
|
|
||||||
convert (bool b) => b ? Visibility.Hidden : Visibility.Visible
|
|
||||||
|
|
||||||
TextBlock {
|
|
||||||
FontSize: 18
|
|
||||||
Text: "DiscordChatExporter needs your authorization token to work."
|
|
||||||
}
|
|
||||||
|
|
||||||
TextBlock {
|
|
||||||
Margin: [0, 8, 0, 0]
|
|
||||||
FontSize: 16
|
|
||||||
Text: "To obtain it, follow these steps:"
|
|
||||||
}
|
|
||||||
|
|
||||||
TextBlock {
|
|
||||||
Margin: [8, 0, 0, 0]
|
|
||||||
FontSize: 14
|
|
||||||
|
|
||||||
Run {
|
|
||||||
Text: "1. Open the Discord app"
|
|
||||||
}
|
|
||||||
LineBreak { }
|
|
||||||
Run {
|
|
||||||
Text: "2. Log in if you haven't"
|
|
||||||
}
|
|
||||||
LineBreak { }
|
|
||||||
Run {
|
|
||||||
Text: "3. Press"
|
|
||||||
}
|
|
||||||
Run {
|
|
||||||
Text: "Ctrl+Shift+I"
|
|
||||||
Foreground: resource dyn "PrimaryTextBrush"
|
|
||||||
}
|
|
||||||
LineBreak { }
|
|
||||||
Run {
|
|
||||||
Text: "4. Navigate to"
|
|
||||||
}
|
|
||||||
Run {
|
|
||||||
Text: "Application"
|
|
||||||
Foreground: resource dyn "PrimaryTextBrush"
|
|
||||||
}
|
|
||||||
Run { Text: "tab" }
|
|
||||||
LineBreak { }
|
|
||||||
Run {
|
|
||||||
Text: "5. Expand"
|
|
||||||
}
|
|
||||||
Run {
|
|
||||||
Text: "Storage > Local Storage > https://discordapp.com"
|
|
||||||
Foreground: resource dyn "PrimaryTextBrush"
|
|
||||||
}
|
|
||||||
LineBreak { }
|
|
||||||
Run {
|
|
||||||
Text: "6. Find"
|
|
||||||
}
|
|
||||||
Run {
|
|
||||||
Text: ""token""
|
|
||||||
Foreground: resource dyn "PrimaryTextBrush"
|
|
||||||
}
|
|
||||||
Run {
|
|
||||||
Text: "under key and copy the value"
|
|
||||||
}
|
|
||||||
LineBreak { }
|
|
||||||
Run {
|
|
||||||
Text: "7. Paste the value in the textbox above"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using DiscordChatExporter.Messages;
|
|
||||||
using GalaSoft.MvvmLight.Messaging;
|
|
||||||
using MaterialDesignThemes.Wpf;
|
|
||||||
using Tyrrrz.Extensions;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Views
|
|
||||||
{
|
|
||||||
public partial class MainWindow
|
|
||||||
{
|
|
||||||
public MainWindow()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
Title += $" v{Assembly.GetExecutingAssembly().GetName().Version}";
|
|
||||||
|
|
||||||
// Dialogs
|
|
||||||
Messenger.Default.Register<ShowErrorMessage>(this,
|
|
||||||
m => DialogHost.Show(new ErrorDialog()).Forget());
|
|
||||||
Messenger.Default.Register<ShowExportDoneMessage>(this,
|
|
||||||
m => DialogHost.Show(new ExportDoneDialog()).Forget());
|
|
||||||
Messenger.Default.Register<ShowExportSetupMessage>(this,
|
|
||||||
m => DialogHost.Show(new ExportSetupDialog()).Forget());
|
|
||||||
Messenger.Default.Register<ShowSettingsMessage>(this,
|
|
||||||
m => DialogHost.Show(new SettingsDialog()).Forget());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using MaterialDesignThemes.Wpf
|
|
||||||
|
|
||||||
UserControl "DiscordChatExporter.Views.SettingsDialog" {
|
|
||||||
DataContext: bind SettingsViewModel from $resource Container
|
|
||||||
Width: 250
|
|
||||||
|
|
||||||
StackPanel {
|
|
||||||
// Date format
|
|
||||||
TextBox {
|
|
||||||
Margin: [16, 16, 16, 8]
|
|
||||||
HintAssist.Hint: "Date format"
|
|
||||||
HintAssist.IsFloating: true
|
|
||||||
Text: bind DateFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group limit
|
|
||||||
TextBox {
|
|
||||||
Margin: [16, 8, 16, 8]
|
|
||||||
HintAssist.Hint: "Message group limit"
|
|
||||||
HintAssist.IsFloating: true
|
|
||||||
Text: bind MessageGroupLimit
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save
|
|
||||||
Button {
|
|
||||||
Margin: 8
|
|
||||||
Command: DialogHost.CloseDialogCommand
|
|
||||||
Content: "SAVE"
|
|
||||||
HorizontalAlignment: Right
|
|
||||||
Style: resource dyn "MaterialDesignFlatButton"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
mixin TwoColumns (one = "*", two = "*") for Grid {
|
|
||||||
combine ColumnDefinitions: [
|
|
||||||
ColumnDefinition { Width: $one }
|
|
||||||
ColumnDefinition { Width: $two }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin ThreeColumns (one = none, two = none, three = none) for Grid {
|
|
||||||
#TwoColumns($one, $two)
|
|
||||||
combine ColumnDefinitions: ColumnDefinition { Width: $three }
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin FourColumns (one = none, two = none, three = none, four = none) for Grid {
|
|
||||||
#ThreeColumns($one, $two, $three)
|
|
||||||
combine ColumnDefinitions: ColumnDefinition { Width: $four }
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin FiveColumns (one = none, two = none, three = none, four = none, five = none) for Grid {
|
|
||||||
#FourColumns($one, $two, $three, $four)
|
|
||||||
combine ColumnDefinitions: ColumnDefinition { Width: $five }
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin TwoRows (one = none, two = none) for Grid
|
|
||||||
{
|
|
||||||
combine RowDefinitions: [
|
|
||||||
RowDefinition { Height: $one }
|
|
||||||
RowDefinition { Height: $two }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin ThreeRows (one = none, two = none, three = none) for Grid
|
|
||||||
{
|
|
||||||
#TwoRows($one, $two)
|
|
||||||
combine RowDefinitions: RowDefinition { Height: $three }
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin FourRows (one = none, two = none, three = none, four = none) for Grid
|
|
||||||
{
|
|
||||||
#ThreeRows($one, $two, $three)
|
|
||||||
combine RowDefinitions: RowDefinition { Height: $four }
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin FiveRows (one = none, two = none, three = none, four = none, five = none) for Grid
|
|
||||||
{
|
|
||||||
#FourRows($one, $two, $three, $four)
|
|
||||||
combine RowDefinitions: RowDefinition { Height: $five }
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin Cell (row = none, column = none, rowSpan = none, columnSpan = none) for FrameworkElement {
|
|
||||||
Grid.Row: $row
|
|
||||||
Grid.Column: $column
|
|
||||||
Grid.RowSpan: $rowSpan
|
|
||||||
Grid.ColumnSpan: $columnSpan
|
|
||||||
}
|
|
||||||
|
|
||||||
alias ImageCached(source) {
|
|
||||||
Image {
|
|
||||||
Source: BitmapImage {
|
|
||||||
UriCachePolicy: "Revalidate"
|
|
||||||
UriSource: $source
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin Setter(property, value, targetName=none) for Style {
|
|
||||||
Setter { Property: $property, Value: $value, TargetName: $targetName }
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
mixin AddSetter(property, value, targetName=none) for Style {
|
|
||||||
combine Setters: #Setter($property, $value, $targetName) {}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
alias DataTrigger(binding, bindingValue) {
|
|
||||||
DataTrigger { Binding: $binding, Value: $bindingValue }
|
|
||||||
}
|
|
||||||
|
|
||||||
alias Trigger(property, value) {
|
|
||||||
Trigger { Property: $property, Value: $value }
|
|
||||||
}
|
|
||||||
|
|
||||||
alias EventTrigger(event, sourceName=none) {
|
|
||||||
EventTrigger { RoutedEvent: $event, SourceName: $sourceName }
|
|
||||||
}
|
|
||||||
|
|
||||||
alias DataTrigger_SetProperty(binding, bindingValue, property, propertyValue) {
|
|
||||||
@DataTrigger ($binding, $bindingValue) {
|
|
||||||
#Setter($property, $propertyValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
alias Trigger_SetProperty(triggerProperty, triggerValue, property, propertyValue) {
|
|
||||||
@Trigger ($triggerProperty, $triggerValue) {
|
|
||||||
#Setter($property, $propertyValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
alias EventTrigger_SetProperty(event, property, propertyValue) {
|
|
||||||
@EventTrigger ($event) {
|
|
||||||
#Setter($property, $propertyValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
alias VisibleIf_DataTrigger(binding, valueForVisible) {
|
|
||||||
@DataTrigger_SetProperty($binding, $valueForVisible, "Visibility", "Visible") {}
|
|
||||||
}
|
|
||||||
|
|
||||||
alias CollapsedIf_DataTrigger(binding, valueForCollapsed) {
|
|
||||||
@DataTrigger_SetProperty($binding, $valueForCollapsed, "Visibility", "Collapsed") {}
|
|
||||||
}
|
|
||||||
|
|
||||||
alias StackPanelHorizontal() {
|
|
||||||
StackPanel {
|
|
||||||
Orientation: Horizontal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
alias GridItemsControl() {
|
|
||||||
ItemsControl {
|
|
||||||
ScrollViewer.HorizontalScrollBarVisibility: Disabled,
|
|
||||||
|
|
||||||
ItemsPanel: ItemsPanelTemplate {
|
|
||||||
WrapPanel {
|
|
||||||
IsItemsHost: true
|
|
||||||
Orientation: Horizontal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////
|
|
||||||
// Animations //
|
|
||||||
////////////////
|
|
||||||
|
|
||||||
alias DoubleAnimation(property, frm = "0", to = "1", duration = "0:0:1", targetName=none, beginTime=none) {
|
|
||||||
DoubleAnimation {
|
|
||||||
Storyboard.TargetProperty: $property
|
|
||||||
Storyboard.TargetName: $targetName
|
|
||||||
From: $frm
|
|
||||||
To: $to
|
|
||||||
Duration: $duration
|
|
||||||
BeginTime: $beginTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
alias DoubleAnimationStoryboard (property, frm = "0", to = "1", duration = "0:0:1", targetName=none) {
|
|
||||||
BeginStoryboard {
|
|
||||||
Storyboard {
|
|
||||||
@DoubleAnimation($property, $frm, $to, $duration, $targetName) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin DoubleAnimation_PropertyTrigger(triggerProperty, triggerValue, animationProperty, frm, to, duration) for Style {
|
|
||||||
combine Triggers: @Trigger ($triggerProperty, $triggerValue) {
|
|
||||||
EnterActions: @DoubleAnimationStoryboard($animationProperty, $frm, $to, $duration) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin DoubleAnimation_PropertyTrigger_Toggle(triggerProperty, triggerValue, animationProperty, frm, to, duration) for Style {
|
|
||||||
combine Triggers: @Trigger ($triggerProperty, $triggerValue) {
|
|
||||||
EnterActions: @DoubleAnimationStoryboard($animationProperty, $frm, $to, $duration) {}
|
|
||||||
ExitActions: @DoubleAnimationStoryboard($animationProperty, $to, $frm, $duration) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin DoubleAnimation_EventTrigger(triggerEvent, animationProperty, frm, to, duration) for Style {
|
|
||||||
combine Triggers: EventTrigger {
|
|
||||||
RoutedEvent: $triggerEvent
|
|
||||||
@DoubleAnimationStoryboard($animationProperty, $frm, $to, $duration) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin DoubleAnimation_DataTrigger(binding, value, animationProperty, frm, to, duration) for Style {
|
|
||||||
combine Triggers: DataTrigger {
|
|
||||||
Binding: $binding
|
|
||||||
Value: $value
|
|
||||||
EnterActions: @DoubleAnimationStoryboard($animationProperty, $frm, $to, $duration) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin FadeIn_OnProperty(property, value, frm = "0", to = "1", duration = "0:0:1") for Style {
|
|
||||||
#DoubleAnimation_PropertyTrigger($property, $value, "Opacity", $frm, $to, $duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin FadeOut_OnProperty(property, value, frm = "1", to = "0", duration = "0:0:1") for Style {
|
|
||||||
#DoubleAnimation_PropertyTrigger($property, $value, "Opacity", $frm, $to, $duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin FadeIn_OnEvent(event, frm = "0", to = "1", duration = "0:0:1") for Style {
|
|
||||||
#DoubleAnimation_EventTrigger($event, "Opacity", $frm, $to, $duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin FadeOut_OnEvent(event, frm = "1", to = "0", duration = "0:0:1") for Style {
|
|
||||||
#DoubleAnimation_EventTrigger($event, "Opacity", $frm, $to, $duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin FadeIn_OnData(binding, value, from_ = "0", to = "1", duration = "0:0:1") for Style {
|
|
||||||
#DoubleAnimation_DataTrigger($binding, $value, "Opacity", $from_, $to, $duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin FadeOut_OnData(binding, value, from_ = "1", to = "0", duration = "0:0:1") for Style {
|
|
||||||
#DoubleAnimation_DataTrigger($binding, $value, "Opacity", $from_, $to, $duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin Property_OnBinding(binding, bindingValue, property, propertyValue, initialValue) for Style {
|
|
||||||
#Setter("Visibility", $initialValue)
|
|
||||||
combine Triggers: [
|
|
||||||
@DataTrigger_SetProperty($binding, $bindingValue, $property, $propertyValue) {}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin Visibility_OnBinding(binding, bindingValue, visibilityValue="Visible", initialValue="Collapsed") for Style {
|
|
||||||
#Property_OnBinding($binding, $bindingValue, "Visibility", $visibilityValue, $initialValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin Fade_OnBinding(binding, bindingValue) for Style {
|
|
||||||
#Setter("Visibility", "Visible")
|
|
||||||
#Setter("Opacity", "0")
|
|
||||||
|
|
||||||
combine Triggers: [
|
|
||||||
@DataTrigger($binding, $bindingValue) {
|
|
||||||
EnterActions: [
|
|
||||||
@DoubleAnimationStoryboard("Opacity", 0, 1, "0:0:0.5") {}
|
|
||||||
]
|
|
||||||
ExitActions: [
|
|
||||||
@DoubleAnimationStoryboard("Opacity", 1, 0, "0:0:0.5") {}
|
|
||||||
]
|
|
||||||
#Setter("Opacity", 1)
|
|
||||||
}
|
|
||||||
@Trigger("Opacity", 0) {
|
|
||||||
#Setter("Visibility", "Hidden")
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin MergeDictionary (source) for ResourceDictionary {
|
|
||||||
combine MergedDictionaries: ResourceDictionary { Source: $source }
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<packages>
|
|
||||||
<package id="Ammy" version="1.2.87" targetFramework="net461" />
|
|
||||||
<package id="Ammy.WPF" version="1.2.87" targetFramework="net461" />
|
|
||||||
<package id="CommonServiceLocator" version="1.3" targetFramework="net461" />
|
|
||||||
<package id="MaterialDesignColors" version="1.1.3" targetFramework="net461" />
|
|
||||||
<package id="MaterialDesignThemes" version="2.3.1.953" targetFramework="net461" />
|
|
||||||
<package id="MvvmLightLibs" version="5.3.0.0" targetFramework="net461" />
|
|
||||||
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net461" />
|
|
||||||
<package id="Tyrrrz.Extensions" version="1.4.1" targetFramework="net461" />
|
|
||||||
<package id="Tyrrrz.Settings" version="1.3.0" targetFramework="net461" />
|
|
||||||
</packages>
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2017 Alexey Golub
|
Copyright (c) 2017-2018 Alexey Golub
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -19,22 +19,29 @@ DiscordChatExporter can be used to export message history from a [Discord](https
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Intuitive GUI that displays available guilds and channels
|
- Intuitive GUI that displays available guilds and channels
|
||||||
|
- CLI as additional alternative to GUI
|
||||||
- Date ranges to limit messages
|
- Date ranges to limit messages
|
||||||
- Groups messages by author and time
|
- Groups messages by author and time
|
||||||
- Export to a plain text file
|
- Exports to a plain text file
|
||||||
- Export to an HTML file
|
- Exports to an HTML file
|
||||||
- Dark and light themes
|
- Dark and light themes
|
||||||
- User avatars
|
- User avatars
|
||||||
- Inline image attachments
|
- Inline image attachments
|
||||||
- Full markdown support
|
- Full markdown support
|
||||||
- Automatic links
|
- Automatic links
|
||||||
- Styled similarly to the app
|
- Styled similarly to the app
|
||||||
|
- Exports to a CSV file
|
||||||
|
- Renders custom emojis
|
||||||
|
- Resolves user, role and channel mentions
|
||||||
|
|
||||||
## Libraries used
|
## Libraries used
|
||||||
|
|
||||||
- [AmmyUI](https://github.com/AmmyUI/AmmyUI)
|
|
||||||
- [GalaSoft.MVVMLight](http://www.mvvmlight.net)
|
- [GalaSoft.MVVMLight](http://www.mvvmlight.net)
|
||||||
- [MaterialDesignInXamlToolkit](https://github.com/ButchersBoy/MaterialDesignInXamlToolkit)
|
- [MaterialDesignInXamlToolkit](https://github.com/ButchersBoy/MaterialDesignInXamlToolkit)
|
||||||
- [Newtonsoft.Json](http://www.newtonsoft.com/json)
|
- [Newtonsoft.Json](http://www.newtonsoft.com/json)
|
||||||
|
- [CsvHelper](https://github.com/JoshClose/CsvHelper)
|
||||||
|
- [Onova](https://github.com/Tyrrrz/Onova)
|
||||||
|
- [FluentCommandLineParser](https://github.com/fclp/fluent-command-line-parser)
|
||||||
- [Tyrrrz.Extensions](https://github.com/Tyrrrz/Extensions)
|
- [Tyrrrz.Extensions](https://github.com/Tyrrrz/Extensions)
|
||||||
|
- [Tyrrrz.WpfExtensions](https://github.com/Tyrrrz/WpfExtensions)
|
||||||
- [Tyrrrz.Settings](https://github.com/Tyrrrz/Settings)
|
- [Tyrrrz.Settings](https://github.com/Tyrrrz/Settings)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
version: '{build}'
|
||||||
|
|
||||||
|
image: Visual Studio 2017
|
||||||
|
configuration: Release
|
||||||
|
|
||||||
|
before_build:
|
||||||
|
- ps: nuget restore
|
||||||
|
|
||||||
|
build:
|
||||||
|
verbosity: minimal
|
||||||
|
|
||||||
|
after_build:
|
||||||
|
- ps: Deploy\Prepare.ps1
|
||||||
|
|
||||||
|
artifacts:
|
||||||
|
- path: Deploy\bin\DiscordChatExporter.zip
|
||||||
|
name: DiscordChatExporter.zip
|
||||||
|
- path: Deploy\bin\DiscordChatExporter.Cli.zip
|
||||||
|
name: DiscordChatExporter.Cli.zip
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
- provider: GitHub
|
||||||
|
auth_token:
|
||||||
|
secure: sjQHWRw29AMiVMn3MtidtWnAzAf1mJ+mkJ/7h1B9TIAHhkFrqwMK7LtXV+uNJ9AO
|
||||||
|
artifact: DiscordChatExporter.zip,DiscordChatExporter.Cli.zip
|
||||||
|
description: '[Changelog](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/Changelog.md)'
|
||||||
|
on:
|
||||||
|
branch: master
|
||||||
|
appveyor_repo_tag: true
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Reference in New Issue
Block a user