Group channels by category in GUI

Closes #77
pull/145/head
Oleksii Holub 6 years ago
parent ba5790e312
commit b3e2dd3994

@ -6,6 +6,8 @@
{ {
public string Id { get; } public string Id { get; }
public string ParentId { get; }
public string GuildId { get; } public string GuildId { get; }
public string Name { get; } public string Name { get; }
@ -13,10 +15,11 @@
public string Topic { get; } public string Topic { get; }
public ChannelType Type { get; } public ChannelType Type { get; }
public Channel(string id, string guildId, string name, string topic, ChannelType type) public Channel(string id, string parentId, string guildId, string name, string topic, ChannelType type)
{ {
Id = id; Id = id;
ParentId = parentId;
GuildId = guildId; GuildId = guildId;
Name = name; Name = name;
Topic = topic; Topic = topic;
@ -29,6 +32,6 @@
public partial class Channel public partial class Channel
{ {
public static Channel CreateDeletedChannel(string id) => public static Channel CreateDeletedChannel(string id) =>
new Channel(id, null, "deleted-channel", null, ChannelType.GuildTextChat); new Channel(id, null, null, "deleted-channel", null, ChannelType.GuildTextChat);
} }
} }

@ -33,6 +33,7 @@ namespace DiscordChatExporter.Core.Services
{ {
// Get basic data // Get basic data
var id = json["id"].Value<string>(); var id = json["id"].Value<string>();
var parentId = json["parent_id"]?.Value<string>();
var type = (ChannelType) json["type"].Value<int>(); var type = (ChannelType) json["type"].Value<int>();
var topic = json["topic"]?.Value<string>(); var topic = json["topic"]?.Value<string>();
@ -50,7 +51,7 @@ namespace DiscordChatExporter.Core.Services
if (name.IsBlank()) if (name.IsBlank())
name = json["recipients"].Select(ParseUser).Select(u => u.Name).JoinToString(", "); name = json["recipients"].Select(ParseUser).Select(u => u.Name).JoinToString(", ");
return new Channel(id, guildId, name, topic, type); return new Channel(id, parentId, guildId, name, topic, type);
} }
private Role ParseRole(JToken json) private Role ParseRole(JToken json)

@ -57,6 +57,8 @@
</Compile> </Compile>
<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\GuildViewModel.cs" />
<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" />

@ -0,0 +1,12 @@
using DiscordChatExporter.Core.Models;
using Stylet;
namespace DiscordChatExporter.Gui.ViewModels.Components
{
public class ChannelViewModel : PropertyChangedBase
{
public Channel Model { get; set; }
public string Category { get; set; }
}
}

@ -0,0 +1,13 @@
using System.Collections.Generic;
using DiscordChatExporter.Core.Models;
using Stylet;
namespace DiscordChatExporter.Gui.ViewModels.Components
{
public class GuildViewModel : PropertyChangedBase
{
public Guild Model { get; set; }
public IReadOnlyList<ChannelViewModel> Channels { get; set; }
}
}

@ -4,6 +4,7 @@ using System.Linq;
using DiscordChatExporter.Core.Helpers; 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.Framework; using DiscordChatExporter.Gui.ViewModels.Framework;
using Tyrrrz.Extensions; using Tyrrrz.Extensions;
@ -14,9 +15,9 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
private readonly DialogManager _dialogManager; private readonly DialogManager _dialogManager;
private readonly SettingsService _settingsService; private readonly SettingsService _settingsService;
public Guild Guild { get; set; } public GuildViewModel Guild { get; set; }
public Channel Channel { get; set; } public ChannelViewModel Channel { get; set; }
public string FilePath { get; set; } public string FilePath { get; set; }
@ -59,7 +60,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
To = From; To = From;
// Generate default file name // Generate default file name
var defaultFileName = ExportHelper.GetDefaultExportFileName(SelectedFormat, Guild, Channel, From, To); var defaultFileName = ExportHelper.GetDefaultExportFileName(SelectedFormat, Guild.Model, Channel.Model, From, To);
// Prompt for output file path // Prompt for output file path
var ext = SelectedFormat.GetFileExtension(); var ext = SelectedFormat.GetFileExtension();

@ -1,10 +1,15 @@
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
{ {
ChannelViewModel CreateChannelViewModel();
GuildViewModel CreateGuildViewModel();
ExportSetupViewModel CreateExportSetupViewModel(); ExportSetupViewModel CreateExportSetupViewModel();
SettingsViewModel CreateSettingsViewModel(); SettingsViewModel CreateSettingsViewModel();

@ -6,6 +6,7 @@ using System.Reflection;
using DiscordChatExporter.Core.Exceptions; using DiscordChatExporter.Core.Exceptions;
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.Framework; using DiscordChatExporter.Gui.ViewModels.Framework;
using MaterialDesignThemes.Wpf; using MaterialDesignThemes.Wpf;
using Stylet; using Stylet;
@ -22,9 +23,6 @@ namespace DiscordChatExporter.Gui.ViewModels
private readonly DataService _dataService; private readonly DataService _dataService;
private readonly ExportService _exportService; private readonly ExportService _exportService;
private readonly Dictionary<Guild, IReadOnlyList<Channel>> _guildChannelsMap =
new Dictionary<Guild, IReadOnlyList<Channel>>();
public SnackbarMessageQueue Notifications { get; } = new SnackbarMessageQueue(TimeSpan.FromSeconds(5)); public SnackbarMessageQueue Notifications { get; } = new SnackbarMessageQueue(TimeSpan.FromSeconds(5));
public bool IsEnabled { get; private set; } = true; public bool IsEnabled { get; private set; } = true;
@ -37,12 +35,9 @@ namespace DiscordChatExporter.Gui.ViewModels
public string TokenValue { get; set; } public string TokenValue { get; set; }
public IReadOnlyList<Guild> AvailableGuilds { get; private set; } public IReadOnlyList<GuildViewModel> AvailableGuilds { get; private set; }
public Guild SelectedGuild { get; set; }
public IReadOnlyList<Channel> AvailableChannels => public GuildViewModel SelectedGuild { get; set; }
SelectedGuild != null ? _guildChannelsMap[SelectedGuild] : Array.Empty<Channel>();
public RootViewModel(IViewModelFactory viewModelFactory, DialogManager dialogManager, public RootViewModel(IViewModelFactory viewModelFactory, DialogManager dialogManager,
SettingsService settingsService, UpdateService updateService, DataService dataService, SettingsService settingsService, UpdateService updateService, DataService dataService,
@ -136,38 +131,92 @@ namespace DiscordChatExporter.Gui.ViewModels
// Save token // Save token
_settingsService.LastToken = token; _settingsService.LastToken = token;
// Clear guild to channel map // Prepare available guild list
_guildChannelsMap.Clear(); var availableGuilds = new List<GuildViewModel>();
// Get DM channels // Direct Messages
{ {
// Get fake guild
var guild = Guild.DirectMessages; var guild = Guild.DirectMessages;
// Get channels
var channels = await _dataService.GetDirectMessageChannelsAsync(token); var channels = await _dataService.GetDirectMessageChannelsAsync(token);
// Order channels // Create channel view models
channels = channels.OrderBy(c => c.Name).ToArray(); var channelViewModels = new List<ChannelViewModel>();
foreach (var channel in channels)
{
// Get fake category
var category = channel.Type == ChannelType.DirectTextChat ? "Private" : "Group";
// Create channel view model
var channelViewModel = _viewModelFactory.CreateChannelViewModel();
channelViewModel.Model = channel;
channelViewModel.Category = category;
_guildChannelsMap[guild] = channels; // 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
availableGuilds.Add(guildViewModel);
} }
// Get guild channels // Guilds
{ {
// Get guilds
var guilds = await _dataService.GetUserGuildsAsync(token); var guilds = await _dataService.GetUserGuildsAsync(token);
foreach (var guild in guilds) foreach (var guild in guilds)
{ {
// Get channels
var channels = await _dataService.GetGuildChannelsAsync(token, guild.Id); var channels = await _dataService.GetGuildChannelsAsync(token, guild.Id);
// Filter and order channels // Get category channels
channels = channels.Where(c => c.Type == ChannelType.GuildTextChat).OrderBy(c => c.Name).ToArray(); var categoryChannels = channels.Where(c => c.Type == ChannelType.Category).ToArray();
_guildChannelsMap[guild] = channels; // 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 ??
"<no category>";
// 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
availableGuilds.Add(guildViewModel);
} }
} }
// Update available guilds // Update available guild list
AvailableGuilds = _guildChannelsMap.Keys.ToArray(); AvailableGuilds = availableGuilds;
// Select the first guild // Pre-select first guild
SelectedGuild = AvailableGuilds.FirstOrDefault(); SelectedGuild = AvailableGuilds.FirstOrDefault();
} }
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
@ -188,7 +237,7 @@ namespace DiscordChatExporter.Gui.ViewModels
public bool CanExportChannel => IsEnabled; public bool CanExportChannel => IsEnabled;
public async void ExportChannel(Channel channel) public async void ExportChannel(ChannelViewModel channel)
{ {
try try
{ {
@ -212,7 +261,7 @@ namespace DiscordChatExporter.Gui.ViewModels
var progressHandler = new Progress<double>(p => Progress = p); var progressHandler = new Progress<double>(p => Progress = p);
// Get chat log // Get chat log
var chatLog = await _dataService.GetChatLogAsync(token, dialog.Guild, dialog.Channel, var chatLog = await _dataService.GetChatLogAsync(token, dialog.Guild.Model, dialog.Channel.Model,
dialog.From, dialog.To, progressHandler); dialog.From, dialog.To, progressHandler);
// Export // Export

@ -26,7 +26,7 @@
Width="32" Width="32"
Height="32"> Height="32">
<Ellipse.Fill> <Ellipse.Fill>
<ImageBrush ImageSource="{Binding Guild.IconUrl}" /> <ImageBrush ImageSource="{Binding Guild.Model.IconUrl}" />
</Ellipse.Fill> </Ellipse.Fill>
</Ellipse> </Ellipse>
@ -39,13 +39,13 @@
TextTrimming="CharacterEllipsis"> TextTrimming="CharacterEllipsis">
<Run <Run
Foreground="{DynamicResource SecondaryTextBrush}" Foreground="{DynamicResource SecondaryTextBrush}"
Text="{Binding Guild.Name, Mode=OneWay}" Text="{Binding Channel.Category, Mode=OneWay}"
ToolTip="{Binding Guild.Name, Mode=OneWay}" /> ToolTip="{Binding Channel.Category, Mode=OneWay}" />
<Run Text="/" /> <Run Text="/" />
<Run <Run
Foreground="{DynamicResource PrimaryTextBrush}" Foreground="{DynamicResource PrimaryTextBrush}"
Text="{Binding Channel.Name, Mode=OneWay}" Text="{Binding Channel.Model.Name, Mode=OneWay}"
ToolTip="{Binding Channel.Name, Mode=OneWay}" /> ToolTip="{Binding Channel.Model.Name, Mode=OneWay}" />
</TextBlock> </TextBlock>
</Grid> </Grid>

@ -210,7 +210,7 @@
Margin="-8" Margin="-8"
Background="Transparent" Background="Transparent"
Cursor="Hand" Cursor="Hand"
ToolTip="{Binding Name}"> ToolTip="{Binding Model.Name}">
<!-- Guild icon placeholder --> <!-- Guild icon placeholder -->
<Ellipse <Ellipse
Width="48" Width="48"
@ -224,7 +224,7 @@
Height="48" Height="48"
Margin="12,4,12,4"> Margin="12,4,12,4">
<Ellipse.Fill> <Ellipse.Fill>
<ImageBrush ImageSource="{Binding IconUrl}" /> <ImageBrush ImageSource="{Binding Model.IconUrl}" />
</Ellipse.Fill> </Ellipse.Fill>
</Ellipse> </Ellipse>
</Grid> </Grid>
@ -235,7 +235,7 @@
<!-- Channels --> <!-- Channels -->
<Border Grid.Column="1"> <Border Grid.Column="1">
<ListBox HorizontalContentAlignment="Stretch" ItemsSource="{Binding AvailableChannels}"> <ListBox HorizontalContentAlignment="Stretch" ItemsSource="{Binding SelectedGuild.Channels}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<StackPanel <StackPanel
@ -256,8 +256,11 @@
<TextBlock <TextBlock
Margin="3,8,8,8" Margin="3,8,8,8"
VerticalAlignment="Center" VerticalAlignment="Center"
FontSize="14" FontSize="14">
Text="{Binding Name}" /> <Run Text="{Binding Category, Mode=OneWay}" Foreground="{DynamicResource SecondaryTextBrush}" />
<Run Text="/" Foreground="{DynamicResource SecondaryTextBrush}" />
<Run Text="{Binding Model.Name, Mode=OneWay}" Foreground="{DynamicResource PrimaryTextBrush}" />
</TextBlock>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>

Loading…
Cancel
Save