mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-06-05 01:03:44 -04:00
Refactor portions of RootViewModel into DashboardViewModel
This commit is contained in:
parent
ad84ecf6a4
commit
e29f08264c
9 changed files with 628 additions and 545 deletions
|
@ -0,0 +1,227 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Core.Discord;
|
||||||
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
using DiscordChatExporter.Core.Exceptions;
|
||||||
|
using DiscordChatExporter.Core.Exporting;
|
||||||
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
using DiscordChatExporter.Gui.Services;
|
||||||
|
using DiscordChatExporter.Gui.Utils;
|
||||||
|
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||||
|
using DiscordChatExporter.Gui.ViewModels.Messages;
|
||||||
|
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||||
|
using Gress;
|
||||||
|
using Gress.Completable;
|
||||||
|
using Stylet;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Gui.ViewModels.Components;
|
||||||
|
|
||||||
|
public class DashboardViewModel : PropertyChangedBase
|
||||||
|
{
|
||||||
|
private readonly IViewModelFactory _viewModelFactory;
|
||||||
|
private readonly IEventAggregator _eventAggregator;
|
||||||
|
private readonly DialogManager _dialogManager;
|
||||||
|
private readonly SettingsService _settingsService;
|
||||||
|
|
||||||
|
private readonly AutoResetProgressMuxer _progressMuxer;
|
||||||
|
|
||||||
|
private DiscordClient? _discord;
|
||||||
|
|
||||||
|
public bool IsBusy { get; private set; }
|
||||||
|
|
||||||
|
public ProgressContainer<Percentage> Progress { get; } = new();
|
||||||
|
|
||||||
|
public bool IsProgressIndeterminate => IsBusy && Progress.Current.Fraction is <= 0 or >= 1;
|
||||||
|
|
||||||
|
public string? Token { get; set; }
|
||||||
|
|
||||||
|
private IReadOnlyDictionary<Guild, IReadOnlyList<Channel>>? GuildChannelMap { get; set; }
|
||||||
|
|
||||||
|
public IReadOnlyList<Guild>? AvailableGuilds => GuildChannelMap?.Keys.ToArray();
|
||||||
|
|
||||||
|
public Guild? SelectedGuild { get; set; }
|
||||||
|
|
||||||
|
public IReadOnlyList<Channel>? AvailableChannels => SelectedGuild is not null
|
||||||
|
? GuildChannelMap?[SelectedGuild]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
public IReadOnlyList<Channel>? SelectedChannels { get; set; }
|
||||||
|
|
||||||
|
public DashboardViewModel(
|
||||||
|
IViewModelFactory viewModelFactory,
|
||||||
|
IEventAggregator eventAggregator,
|
||||||
|
DialogManager dialogManager,
|
||||||
|
SettingsService settingsService)
|
||||||
|
{
|
||||||
|
_viewModelFactory = viewModelFactory;
|
||||||
|
_eventAggregator = eventAggregator;
|
||||||
|
_dialogManager = dialogManager;
|
||||||
|
_settingsService = settingsService;
|
||||||
|
|
||||||
|
_progressMuxer = Progress.CreateMuxer().WithAutoReset();
|
||||||
|
|
||||||
|
this.Bind(o => o.IsBusy, (_, _) => NotifyOfPropertyChange(() => IsProgressIndeterminate));
|
||||||
|
Progress.Bind(o => o.Current, (_, _) => NotifyOfPropertyChange(() => IsProgressIndeterminate));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnViewFullyLoaded()
|
||||||
|
{
|
||||||
|
if (_settingsService.LastToken is not null)
|
||||||
|
{
|
||||||
|
Token = _settingsService.LastToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void ShowSettings()
|
||||||
|
{
|
||||||
|
var dialog = _viewModelFactory.CreateSettingsViewModel();
|
||||||
|
await _dialogManager.ShowDialogAsync(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowHelp() => ProcessEx.StartShellExecute(App.GitHubProjectWikiUrl);
|
||||||
|
|
||||||
|
public bool CanPopulateGuildsAndChannels =>
|
||||||
|
!IsBusy && !string.IsNullOrWhiteSpace(Token);
|
||||||
|
|
||||||
|
public async void PopulateGuildsAndChannels()
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
var progress = _progressMuxer.CreateInput();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var token = Token?.Trim('"', ' ');
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_settingsService.LastToken = token;
|
||||||
|
|
||||||
|
var discord = new DiscordClient(token);
|
||||||
|
|
||||||
|
var guildChannelMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
|
||||||
|
await foreach (var guild in discord.GetUserGuildsAsync())
|
||||||
|
{
|
||||||
|
var channels = await discord.GetGuildChannelsAsync(guild.Id);
|
||||||
|
guildChannelMap[guild] = channels.Where(c => c.IsTextChannel).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
_discord = discord;
|
||||||
|
GuildChannelMap = guildChannelMap;
|
||||||
|
SelectedGuild = guildChannelMap.Keys.FirstOrDefault();
|
||||||
|
}
|
||||||
|
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||||
|
{
|
||||||
|
_eventAggregator.Publish(new NotificationMessage(ex.Message.TrimEnd('.')));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||||
|
"Error pulling guilds and channels",
|
||||||
|
ex.ToString()
|
||||||
|
);
|
||||||
|
|
||||||
|
await _dialogManager.ShowDialogAsync(dialog);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
progress.ReportCompletion();
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanExportChannels =>
|
||||||
|
!IsBusy &&
|
||||||
|
_discord is not null &&
|
||||||
|
SelectedGuild is not null &&
|
||||||
|
SelectedChannels is not null &&
|
||||||
|
SelectedChannels.Any();
|
||||||
|
|
||||||
|
public async void ExportChannels()
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_discord is null || SelectedGuild is null || SelectedChannels is null || !SelectedChannels.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild, SelectedChannels);
|
||||||
|
if (await _dialogManager.ShowDialogAsync(dialog) != true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var exporter = new ChannelExporter(_discord);
|
||||||
|
|
||||||
|
var progresses = Enumerable
|
||||||
|
.Range(0, dialog.Channels!.Count)
|
||||||
|
.Select(_ => _progressMuxer.CreateInput())
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var successfulExportCount = 0;
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(
|
||||||
|
dialog.Channels.Zip(progresses),
|
||||||
|
new ParallelOptions
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = Math.Max(1, _settingsService.ParallelLimit)
|
||||||
|
},
|
||||||
|
async (tuple, cancellationToken) =>
|
||||||
|
{
|
||||||
|
var (channel, progress) = tuple;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new ExportRequest(
|
||||||
|
dialog.Guild!,
|
||||||
|
channel,
|
||||||
|
dialog.OutputPath!,
|
||||||
|
dialog.SelectedFormat,
|
||||||
|
dialog.After?.Pipe(Snowflake.FromDate),
|
||||||
|
dialog.Before?.Pipe(Snowflake.FromDate),
|
||||||
|
dialog.PartitionLimit,
|
||||||
|
dialog.MessageFilter,
|
||||||
|
dialog.ShouldDownloadMedia,
|
||||||
|
_settingsService.ShouldReuseMedia,
|
||||||
|
_settingsService.DateFormat
|
||||||
|
);
|
||||||
|
|
||||||
|
await exporter.ExportChannelAsync(request, progress, cancellationToken);
|
||||||
|
|
||||||
|
Interlocked.Increment(ref successfulExportCount);
|
||||||
|
}
|
||||||
|
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||||
|
{
|
||||||
|
_eventAggregator.Publish(new NotificationMessage(ex.Message.TrimEnd('.')));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
progress.ReportCompletion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Notify of overall completion
|
||||||
|
if (successfulExportCount > 0)
|
||||||
|
{
|
||||||
|
_eventAggregator.Publish(
|
||||||
|
new NotificationMessage($"Successfully exported {successfulExportCount} channel(s)")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||||
|
"Error exporting channel(s)",
|
||||||
|
ex.ToString()
|
||||||
|
);
|
||||||
|
|
||||||
|
await _dialogManager.ShowDialogAsync(dialog);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||||
|
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Gui.ViewModels.Framework;
|
namespace DiscordChatExporter.Gui.ViewModels.Framework;
|
||||||
|
|
||||||
// Used to instantiate new view models while making use of dependency injection
|
// Used to instantiate new view models while making use of dependency injection
|
||||||
public interface IViewModelFactory
|
public interface IViewModelFactory
|
||||||
{
|
{
|
||||||
|
DashboardViewModel CreateDashboardViewModel();
|
||||||
|
|
||||||
ExportSetupViewModel CreateExportSetupViewModel();
|
ExportSetupViewModel CreateExportSetupViewModel();
|
||||||
|
|
||||||
MessageBoxViewModel CreateMessageBoxViewModel();
|
MessageBoxViewModel CreateMessageBoxViewModel();
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace DiscordChatExporter.Gui.ViewModels.Messages;
|
||||||
|
|
||||||
|
public class NotificationMessage
|
||||||
|
{
|
||||||
|
public string Text { get; }
|
||||||
|
|
||||||
|
public NotificationMessage(string text) => Text = text;
|
||||||
|
}
|
|
@ -1,59 +1,30 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Core.Discord;
|
|
||||||
using DiscordChatExporter.Core.Discord.Data;
|
|
||||||
using DiscordChatExporter.Core.Exceptions;
|
|
||||||
using DiscordChatExporter.Core.Exporting;
|
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using DiscordChatExporter.Gui.Services;
|
using DiscordChatExporter.Gui.Services;
|
||||||
using DiscordChatExporter.Gui.Utils;
|
using DiscordChatExporter.Gui.Utils;
|
||||||
|
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||||
|
using DiscordChatExporter.Gui.ViewModels.Messages;
|
||||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||||
using Gress;
|
|
||||||
using Gress.Completable;
|
|
||||||
using MaterialDesignThemes.Wpf;
|
using MaterialDesignThemes.Wpf;
|
||||||
using Stylet;
|
using Stylet;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Gui.ViewModels;
|
namespace DiscordChatExporter.Gui.ViewModels;
|
||||||
|
|
||||||
public class RootViewModel : Screen
|
public class RootViewModel : Screen, IHandle<NotificationMessage>, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IViewModelFactory _viewModelFactory;
|
private readonly IViewModelFactory _viewModelFactory;
|
||||||
private readonly DialogManager _dialogManager;
|
private readonly DialogManager _dialogManager;
|
||||||
private readonly SettingsService _settingsService;
|
private readonly SettingsService _settingsService;
|
||||||
private readonly UpdateService _updateService;
|
private readonly UpdateService _updateService;
|
||||||
|
|
||||||
private readonly AutoResetProgressMuxer _progressMuxer;
|
|
||||||
|
|
||||||
private DiscordClient? _discord;
|
|
||||||
|
|
||||||
public bool IsBusy { get; private set; }
|
|
||||||
|
|
||||||
public ProgressContainer<Percentage> Progress { get; } = new();
|
|
||||||
|
|
||||||
public bool IsProgressIndeterminate => IsBusy && Progress.Current.Fraction is <= 0 or >= 1;
|
|
||||||
|
|
||||||
public SnackbarMessageQueue Notifications { get; } = new(TimeSpan.FromSeconds(5));
|
public SnackbarMessageQueue Notifications { get; } = new(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
public string? Token { get; set; }
|
public DashboardViewModel Dashboard { get; }
|
||||||
|
|
||||||
private IReadOnlyDictionary<Guild, IReadOnlyList<Channel>>? GuildChannelMap { get; set; }
|
|
||||||
|
|
||||||
public IReadOnlyList<Guild>? AvailableGuilds => GuildChannelMap?.Keys.ToArray();
|
|
||||||
|
|
||||||
public Guild? SelectedGuild { get; set; }
|
|
||||||
|
|
||||||
public IReadOnlyList<Channel>? AvailableChannels => SelectedGuild is not null
|
|
||||||
? GuildChannelMap?[SelectedGuild]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
public IReadOnlyList<Channel>? SelectedChannels { get; set; }
|
|
||||||
|
|
||||||
public RootViewModel(
|
public RootViewModel(
|
||||||
IViewModelFactory viewModelFactory,
|
IViewModelFactory viewModelFactory,
|
||||||
|
IEventAggregator eventAggregator,
|
||||||
DialogManager dialogManager,
|
DialogManager dialogManager,
|
||||||
SettingsService settingsService,
|
SettingsService settingsService,
|
||||||
UpdateService updateService)
|
UpdateService updateService)
|
||||||
|
@ -62,13 +33,12 @@ public class RootViewModel : Screen
|
||||||
_dialogManager = dialogManager;
|
_dialogManager = dialogManager;
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
_updateService = updateService;
|
_updateService = updateService;
|
||||||
|
|
||||||
_progressMuxer = Progress.CreateMuxer().WithAutoReset();
|
eventAggregator.Subscribe(this);
|
||||||
|
|
||||||
|
Dashboard = _viewModelFactory.CreateDashboardViewModel();
|
||||||
|
|
||||||
DisplayName = $"{App.Name} v{App.VersionString}";
|
DisplayName = $"{App.Name} v{App.VersionString}";
|
||||||
|
|
||||||
this.Bind(o => o.IsBusy, (_, _) => NotifyOfPropertyChange(() => IsProgressIndeterminate));
|
|
||||||
Progress.Bind(o => o.Current, (_, _) => NotifyOfPropertyChange(() => IsProgressIndeterminate));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ShowWarInUkraineMessageAsync()
|
private async Task ShowWarInUkraineMessageAsync()
|
||||||
|
@ -115,23 +85,18 @@ Press LEARN MORE to find ways that you can help.".Trim(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a custom event that fires when the dialog host is loaded
|
|
||||||
public async void OnViewFullyLoaded()
|
public async void OnViewFullyLoaded()
|
||||||
{
|
{
|
||||||
await ShowWarInUkraineMessageAsync();
|
await ShowWarInUkraineMessageAsync();
|
||||||
|
await CheckForUpdatesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnViewLoaded()
|
protected override void OnViewLoaded()
|
||||||
{
|
{
|
||||||
base.OnViewLoaded();
|
base.OnViewLoaded();
|
||||||
|
|
||||||
_settingsService.Load();
|
_settingsService.Load();
|
||||||
|
|
||||||
if (_settingsService.LastToken is not null)
|
|
||||||
{
|
|
||||||
Token = _settingsService.LastToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_settingsService.IsDarkModeEnabled)
|
if (_settingsService.IsDarkModeEnabled)
|
||||||
{
|
{
|
||||||
App.SetDarkTheme();
|
App.SetDarkTheme();
|
||||||
|
@ -140,8 +105,6 @@ Press LEARN MORE to find ways that you can help.".Trim(),
|
||||||
{
|
{
|
||||||
App.SetLightTheme();
|
App.SetLightTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
await CheckForUpdatesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnClose()
|
protected override void OnClose()
|
||||||
|
@ -152,149 +115,8 @@ Press LEARN MORE to find ways that you can help.".Trim(),
|
||||||
_updateService.FinalizeUpdate(false);
|
_updateService.FinalizeUpdate(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void ShowSettings()
|
public void Handle(NotificationMessage message) =>
|
||||||
{
|
Notifications.Enqueue(message.Text);
|
||||||
var dialog = _viewModelFactory.CreateSettingsViewModel();
|
|
||||||
await _dialogManager.ShowDialogAsync(dialog);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ShowHelp() => ProcessEx.StartShellExecute(App.GitHubProjectWikiUrl);
|
public void Dispose() => Notifications.Dispose();
|
||||||
|
|
||||||
public bool CanPopulateGuildsAndChannels =>
|
|
||||||
!IsBusy && !string.IsNullOrWhiteSpace(Token);
|
|
||||||
|
|
||||||
public async void PopulateGuildsAndChannels()
|
|
||||||
{
|
|
||||||
IsBusy = true;
|
|
||||||
var progress = _progressMuxer.CreateInput();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var token = Token?.Trim('"', ' ');
|
|
||||||
if (string.IsNullOrWhiteSpace(token))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_settingsService.LastToken = token;
|
|
||||||
|
|
||||||
var discord = new DiscordClient(token);
|
|
||||||
|
|
||||||
var guildChannelMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
|
|
||||||
await foreach (var guild in discord.GetUserGuildsAsync())
|
|
||||||
{
|
|
||||||
var channels = await discord.GetGuildChannelsAsync(guild.Id);
|
|
||||||
guildChannelMap[guild] = channels.Where(c => c.IsTextChannel).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
_discord = discord;
|
|
||||||
GuildChannelMap = guildChannelMap;
|
|
||||||
SelectedGuild = guildChannelMap.Keys.FirstOrDefault();
|
|
||||||
}
|
|
||||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
|
||||||
{
|
|
||||||
Notifications.Enqueue(ex.Message.TrimEnd('.'));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
|
||||||
"Error pulling guilds and channels",
|
|
||||||
ex.ToString()
|
|
||||||
);
|
|
||||||
|
|
||||||
await _dialogManager.ShowDialogAsync(dialog);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
progress.ReportCompletion();
|
|
||||||
IsBusy = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanExportChannels =>
|
|
||||||
!IsBusy &&
|
|
||||||
_discord is not null &&
|
|
||||||
SelectedGuild is not null &&
|
|
||||||
SelectedChannels is not null &&
|
|
||||||
SelectedChannels.Any();
|
|
||||||
|
|
||||||
public async void ExportChannels()
|
|
||||||
{
|
|
||||||
IsBusy = true;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_discord is null || SelectedGuild is null || SelectedChannels is null || !SelectedChannels.Any())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild, SelectedChannels);
|
|
||||||
if (await _dialogManager.ShowDialogAsync(dialog) != true)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var exporter = new ChannelExporter(_discord);
|
|
||||||
|
|
||||||
var progresses = Enumerable
|
|
||||||
.Range(0, dialog.Channels!.Count)
|
|
||||||
.Select(_ => _progressMuxer.CreateInput())
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var successfulExportCount = 0;
|
|
||||||
|
|
||||||
await Parallel.ForEachAsync(
|
|
||||||
dialog.Channels.Zip(progresses),
|
|
||||||
new ParallelOptions
|
|
||||||
{
|
|
||||||
MaxDegreeOfParallelism = Math.Max(1, _settingsService.ParallelLimit)
|
|
||||||
},
|
|
||||||
async (tuple, cancellationToken) =>
|
|
||||||
{
|
|
||||||
var (channel, progress) = tuple;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var request = new ExportRequest(
|
|
||||||
dialog.Guild!,
|
|
||||||
channel,
|
|
||||||
dialog.OutputPath!,
|
|
||||||
dialog.SelectedFormat,
|
|
||||||
dialog.After?.Pipe(Snowflake.FromDate),
|
|
||||||
dialog.Before?.Pipe(Snowflake.FromDate),
|
|
||||||
dialog.PartitionLimit,
|
|
||||||
dialog.MessageFilter,
|
|
||||||
dialog.ShouldDownloadMedia,
|
|
||||||
_settingsService.ShouldReuseMedia,
|
|
||||||
_settingsService.DateFormat
|
|
||||||
);
|
|
||||||
|
|
||||||
await exporter.ExportChannelAsync(request, progress, cancellationToken);
|
|
||||||
|
|
||||||
Interlocked.Increment(ref successfulExportCount);
|
|
||||||
}
|
|
||||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
|
||||||
{
|
|
||||||
Notifications.Enqueue(ex.Message.TrimEnd('.'));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
progress.ReportCompletion();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Notify of overall completion
|
|
||||||
if (successfulExportCount > 0)
|
|
||||||
Notifications.Enqueue($"Successfully exported {successfulExportCount} channel(s)");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
|
||||||
"Error exporting channel(s)",
|
|
||||||
ex.ToString()
|
|
||||||
);
|
|
||||||
|
|
||||||
await _dialogManager.ShowDialogAsync(dialog);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsBusy = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
359
DiscordChatExporter.Gui/Views/Components/DashboardView.xaml
Normal file
359
DiscordChatExporter.Gui/Views/Components/DashboardView.xaml
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
<UserControl
|
||||||
|
x:Class="DiscordChatExporter.Gui.Views.Components.DashboardView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
|
||||||
|
xmlns:behaviors="clr-namespace:DiscordChatExporter.Gui.Behaviors"
|
||||||
|
xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
|
||||||
|
xmlns:components="clr-namespace:DiscordChatExporter.Gui.ViewModels.Components"
|
||||||
|
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:s="https://github.com/canton7/Stylet"
|
||||||
|
d:DataContext="{d:DesignInstance Type=components:DashboardViewModel}"
|
||||||
|
FocusManager.FocusedElement="{Binding ElementName=TokenValueTextBox}"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<CollectionViewSource x:Key="AvailableChannelsViewSource" Source="{Binding AvailableChannels, Mode=OneWay}">
|
||||||
|
<CollectionViewSource.GroupDescriptions>
|
||||||
|
<PropertyGroupDescription PropertyName="Category.Name" />
|
||||||
|
</CollectionViewSource.GroupDescriptions>
|
||||||
|
<CollectionViewSource.SortDescriptions>
|
||||||
|
<componentModel:SortDescription Direction="Ascending" PropertyName="Position" />
|
||||||
|
<componentModel:SortDescription Direction="Ascending" PropertyName="Name" />
|
||||||
|
</CollectionViewSource.SortDescriptions>
|
||||||
|
</CollectionViewSource>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid Loaded="{s:Action OnViewFullyLoaded}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<Grid Grid.Row="0" Background="{DynamicResource MaterialDesignDarkBackground}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Token and pull data button -->
|
||||||
|
<materialDesign:Card
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="12,12,0,12">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Token icon -->
|
||||||
|
<materialDesign:PackIcon
|
||||||
|
Grid.Column="0"
|
||||||
|
Width="24"
|
||||||
|
Height="24"
|
||||||
|
Margin="8"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||||
|
Kind="Password" />
|
||||||
|
|
||||||
|
<!-- Token value -->
|
||||||
|
<TextBox
|
||||||
|
x:Name="TokenValueTextBox"
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="0,6,6,8"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
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.Column="2"
|
||||||
|
Margin="0,6,6,6"
|
||||||
|
Padding="4"
|
||||||
|
Command="{s:Action PopulateGuildsAndChannels}"
|
||||||
|
IsDefault="True"
|
||||||
|
Style="{DynamicResource MaterialDesignFlatButton}"
|
||||||
|
ToolTip="Pull available guilds and channels (Enter)">
|
||||||
|
<materialDesign:PackIcon
|
||||||
|
Width="24"
|
||||||
|
Height="24"
|
||||||
|
Kind="ArrowRight" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</materialDesign:Card>
|
||||||
|
|
||||||
|
<!-- Settings button -->
|
||||||
|
<Button
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="6"
|
||||||
|
Padding="4"
|
||||||
|
Command="{s:Action ShowSettings}"
|
||||||
|
Foreground="{DynamicResource MaterialDesignDarkForeground}"
|
||||||
|
Style="{DynamicResource MaterialDesignFlatButton}"
|
||||||
|
ToolTip="Settings">
|
||||||
|
<Button.Resources>
|
||||||
|
<SolidColorBrush x:Key="MaterialDesignFlatButtonClick" Color="#4C4C4C" />
|
||||||
|
</Button.Resources>
|
||||||
|
<materialDesign:PackIcon
|
||||||
|
Width="24"
|
||||||
|
Height="24"
|
||||||
|
Kind="Settings" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Progress bar -->
|
||||||
|
<ProgressBar
|
||||||
|
Grid.Row="1"
|
||||||
|
Background="{DynamicResource MaterialDesignDarkBackground}"
|
||||||
|
IsIndeterminate="{Binding IsProgressIndeterminate}"
|
||||||
|
Value="{Binding Progress.Current.Fraction, Mode=OneWay}" />
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<Grid
|
||||||
|
Grid.Row="2"
|
||||||
|
Background="{DynamicResource MaterialDesignCardBackground}"
|
||||||
|
IsEnabled="{Binding IsBusy, Converter={x:Static converters:InverseBoolConverter.Instance}}">
|
||||||
|
<Grid.Resources>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="FontWeight" Value="Light" />
|
||||||
|
</Style>
|
||||||
|
</Grid.Resources>
|
||||||
|
<!-- Placeholder / usage instructions -->
|
||||||
|
<Grid Visibility="{Binding AvailableGuilds, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}">
|
||||||
|
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||||
|
<TextBlock Margin="32,16" FontSize="14">
|
||||||
|
<Run FontSize="18" Text="Please provide authentication token to continue" />
|
||||||
|
<LineBreak />
|
||||||
|
<LineBreak />
|
||||||
|
|
||||||
|
<!-- User token -->
|
||||||
|
<InlineUIContainer>
|
||||||
|
<materialDesign:PackIcon
|
||||||
|
Margin="1,0,0,-2"
|
||||||
|
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||||
|
Kind="Account" />
|
||||||
|
</InlineUIContainer>
|
||||||
|
<Run FontSize="16" Text="Authenticate using your personal account" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="1. Open Discord in your" />
|
||||||
|
<Run FontWeight="SemiBold" Text="web browser" />
|
||||||
|
<Run Text="and login" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="2. Press" />
|
||||||
|
<Run FontWeight="SemiBold" Text="Ctrl+Shift+I" />
|
||||||
|
<Run Text="to show developer tools" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="3. Press" />
|
||||||
|
<Run FontWeight="SemiBold" Text="Ctrl+Shift+M" />
|
||||||
|
<Run Text="to toggle device toolbar" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="4. Navigate to the" />
|
||||||
|
<Run FontWeight="SemiBold" Text="Application" />
|
||||||
|
<Run Text="tab" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="5. On the left, expand" />
|
||||||
|
<Run FontWeight="SemiBold" Text="Local Storage" />
|
||||||
|
<Run Text="and select" />
|
||||||
|
<Run FontWeight="SemiBold" Text="https://discord.com" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="6. Type" />
|
||||||
|
<Run FontWeight="SemiBold" Text="token" />
|
||||||
|
<Run Text="into the" />
|
||||||
|
<Run FontWeight="SemiBold" Text="Filter" />
|
||||||
|
<Run Text="box" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="7. If the token key does not appear, press" />
|
||||||
|
<Run FontWeight="SemiBold" Text="Ctrl+R" />
|
||||||
|
<Run Text="to reload" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="8. Copy the value of the" />
|
||||||
|
<Run FontWeight="SemiBold" Text="token" />
|
||||||
|
<Run Text="key" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="* Automating user accounts is technically against TOS, use at your own risk!" />
|
||||||
|
<LineBreak />
|
||||||
|
<LineBreak />
|
||||||
|
|
||||||
|
<!-- Bot token -->
|
||||||
|
<InlineUIContainer>
|
||||||
|
<materialDesign:PackIcon
|
||||||
|
Margin="1,0,0,-2"
|
||||||
|
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||||
|
Kind="Robot" />
|
||||||
|
</InlineUIContainer>
|
||||||
|
<Run FontSize="16" Text="Authenticate using a bot account" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="1. Open Discord developer portal" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="2. Open your application's settings" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="3. Navigate to the" />
|
||||||
|
<Run FontWeight="SemiBold" Text="Bot" />
|
||||||
|
<Run Text="section on the left" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="4. Under" />
|
||||||
|
<Run FontWeight="SemiBold" Text="Token" />
|
||||||
|
<Run Text="click" />
|
||||||
|
<Run FontWeight="SemiBold" Text="Copy" />
|
||||||
|
<LineBreak />
|
||||||
|
<LineBreak />
|
||||||
|
|
||||||
|
<Run FontSize="16" Text="If you have questions or issues, please refer to the" />
|
||||||
|
<Hyperlink Command="{s:Action ShowHelp}" FontSize="16">wiki</Hyperlink><Run FontSize="16" Text="." />
|
||||||
|
</TextBlock>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Guilds and channels -->
|
||||||
|
<Grid Background="{DynamicResource MaterialDesignCardBackground}" Visibility="{Binding AvailableGuilds, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Guilds -->
|
||||||
|
<Border
|
||||||
|
Grid.Column="0"
|
||||||
|
BorderBrush="{DynamicResource MaterialDesignDivider}"
|
||||||
|
BorderThickness="0,0,1,0">
|
||||||
|
<ListBox
|
||||||
|
ItemsSource="{Binding AvailableGuilds}"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Hidden"
|
||||||
|
SelectedItem="{Binding SelectedGuild}"
|
||||||
|
SelectionMode="Single">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid
|
||||||
|
Margin="-8"
|
||||||
|
Background="Transparent"
|
||||||
|
Cursor="Hand"
|
||||||
|
ToolTip="{Binding Name}">
|
||||||
|
<!-- Guild icon placeholder -->
|
||||||
|
<Ellipse
|
||||||
|
Width="48"
|
||||||
|
Height="48"
|
||||||
|
Margin="12,4,12,4"
|
||||||
|
Fill="{DynamicResource MaterialDesignDivider}" />
|
||||||
|
|
||||||
|
<!-- Guild icon -->
|
||||||
|
<Ellipse
|
||||||
|
Width="48"
|
||||||
|
Height="48"
|
||||||
|
Margin="12,4,12,4"
|
||||||
|
Stroke="{DynamicResource MaterialDesignDivider}"
|
||||||
|
StrokeThickness="1">
|
||||||
|
<Ellipse.Fill>
|
||||||
|
<ImageBrush ImageSource="{Binding IconUrl}" />
|
||||||
|
</Ellipse.Fill>
|
||||||
|
</Ellipse>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Channels -->
|
||||||
|
<Border Grid.Column="1">
|
||||||
|
<ListBox
|
||||||
|
HorizontalContentAlignment="Stretch"
|
||||||
|
ItemsSource="{Binding Source={StaticResource AvailableChannelsViewSource}}"
|
||||||
|
SelectionMode="Extended"
|
||||||
|
TextSearch.TextPath="Model.Name"
|
||||||
|
VirtualizingPanel.IsVirtualizingWhenGrouping="True">
|
||||||
|
<b:Interaction.Behaviors>
|
||||||
|
<behaviors:ChannelMultiSelectionListBoxBehavior SelectedItems="{Binding SelectedChannels}" />
|
||||||
|
</b:Interaction.Behaviors>
|
||||||
|
<ListBox.GroupStyle>
|
||||||
|
<GroupStyle>
|
||||||
|
<GroupStyle.ContainerStyle>
|
||||||
|
<Style TargetType="{x:Type GroupItem}">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate d:DataContext="{x:Type CollectionViewGroup}">
|
||||||
|
<Expander
|
||||||
|
Margin="0"
|
||||||
|
Padding="0"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderBrush="{DynamicResource MaterialDesignDivider}"
|
||||||
|
BorderThickness="0,0,0,1"
|
||||||
|
Header="{Binding Name}"
|
||||||
|
IsExpanded="False">
|
||||||
|
<ItemsPresenter />
|
||||||
|
</Expander>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</GroupStyle.ContainerStyle>
|
||||||
|
</GroupStyle>
|
||||||
|
</ListBox.GroupStyle>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid Margin="-8" Background="Transparent">
|
||||||
|
<Grid.InputBindings>
|
||||||
|
<MouseBinding Command="{s:Action ExportChannels}" MouseAction="LeftDoubleClick" />
|
||||||
|
</Grid.InputBindings>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Channel icon -->
|
||||||
|
<materialDesign:PackIcon
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="16,7,0,6"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Kind="Pound" />
|
||||||
|
|
||||||
|
<!-- Channel name -->
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="3,8,8,8"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="14"
|
||||||
|
Text="{Binding Name, Mode=OneWay}" />
|
||||||
|
|
||||||
|
<!-- Is selected checkmark -->
|
||||||
|
<materialDesign:PackIcon
|
||||||
|
Grid.Column="2"
|
||||||
|
Width="24"
|
||||||
|
Height="24"
|
||||||
|
Margin="8,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Kind="Check"
|
||||||
|
Visibility="{Binding IsSelected, RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Export button -->
|
||||||
|
<Button
|
||||||
|
Margin="32,24"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Command="{s:Action ExportChannels}"
|
||||||
|
Style="{DynamicResource MaterialDesignFloatingActionAccentButton}"
|
||||||
|
Visibility="{Binding CanExportChannels, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||||
|
<materialDesign:PackIcon
|
||||||
|
Width="32"
|
||||||
|
Height="32"
|
||||||
|
Kind="Download" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace DiscordChatExporter.Gui.Views.Components;
|
||||||
|
|
||||||
|
public partial class DashboardView
|
||||||
|
{
|
||||||
|
public DashboardView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@
|
||||||
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"
|
||||||
xmlns:system="clr-namespace:System;assembly=System.Runtime"
|
xmlns:system="clr-namespace:System;assembly=System.Runtime"
|
||||||
MinWidth="500"
|
Width="500"
|
||||||
d:DataContext="{d:DesignInstance Type=dialogs:MessageBoxViewModel}"
|
d:DataContext="{d:DesignInstance Type=dialogs:MessageBoxViewModel}"
|
||||||
Style="{DynamicResource MaterialDesignRoot}"
|
Style="{DynamicResource MaterialDesignRoot}"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
xmlns:dialogs="clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs"
|
xmlns:dialogs="clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs"
|
||||||
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"
|
||||||
Width="310"
|
Width="380"
|
||||||
d:DataContext="{d:DesignInstance Type=dialogs:SettingsViewModel}"
|
d:DataContext="{d:DesignInstance Type=dialogs:SettingsViewModel}"
|
||||||
Style="{DynamicResource MaterialDesignRoot}"
|
Style="{DynamicResource MaterialDesignRoot}"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
|
@ -2,11 +2,7 @@
|
||||||
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:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
|
|
||||||
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
|
|
||||||
xmlns: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"
|
||||||
|
@ -16,362 +12,21 @@
|
||||||
MinWidth="325"
|
MinWidth="325"
|
||||||
d:DataContext="{d:DesignInstance Type=viewModels:RootViewModel}"
|
d:DataContext="{d:DesignInstance Type=viewModels:RootViewModel}"
|
||||||
Background="{DynamicResource MaterialDesignPaper}"
|
Background="{DynamicResource MaterialDesignPaper}"
|
||||||
FocusManager.FocusedElement="{Binding ElementName=TokenValueTextBox}"
|
|
||||||
Icon="/DiscordChatExporter;component/favicon.ico"
|
Icon="/DiscordChatExporter;component/favicon.ico"
|
||||||
Style="{DynamicResource MaterialDesignRoot}"
|
Style="{DynamicResource MaterialDesignRoot}"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
<Window.TaskbarItemInfo>
|
<Window.TaskbarItemInfo>
|
||||||
<TaskbarItemInfo ProgressState="Normal" ProgressValue="{Binding Progress.Current.Fraction}" />
|
<TaskbarItemInfo ProgressState="Normal" ProgressValue="{Binding Dashboard.Progress.Current.Fraction}" />
|
||||||
</Window.TaskbarItemInfo>
|
</Window.TaskbarItemInfo>
|
||||||
<Window.Resources>
|
|
||||||
<CollectionViewSource x:Key="AvailableChannelsViewSource" Source="{Binding AvailableChannels, Mode=OneWay}">
|
|
||||||
<CollectionViewSource.GroupDescriptions>
|
|
||||||
<PropertyGroupDescription PropertyName="Category.Name" />
|
|
||||||
</CollectionViewSource.GroupDescriptions>
|
|
||||||
<CollectionViewSource.SortDescriptions>
|
|
||||||
<componentModel:SortDescription Direction="Ascending" PropertyName="Position" />
|
|
||||||
<componentModel:SortDescription Direction="Ascending" PropertyName="Name" />
|
|
||||||
</CollectionViewSource.SortDescriptions>
|
|
||||||
</CollectionViewSource>
|
|
||||||
</Window.Resources>
|
|
||||||
|
|
||||||
<materialDesign:DialogHost
|
<materialDesign:DialogHost
|
||||||
Loaded="{s:Action OnViewFullyLoaded}"
|
Loaded="{s:Action OnViewFullyLoaded}"
|
||||||
SnackbarMessageQueue="{Binding Notifications}"
|
SnackbarMessageQueue="{Binding Notifications}"
|
||||||
Style="{DynamicResource MaterialDesignEmbeddedDialogHost}">
|
Style="{DynamicResource MaterialDesignEmbeddedDialogHost}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<ContentControl s:View.Model="{Binding Dashboard}" />
|
||||||
<RowDefinition Height="Auto" />
|
<materialDesign:Snackbar MessageQueue="{Binding Notifications}" />
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- Toolbar -->
|
|
||||||
<Grid Grid.Row="0" Background="{DynamicResource MaterialDesignDarkBackground}">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- Token and pull data button -->
|
|
||||||
<materialDesign:Card
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="0"
|
|
||||||
Margin="12,12,0,12">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- Token icon -->
|
|
||||||
<materialDesign:PackIcon
|
|
||||||
Grid.Column="0"
|
|
||||||
Width="24"
|
|
||||||
Height="24"
|
|
||||||
Margin="8"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
|
||||||
Kind="Password" />
|
|
||||||
|
|
||||||
<!-- Token value -->
|
|
||||||
<TextBox
|
|
||||||
x:Name="TokenValueTextBox"
|
|
||||||
Grid.Column="1"
|
|
||||||
Margin="0,6,6,8"
|
|
||||||
VerticalAlignment="Bottom"
|
|
||||||
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.Column="2"
|
|
||||||
Margin="0,6,6,6"
|
|
||||||
Padding="4"
|
|
||||||
Command="{s:Action PopulateGuildsAndChannels}"
|
|
||||||
IsDefault="True"
|
|
||||||
Style="{DynamicResource MaterialDesignFlatButton}"
|
|
||||||
ToolTip="Pull available guilds and channels (Enter)">
|
|
||||||
<materialDesign:PackIcon
|
|
||||||
Width="24"
|
|
||||||
Height="24"
|
|
||||||
Kind="ArrowRight" />
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</materialDesign:Card>
|
|
||||||
|
|
||||||
<!-- Settings button -->
|
|
||||||
<Button
|
|
||||||
Grid.Column="1"
|
|
||||||
Margin="6"
|
|
||||||
Padding="4"
|
|
||||||
Command="{s:Action ShowSettings}"
|
|
||||||
Foreground="{DynamicResource MaterialDesignDarkForeground}"
|
|
||||||
Style="{DynamicResource MaterialDesignFlatButton}"
|
|
||||||
ToolTip="Settings">
|
|
||||||
<Button.Resources>
|
|
||||||
<SolidColorBrush x:Key="MaterialDesignFlatButtonClick" Color="#4C4C4C" />
|
|
||||||
</Button.Resources>
|
|
||||||
<materialDesign:PackIcon
|
|
||||||
Width="24"
|
|
||||||
Height="24"
|
|
||||||
Kind="Settings" />
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Progress bar -->
|
|
||||||
<ProgressBar
|
|
||||||
Grid.Row="1"
|
|
||||||
Background="{DynamicResource MaterialDesignDarkBackground}"
|
|
||||||
IsIndeterminate="{Binding IsProgressIndeterminate}"
|
|
||||||
Value="{Binding Progress.Current.Fraction, Mode=OneWay}" />
|
|
||||||
|
|
||||||
<!-- Content -->
|
|
||||||
<Grid
|
|
||||||
Grid.Row="2"
|
|
||||||
Background="{DynamicResource MaterialDesignCardBackground}"
|
|
||||||
IsEnabled="{Binding IsBusy, Converter={x:Static converters:InverseBoolConverter.Instance}}">
|
|
||||||
<Grid.Resources>
|
|
||||||
<Style TargetType="TextBlock">
|
|
||||||
<Setter Property="FontWeight" Value="Light" />
|
|
||||||
</Style>
|
|
||||||
</Grid.Resources>
|
|
||||||
<!-- Placeholder / usage instructions -->
|
|
||||||
<Grid Visibility="{Binding AvailableGuilds, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}">
|
|
||||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
|
||||||
<TextBlock Margin="32,16" FontSize="14">
|
|
||||||
<Run FontSize="18" Text="Please provide authentication token to continue" />
|
|
||||||
<LineBreak />
|
|
||||||
<LineBreak />
|
|
||||||
|
|
||||||
<!-- User token -->
|
|
||||||
<InlineUIContainer>
|
|
||||||
<materialDesign:PackIcon
|
|
||||||
Margin="1,0,0,-2"
|
|
||||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
|
||||||
Kind="Account" />
|
|
||||||
</InlineUIContainer>
|
|
||||||
<Run FontSize="16" Text="Authenticate using your personal account" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="1. Open Discord in your" />
|
|
||||||
<Run FontWeight="SemiBold" Text="web browser" />
|
|
||||||
<Run Text="and login" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="2. Press" />
|
|
||||||
<Run FontWeight="SemiBold" Text="Ctrl+Shift+I" />
|
|
||||||
<Run Text="to show developer tools" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="3. Press" />
|
|
||||||
<Run FontWeight="SemiBold" Text="Ctrl+Shift+M" />
|
|
||||||
<Run Text="to toggle device toolbar" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="4. Navigate to the" />
|
|
||||||
<Run FontWeight="SemiBold" Text="Application" />
|
|
||||||
<Run Text="tab" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="5. On the left, expand" />
|
|
||||||
<Run FontWeight="SemiBold" Text="Local Storage" />
|
|
||||||
<Run Text="and select" />
|
|
||||||
<Run FontWeight="SemiBold" Text="https://discord.com" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="6. Type" />
|
|
||||||
<Run FontWeight="SemiBold" Text="token" />
|
|
||||||
<Run Text="into the" />
|
|
||||||
<Run FontWeight="SemiBold" Text="Filter" />
|
|
||||||
<Run Text="box" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="7. If the token key does not appear, press" />
|
|
||||||
<Run FontWeight="SemiBold" Text="Ctrl+R" />
|
|
||||||
<Run Text="to reload" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="8. Copy the value of the" />
|
|
||||||
<Run FontWeight="SemiBold" Text="token" />
|
|
||||||
<Run Text="key" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="* Automating user accounts is technically against TOS, use at your own risk!" />
|
|
||||||
<LineBreak />
|
|
||||||
<LineBreak />
|
|
||||||
|
|
||||||
<!-- Bot token -->
|
|
||||||
<InlineUIContainer>
|
|
||||||
<materialDesign:PackIcon
|
|
||||||
Margin="1,0,0,-2"
|
|
||||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
|
||||||
Kind="Robot" />
|
|
||||||
</InlineUIContainer>
|
|
||||||
<Run FontSize="16" Text="Authenticate using a bot account" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="1. Open Discord developer portal" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="2. Open your application's settings" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="3. Navigate to the" />
|
|
||||||
<Run FontWeight="SemiBold" Text="Bot" />
|
|
||||||
<Run Text="section on the left" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="4. Under" />
|
|
||||||
<Run FontWeight="SemiBold" Text="Token" />
|
|
||||||
<Run Text="click" />
|
|
||||||
<Run FontWeight="SemiBold" Text="Copy" />
|
|
||||||
<LineBreak />
|
|
||||||
<LineBreak />
|
|
||||||
|
|
||||||
<Run FontSize="16" Text="If you have questions or issues, please refer to the" />
|
|
||||||
<Hyperlink Command="{s:Action ShowHelp}" FontSize="16">wiki</Hyperlink><Run FontSize="16" Text="." />
|
|
||||||
</TextBlock>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Guilds and channels -->
|
|
||||||
<Grid Background="{DynamicResource MaterialDesignCardBackground}" Visibility="{Binding AvailableGuilds, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- Guilds -->
|
|
||||||
<Border
|
|
||||||
Grid.Column="0"
|
|
||||||
BorderBrush="{DynamicResource MaterialDesignDivider}"
|
|
||||||
BorderThickness="0,0,1,0">
|
|
||||||
<ListBox
|
|
||||||
ItemsSource="{Binding AvailableGuilds}"
|
|
||||||
ScrollViewer.VerticalScrollBarVisibility="Hidden"
|
|
||||||
SelectedItem="{Binding SelectedGuild}"
|
|
||||||
SelectionMode="Single">
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<Grid
|
|
||||||
Margin="-8"
|
|
||||||
Background="Transparent"
|
|
||||||
Cursor="Hand"
|
|
||||||
ToolTip="{Binding Name}">
|
|
||||||
<!-- Guild icon placeholder -->
|
|
||||||
<Ellipse
|
|
||||||
Width="48"
|
|
||||||
Height="48"
|
|
||||||
Margin="12,4,12,4"
|
|
||||||
Fill="{DynamicResource MaterialDesignDivider}" />
|
|
||||||
|
|
||||||
<!-- Guild icon -->
|
|
||||||
<Ellipse
|
|
||||||
Width="48"
|
|
||||||
Height="48"
|
|
||||||
Margin="12,4,12,4"
|
|
||||||
Stroke="{DynamicResource MaterialDesignDivider}"
|
|
||||||
StrokeThickness="1">
|
|
||||||
<Ellipse.Fill>
|
|
||||||
<ImageBrush ImageSource="{Binding IconUrl}" />
|
|
||||||
</Ellipse.Fill>
|
|
||||||
</Ellipse>
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Channels -->
|
|
||||||
<Border Grid.Column="1">
|
|
||||||
<ListBox
|
|
||||||
HorizontalContentAlignment="Stretch"
|
|
||||||
ItemsSource="{Binding Source={StaticResource AvailableChannelsViewSource}}"
|
|
||||||
SelectionMode="Extended"
|
|
||||||
TextSearch.TextPath="Model.Name"
|
|
||||||
VirtualizingPanel.IsVirtualizingWhenGrouping="True">
|
|
||||||
<i:Interaction.Behaviors>
|
|
||||||
<behaviors:ChannelMultiSelectionListBoxBehavior SelectedItems="{Binding SelectedChannels}" />
|
|
||||||
</i:Interaction.Behaviors>
|
|
||||||
<ListBox.GroupStyle>
|
|
||||||
<GroupStyle>
|
|
||||||
<GroupStyle.ContainerStyle>
|
|
||||||
<Style TargetType="{x:Type GroupItem}">
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate d:DataContext="{x:Type CollectionViewGroup}">
|
|
||||||
<Expander
|
|
||||||
Margin="0"
|
|
||||||
Padding="0"
|
|
||||||
Background="Transparent"
|
|
||||||
BorderBrush="{DynamicResource MaterialDesignDivider}"
|
|
||||||
BorderThickness="0,0,0,1"
|
|
||||||
Header="{Binding Name}"
|
|
||||||
IsExpanded="False">
|
|
||||||
<ItemsPresenter />
|
|
||||||
</Expander>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
</GroupStyle.ContainerStyle>
|
|
||||||
</GroupStyle>
|
|
||||||
</ListBox.GroupStyle>
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<Grid Margin="-8" Background="Transparent">
|
|
||||||
<Grid.InputBindings>
|
|
||||||
<MouseBinding Command="{s:Action ExportChannels}" MouseAction="LeftDoubleClick" />
|
|
||||||
</Grid.InputBindings>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- Channel icon -->
|
|
||||||
<materialDesign:PackIcon
|
|
||||||
Grid.Column="0"
|
|
||||||
Margin="16,7,0,6"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Kind="Pound" />
|
|
||||||
|
|
||||||
<!-- Channel name -->
|
|
||||||
<TextBlock
|
|
||||||
Grid.Column="1"
|
|
||||||
Margin="3,8,8,8"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
FontSize="14"
|
|
||||||
Text="{Binding Name, Mode=OneWay}" />
|
|
||||||
|
|
||||||
<!-- Is selected checkmark -->
|
|
||||||
<materialDesign:PackIcon
|
|
||||||
Grid.Column="2"
|
|
||||||
Width="24"
|
|
||||||
Height="24"
|
|
||||||
Margin="8,0"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Kind="Check"
|
|
||||||
Visibility="{Binding IsSelected, RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}" />
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Export button -->
|
|
||||||
<Button
|
|
||||||
Margin="32,24"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
VerticalAlignment="Bottom"
|
|
||||||
Command="{s:Action ExportChannels}"
|
|
||||||
Style="{DynamicResource MaterialDesignFloatingActionAccentButton}"
|
|
||||||
Visibility="{Binding CanExportChannels, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
|
||||||
<materialDesign:PackIcon
|
|
||||||
Width="32"
|
|
||||||
Height="32"
|
|
||||||
Kind="Download" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<!-- Notifications snackbar -->
|
|
||||||
<materialDesign:Snackbar MessageQueue="{Binding Notifications}" />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</materialDesign:DialogHost>
|
</materialDesign:DialogHost>
|
||||||
</Window>
|
</Window>
|
Loading…
Add table
Add a link
Reference in a new issue