mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-02-14 07:43:31 -07:00
parent
65c5df89f4
commit
e4b0d60c40
|
|
@ -1,12 +1,19 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using DiscordChatExporter.Core.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Helpers
|
namespace DiscordChatExporter.Core.Helpers
|
||||||
{
|
{
|
||||||
public static class ExportHelper
|
public static class ExportHelper
|
||||||
{
|
{
|
||||||
|
public static bool IsDirectoryPath(string path)
|
||||||
|
=> path.Last() == Path.DirectorySeparatorChar ||
|
||||||
|
path.Last() == Path.AltDirectorySeparatorChar ||
|
||||||
|
Path.GetExtension(path).IsBlank();
|
||||||
|
|
||||||
public static string GetDefaultExportFileName(ExportFormat format, Guild guild, Channel channel,
|
public static string GetDefaultExportFileName(ExportFormat format, Guild guild, Channel channel,
|
||||||
DateTime? from = null, DateTime? to = null)
|
DateTime? from = null, DateTime? to = null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -121,9 +121,6 @@ namespace DiscordChatExporter.Core.Services
|
||||||
{
|
{
|
||||||
var result = new List<Message>();
|
var result = new List<Message>();
|
||||||
|
|
||||||
// Report indeterminate progress
|
|
||||||
progress?.Report(-1);
|
|
||||||
|
|
||||||
// Get the snowflakes for the selected range
|
// Get the snowflakes for the selected range
|
||||||
var firstId = from != null ? from.Value.ToSnowflake() : "0";
|
var firstId = from != null ? from.Value.ToSnowflake() : "0";
|
||||||
var lastId = to != null ? to.Value.ToSnowflake() : DateTime.MaxValue.ToSnowflake();
|
var lastId = to != null ? to.Value.ToSnowflake() : DateTime.MaxValue.ToSnowflake();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Gui.Behaviors
|
||||||
|
{
|
||||||
|
public class ChannelViewModelMultiSelectionListBoxBehavior : MultiSelectionListBoxBehavior<ChannelViewModel>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Interactivity;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Gui.Behaviors
|
||||||
|
{
|
||||||
|
public class MultiSelectionListBoxBehavior<T> : Behavior<ListBox>
|
||||||
|
{
|
||||||
|
public static readonly DependencyProperty SelectedItemsProperty =
|
||||||
|
DependencyProperty.Register(nameof(SelectedItems), typeof(IList),
|
||||||
|
typeof(MultiSelectionListBoxBehavior<T>),
|
||||||
|
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||||
|
OnSelectedItemsChanged));
|
||||||
|
|
||||||
|
private static void OnSelectedItemsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
|
||||||
|
{
|
||||||
|
var behavior = (MultiSelectionListBoxBehavior<T>) sender;
|
||||||
|
if (behavior._modelHandled) return;
|
||||||
|
|
||||||
|
if (behavior.AssociatedObject == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
behavior._modelHandled = true;
|
||||||
|
behavior.SelectItems();
|
||||||
|
behavior._modelHandled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _viewHandled;
|
||||||
|
private bool _modelHandled;
|
||||||
|
|
||||||
|
public IList SelectedItems
|
||||||
|
{
|
||||||
|
get => (IList) GetValue(SelectedItemsProperty);
|
||||||
|
set => SetValue(SelectedItemsProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagate selected items from model to view
|
||||||
|
private void SelectItems()
|
||||||
|
{
|
||||||
|
_viewHandled = true;
|
||||||
|
|
||||||
|
AssociatedObject.SelectedItems.Clear();
|
||||||
|
if (SelectedItems != null)
|
||||||
|
{
|
||||||
|
foreach (var item in SelectedItems)
|
||||||
|
AssociatedObject.SelectedItems.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
_viewHandled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagate selected items from view to model
|
||||||
|
private void OnListBoxSelectionChanged(object sender, SelectionChangedEventArgs args)
|
||||||
|
{
|
||||||
|
if (_viewHandled) return;
|
||||||
|
if (AssociatedObject.Items.SourceCollection == null) return;
|
||||||
|
|
||||||
|
SelectedItems = AssociatedObject.SelectedItems.Cast<T>().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-select items when the set of items changes
|
||||||
|
private void OnListBoxItemsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
||||||
|
{
|
||||||
|
if (_viewHandled) return;
|
||||||
|
if (AssociatedObject.Items.SourceCollection == null) return;
|
||||||
|
SelectItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnAttached()
|
||||||
|
{
|
||||||
|
base.OnAttached();
|
||||||
|
|
||||||
|
AssociatedObject.SelectionChanged += OnListBoxSelectionChanged;
|
||||||
|
((INotifyCollectionChanged) AssociatedObject.Items).CollectionChanged += OnListBoxItemsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void OnDetaching()
|
||||||
|
{
|
||||||
|
base.OnDetaching();
|
||||||
|
|
||||||
|
if (AssociatedObject != null)
|
||||||
|
{
|
||||||
|
AssociatedObject.SelectionChanged -= OnListBoxSelectionChanged;
|
||||||
|
((INotifyCollectionChanged) AssociatedObject.Items).CollectionChanged -= OnListBoxItemsChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ namespace DiscordChatExporter.Gui.Converters
|
||||||
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
var format = (ExportFormat?) value;
|
var format = value as ExportFormat?;
|
||||||
return format?.GetDisplayName();
|
return format?.GetDisplayName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@
|
||||||
<Compile Include="App.xaml.cs">
|
<Compile Include="App.xaml.cs">
|
||||||
<DependentUpon>App.xaml</DependentUpon>
|
<DependentUpon>App.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="Behaviors\ChannelViewModelMultiSelectionListBoxBehavior.cs" />
|
||||||
|
<Compile Include="Behaviors\MultiSelectionListBoxBehavior.cs" />
|
||||||
<Compile Include="Bootstrapper.cs" />
|
<Compile Include="Bootstrapper.cs" />
|
||||||
<Compile Include="Converters\ExportFormatToStringConverter.cs" />
|
<Compile Include="Converters\ExportFormatToStringConverter.cs" />
|
||||||
<Compile Include="ViewModels\Components\ChannelViewModel.cs" />
|
<Compile Include="ViewModels\Components\ChannelViewModel.cs" />
|
||||||
|
|
@ -62,6 +64,7 @@
|
||||||
<Compile Include="ViewModels\Dialogs\ExportSetupViewModel.cs" />
|
<Compile Include="ViewModels\Dialogs\ExportSetupViewModel.cs" />
|
||||||
<Compile Include="ViewModels\Framework\DialogManager.cs" />
|
<Compile Include="ViewModels\Framework\DialogManager.cs" />
|
||||||
<Compile Include="ViewModels\Framework\DialogScreen.cs" />
|
<Compile Include="ViewModels\Framework\DialogScreen.cs" />
|
||||||
|
<Compile Include="ViewModels\Framework\Extensions.cs" />
|
||||||
<Compile Include="ViewModels\Framework\IViewModelFactory.cs" />
|
<Compile Include="ViewModels\Framework\IViewModelFactory.cs" />
|
||||||
<Compile Include="ViewModels\Dialogs\SettingsViewModel.cs" />
|
<Compile Include="ViewModels\Dialogs\SettingsViewModel.cs" />
|
||||||
<Compile Include="ViewModels\RootViewModel.cs" />
|
<Compile Include="ViewModels\RootViewModel.cs" />
|
||||||
|
|
@ -117,18 +120,27 @@
|
||||||
</Page>
|
</Page>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Gress">
|
||||||
|
<Version>1.0.2</Version>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="MaterialDesignColors">
|
<PackageReference Include="MaterialDesignColors">
|
||||||
<Version>1.1.3</Version>
|
<Version>1.1.3</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="MaterialDesignThemes">
|
<PackageReference Include="MaterialDesignThemes">
|
||||||
<Version>2.5.0.1205</Version>
|
<Version>2.5.0.1205</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Ookii.Dialogs.Wpf">
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="PropertyChanged.Fody">
|
<PackageReference Include="PropertyChanged.Fody">
|
||||||
<Version>2.6.0</Version>
|
<Version>2.6.0</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Stylet">
|
<PackageReference Include="Stylet">
|
||||||
<Version>1.1.22</Version>
|
<Version>1.1.22</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="System.Windows.Interactivity.WPF">
|
||||||
|
<Version>2.0.20525</Version>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="Tyrrrz.Extensions">
|
<PackageReference Include="Tyrrrz.Extensions">
|
||||||
<Version>1.5.1</Version>
|
<Version>1.5.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,11 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
||||||
|
|
||||||
public GuildViewModel Guild { get; set; }
|
public GuildViewModel Guild { get; set; }
|
||||||
|
|
||||||
public ChannelViewModel Channel { get; set; }
|
public IReadOnlyList<ChannelViewModel> Channels { get; set; }
|
||||||
|
|
||||||
public string FilePath { get; set; }
|
public bool IsSingleChannel => Channels.Count == 1;
|
||||||
|
|
||||||
|
public string OutputPath { get; set; }
|
||||||
|
|
||||||
public IReadOnlyList<ExportFormat> AvailableFormats =>
|
public IReadOnlyList<ExportFormat> AvailableFormats =>
|
||||||
Enum.GetValues(typeof(ExportFormat)).Cast<ExportFormat>().ToArray();
|
Enum.GetValues(typeof(ExportFormat)).Cast<ExportFormat>().ToArray();
|
||||||
|
|
@ -59,18 +61,33 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
||||||
if (To < From)
|
if (To < From)
|
||||||
To = From;
|
To = From;
|
||||||
|
|
||||||
// Generate default file name
|
// If single channel - prompt file path
|
||||||
var defaultFileName = ExportHelper.GetDefaultExportFileName(SelectedFormat, Guild, Channel, From, To);
|
if (IsSingleChannel)
|
||||||
|
{
|
||||||
|
// Get single channel
|
||||||
|
var channel = Channels.Single();
|
||||||
|
|
||||||
// Prompt for output file path
|
// Generate default file name
|
||||||
var ext = SelectedFormat.GetFileExtension();
|
var defaultFileName = ExportHelper.GetDefaultExportFileName(SelectedFormat, Guild, channel, From, To);
|
||||||
var filter = $"{ext.ToUpperInvariant()} files|*.{ext}";
|
|
||||||
FilePath = _dialogManager.PromptSaveFilePath(filter, defaultFileName);
|
// Generate filter
|
||||||
|
var ext = SelectedFormat.GetFileExtension();
|
||||||
|
var filter = $"{ext.ToUpperInvariant()} files|*.{ext}";
|
||||||
|
|
||||||
|
// Prompt user
|
||||||
|
OutputPath = _dialogManager.PromptSaveFilePath(filter, defaultFileName);
|
||||||
|
}
|
||||||
|
// If multiple channels - prompt dir path
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Prompt user
|
||||||
|
OutputPath = _dialogManager.PromptDirectoryPath();
|
||||||
|
}
|
||||||
|
|
||||||
// If canceled - return
|
// If canceled - return
|
||||||
if (FilePath.IsBlank())
|
if (OutputPath.IsBlank())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Close dialog
|
// Close dialog
|
||||||
Close(true);
|
Close(true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MaterialDesignThemes.Wpf;
|
using MaterialDesignThemes.Wpf;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
using Ookii.Dialogs.Wpf;
|
||||||
using Stylet;
|
using Stylet;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Gui.ViewModels.Framework
|
namespace DiscordChatExporter.Gui.ViewModels.Framework
|
||||||
|
|
@ -54,5 +55,17 @@ namespace DiscordChatExporter.Gui.ViewModels.Framework
|
||||||
// Show dialog and return result
|
// Show dialog and return result
|
||||||
return dialog.ShowDialog() == true ? dialog.FileName : null;
|
return dialog.ShowDialog() == true ? dialog.FileName : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string PromptDirectoryPath(string initialDirPath = "")
|
||||||
|
{
|
||||||
|
// Create dialog
|
||||||
|
var dialog = new VistaFolderBrowserDialog
|
||||||
|
{
|
||||||
|
SelectedPath = initialDirPath
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show dialog and return result
|
||||||
|
return dialog.ShowDialog() == true ? dialog.SelectedPath : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
44
DiscordChatExporter.Gui/ViewModels/Framework/Extensions.cs
Normal file
44
DiscordChatExporter.Gui/ViewModels/Framework/Extensions.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||||
|
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Gui.ViewModels.Framework
|
||||||
|
{
|
||||||
|
public static class Extensions
|
||||||
|
{
|
||||||
|
public static ChannelViewModel CreateChannelViewModel(this IViewModelFactory factory, Channel model,
|
||||||
|
string category = null)
|
||||||
|
{
|
||||||
|
var viewModel = factory.CreateChannelViewModel();
|
||||||
|
viewModel.Model = model;
|
||||||
|
viewModel.Category = category;
|
||||||
|
|
||||||
|
return viewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GuildViewModel CreateGuildViewModel(this IViewModelFactory factory, Guild model,
|
||||||
|
IReadOnlyList<ChannelViewModel> channels)
|
||||||
|
{
|
||||||
|
var viewModel = factory.CreateGuildViewModel();
|
||||||
|
viewModel.Model = model;
|
||||||
|
viewModel.Channels = channels;
|
||||||
|
|
||||||
|
return viewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExportSetupViewModel CreateExportSetupViewModel(this IViewModelFactory factory,
|
||||||
|
GuildViewModel guild, IReadOnlyList<ChannelViewModel> channels)
|
||||||
|
{
|
||||||
|
var viewModel = factory.CreateExportSetupViewModel();
|
||||||
|
viewModel.Guild = guild;
|
||||||
|
viewModel.Channels = channels;
|
||||||
|
|
||||||
|
return viewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExportSetupViewModel CreateExportSetupViewModel(this IViewModelFactory factory,
|
||||||
|
GuildViewModel guild, ChannelViewModel channel)
|
||||||
|
=> factory.CreateExportSetupViewModel(guild, new[] {channel});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using DiscordChatExporter.Core.Exceptions;
|
using DiscordChatExporter.Core.Exceptions;
|
||||||
|
using DiscordChatExporter.Core.Helpers;
|
||||||
using DiscordChatExporter.Core.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
using DiscordChatExporter.Core.Services;
|
using DiscordChatExporter.Core.Services;
|
||||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||||
|
using Gress;
|
||||||
using MaterialDesignThemes.Wpf;
|
using MaterialDesignThemes.Wpf;
|
||||||
using Stylet;
|
using Stylet;
|
||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
|
|
@ -23,13 +26,13 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||||
private readonly DataService _dataService;
|
private readonly DataService _dataService;
|
||||||
private readonly ExportService _exportService;
|
private readonly ExportService _exportService;
|
||||||
|
|
||||||
public SnackbarMessageQueue Notifications { get; } = new SnackbarMessageQueue(TimeSpan.FromSeconds(5));
|
public ISnackbarMessageQueue Notifications { get; } = new SnackbarMessageQueue(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
public bool IsEnabled { get; private set; } = true;
|
public IProgressManager ProgressManager { get; } = new ProgressManager();
|
||||||
|
|
||||||
public bool IsProgressIndeterminate => Progress < 0;
|
public bool IsBusy { get; private set; }
|
||||||
|
|
||||||
public double Progress { get; private set; }
|
public bool IsProgressIndeterminate { get; private set; }
|
||||||
|
|
||||||
public bool IsBotToken { get; set; }
|
public bool IsBotToken { get; set; }
|
||||||
|
|
||||||
|
|
@ -39,6 +42,8 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||||
|
|
||||||
public GuildViewModel SelectedGuild { get; set; }
|
public GuildViewModel SelectedGuild { get; set; }
|
||||||
|
|
||||||
|
public IReadOnlyList<ChannelViewModel> SelectedChannels { get; set; }
|
||||||
|
|
||||||
public RootViewModel(IViewModelFactory viewModelFactory, DialogManager dialogManager,
|
public RootViewModel(IViewModelFactory viewModelFactory, DialogManager dialogManager,
|
||||||
SettingsService settingsService, UpdateService updateService, DataService dataService,
|
SettingsService settingsService, UpdateService updateService, DataService dataService,
|
||||||
ExportService exportService)
|
ExportService exportService)
|
||||||
|
|
@ -52,7 +57,14 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||||
|
|
||||||
// Set title
|
// Set title
|
||||||
var version = Assembly.GetExecutingAssembly().GetName().Version.ToString(3);
|
var version = Assembly.GetExecutingAssembly().GetName().Version.ToString(3);
|
||||||
DisplayName = $"DiscordChatExporter v{version}";
|
DisplayName = $"DiscordChatExporter v{version}";
|
||||||
|
|
||||||
|
// Update busy state when progress manager changes
|
||||||
|
ProgressManager.Bind(o => o.IsActive, (sender, args) => IsBusy = ProgressManager.IsActive);
|
||||||
|
ProgressManager.Bind(o => o.IsActive,
|
||||||
|
(sender, args) => IsProgressIndeterminate = ProgressManager.IsActive && ProgressManager.Progress <= 0);
|
||||||
|
ProgressManager.Bind(o => o.Progress,
|
||||||
|
(sender, args) => IsProgressIndeterminate = ProgressManager.IsActive && ProgressManager.Progress <= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnViewLoaded()
|
protected override async void OnViewLoaded()
|
||||||
|
|
@ -110,16 +122,15 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||||
await _dialogManager.ShowDialogAsync(dialog);
|
await _dialogManager.ShowDialogAsync(dialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanPopulateGuildsAndChannels => IsEnabled && TokenValue.IsNotBlank();
|
public bool CanPopulateGuildsAndChannels => !IsBusy && TokenValue.IsNotBlank();
|
||||||
|
|
||||||
public async void PopulateGuildsAndChannels()
|
public async void PopulateGuildsAndChannels()
|
||||||
{
|
{
|
||||||
|
// Create progress operation
|
||||||
|
var operation = ProgressManager.CreateOperation();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Set busy state and indeterminate progress
|
|
||||||
IsEnabled = false;
|
|
||||||
Progress = -1;
|
|
||||||
|
|
||||||
// Sanitize token
|
// Sanitize token
|
||||||
TokenValue = TokenValue.Trim('"');
|
TokenValue = TokenValue.Trim('"');
|
||||||
|
|
||||||
|
|
@ -134,7 +145,7 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||||
// Prepare available guild list
|
// Prepare available guild list
|
||||||
var availableGuilds = new List<GuildViewModel>();
|
var availableGuilds = new List<GuildViewModel>();
|
||||||
|
|
||||||
// Direct Messages
|
// Get direct messages
|
||||||
{
|
{
|
||||||
// Get fake guild
|
// Get fake guild
|
||||||
var guild = Guild.DirectMessages;
|
var guild = Guild.DirectMessages;
|
||||||
|
|
@ -150,66 +161,57 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||||
var category = channel.Type == ChannelType.DirectTextChat ? "Private" : "Group";
|
var category = channel.Type == ChannelType.DirectTextChat ? "Private" : "Group";
|
||||||
|
|
||||||
// Create channel view model
|
// Create channel view model
|
||||||
var channelViewModel = _viewModelFactory.CreateChannelViewModel();
|
var channelViewModel = _viewModelFactory.CreateChannelViewModel(channel, category);
|
||||||
channelViewModel.Model = channel;
|
|
||||||
channelViewModel.Category = category;
|
|
||||||
|
|
||||||
// Add to list
|
// Add to list
|
||||||
channelViewModels.Add(channelViewModel);
|
channelViewModels.Add(channelViewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create guild view model
|
// Create guild view model
|
||||||
var guildViewModel = _viewModelFactory.CreateGuildViewModel();
|
var guildViewModel = _viewModelFactory.CreateGuildViewModel(guild,
|
||||||
guildViewModel.Model = guild;
|
channelViewModels.OrderBy(c => c.Category)
|
||||||
guildViewModel.Channels = channelViewModels.OrderBy(c => c.Category)
|
.ThenBy(c => c.Model.Name)
|
||||||
.ThenBy(c => c.Model.Name)
|
.ToArray());
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
// Add to list
|
// Add to list
|
||||||
availableGuilds.Add(guildViewModel);
|
availableGuilds.Add(guildViewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guilds
|
// Get guilds
|
||||||
|
var guilds = await _dataService.GetUserGuildsAsync(token);
|
||||||
|
foreach (var guild in guilds)
|
||||||
{
|
{
|
||||||
// Get guilds
|
// Get channels
|
||||||
var guilds = await _dataService.GetUserGuildsAsync(token);
|
var channels = await _dataService.GetGuildChannelsAsync(token, guild.Id);
|
||||||
foreach (var guild in guilds)
|
|
||||||
|
// Get category channels
|
||||||
|
var categoryChannels = channels.Where(c => c.Type == ChannelType.Category).ToArray();
|
||||||
|
|
||||||
|
// Get text channels
|
||||||
|
var textChannels = channels.Where(c => c.Type == ChannelType.GuildTextChat).ToArray();
|
||||||
|
|
||||||
|
// Create channel view models
|
||||||
|
var channelViewModels = new List<ChannelViewModel>();
|
||||||
|
foreach (var channel in textChannels)
|
||||||
{
|
{
|
||||||
// Get channels
|
// Get category
|
||||||
var channels = await _dataService.GetGuildChannelsAsync(token, guild.Id);
|
var category = categoryChannels.FirstOrDefault(c => c.Id == channel.ParentId)?.Name;
|
||||||
|
|
||||||
// Get category channels
|
// Create channel view model
|
||||||
var categoryChannels = channels.Where(c => c.Type == ChannelType.Category).ToArray();
|
var channelViewModel = _viewModelFactory.CreateChannelViewModel(channel, category);
|
||||||
|
|
||||||
// Get text channels
|
|
||||||
var textChannels = channels.Where(c => c.Type == ChannelType.GuildTextChat).ToArray();
|
|
||||||
|
|
||||||
// Create channel view models
|
|
||||||
var channelViewModels = new List<ChannelViewModel>();
|
|
||||||
foreach (var channel in textChannels)
|
|
||||||
{
|
|
||||||
// Get category
|
|
||||||
var category = categoryChannels.FirstOrDefault(c => c.Id == channel.ParentId)?.Name;
|
|
||||||
|
|
||||||
// Create channel view model
|
|
||||||
var channelViewModel = _viewModelFactory.CreateChannelViewModel();
|
|
||||||
channelViewModel.Model = channel;
|
|
||||||
channelViewModel.Category = category;
|
|
||||||
|
|
||||||
// Add to list
|
|
||||||
channelViewModels.Add(channelViewModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create guild view model
|
|
||||||
var guildViewModel = _viewModelFactory.CreateGuildViewModel();
|
|
||||||
guildViewModel.Model = guild;
|
|
||||||
guildViewModel.Channels = channelViewModels.OrderBy(c => c.Category)
|
|
||||||
.ThenBy(c => c.Model.Name)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
// Add to list
|
// Add to list
|
||||||
availableGuilds.Add(guildViewModel);
|
channelViewModels.Add(channelViewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create guild view model
|
||||||
|
var guildViewModel = _viewModelFactory.CreateGuildViewModel(guild,
|
||||||
|
channelViewModels.OrderBy(c => c.Category)
|
||||||
|
.ThenBy(c => c.Model.Name)
|
||||||
|
.ToArray());
|
||||||
|
|
||||||
|
// Add to list
|
||||||
|
availableGuilds.Add(guildViewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update available guild list
|
// Update available guild list
|
||||||
|
|
@ -228,61 +230,73 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// Reset busy state and progress
|
// Dispose progress operation
|
||||||
Progress = 0;
|
operation.Dispose();
|
||||||
IsEnabled = true;
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanExportChannel => IsEnabled;
|
public bool CanExportChannels => !IsBusy && SelectedChannels.NotNullAndAny();
|
||||||
|
|
||||||
public async void ExportChannel(ChannelViewModel channel)
|
public async void ExportChannels()
|
||||||
{
|
{
|
||||||
try
|
// Get last used token
|
||||||
|
var token = _settingsService.LastToken;
|
||||||
|
|
||||||
|
// Create dialog
|
||||||
|
var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild, SelectedChannels);
|
||||||
|
|
||||||
|
// Show dialog, if canceled - return
|
||||||
|
if (await _dialogManager.ShowDialogAsync(dialog) != true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Create a progress operation for each channel to export
|
||||||
|
var operations = ProgressManager.CreateOperations(dialog.Channels.Count);
|
||||||
|
|
||||||
|
// Export channels
|
||||||
|
for (var i = 0; i < dialog.Channels.Count; i++)
|
||||||
{
|
{
|
||||||
// Set busy state and indeterminate progress
|
// Get operation and channel
|
||||||
IsEnabled = false;
|
var operation = operations[i];
|
||||||
Progress = -1;
|
var channel = dialog.Channels[i];
|
||||||
|
|
||||||
// Get last used token
|
try
|
||||||
var token = _settingsService.LastToken;
|
{
|
||||||
|
// Generate file path if necessary
|
||||||
|
var filePath = dialog.OutputPath;
|
||||||
|
if (ExportHelper.IsDirectoryPath(filePath))
|
||||||
|
{
|
||||||
|
// Generate default file name
|
||||||
|
var fileName = ExportHelper.GetDefaultExportFileName(dialog.SelectedFormat, dialog.Guild,
|
||||||
|
channel, dialog.From, dialog.To);
|
||||||
|
|
||||||
// Create dialog
|
// Combine paths
|
||||||
var dialog = _viewModelFactory.CreateExportSetupViewModel();
|
filePath = Path.Combine(filePath, fileName);
|
||||||
dialog.Guild = SelectedGuild;
|
}
|
||||||
dialog.Channel = channel;
|
|
||||||
|
|
||||||
// Show dialog, if canceled - return
|
// Get chat log
|
||||||
if (await _dialogManager.ShowDialogAsync(dialog) != true)
|
var chatLog = await _dataService.GetChatLogAsync(token, dialog.Guild, channel,
|
||||||
return;
|
dialog.From, dialog.To, operation);
|
||||||
|
|
||||||
// Create progress handler
|
// Export
|
||||||
var progressHandler = new Progress<double>(p => Progress = p);
|
_exportService.ExportChatLog(chatLog, filePath, dialog.SelectedFormat,
|
||||||
|
dialog.PartitionLimit);
|
||||||
|
|
||||||
// Get chat log
|
// Notify completion
|
||||||
var chatLog = await _dataService.GetChatLogAsync(token, dialog.Guild, dialog.Channel, dialog.From,
|
Notifications.Enqueue($"Channel [{channel.Model.Name}] successfully exported");
|
||||||
dialog.To, progressHandler);
|
}
|
||||||
|
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
|
||||||
// Export
|
{
|
||||||
_exportService.ExportChatLog(chatLog, dialog.FilePath, dialog.SelectedFormat,
|
Notifications.Enqueue($"You don't have access to channel [{channel.Model.Name}]");
|
||||||
dialog.PartitionLimit);
|
}
|
||||||
|
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
|
||||||
// Notify completion
|
{
|
||||||
Notifications.Enqueue("Export complete");
|
Notifications.Enqueue($"Channel [{channel.Model.Name}] doesn't exist");
|
||||||
}
|
}
|
||||||
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
|
finally
|
||||||
{
|
{
|
||||||
Notifications.Enqueue("You don't have access to this channel");
|
// Dispose progress operation
|
||||||
}
|
operation.Dispose();
|
||||||
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
|
}
|
||||||
{
|
|
||||||
Notifications.Enqueue("This channel doesn't exist");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
// Reset busy state and progress
|
|
||||||
Progress = 0;
|
|
||||||
IsEnabled = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,22 +30,33 @@
|
||||||
</Ellipse.Fill>
|
</Ellipse.Fill>
|
||||||
</Ellipse>
|
</Ellipse>
|
||||||
|
|
||||||
<!-- Guild and channel name -->
|
<!-- Placeholder (for multiple channels) -->
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="8,0,0,0"
|
Margin="8,0,0,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
FontSize="19"
|
FontSize="19"
|
||||||
TextTrimming="CharacterEllipsis">
|
Text="Multiple channels"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
Visibility="{Binding IsSingleChannel, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}" />
|
||||||
|
|
||||||
|
<!-- Category and channel name (for single channel) -->
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="19"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
Visibility="{Binding IsSingleChannel, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||||
<Run
|
<Run
|
||||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||||
Text="{Binding Channel.Category, Mode=OneWay}"
|
Text="{Binding Channels[0].Category, Mode=OneWay}"
|
||||||
ToolTip="{Binding Channel.Category, Mode=OneWay}" />
|
ToolTip="{Binding Channels[0].Category, Mode=OneWay}" />
|
||||||
<Run Text="/" />
|
<Run Text="/" />
|
||||||
<Run
|
<Run
|
||||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||||
Text="{Binding Channel.Model.Name, Mode=OneWay}"
|
Text="{Binding Channels[0].Model.Name, Mode=OneWay}"
|
||||||
ToolTip="{Binding Channel.Model.Name, Mode=OneWay}" />
|
ToolTip="{Binding Channels[0].Model.Name, Mode=OneWay}" />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
x:Class="DiscordChatExporter.Gui.Views.RootView"
|
x:Class="DiscordChatExporter.Gui.Views.RootView"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:behaviors="clr-namespace:DiscordChatExporter.Gui.Behaviors"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:s="https://github.com/canton7/Stylet"
|
xmlns:s="https://github.com/canton7/Stylet"
|
||||||
|
|
@ -30,7 +32,6 @@
|
||||||
<Grid
|
<Grid
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Background="{DynamicResource PrimaryHueMidBrush}"
|
Background="{DynamicResource PrimaryHueMidBrush}"
|
||||||
IsEnabled="{Binding IsEnabled}"
|
|
||||||
TextElement.Foreground="{DynamicResource SecondaryInverseTextBrush}">
|
TextElement.Foreground="{DynamicResource SecondaryInverseTextBrush}">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
|
|
@ -117,7 +118,7 @@
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Background="{DynamicResource PrimaryHueMidBrush}"
|
Background="{DynamicResource PrimaryHueMidBrush}"
|
||||||
IsIndeterminate="{Binding IsProgressIndeterminate}"
|
IsIndeterminate="{Binding IsProgressIndeterminate}"
|
||||||
Value="{Binding Progress, Mode=OneWay}" />
|
Value="{Binding ProgressManager.Progress, Mode=OneWay}" />
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<Grid Grid.Row="2">
|
<Grid Grid.Row="2">
|
||||||
|
|
@ -186,10 +187,7 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Guilds and channels -->
|
<!-- Guilds and channels -->
|
||||||
<Grid
|
<Grid Background="{DynamicResource MaterialDesignCardBackground}" Visibility="{Binding AvailableGuilds, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||||
Background="{DynamicResource MaterialDesignCardBackground}"
|
|
||||||
IsEnabled="{Binding IsEnabled}"
|
|
||||||
Visibility="{Binding AvailableGuilds, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
|
|
@ -203,7 +201,8 @@
|
||||||
<ListBox
|
<ListBox
|
||||||
ItemsSource="{Binding AvailableGuilds}"
|
ItemsSource="{Binding AvailableGuilds}"
|
||||||
ScrollViewer.VerticalScrollBarVisibility="Hidden"
|
ScrollViewer.VerticalScrollBarVisibility="Hidden"
|
||||||
SelectedItem="{Binding SelectedGuild}">
|
SelectedItem="{Binding SelectedGuild}"
|
||||||
|
SelectionMode="Single">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Grid
|
<Grid
|
||||||
|
|
@ -235,7 +234,13 @@
|
||||||
|
|
||||||
<!-- Channels -->
|
<!-- Channels -->
|
||||||
<Border Grid.Column="1">
|
<Border Grid.Column="1">
|
||||||
<ListBox HorizontalContentAlignment="Stretch" ItemsSource="{Binding SelectedGuild.Channels}">
|
<ListBox
|
||||||
|
HorizontalContentAlignment="Stretch"
|
||||||
|
ItemsSource="{Binding SelectedGuild.Channels}"
|
||||||
|
SelectionMode="Extended">
|
||||||
|
<i:Interaction.Behaviors>
|
||||||
|
<behaviors:ChannelViewModelMultiSelectionListBoxBehavior SelectedItems="{Binding SelectedChannels}" />
|
||||||
|
</i:Interaction.Behaviors>
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
|
|
@ -243,12 +248,6 @@
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
Cursor="Hand"
|
Cursor="Hand"
|
||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<StackPanel.InputBindings>
|
|
||||||
<MouseBinding
|
|
||||||
Command="{s:Action ExportChannel}"
|
|
||||||
CommandParameter="{Binding}"
|
|
||||||
MouseAction="LeftClick" />
|
|
||||||
</StackPanel.InputBindings>
|
|
||||||
<materialDesign:PackIcon
|
<materialDesign:PackIcon
|
||||||
Margin="16,7,0,6"
|
Margin="16,7,0,6"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
@ -257,9 +256,9 @@
|
||||||
Margin="3,8,8,8"
|
Margin="3,8,8,8"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
FontSize="14">
|
FontSize="14">
|
||||||
<Run Text="{Binding Category, Mode=OneWay}" Foreground="{DynamicResource SecondaryTextBrush}" />
|
<Run Foreground="{DynamicResource SecondaryTextBrush}" Text="{Binding Category, Mode=OneWay}" />
|
||||||
<Run Text="/" Foreground="{DynamicResource SecondaryTextBrush}" />
|
<Run Text="/" />
|
||||||
<Run Text="{Binding Model.Name, Mode=OneWay}" Foreground="{DynamicResource PrimaryTextBrush}" />
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="{Binding Model.Name, Mode=OneWay}" />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
@ -268,6 +267,20 @@
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Export button -->
|
||||||
|
<Button
|
||||||
|
Margin="32,24"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Command="{s:Action ExportChannels}"
|
||||||
|
Style="{StaticResource MaterialDesignFloatingActionAccentButton}"
|
||||||
|
Visibility="{Binding IsEnabled, RelativeSource={RelativeSource Self}, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||||
|
<materialDesign:PackIcon
|
||||||
|
Width="32"
|
||||||
|
Height="32"
|
||||||
|
Kind="Download" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
<!-- Notifications snackbar -->
|
<!-- Notifications snackbar -->
|
||||||
<materialDesign:Snackbar MessageQueue="{Binding Notifications}" />
|
<materialDesign:Snackbar MessageQueue="{Binding Notifications}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,9 @@ DiscordChatExporter can be used to export message history from a [Discord](https
|
||||||
- [Newtonsoft.Json](http://www.newtonsoft.com/json)
|
- [Newtonsoft.Json](http://www.newtonsoft.com/json)
|
||||||
- [Scriban](https://github.com/lunet-io/scriban)
|
- [Scriban](https://github.com/lunet-io/scriban)
|
||||||
- [CommandLineParser](https://github.com/commandlineparser/commandline)
|
- [CommandLineParser](https://github.com/commandlineparser/commandline)
|
||||||
|
- [Ookii.Dialogs](https://github.com/caioproiete/ookii-dialogs-wpf)
|
||||||
- [Failsafe](https://github.com/Tyrrrz/Failsafe)
|
- [Failsafe](https://github.com/Tyrrrz/Failsafe)
|
||||||
|
- [Gress](https://github.com/Tyrrrz/Gress)
|
||||||
- [Onova](https://github.com/Tyrrrz/Onova)
|
- [Onova](https://github.com/Tyrrrz/Onova)
|
||||||
- [Tyrrrz.Extensions](https://github.com/Tyrrrz/Extensions)
|
- [Tyrrrz.Extensions](https://github.com/Tyrrrz/Extensions)
|
||||||
- [Tyrrrz.Settings](https://github.com/Tyrrrz/Settings)
|
- [Tyrrrz.Settings](https://github.com/Tyrrrz/Settings)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue