Compare commits

...

17 Commits
2.43 ... 2.43.2

Author SHA1 Message Date
Tyrrrz
dc35f25293 Suppress warnings more intelligently 2024-05-23 01:57:19 +03:00
Tyrrrz
0e3969ca2d Use compile-time serialization for settings 2024-05-22 21:41:22 +03:00
Oleksii Holub
91b7486f45 Publish trimmed builds (#1235) 2024-05-21 22:02:46 +03:00
Oleksii Holub
b7548756a8 Use self-contained builds (#1224) 2024-05-16 20:42:15 +03:00
Tyrrrz
8f9c0f8660 Clean up 2024-05-15 23:16:14 +03:00
Tyrrrz
b12be796c7 Simplify 2024-05-14 00:05:46 +03:00
Oleksii Holub
7a69c87b56 Use a 3-way theme switcher instead of a 2-way switcher (#1233) 2024-05-13 23:56:21 +03:00
Tyrrrz
9e7ad4d85c Fix naming inconsistency 2024-05-13 22:12:43 +03:00
Tyrrrz
0fb4bea64d Update issue forms 2024-05-05 20:00:28 +03:00
Tyrrrz
d8abd011e5 Add helper link for OS/arch in readme 2024-05-02 02:13:17 +03:00
Oleksii Holub
9a5697b5b5 Skip invalid reactions instead of crashing (#1227) 2024-04-30 20:08:03 +03:00
LostXOR
886b8442d3 Improve documentation (#1225) 2024-04-30 00:07:29 +03:00
Tyrrrz
bedf7de8c6 Update readme 2024-04-29 19:55:14 +03:00
Tyrrrz
5ef329b63c Remove unneeded attribute 2024-04-29 02:39:08 +03:00
Tyrrrz
060e55a0d7 Fix tiny issues in the readme 2024-04-28 19:33:01 +03:00
Oleksii Holub
61444aad30 Set correct file permissions for Linux/OSX release assets (#1223) 2024-04-28 18:53:36 +03:00
Tyrrrz
9b3421e8a5 Remove changelog notification 2024-04-28 03:06:02 +03:00
36 changed files with 467 additions and 475 deletions

View File

@@ -1,51 +0,0 @@
# Install .NET runtime
**.NET 8.0 Runtime** is required by **DiscordChatExporter**.
> **Note**:
> Installing the **.NET Runtime** is not required if you running **DiscordChatExporter** using [Docker](Docker.md).
## Windows
Both the GUI and the CLI flavors of **DiscordChatExporter** for Windows come with a bootstrapper that should automatically install all the required prerequisites. Simply run the application and follow the presented instructions.
If, for some reason, you need to install the runtime manually, use one of the download links below:
- [.NET 8.0 Runtime for **Windows x64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.0-windows-x86-installer)
- [.NET 8.0 Runtime for **Windows x86**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.0-windows-x64-installer)
- [.NET 8.0 Runtime for **Windows arm64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.0-windows-arm64-installer)
> **Note**:
> [How can I tell if my computer is running a 32-bit (x86) or a 64-bit (x64) version of Windows?](https://support.microsoft.com/help/15056/windows-32-64-bit-faq)
## macOS
Use one of the download links below:
- [.NET 8.0 Runtime for **macOS x64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.0-macos-x64-installer)
- [.NET 8.0 Runtime for **macOS arm64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.0-macos-arm64-installer)
> **Note**:
> [Is my Mac using an Intel processor (x64) or Apple Silicon (arm64)?](https://support.apple.com/HT211814)
## Linux
Installing the .NET Runtime works differently depending on which Linux distribution you use. Check out the instructions for your distro:
- [Ubuntu](https://docs.microsoft.com/dotnet/core/install/linux-ubuntu)
- [Debian](https://docs.microsoft.com/dotnet/core/install/linux-debian)
- [Fedora](https://docs.microsoft.com/dotnet/core/install/linux-fedora)
For other distros, please check the _'Install on Linux'_ menu on the left of [this page](https://docs.microsoft.com/dotnet/core/install/linux).
## Verify installation (optional)
You can check which version of **.NET Runtime** is installed by running the following command in a terminal:
```console
$ dotnet --info
.NET runtimes installed:
Microsoft.NETCore.App 7.0.x [C:\path\to\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 7.0.x [C:\path\to\dotnet\shared\Microsoft.WindowsDesktop.App]
```

View File

@@ -35,13 +35,13 @@ You can open `.html` files with a web browser, such as Google Chrome.
> **Warning**:
> If a picture is deleted, or if a user changes its avatar, the respective images will no longer be displayed.
> Export using the "Download referenced assets" (`--media`) option to avoid this.
> Export using the "Download assets" (`--media`) option to avoid this.
### Plain Text
<img src="https://i.imgur.com/PbUyRXD.png" height="400"/>
The Plain Text format is the best option for archiving due to its small size.
The Plain Text format formats messages as plain text, and has the smallest size.
You can open `.txt` files with a text editor, such as Notepad.
### JSON

View File

@@ -2,7 +2,6 @@
## Installation & Usage
- [Get .NET Runtime](Dotnet.md)
- Getting started:
- [Using the GUI](Using-the-GUI.md)
- [Using the CLI](Using-the-CLI.md)
@@ -28,6 +27,5 @@
- [General questions](Troubleshooting.md#general)
- [First steps help](Troubleshooting.md#first-steps)
- [It's crashing/failing](Troubleshooting.md#DCE-is-crashingfailing)
- [.NET Core Runtime is required](Troubleshooting.md#net-core-runtime-is-required)
- [Errors](Troubleshooting.md#errors)
- [**More help**](Troubleshooting.md)

View File

@@ -1,7 +1,5 @@
# Scheduling exports with Cron
Make sure you already have **DiscordChatExporter.CLI** downloaded and **.NET Runtime** installed.
## Creating the script
1. Open Terminal and create a new text file with `nano /path/to/DiscordChatExporter/cron.sh`

View File

@@ -1,7 +1,5 @@
# Scheduling exports on macOS
Make sure you already have **DiscordChatExporter.CLI** downloaded and **.NET Runtime** installed.
## Creating the script
1. Open TextEdit.app and create a new file

View File

@@ -1,8 +1,5 @@
# Scheduling exports on Windows
We'll be using [DiscordChatExporter CLI](https://github.com/Tyrrrz/DiscordChatExporter/releases/latest), PowerShell, and Task Scheduler.
Make sure you already have **DiscordChatExporter.CLI** downloaded and **.NET Runtime** installed.
## Creating the script
1. Open a text editor such as Notepad and paste:

View File

@@ -70,7 +70,7 @@ Check the following page: [Obtaining token](Token-and-IDs.md)
### When I open DCE a black window pops up quickly or nothing shows up
If you have [.NET Runtime correctly installed](Dotnet.md), you might have downloaded the CLI flavor, try [downloading the GUI](Getting-started.md#gui-or-cli) instead.
You might have downloaded the CLI flavor of the app, which is meant to be run in a terminal. Try [downloading the GUI](Getting-started.md#gui-or-cli) instead if that's what you want.
### How can I set DCE to export automatically at certain times?
@@ -84,16 +84,6 @@ Check the following pages to learn how to schedule **DiscordChatExporter.CLI** r
Try opening it with a different program, try partitioning or use a different file format, like `PlainText`.
### DCE is crashing/failing
Check the following page: [Installing .NET Core Runtime](Dotnet.md)
If you already have .NET Core installed, please check if your problem is a [known issue](https://github.com/Tyrrrz/DiscordChatExporter/issues?q=is%3Aissue) before [opening a bug report](https://github.com/Tyrrrz/DiscordChatExporter/issues/new).
### .NET Core Runtime is required
Check the following page: [Installing .NET Core Runtime](Dotnet.md)
### I see messages in the export, but they have no content
Your bot is missing the 'Message Content Intent'. Go to the [Discord Developer Portal](https://discord.com/developers/applications), navigate to the 'Bot' section and enable it.

View File

@@ -1,24 +1,17 @@
# Using the CLI
## Guide
> **Note**:
> Make sure you have [.NET Core installed](Dotnet.md) before attempting to run the commands below.
> **Docker** users, please refer to the [Docker usage instructions](Docker.md).
## Step 1
After extracting the `.zip`, open Command Prompt, aka `cmd` (`Terminal` on **macOS** and **Linux**).
## Step 2
Change the current directory to DCE's folder with `cd C:\path\to\directory`, then press ENTER to run the command.
Change the current directory to DCE's folder with `cd C:\path\to\DiscordChatExporter` (`cd /path/to/DiscordChatExporter` on **MacOS** and **Linux**), then press ENTER to run the command.
**Windows** users can quickly get the directory's path by clicking the address bar while inside the folder.
**Windows** users can quickly get the folder's path by clicking the address bar while inside the folder.
![Copy path from Explorer](https://i.imgur.com/XncnhC2.gif)
**macOS** users can select the `.exe`, hit Command+I (⌘I), and copy what's after `Where:` to get the directory
![Mac info panel](https://camo.githubusercontent.com/3c51a904b0099c9f68a4797461d4a7914902fc04/68747470733a2f2f692e696d6775722e636f6d2f323975364e79782e706e67)
**macOS** users can press Command+Option+C (⌘⌥C) while inside the folder (or selecting it) to copy its path to the clipboard.
You can also drag and drop the folder on **every platform**.
![Drag and drop folder](https://i.imgur.com/sOpZQAb.gif)

View File

@@ -10,7 +10,7 @@
### Step 1
After extracting the `.zip`, open `DiscordChatExporter.exe`
After extracting the `.zip`, run `DiscordChatExporter.exe` (Windows), or `DiscordChatExporter` (Mac OS and Linux).
### Step 2
@@ -61,37 +61,30 @@ In this screen you can customize the following:
## Settings
- **Auto-update**
Perform automatic updates on every launch.
- **Auto-update** - Perform automatic updates on every launch.
Default: Enabled
> **Note**:
> Keep this option enabled to receive the latest features and bug fixes!
- **Dark mode**
Use darker colors in the UI (User Interface).
- **Dark mode** - Use darker colors in the UI (User Interface).
Default: Disabled
- **Persist token**
Persist last used token between sessions.
- **Persist token** - Persist last used token between sessions.
Default: Enabled
- **Show threads**
Controls whether threads are shown in the channel list.
- **Show threads** - Controls whether threads are shown in the channel list.
Default: none
- **Locale**
Customize how dates are formatted in the exported files.
- **Locale** - Customize how dates are formatted in the exported files.
- **Date format**
Customize how dates are formatted in the exported files in the settings menu ().
- **Date format** - Customize how dates are formatted in the exported files in the settings menu ().
- **Parallel limit**
The number of channels that will be exported at the same time.
- **Parallel limit** - The number of channels that will be exported at the same time.
Default: 1
- **Normalize to UTC**
Convert all dates to UTC before exporting.
> **Note**:
> Try to keep this number low so that your account doesn't get flagged.
- **Normalize to UTC** - Convert all dates to UTC before exporting.

View File

@@ -100,6 +100,8 @@ body:
required: true
- label: I have sponsored this project
required: false
- label: I have not read any of the above and just checked all the boxes to submit the issue
required: false
- type: markdown
attributes:

View File

@@ -99,8 +99,7 @@ jobs:
# GUI assets aren't suffixed, unlike the CLI assets
asset: DiscordChatExporter
# Need to run on Windows because of DotnetRuntimeBootstrapper's dependency on Ressy
runs-on: windows-latest
runs-on: ${{ startsWith(matrix.rid, 'win-') && 'windows-latest' || startsWith(matrix.rid, 'osx-') && 'macos-latest' || 'ubuntu-latest' }}
timeout-minutes: 10
permissions:
@@ -124,7 +123,7 @@ jobs:
--output ${{ matrix.app }}/bin/publish/
--configuration Release
--runtime ${{ matrix.rid }}
--no-self-contained
--self-contained
- name: Upload artifacts
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
@@ -196,7 +195,11 @@ jobs:
with:
name: ${{ matrix.asset }}.${{ matrix.rid }}
path: ${{ matrix.app }}/
- name: Set permissions
if: ${{ !startsWith(matrix.rid, 'win') }}
run: chmod +x ${{ matrix.app }}/${{ matrix.asset }}
- name: Create package
# Change into the artifacts directory to avoid including the directory itself in the zip archive
working-directory: ${{ matrix.app }}/

View File

@@ -8,6 +8,7 @@
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ILLinkTreatWarningsAsErrors>false</ILLinkTreatWarningsAsErrors>
</PropertyGroup>
</Project>

View File

@@ -20,7 +20,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="xunit" Version="2.8.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0" PrivateAssets="all" />
</ItemGroup>

View File

@@ -3,13 +3,14 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<ApplicationIcon>..\favicon.ico</ApplicationIcon>
<PublishTrimmed>true</PublishTrimmed>
<NoWarn>$(NoWarn);IL2104</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CliFx" Version="2.3.5" />
<PackageReference Include="CSharpier.MsBuild" Version="0.28.2" PrivateAssets="all" />
<PackageReference Include="Deorcify" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.5.4" PrivateAssets="all" />
<PackageReference Include="Gress" Version="2.1.1" />
<PackageReference Include="Spectre.Console" Version="0.49.1" />
</ItemGroup>

View File

@@ -1,3 +1,37 @@
using CliFx;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using CliFx;
using DiscordChatExporter.Cli.Commands;
using DiscordChatExporter.Cli.Commands.Converters;
return await new CliApplicationBuilder().AddCommandsFromThisAssembly().Build().RunAsync(args);
namespace DiscordChatExporter.Cli;
public static class Program
{
// Explicit references because CliFx relies on reflection and we're publishing with trimming enabled
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ExportAllCommand))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ExportChannelsCommand))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ExportDirectMessagesCommand))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ExportGuildCommand))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GetChannelsCommand))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GetDirectChannelsCommand))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GetGuildsCommand))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GuideCommand))]
[DynamicDependency(
DynamicallyAccessedMemberTypes.All,
typeof(ThreadInclusionModeBindingConverter)
)]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(TruthyBooleanBindingConverter))]
public static async Task<int> Main(string[] args) =>
await new CliApplicationBuilder()
.AddCommand<ExportAllCommand>()
.AddCommand<ExportChannelsCommand>()
.AddCommand<ExportDirectMessagesCommand>()
.AddCommand<ExportGuildCommand>()
.AddCommand<GetChannelsCommand>()
.AddCommand<GetDirectChannelsCommand>()
.AddCommand<GetGuildsCommand>()
.AddCommand<GuideCommand>()
.Build()
.RunAsync(args);
}

View File

@@ -676,10 +676,14 @@ public class DiscordClient(string token)
.SetQueryParameter("after", currentAfter.ToString())
.Build();
var response = await GetJsonResponseAsync(url, cancellationToken);
// Can be null on reactions with an emoji that has been deleted (?)
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1226
var response = await TryGetJsonResponseAsync(url, cancellationToken);
if (response is null)
yield break;
var count = 0;
foreach (var userJson in response.EnumerateArray())
foreach (var userJson in response.Value.EnumerateArray())
{
var user = User.Parse(userJson);
yield return user;

View File

@@ -5,11 +5,11 @@
<PackageReference Include="CSharpier.MsBuild" Version="0.28.2" PrivateAssets="all" />
<PackageReference Include="Gress" Version="2.1.1" />
<PackageReference Include="JsonExtensions" Version="1.2.0" />
<PackageReference Include="Polly" Version="8.3.1" />
<PackageReference Include="Polly" Version="8.4.0" />
<PackageReference Include="RazorBlade" Version="0.6.0" />
<PackageReference Include="Superpower" Version="3.0.0" />
<PackageReference Include="WebMarkupMin.Core" Version="2.16.0" />
<PackageReference Include="YoutubeExplode" Version="6.3.14" />
<PackageReference Include="YoutubeExplode" Version="6.3.16" />
</ItemGroup>
</Project>

View File

@@ -7,13 +7,18 @@
xmlns:materialAssists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles"
xmlns:materialControls="clr-namespace:Material.Styles.Controls;assembly=Material.Styles"
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:materialStyles="clr-namespace:Material.Styles.Themes;assembly=Material.Styles">
xmlns:materialStyles="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"
ActualThemeVariantChanged="Application_OnActualThemeVariantChanged">
<Application.DataTemplates>
<framework:ViewManager />
</Application.DataTemplates>
<Application.Styles>
<materialStyles:MaterialTheme />
<!-- This theme is used as a stub to pre-load default resources, the actual colors are set through code -->
<materialStyles:MaterialTheme
BaseTheme="Light"
PrimaryColor="Grey"
SecondaryColor="DeepOrange" />
<materialIcons:MaterialIconStyles />
<dialogHostAvalonia:DialogHostStyles />
@@ -121,7 +126,7 @@
<!-- Toggle switch -->
<Style Selector="ToggleSwitch">
<Setter Property="materialAssists:ToggleSwitchAssist.SwitchThumbOffBackground" Value="{DynamicResource MaterialBackgroundBrush}" />
<Setter Property="materialAssists:ToggleSwitchAssist.SwitchThumbOffBackground" Value="{DynamicResource ToggleBackgroundBrush}" />
</Style>
<!-- Tooltip -->
@@ -132,4 +137,17 @@
<Setter Property="TextElement.FontStretch" Value="Normal" />
</Style>
</Application.Styles>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<SolidColorBrush x:Key="ToggleBackgroundBrush" Color="#FFFFFF" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="ToggleBackgroundBrush" Color="#8E8E8E" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -7,6 +7,8 @@ using Avalonia.Media;
using Avalonia.Platform;
using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.Services;
using DiscordChatExporter.Gui.Utils;
using DiscordChatExporter.Gui.Utils.Extensions;
using DiscordChatExporter.Gui.ViewModels;
using DiscordChatExporter.Gui.ViewModels.Components;
using DiscordChatExporter.Gui.ViewModels.Dialogs;
@@ -16,9 +18,12 @@ using Microsoft.Extensions.DependencyInjection;
namespace DiscordChatExporter.Gui;
public partial class App : Application, IDisposable
public class App : Application, IDisposable
{
private readonly DisposableCollector _eventRoot = new();
private readonly ServiceProvider _services;
private readonly SettingsService _settingsService;
private readonly MainViewModel _mainViewModel;
public App()
@@ -43,17 +48,54 @@ public partial class App : Application, IDisposable
services.AddTransient<SettingsViewModel>();
_services = services.BuildServiceProvider(true);
_settingsService = _services.GetRequiredService<SettingsService>();
_mainViewModel = _services.GetRequiredService<ViewModelManager>().CreateMainViewModel();
// Re-initialize the theme when the user changes it
_eventRoot.Add(
_settingsService.WatchProperty(
o => o.Theme,
() =>
{
RequestedThemeVariant = _settingsService.Theme switch
{
ThemeVariant.Light => Avalonia.Styling.ThemeVariant.Light,
ThemeVariant.Dark => Avalonia.Styling.ThemeVariant.Dark,
_ => Avalonia.Styling.ThemeVariant.Default
};
InitializeTheme();
},
false
)
);
}
public override void Initialize()
{
base.Initialize();
// Increase maximum concurrent connections
ServicePointManager.DefaultConnectionLimit = 20;
AvaloniaXamlLoader.Load(this);
}
private void InitializeTheme()
{
var actualTheme = RequestedThemeVariant?.Key switch
{
"Light" => PlatformThemeVariant.Light,
"Dark" => PlatformThemeVariant.Dark,
_ => PlatformSettings?.GetColorValues().ThemeVariant ?? PlatformThemeVariant.Light
};
this.LocateMaterialTheme<MaterialThemeBase>().CurrentTheme =
actualTheme == PlatformThemeVariant.Light
? Theme.Create(Theme.Light, Color.Parse("#343838"), Color.Parse("#F9A825"))
: Theme.Create(Theme.Dark, Color.Parse("#E8E8E8"), Color.Parse("#F9A825"));
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
@@ -61,50 +103,20 @@ public partial class App : Application, IDisposable
base.OnFrameworkInitializationCompleted();
// Set custom theme colors
SetDefaultTheme();
// Set up custom theme colors
InitializeTheme();
// Load settings
_settingsService.Load();
}
public void Dispose() => _services.Dispose();
}
private void Application_OnActualThemeVariantChanged(object? sender, EventArgs args) =>
// Re-initialize the theme when the system theme changes
InitializeTheme();
public partial class App
{
public static void SetLightTheme()
public void Dispose()
{
if (Current is null)
return;
Current.LocateMaterialTheme<MaterialThemeBase>().CurrentTheme = Theme.Create(
Theme.Light,
Color.Parse("#343838"),
Color.Parse("#F9A825")
);
}
public static void SetDarkTheme()
{
if (Current is null)
return;
Current.LocateMaterialTheme<MaterialThemeBase>().CurrentTheme = Theme.Create(
Theme.Dark,
Color.Parse("#E8E8E8"),
Color.Parse("#F9A825")
);
}
public static void SetDefaultTheme()
{
if (Current is null)
return;
var isDarkModeEnabledByDefault =
Current.PlatformSettings?.GetColorValues().ThemeVariant == PlatformThemeVariant.Dark;
if (isDarkModeEnabledByDefault)
SetDarkTheme();
else
SetLightTheme();
_eventRoot.Dispose();
_services.Dispose();
}
}

View File

@@ -11,7 +11,7 @@ public class LocaleToDisplayNameStringConverter : IValueConverter
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
value is string locale && !string.IsNullOrWhiteSpace(locale)
? CultureInfo.GetCultureInfo(locale).DisplayName
: "System default";
: "System";
public object ConvertBack(
object? value,

View File

@@ -4,6 +4,8 @@
<OutputType>WinExe</OutputType>
<AssemblyName>DiscordChatExporter</AssemblyName>
<ApplicationIcon>..\favicon.ico</ApplicationIcon>
<PublishTrimmed>true</PublishTrimmed>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
@@ -15,14 +17,13 @@
<PackageReference Include="Avalonia" Version="11.0.10" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.10" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="Cogwheel" Version="2.0.4" />
<PackageReference Include="Cogwheel" Version="2.1.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="CSharpier.MsBuild" Version="0.28.2" PrivateAssets="all" />
<PackageReference Include="Deorcify" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="DialogHost.Avalonia" Version="0.7.7" />
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.5.4" PrivateAssets="all" />
<PackageReference Include="Gress" Version="2.1.1" />
<PackageReference Include="Material.Avalonia" Version="3.5.0" />
<PackageReference Include="Material.Avalonia" Version="3.6.0" />
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.9" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Onova" Version="2.6.11" />

View File

@@ -0,0 +1,8 @@
namespace DiscordChatExporter.Gui.Framework;
public enum ThemeVariant
{
System,
Light,
Dark
}

View File

@@ -1,25 +1,31 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using DiscordChatExporter.Gui.ViewModels;
using DiscordChatExporter.Gui.ViewModels.Components;
using DiscordChatExporter.Gui.ViewModels.Dialogs;
using DiscordChatExporter.Gui.Views;
using DiscordChatExporter.Gui.Views.Components;
using DiscordChatExporter.Gui.Views.Dialogs;
namespace DiscordChatExporter.Gui.Framework;
public partial class ViewManager
{
private Control? TryCreateView(ViewModelBase viewModel) =>
viewModel switch
{
MainViewModel => new MainView(),
DashboardViewModel => new DashboardView(),
ExportSetupViewModel => new ExportSetupView(),
MessageBoxViewModel => new MessageBoxView(),
SettingsViewModel => new SettingsView(),
_ => null
};
public Control? TryBindView(ViewModelBase viewModel)
{
var name = viewModel
.GetType()
.FullName?.Replace("ViewModel", "View", StringComparison.Ordinal);
if (string.IsNullOrWhiteSpace(name))
return null;
var type = Type.GetType(name);
if (type is null)
return null;
if (Activator.CreateInstance(type) is not Control view)
var view = TryCreateView(viewModel);
if (view is null)
return null;
view.DataContext ??= viewModel;

View File

@@ -17,8 +17,6 @@ public static class Program
public static string ProjectUrl { get; } = "https://github.com/Tyrrrz/DiscordChatExporter";
public static string LatestReleaseUrl { get; } = ProjectUrl + "/releases/latest";
public static string DocumentationUrl { get; } = ProjectUrl + "/tree/master/.docs";
public static AppBuilder BuildAvaloniaApp() =>

View File

@@ -1,79 +1,133 @@
using System;
using System.IO;
using Avalonia;
using Avalonia.Platform;
using System.Text.Json.Serialization;
using Cogwheel;
using CommunityToolkit.Mvvm.ComponentModel;
using DiscordChatExporter.Core.Exporting;
using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.Models;
using Microsoft.Win32;
namespace DiscordChatExporter.Gui.Services;
// Can't use [ObservableProperty] here because System.Text.Json's source generator doesn't see
// the generated properties.
[INotifyPropertyChanged]
public partial class SettingsService()
: SettingsBase(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Settings.dat"))
: SettingsBase(
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Settings.dat"),
SerializerContext.Default
)
{
[ObservableProperty]
private bool _isUkraineSupportMessageEnabled = true;
[ObservableProperty]
private bool _isAutoUpdateEnabled = true;
[ObservableProperty]
private bool _isDarkModeEnabled;
[ObservableProperty]
private bool _isTokenPersisted = true;
[ObservableProperty]
private ThreadInclusionMode _threadInclusionMode;
[ObservableProperty]
private string? _locale;
[ObservableProperty]
private bool _isUtcNormalizationEnabled;
[ObservableProperty]
private int _parallelLimit = 1;
[ObservableProperty]
private Version? _lastAppVersion;
[ObservableProperty]
private string? _lastToken;
[ObservableProperty]
private ExportFormat _lastExportFormat = ExportFormat.HtmlDark;
[ObservableProperty]
private string? _lastPartitionLimitValue;
[ObservableProperty]
private string? _lastMessageFilterValue;
[ObservableProperty]
private bool _lastShouldFormatMarkdown = true;
[ObservableProperty]
private bool _lastShouldDownloadAssets;
[ObservableProperty]
private bool _lastShouldReuseAssets;
[ObservableProperty]
private string? _lastAssetsDirPath;
public override void Reset()
public bool IsUkraineSupportMessageEnabled
{
base.Reset();
get => _isUkraineSupportMessageEnabled;
set => SetProperty(ref _isUkraineSupportMessageEnabled, value);
}
// Reset the dark mode setting separately because its default value is evaluated dynamically
// and cannot be set by the field initializer.
IsDarkModeEnabled =
Application.Current?.PlatformSettings?.GetColorValues().ThemeVariant
== PlatformThemeVariant.Dark;
private ThemeVariant _theme;
public ThemeVariant Theme
{
get => _theme;
set => SetProperty(ref _theme, value);
}
private bool _isAutoUpdateEnabled = true;
public bool IsAutoUpdateEnabled
{
get => _isAutoUpdateEnabled;
set => SetProperty(ref _isAutoUpdateEnabled, value);
}
private bool _isTokenPersisted = true;
public bool IsTokenPersisted
{
get => _isTokenPersisted;
set => SetProperty(ref _isTokenPersisted, value);
}
private ThreadInclusionMode _threadInclusionMode;
public ThreadInclusionMode ThreadInclusionMode
{
get => _threadInclusionMode;
set => SetProperty(ref _threadInclusionMode, value);
}
private string? _locale;
public string? Locale
{
get => _locale;
set => SetProperty(ref _locale, value);
}
private bool _isUtcNormalizationEnabled;
public bool IsUtcNormalizationEnabled
{
get => _isUtcNormalizationEnabled;
set => SetProperty(ref _isUtcNormalizationEnabled, value);
}
private int _parallelLimit = 1;
public int ParallelLimit
{
get => _parallelLimit;
set => SetProperty(ref _parallelLimit, value);
}
private string? _lastToken;
public string? LastToken
{
get => _lastToken;
set => SetProperty(ref _lastToken, value);
}
private ExportFormat _lastExportFormat = ExportFormat.HtmlDark;
public ExportFormat LastExportFormat
{
get => _lastExportFormat;
set => SetProperty(ref _lastExportFormat, value);
}
private string? _lastPartitionLimitValue;
public string? LastPartitionLimitValue
{
get => _lastPartitionLimitValue;
set => SetProperty(ref _lastPartitionLimitValue, value);
}
private string? _lastMessageFilterValue;
public string? LastMessageFilterValue
{
get => _lastMessageFilterValue;
set => SetProperty(ref _lastMessageFilterValue, value);
}
private bool _lastShouldFormatMarkdown = true;
public bool LastShouldFormatMarkdown
{
get => _lastShouldFormatMarkdown;
set => SetProperty(ref _lastShouldFormatMarkdown, value);
}
private bool _lastShouldDownloadAssets;
public bool LastShouldDownloadAssets
{
get => _lastShouldDownloadAssets;
set => SetProperty(ref _lastShouldDownloadAssets, value);
}
private bool _lastShouldReuseAssets;
public bool LastShouldReuseAssets
{
get => _lastShouldReuseAssets;
set => SetProperty(ref _lastShouldReuseAssets, value);
}
private string? _lastAssetsDirPath;
public string? LastAssetsDirPath
{
get => _lastAssetsDirPath;
set => SetProperty(ref _lastAssetsDirPath, value);
}
public override void Save()
@@ -88,3 +142,9 @@ public partial class SettingsService()
LastToken = lastToken;
}
}
public partial class SettingsService
{
[JsonSerializable(typeof(SettingsService))]
private partial class SerializerContext : JsonSerializerContext;
}

View File

@@ -1,5 +1,4 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.VisualTree;

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DiscordChatExporter.Core.Utils.Extensions;
using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.Models;
@@ -23,25 +22,27 @@ public class SettingsViewModel : DialogViewModelBase
_eventRoot.Add(_settingsService.WatchAllProperties(OnAllPropertiesChanged));
}
public IReadOnlyList<ThemeVariant> AvailableThemes { get; } = Enum.GetValues<ThemeVariant>();
public ThemeVariant Theme
{
get => _settingsService.Theme;
set => _settingsService.Theme = value;
}
public bool IsAutoUpdateEnabled
{
get => _settingsService.IsAutoUpdateEnabled;
set => _settingsService.IsAutoUpdateEnabled = value;
}
public bool IsDarkModeEnabled
{
get => _settingsService.IsDarkModeEnabled;
set => _settingsService.IsDarkModeEnabled = value;
}
public bool IsTokenPersisted
{
get => _settingsService.IsTokenPersisted;
set => _settingsService.IsTokenPersisted = value;
}
public IReadOnlyList<ThreadInclusionMode> AvailableThreadInclusions { get; } =
public IReadOnlyList<ThreadInclusionMode> AvailableThreadInclusionModes { get; } =
Enum.GetValues<ThreadInclusionMode>();
public ThreadInclusionMode ThreadInclusionMode

View File

@@ -79,36 +79,8 @@ public partial class MainViewModel(
[RelayCommand]
private async Task InitializeAsync()
{
// Reset settings (needed to resolve the default dark mode setting)
settingsService.Reset();
// Load settings
settingsService.Load();
// Set the correct theme
if (settingsService.IsDarkModeEnabled)
App.SetDarkTheme();
else
App.SetLightTheme();
await ShowUkraineSupportMessageAsync();
await CheckForUpdatesAsync();
// App has just been updated, display the changelog
if (
settingsService.LastAppVersion is not null
&& settingsService.LastAppVersion != Program.Version
)
{
snackbarManager.Notify(
$"Successfully updated to {Program.Name} v{Program.VersionString}",
"WHAT'S NEW",
() => ProcessEx.StartShellExecute(Program.LatestReleaseUrl)
);
settingsService.LastAppVersion = Program.Version;
settingsService.Save();
}
}
protected override void Dispose(bool disposing)

View File

@@ -9,11 +9,8 @@
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:materialStyles="clr-namespace:Material.Styles.Controls;assembly=Material.Styles"
x:Name="UserControl"
x:DataType="components:DashboardViewModel"
Loaded="UserControl_OnLoaded">
<Design.DataContext>
<components:DashboardViewModel />
</Design.DataContext>
<DockPanel>
<!-- Header -->
<StackPanel
@@ -95,6 +92,130 @@
</Style>
</Style>
</Panel.Styles>
<!-- Guilds and channels -->
<Grid ColumnDefinitions="Auto,*" IsVisible="{Binding !!AvailableGuilds.Count}">
<!-- Guilds -->
<Border
Grid.Column="0"
BorderBrush="{DynamicResource MaterialDividerBrush}"
BorderThickness="0,0,1,0">
<ListBox
x:Name="AvailableGuildsListBox"
ItemsSource="{Binding AvailableGuilds}"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
SelectedItem="{Binding SelectedGuild}"
SelectionChanged="AvailableGuildsListBox_OnSelectionChanged"
SelectionMode="Single">
<ListBox.Styles>
<Style Selector="ListBox">
<Style Selector="^ ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Cursor" Value="Hand" />
</Style>
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>
<Panel Background="Transparent" ToolTip.Tip="{Binding Name}">
<!-- Guild icon placeholder -->
<Ellipse
Width="48"
Height="48"
Margin="12"
Fill="{DynamicResource MaterialDividerBrush}" />
<!-- Guild icon -->
<Ellipse
Width="48"
Height="48"
Margin="12">
<Ellipse.Fill>
<ImageBrush asyncImageLoader:ImageBrushLoader.Source="{Binding IconUrl}" />
</Ellipse.Fill>
</Ellipse>
</Panel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<!-- Channels -->
<Border Grid.Column="1">
<TreeView
x:Name="AvailableChannelsTreeView"
ItemsSource="{Binding AvailableChannels}"
SelectedItems="{Binding SelectedChannels}"
SelectionChanged="AvailableChannelsTreeView_OnSelectionChanged"
SelectionMode="Multiple"
TextSearch.Text="Name">
<TreeView.Styles>
<Style Selector="TreeView">
<Style Selector="^ TreeViewItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Cursor" Value="Hand" />
</Style>
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<Grid
Background="Transparent"
Classes.category="{Binding Channel.IsCategory}"
ColumnDefinitions="Auto,*,Auto">
<Grid.Styles>
<Style Selector="Grid">
<Style Selector="^:not(.category)">
<Setter Property="ToolTip.Tip">
<Template>
<TextBlock>
<Run Text="Last message sent:" />
<Run FontWeight="SemiBold" Text="{Binding Channel.LastMessageId, Converter={x:Static converters:SnowflakeToTimestampStringConverter.Instance}, TargetNullValue=never, Mode=OneWay}" />
</TextBlock>
</Template>
</Setter>
</Style>
</Style>
</Grid.Styles>
<!-- Channel icon -->
<materialIcons:MaterialIcon
Grid.Column="0"
Margin="0,0,4,0"
Classes.voice="{Binding Channel.IsVoice}"
IsVisible="{Binding !Channel.IsCategory}">
<materialIcons:MaterialIcon.Styles>
<Style Selector="materialIcons|MaterialIcon">
<Setter Property="Kind" Value="Pound" />
<Style Selector="^.voice">
<Setter Property="Kind" Value="VolumeHigh" />
</Style>
</Style>
</materialIcons:MaterialIcon.Styles>
</materialIcons:MaterialIcon>
<!-- Channel name -->
<TextBlock
Grid.Column="1"
Margin="0,12"
FontSize="14"
Text="{Binding Channel.Name, Mode=OneWay}" />
<!-- Checkmark -->
<materialIcons:MaterialIcon
Grid.Column="2"
Width="24"
Height="24"
Margin="16,0"
IsVisible="{Binding $parent[TreeViewItem].IsSelected}"
Kind="Check" />
</Grid>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Border>
</Grid>
<!-- Placeholder / usage instructions -->
<Panel IsVisible="{Binding !AvailableGuilds.Count}">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
@@ -215,131 +336,6 @@
</ScrollViewer>
</Panel>
<!-- Guilds and channels -->
<Grid ColumnDefinitions="Auto,*" IsVisible="{Binding !!AvailableGuilds.Count}">
<!-- Guilds -->
<Border
Grid.Column="0"
BorderBrush="{DynamicResource MaterialDividerBrush}"
BorderThickness="0,0,1,0">
<ListBox
x:Name="AvailableGuildsListBox"
ItemsSource="{Binding AvailableGuilds}"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
SelectedItem="{Binding SelectedGuild}"
SelectionChanged="AvailableGuildsListBox_OnSelectionChanged"
SelectionMode="Single">
<ListBox.Styles>
<Style Selector="ListBox">
<Style Selector="^ ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Cursor" Value="Hand" />
</Style>
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>
<Panel Background="Transparent" ToolTip.Tip="{Binding Name}">
<!-- Guild icon placeholder -->
<Ellipse
Width="48"
Height="48"
Margin="12"
Fill="{DynamicResource MaterialDividerBrush}" />
<!-- Guild icon -->
<Ellipse
Width="48"
Height="48"
Margin="12">
<Ellipse.Fill>
<ImageBrush asyncImageLoader:ImageBrushLoader.Source="{Binding IconUrl}" />
</Ellipse.Fill>
</Ellipse>
</Panel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<!-- Channels -->
<Border Grid.Column="1">
<TreeView
x:Name="AvailableChannelsTreeView"
AutoScrollToSelectedItem="False"
ItemsSource="{Binding AvailableChannels}"
SelectedItems="{Binding SelectedChannels}"
SelectionChanged="AvailableChannelsTreeView_OnSelectionChanged"
SelectionMode="Multiple"
TextSearch.Text="Name">
<TreeView.Styles>
<Style Selector="TreeView">
<Style Selector="^ TreeViewItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Cursor" Value="Hand" />
</Style>
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<Grid
Background="Transparent"
Classes.category="{Binding Channel.IsCategory}"
ColumnDefinitions="Auto,*,Auto">
<Grid.Styles>
<Style Selector="Grid">
<Style Selector="^:not(.category)">
<Setter Property="ToolTip.Tip">
<Template>
<TextBlock>
<Run Text="Last message sent:" />
<Run FontWeight="SemiBold" Text="{Binding Channel.LastMessageId, Converter={x:Static converters:SnowflakeToTimestampStringConverter.Instance}, TargetNullValue=never, Mode=OneWay}" />
</TextBlock>
</Template>
</Setter>
</Style>
</Style>
</Grid.Styles>
<!-- Channel icon -->
<materialIcons:MaterialIcon
Grid.Column="0"
Margin="0,0,4,0"
Classes.voice="{Binding Channel.IsVoice}"
IsVisible="{Binding !Channel.IsCategory}">
<materialIcons:MaterialIcon.Styles>
<Style Selector="materialIcons|MaterialIcon">
<Setter Property="Kind" Value="Pound" />
<Style Selector="^.voice">
<Setter Property="Kind" Value="VolumeHigh" />
</Style>
</Style>
</materialIcons:MaterialIcon.Styles>
</materialIcons:MaterialIcon>
<!-- Channel name -->
<TextBlock
Grid.Column="1"
Margin="0,12"
FontSize="14"
Text="{Binding Channel.Name, Mode=OneWay}" />
<!-- Checkmark -->
<materialIcons:MaterialIcon
Grid.Column="2"
Width="24"
Height="24"
Margin="16,0"
IsVisible="{Binding $parent[TreeViewItem].IsSelected}"
Kind="Check" />
</Grid>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Border>
</Grid>
<!-- Export button -->
<Button
Width="56"

View File

@@ -1,13 +1,14 @@
<UserControl
x:Class="DiscordChatExporter.Gui.Views.Controls.HyperLink"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:DiscordChatExporter.Gui.Views.Controls">
<TextBlock
x:Name="TextBlock"
Cursor="Hand"
Foreground="{DynamicResource MaterialSecondaryDarkBrush}"
PointerReleased="TextBlock_OnPointerReleased"
Text="{Binding $parent[UserControl].Text, Mode=OneWay}">
Text="{Binding $parent[controls:HyperLink].Text, Mode=OneWay}">
<TextBlock.Styles>
<Style Selector="TextBlock">
<Style Selector="^:pointerover">

View File

@@ -10,11 +10,8 @@
xmlns:utils="clr-namespace:DiscordChatExporter.Gui.Utils"
x:Name="UserControl"
Width="380"
x:DataType="dialogs:ExportSetupViewModel"
Loaded="UserControl_OnLoaded">
<Design.DataContext>
<dialogs:ExportSetupViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,*,Auto">
<!-- Guild/channel info -->
<Grid
@@ -40,7 +37,7 @@
FontWeight="Light"
IsVisible="{Binding !IsSingleChannel}"
TextTrimming="CharacterEllipsis">
<Run Text="{Binding Channels.Count, Mode=OneWay}" />
<Run Text="{Binding Channels.Count, FallbackValue=0, Mode=OneWay}" />
<Run Text="channels selected" />
</TextBlock>

View File

@@ -4,11 +4,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dialogs="clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
Width="500">
<Design.DataContext>
<dialogs:MessageBoxViewModel />
</Design.DataContext>
Width="500"
x:DataType="dialogs:MessageBoxViewModel">
<Grid RowDefinitions="Auto,*,Auto">
<!-- Title -->
<TextBlock

View File

@@ -4,11 +4,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
xmlns:dialogs="clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs"
Width="380">
<Design.DataContext>
<dialogs:SettingsViewModel />
</Design.DataContext>
Width="380"
x:DataType="dialogs:SettingsViewModel">
<Grid RowDefinitions="Auto,*,Auto">
<TextBlock
Grid.Row="0"
@@ -24,6 +21,19 @@
BorderThickness="0,1">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Orientation="Vertical">
<!-- Theme -->
<DockPanel
Margin="16,8"
LastChildFill="False"
ToolTip.Tip="Preferred user interface theme">
<TextBlock DockPanel.Dock="Left" Text="Theme" />
<ComboBox
Width="150"
DockPanel.Dock="Right"
ItemsSource="{Binding AvailableThemes}"
SelectedItem="{Binding Theme}" />
</DockPanel>
<!-- Auto-updates -->
<DockPanel
Margin="16,8"
@@ -35,19 +45,6 @@
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding IsAutoUpdateEnabled}" />
</DockPanel>
<!-- Dark mode -->
<DockPanel
Margin="16,8"
LastChildFill="False"
ToolTip.Tip="Use darker colors in the UI">
<TextBlock DockPanel.Dock="Left" Text="Dark mode" />
<ToggleSwitch
x:Name="DarkModeToggleSwitch"
DockPanel.Dock="Right"
IsChecked="{Binding IsDarkModeEnabled}"
IsCheckedChanged="DarkModeToggleSwitch_OnIsCheckedChanged" />
</DockPanel>
<!-- Persist token -->
<DockPanel
Margin="16,8"
@@ -66,7 +63,7 @@
<ComboBox
Width="150"
DockPanel.Dock="Right"
ItemsSource="{Binding AvailableThreadInclusions}"
ItemsSource="{Binding AvailableThreadInclusionModes}"
SelectedItem="{Binding ThreadInclusionMode}" />
</DockPanel>

View File

@@ -1,6 +1,4 @@
using System.Windows;
using Avalonia.Interactivity;
using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.ViewModels.Dialogs;
namespace DiscordChatExporter.Gui.Views.Dialogs;
@@ -8,20 +6,4 @@ namespace DiscordChatExporter.Gui.Views.Dialogs;
public partial class SettingsView : UserControl<SettingsViewModel>
{
public SettingsView() => InitializeComponent();
private void DarkModeToggleSwitch_OnIsCheckedChanged(object? sender, RoutedEventArgs args)
{
if (DarkModeToggleSwitch.IsChecked is true)
{
App.SetDarkTheme();
}
else if (DarkModeToggleSwitch.IsChecked is false)
{
App.SetLightTheme();
}
else
{
App.SetDefaultTheme();
}
}
}

View File

@@ -10,13 +10,10 @@
Height="625"
MinWidth="600"
MinHeight="400"
x:DataType="viewModels:MainViewModel"
Icon="/favicon.ico"
RenderOptions.BitmapInterpolationMode="HighQuality"
WindowStartupLocation="CenterScreen">
<Design.DataContext>
<viewModels:MainViewModel />
</Design.DataContext>
<dialogHostAvalonia:DialogHost
x:Name="DialogHost"
CloseOnClickAway="False"

View File

@@ -23,8 +23,7 @@
**DiscordChatExporter** is an application that can be used to export message history from any [Discord](https://discord.com) channel to a file.
It works with direct messages, group messages, and server channels, and supports Discord's dialect of markdown as well as most other rich media features.
<!-- Can't use a relative link here due to a bug in markdown parsing -->
> ❔ If you have questions or issues, **please refer to the [docs](https://github.com/Tyrrrz/DiscordChatExporter/tree/master/.docs)**.
> ❔ If you have questions or issues, **please refer to the [docs](.docs)**.
> 💬 If you want to chat, **join my [Discord server](https://discord.gg/2SUWKFnHSm)**.
@@ -51,17 +50,8 @@ To learn more about the war and how you can help, [click here](https://tyrrrz.me
- 📦 [AUR](https://aur.archlinux.org/packages/discord-chat-exporter-cli): `discord-chat-exporter-cli`
- 📦 [Nix](https://search.nixos.org/packages?query=discordchatexporter-cli): `discordchatexporter-cli`
> **Important**:
> To run **DiscordChatExporter**, you need to make sure that the **.NET 8.0 Runtime** is installed.
> You can download it here:
>
> - [.NET 8.0 Runtime for **macOS x64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.0-macos-x64-installer)
> - [.NET 8.0 Runtime for **macOS arm64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.0-macos-arm64-installer)
> - [.NET 8.0 Runtime for **Linux**](https://learn.microsoft.com/dotnet/core/install/linux) (find the correct download for your distro)
> - On Windows, the runtime should be installed automatically when you run the application for the first time
>
> This should not be necessary if you install **DiscordChatExporter** using a package manager, as it should take care of the dependencies for you.
> This is also not necessary if you are running **DiscordChatExporter** via Docker, because the image already contains the runtime.
> **Note**:
> If you're unsure which build is right for your system, consult with [this page](https://useragent.cc) to determine your OS and CPU architecture.
> **Note**:
> AUR and Nix packages linked above are maintained by the community.
@@ -69,8 +59,7 @@ To learn more about the war and how you can help, [click here](https://tyrrrz.me
## Features
- Graphical and command-line interfaces
- Fully cross-platform
- Cross-platform graphical and command-line interfaces
- Authentication via either a user or a bot token
- Multiple output formats: HTML (dark/light), TXT, CSV, JSON
- Support for markdown, attachments, embeds, emoji, and other rich media features