1
0
mirror of https://github.com/chylex/Discord-History-Tracker.git synced 2024-10-18 11:42:52 +02:00

Compare commits

..

8 Commits

130 changed files with 4817 additions and 4947 deletions

View File

@ -19,7 +19,7 @@ Folder organization:
* `lib/` contains utilities required to build the project
* `web/` contains source code of the [official website](https://dht.chylex.com), which can be used as a template when making your own website
To start editing source code for the desktop app, install the [.NET 5 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/5.0), and then open `app/DiscordHistoryTracker.sln` in [Visual Studio](https://visualstudio.microsoft.com/downloads/) or [Rider](https://www.jetbrains.com/rider/).
To start editing source code for the desktop app, install the [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0), and then open `app/DiscordHistoryTracker.sln` in [Visual Studio](https://visualstudio.microsoft.com/downloads/) or [Rider](https://www.jetbrains.com/rider/).
### Building
@ -50,6 +50,6 @@ Run the `app/build.sh` script, and read the [Distribution](#distribution) sectio
#### Distribution
The mentioned build scripts will prepare `Release` builds ready for distribution. Once the script finishes, the `app/bin` folder will contain self-contained executables for each major operating system, and a portable version that works on all other systems but requires .NET 5 to be installed.
The mentioned build scripts will prepare `Release` builds ready for distribution. Once the script finishes, the `app/bin` folder will contain self-contained executables for each major operating system, and a portable version that works on all other systems but requires .NET 8 to be installed.
Note that when building on Windows, the generated `.zip` files for Linux and Mac will not have correct file permissions, so it will not be possible to run them by double-clicking `DiscordHistoryTracker`. I tried using Python to re-create the archives with correct file permissions, but found that Linux `zip` tools could not see them. The only working solution is building the Windows + portable version on Windows, and Linux + Mac version on Linux.

View File

@ -6,8 +6,8 @@
<Application.Styles>
<FluentTheme Mode="Light" />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Default.xaml" />
<FluentTheme />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml" />
<Style Selector="Button, CheckBox, RadioButton, Expander /template/ ToggleButton#ExpanderHeader">
<Setter Property="Cursor" Value="Hand" />
@ -48,68 +48,9 @@
<Setter Property="Template" Value="{StaticResource InlineDataValidationContentTemplate}" />
</Style>
<Style Selector="Expander /template/ ToggleButton#ExpanderHeader">
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="ToggleButtonBackground">
<Grid ColumnDefinitions="Auto,*" RowDefinitions="35">
<Border x:Name="ExpandCollapseChevronBorder"
Grid.Column="0"
Width="35"
Height="35"
Margin="2,0"
RenderTransformOrigin="50%,50%">
<Path x:Name="ExpandCollapseChevron"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderTransformOrigin="50%,50%"
Stretch="None"
Stroke="{DynamicResource ExpanderChevronForeground}"
StrokeThickness="1" />
<Border.RenderTransform>
<RotateTransform />
</Border.RenderTransform>
</Border>
<ContentPresenter x:Name="PART_ContentPresenter"
Grid.Column="1"
Margin="0"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="Center"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
TextBlock.Foreground="{DynamicResource ExpanderForeground}" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="Expander:expanded /template/ ToggleButton#ExpanderHeader /template/ Border#ExpandCollapseChevronBorder">
<Style.Animations>
<Animation FillMode="Both" Duration="0:0:0.0625">
<KeyFrame Cue="0%">
<Setter Property="RotateTransform.Angle" Value="180" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="RotateTransform.Angle" Value="180" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Expander:not(:expanded) /template/ ToggleButton#ExpanderHeader /template/ Border#ExpandCollapseChevronBorder">
<Style.Animations>
<Animation FillMode="Both" Duration="0:0:0.0625">
<KeyFrame Cue="0%">
<Setter Property="RotateTransform.Angle" Value="0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="RotateTransform.Angle" Value="0" />
</KeyFrame>
</Animation>
</Style.Animations>
<Style Selector="Expander">
<Setter Property="MinHeight" Value="40" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
</Application.Styles>
@ -168,12 +109,26 @@
<Thickness x:Key="ExpanderHeaderPadding">15,0</Thickness>
<Thickness x:Key="ExpanderContentPadding">12</Thickness>
<SolidColorBrush x:Key="ExpanderBorderBrush" Color="#697DAB" />
<SolidColorBrush x:Key="ExpanderBackground" Color="#697DAB" />
<SolidColorBrush x:Key="ExpanderForeground" Color="#FFFFFF" />
<SolidColorBrush x:Key="ExpanderHeaderBorderBrush" Color="#697DAB" />
<SolidColorBrush x:Key="ExpanderHeaderBorderBrushPointerOver" Color="#697DAB" />
<SolidColorBrush x:Key="ExpanderHeaderBorderBrushPressed" Color="#697DAB" />
<SolidColorBrush x:Key="ExpanderHeaderBorderBrushDisabled" Color="#697DAB" />
<SolidColorBrush x:Key="ExpanderHeaderBackground" Color="#697DAB" />
<SolidColorBrush x:Key="ExpanderHeaderBackgroundPointerOver" Color="#536794" />
<SolidColorBrush x:Key="ExpanderHeaderBackgroundPressed" Color="#47587F" />
<SolidColorBrush x:Key="ExpanderHeaderBackgroundDisabled" Color="#697DAB" />
<SolidColorBrush x:Key="ExpanderHeaderForeground" Color="#FFFFFF" />
<SolidColorBrush x:Key="ExpanderHeaderForegroundPointerOver" Color="#FFFFFF" />
<SolidColorBrush x:Key="ExpanderHeaderForegroundPressed" Color="#FFFFFF" />
<SolidColorBrush x:Key="ExpanderHeaderForegroundDisabled" Color="#FFFFFF" />
<SolidColorBrush x:Key="ExpanderChevronBackground" Color="Transparent" />
<SolidColorBrush x:Key="ExpanderChevronBackgroundPointerOver" Color="#536794" />
<SolidColorBrush x:Key="ExpanderChevronBackgroundPressed" Color="#47587F" />
<SolidColorBrush x:Key="ExpanderChevronBackgroundDisabled" Color="Transparent" />
<SolidColorBrush x:Key="ExpanderChevronForeground" Color="#FFFFFF" />
<SolidColorBrush x:Key="ExpanderDropDownBorderBrush" Color="#697DAB" />
<SolidColorBrush x:Key="ExpanderDropDownBackground" Color="#FFFFFF" />
<SolidColorBrush x:Key="ExpanderChevronForegroundPointerOver" Color="#FFFFFF" />
<SolidColorBrush x:Key="ExpanderChevronForegroundPressed" Color="#FFFFFF" />
<SolidColorBrush x:Key="ExpanderChevronForegroundDisabled" Color="#FFFFFF" />
</Application.Resources>

View File

@ -1,20 +1,21 @@
using System;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using DHT.Desktop.Main;
namespace DHT.Desktop {
sealed class App : Application {
namespace DHT.Desktop;
sealed class App : Application {
public override void Initialize() {
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted() {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
desktop.MainWindow = new MainWindow(new Arguments(desktop.Args));
desktop.MainWindow = new MainWindow(new Arguments(desktop.Args ?? Array.Empty<string>()));
}
base.OnFrameworkInitializationCompleted();
}
}
}

View File

@ -1,8 +1,9 @@
using System;
using DHT.Utils.Logging;
namespace DHT.Desktop {
sealed class Arguments {
namespace DHT.Desktop;
sealed class Arguments {
private static readonly Log Log = Log.ForType<Arguments>();
public static Arguments Empty => new(Array.Empty<string>());
@ -61,5 +62,4 @@ namespace DHT.Desktop {
}
}
}
}
}

View File

@ -2,8 +2,9 @@ using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace DHT.Desktop.Common {
sealed class BytesValueConverter : IValueConverter {
namespace DHT.Desktop.Common;
sealed class BytesValueConverter : IValueConverter {
private sealed class Unit {
private readonly string label;
private readonly string numberFormat;
@ -49,5 +50,4 @@ namespace DHT.Desktop.Common {
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotSupportedException();
}
}
}

View File

@ -3,39 +3,42 @@ using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using DHT.Desktop.Dialogs.File;
using DHT.Desktop.Dialogs.Message;
using DHT.Server.Database;
using DHT.Server.Database.Exceptions;
using DHT.Server.Database.Sqlite;
using DHT.Utils.Logging;
namespace DHT.Desktop.Common {
static class DatabaseGui {
namespace DHT.Desktop.Common;
static class DatabaseGui {
private static readonly Log Log = Log.ForType(typeof(DatabaseGui));
private const string DatabaseFileInitialName = "archive.dht";
private static readonly List<FileDialogFilter> DatabaseFileDialogFilter = new() {
new FileDialogFilter {
Name = "Discord History Tracker Database",
Extensions = { "dht" }
}
private static readonly IReadOnlyList<FilePickerFileType> DatabaseFileDialogFilter = new List<FilePickerFileType> {
FileDialogs.CreateFilter("Discord History Tracker Database", new [] { "dht" })
};
public static OpenFileDialog NewOpenDatabaseFileDialog() {
return new OpenFileDialog {
public static async Task<string[]> NewOpenDatabaseFilesDialog(Window window, string? suggestedDirectory) {
return await window.StorageProvider.OpenFiles(new FilePickerOpenOptions {
Title = "Open Database File",
InitialFileName = DatabaseFileInitialName,
Filters = DatabaseFileDialogFilter
};
FileTypeFilter = DatabaseFileDialogFilter,
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, suggestedDirectory),
AllowMultiple = true
});
}
public static SaveFileDialog NewOpenOrCreateDatabaseFileDialog() {
return new SaveFileDialog {
public static async Task<string?> NewOpenOrCreateDatabaseFileDialog(Window window, string? suggestedDirectory) {
return await window.StorageProvider.SaveFile(new FilePickerSaveOptions {
Title = "Open or Create Database File",
InitialFileName = DatabaseFileInitialName,
Filters = DatabaseFileDialogFilter
};
FileTypeChoices = DatabaseFileDialogFilter,
SuggestedFileName = DatabaseFileInitialName,
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, suggestedDirectory),
ShowOverwritePrompt = false
});
}
public static async Task<IDatabaseFile?> TryOpenOrCreateDatabaseFromPath(string path, Window window, Func<Task<bool>> checkCanUpgradeDatabase) {
@ -62,5 +65,4 @@ namespace DHT.Desktop.Common {
public static async Task<DialogResult.YesNo> ShowCanUpgradeMultipleDatabaseDialog(Window window) {
return await Dialog.ShowYesNo(window, "Database Upgrade", "One or more databases were created with an older version of DHT. If you proceed, these databases will be upgraded and will no longer open in previous versions of DHT. Otherwise, these databases will be skipped.\n\nPlease ensure you have a backup of the databases. Do you want to proceed with the upgrade?");
}
}
}

View File

@ -2,8 +2,9 @@ using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace DHT.Desktop.Common {
sealed class NumberValueConverter : IValueConverter {
namespace DHT.Desktop.Common;
sealed class NumberValueConverter : IValueConverter {
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
return value == null ? "-" : string.Format(Program.Culture, "{0:n0}", value);
}
@ -11,5 +12,4 @@ namespace DHT.Desktop.Common {
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotSupportedException();
}
}
}

View File

@ -1,5 +1,6 @@
namespace DHT.Desktop.Common {
static class TextFormat {
namespace DHT.Desktop.Common;
static class TextFormat {
public static string Format(this int number) {
return number.ToString("N0", Program.Culture);
}
@ -15,5 +16,4 @@ namespace DHT.Desktop.Common {
public static string Pluralize(this long number, string singular) {
return number.Format() + "\u00A0" + (number == 1 ? singular : singular + "s");
}
}
}

View File

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
<AssemblyName>DiscordHistoryTracker</AssemblyName>
<RootNamespace>DHT.Desktop</RootNamespace>
@ -21,10 +20,13 @@
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.18" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.18" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.18" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.18" Condition=" '$(Configuration)' == 'Debug' " />
<PackageReference Include="Avalonia" Version="11.0.0" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0" Condition=" '$(Configuration)' == 'Debug' " />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Server\Server.csproj" />

View File

@ -34,7 +34,7 @@
<StackPanel Margin="20">
<ScrollViewer MaxHeight="400">
<ItemsRepeater Items="{Binding Items}">
<ItemsRepeater ItemsSource="{Binding Items}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Checked}">

View File

@ -1,22 +1,14 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using DHT.Desktop.Dialogs.Message;
namespace DHT.Desktop.Dialogs.CheckBox {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class CheckBoxDialog : Window {
namespace DHT.Desktop.Dialogs.CheckBox;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class CheckBoxDialog : Window {
public CheckBoxDialog() {
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
public void ClickOk(object? sender, RoutedEventArgs e) {
@ -26,6 +18,4 @@ namespace DHT.Desktop.Dialogs.CheckBox {
public void ClickCancel(object? sender, RoutedEventArgs e) {
Close(DialogResult.OkCancel.Cancel);
}
}
}

View File

@ -4,8 +4,9 @@ using System.ComponentModel;
using System.Linq;
using DHT.Utils.Models;
namespace DHT.Desktop.Dialogs.CheckBox {
class CheckBoxDialogModel : BaseModel {
namespace DHT.Desktop.Dialogs.CheckBox;
class CheckBoxDialogModel : BaseModel {
public string Title { get; init; } = "";
private IReadOnlyList<CheckBoxItem> items = Array.Empty<CheckBoxItem>();
@ -55,9 +56,9 @@ namespace DHT.Desktop.Dialogs.CheckBox {
UpdateBulkButtons();
}
}
}
}
sealed class CheckBoxDialogModel<T> : CheckBoxDialogModel {
sealed class CheckBoxDialogModel<T> : CheckBoxDialogModel {
public new IReadOnlyList<CheckBoxItem<T>> Items { get; }
public IEnumerable<CheckBoxItem<T>> SelectedItems => Items.Where(static item => item.Checked);
@ -66,5 +67,4 @@ namespace DHT.Desktop.Dialogs.CheckBox {
this.Items = new List<CheckBoxItem<T>>(items);
base.Items = this.Items;
}
}
}

View File

@ -1,7 +1,8 @@
using DHT.Utils.Models;
namespace DHT.Desktop.Dialogs.CheckBox {
class CheckBoxItem : BaseModel {
namespace DHT.Desktop.Dialogs.CheckBox;
class CheckBoxItem : BaseModel {
public string Title { get; init; } = "";
public object? Item { get; init; } = null;
@ -11,14 +12,13 @@ namespace DHT.Desktop.Dialogs.CheckBox {
get => isChecked;
set => Change(ref isChecked, value);
}
}
}
sealed class CheckBoxItem<T> : CheckBoxItem {
sealed class CheckBoxItem<T> : CheckBoxItem {
public new T Item { get; }
public CheckBoxItem(T item) {
this.Item = item;
base.Item = item;
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
namespace DHT.Desktop.Dialogs.File;
static class FileDialogs {
public static async Task<string[]> OpenFiles(this IStorageProvider storageProvider, FilePickerOpenOptions options) {
return (await storageProvider.OpenFilePickerAsync(options)).ToLocalPaths();
}
public static async Task<string?> SaveFile(this IStorageProvider storageProvider, FilePickerSaveOptions options) {
return (await storageProvider.SaveFilePickerAsync(options))?.ToLocalPath();
}
public static FilePickerFileType CreateFilter(string name, string[] extensions) {
return new FilePickerFileType(name) {
Patterns = extensions.Select(static ext => "*." + ext).ToArray()
};
}
public static Task<IStorageFolder?> GetSuggestedStartLocation(Window window, string? suggestedDirectory) {
return suggestedDirectory == null ? Task.FromResult<IStorageFolder?>(null) : window.StorageProvider.TryGetFolderFromPathAsync(suggestedDirectory);
}
private static string ToLocalPath(this IStorageFile file) {
return file.TryGetLocalPath() ?? throw new NotSupportedException("Local filesystem is not supported.");
}
private static string[] ToLocalPaths(this IReadOnlyList<IStorageFile> files) {
return files.Select(ToLocalPath).ToArray();
}
}

View File

@ -2,8 +2,9 @@ using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Threading;
namespace DHT.Desktop.Dialogs.Message {
static class Dialog {
namespace DHT.Desktop.Dialogs.Message;
static class Dialog {
public static async Task ShowOk(Window owner, string title, string message) {
if (!Dispatcher.UIThread.CheckAccess()) {
await Dispatcher.UIThread.InvokeAsync(() => ShowOk(owner, title, message));
@ -70,5 +71,4 @@ namespace DHT.Desktop.Dialogs.Message {
return result.ToYesNoCancel();
}
}
}

View File

@ -1,7 +1,8 @@
using System;
namespace DHT.Desktop.Dialogs.Message {
static class DialogResult {
namespace DHT.Desktop.Dialogs.Message;
static class DialogResult {
public enum All {
Ok,
Yes,
@ -55,5 +56,4 @@ namespace DHT.Desktop.Dialogs.Message {
_ => throw new ArgumentException("Cannot convert dialog result " + result + " to yes/no/cancel.")
};
}
}
}

View File

@ -1,21 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Dialogs.Message {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class MessageDialog : Window {
namespace DHT.Desktop.Dialogs.Message;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class MessageDialog : Window {
public MessageDialog() {
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
public void ClickOk(object? sender, RoutedEventArgs e) {
@ -33,5 +25,4 @@ namespace DHT.Desktop.Dialogs.Message {
public void ClickCancel(object? sender, RoutedEventArgs e) {
Close(DialogResult.All.Cancel);
}
}
}

View File

@ -1,5 +1,6 @@
namespace DHT.Desktop.Dialogs.Message {
sealed class MessageDialogModel {
namespace DHT.Desktop.Dialogs.Message;
sealed class MessageDialogModel {
public string Title { get; init; } = "";
public string Message { get; init; } = "";
@ -7,5 +8,4 @@ namespace DHT.Desktop.Dialogs.Message {
public bool IsYesVisible { get; init; } = false;
public bool IsNoVisible { get; init; } = false;
public bool IsCancelVisible { get; init; } = false;
}
}

View File

@ -1,7 +1,7 @@
using System.Threading.Tasks;
namespace DHT.Desktop.Dialogs.Progress {
interface IProgressCallback {
namespace DHT.Desktop.Dialogs.Progress;
interface IProgressCallback {
Task Update(string message, int finishedItems, int totalItems);
}
}

View File

@ -7,7 +7,7 @@
x:Class="DHT.Desktop.Dialogs.Progress.ProgressDialog"
Title="{Binding Title}"
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
Opened="Loaded"
Opened="OnOpened"
Closing="OnClosing"
Width="500" SizeToContent="Height" CanResize="False"
WindowStartupLocation="CenterOwner">

View File

@ -1,40 +1,30 @@
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Dialogs.Progress {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class ProgressDialog : Window {
namespace DHT.Desktop.Dialogs.Progress;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class ProgressDialog : Window {
private bool isFinished = false;
public ProgressDialog() {
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
public void OnClosing(object? sender, CancelEventArgs e) {
e.Cancel = !isFinished;
}
public void Loaded(object? sender, EventArgs e) {
public void OnOpened(object? sender, EventArgs e) {
if (DataContext is ProgressDialogModel model) {
Task.Run(model.StartTask).ContinueWith(OnFinished, TaskScheduler.FromCurrentSynchronizationContext());
}
}
public void OnClosing(object? sender, WindowClosingEventArgs e) {
e.Cancel = !isFinished;
}
private void OnFinished(Task task) {
isFinished = true;
Close();
}
}
}

View File

@ -4,8 +4,9 @@ using Avalonia.Threading;
using DHT.Desktop.Common;
using DHT.Utils.Models;
namespace DHT.Desktop.Dialogs.Progress {
sealed class ProgressDialogModel : BaseModel {
namespace DHT.Desktop.Dialogs.Progress;
sealed class ProgressDialogModel : BaseModel {
public string Title { get; init; } = "";
private string message = "";
@ -61,5 +62,4 @@ namespace DHT.Desktop.Dialogs.Progress {
});
}
}
}
}

View File

@ -31,7 +31,7 @@
<ScrollViewer MaxHeight="400">
<StackPanel Spacing="10">
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" />
<ItemsRepeater Items="{Binding Items}">
<ItemsRepeater ItemsSource="{Binding Items}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<DockPanel Margin="0 5 25 0">

View File

@ -1,22 +1,14 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using DHT.Desktop.Dialogs.Message;
namespace DHT.Desktop.Dialogs.TextBox {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class TextBoxDialog : Window {
namespace DHT.Desktop.Dialogs.TextBox;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class TextBoxDialog : Window {
public TextBoxDialog() {
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
public void ClickOk(object? sender, RoutedEventArgs e) {
@ -26,6 +18,4 @@ namespace DHT.Desktop.Dialogs.TextBox {
public void ClickCancel(object? sender, RoutedEventArgs e) {
Close(DialogResult.OkCancel.Cancel);
}
}
}

View File

@ -4,8 +4,9 @@ using System.ComponentModel;
using System.Linq;
using DHT.Utils.Models;
namespace DHT.Desktop.Dialogs.TextBox {
class TextBoxDialogModel : BaseModel {
namespace DHT.Desktop.Dialogs.TextBox;
class TextBoxDialogModel : BaseModel {
public string Title { get; init; } = "";
public string Description { get; init; } = "";
@ -32,9 +33,9 @@ namespace DHT.Desktop.Dialogs.TextBox {
private void OnItemErrorsChanged(object? sender, DataErrorsChangedEventArgs e) {
OnPropertyChanged(nameof(HasErrors));
}
}
}
sealed class TextBoxDialogModel<T> : TextBoxDialogModel {
sealed class TextBoxDialogModel<T> : TextBoxDialogModel {
public new IReadOnlyList<TextBoxItem<T>> Items { get; }
public IEnumerable<TextBoxItem<T>> ValidItems => Items.Where(static item => item.IsValid);
@ -43,5 +44,4 @@ namespace DHT.Desktop.Dialogs.TextBox {
this.Items = new List<TextBoxItem<T>>(items);
base.Items = this.Items;
}
}
}

View File

@ -3,8 +3,9 @@ using System.Collections;
using System.ComponentModel;
using DHT.Utils.Models;
namespace DHT.Desktop.Dialogs.TextBox {
class TextBoxItem : BaseModel, INotifyDataErrorInfo {
namespace DHT.Desktop.Dialogs.TextBox;
class TextBoxItem : BaseModel, INotifyDataErrorInfo {
public string Title { get; init; } = "";
public object? Item { get; init; } = null;
@ -29,14 +30,13 @@ namespace DHT.Desktop.Dialogs.TextBox {
public bool HasErrors => !IsValid;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
}
}
sealed class TextBoxItem<T> : TextBoxItem {
sealed class TextBoxItem<T> : TextBoxItem {
public new T Item { get; }
public TextBoxItem(T item) {
this.Item = item;
base.Item = item;
}
}
}

View File

@ -9,8 +9,9 @@ using DHT.Utils.Logging;
using static System.Environment.SpecialFolder;
using static System.Environment.SpecialFolderOption;
namespace DHT.Desktop.Discord {
static class DiscordAppSettings {
namespace DHT.Desktop.Discord;
static class DiscordAppSettings {
private static readonly Log Log = Log.ForType(typeof(DiscordAppSettings));
private const string JsonKeyDevTools = "DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING";
@ -115,5 +116,4 @@ namespace DHT.Desktop.Discord {
await using var stream = new FileStream(JsonFilePath, FileMode.Truncate, FileAccess.Write, FileShare.None);
await JsonSerializer.SerializeAsync(stream, json, new JsonSerializerOptions { WriteIndented = true });
}
}
}

View File

@ -1,10 +1,10 @@
namespace DHT.Desktop.Discord {
enum SettingsJsonResult {
namespace DHT.Desktop.Discord;
enum SettingsJsonResult {
Success,
AlreadySet,
FileNotFound,
ReadError,
InvalidJson,
WriteError
}
}

View File

@ -46,7 +46,7 @@
<TextBlock Grid.Row="0" Grid.Column="1" FontWeight="Bold">License</TextBlock>
<TextBlock Grid.Row="0" Grid.Column="2" FontWeight="Bold">Link</TextBlock>
<TextBlock Grid.Row="2" Grid.Column="0">.NET 5</TextBlock>
<TextBlock Grid.Row="2" Grid.Column="0">.NET 8</TextBlock>
<TextBlock Grid.Row="2" Grid.Column="1">MIT</TextBlock>
<Button Grid.Row="2" Grid.Column="2" Command="{Binding ShowLibraryNetCore}">GitHub</Button>

View File

@ -1,20 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class AboutWindow : Window {
namespace DHT.Desktop.Main;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class AboutWindow : Window {
public AboutWindow() {
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -1,7 +1,8 @@
using System.Diagnostics;
namespace DHT.Desktop.Main {
sealed class AboutWindowModel {
namespace DHT.Desktop.Main;
sealed class AboutWindowModel {
public void ShowOfficialWebsite() {
OpenUrl("https://dht.chylex.com");
}
@ -29,5 +30,4 @@ namespace DHT.Desktop.Main {
private static void OpenUrl(string url) {
Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true });
}
}
}

View File

@ -36,7 +36,7 @@
<CheckBox IsChecked="{Binding LimitSize}">Limit Size</CheckBox>
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding MaximumSize}" IsEnabled="{Binding LimitSize}" HorizontalContentAlignment="Right" />
<ComboBox IsEnabled="{Binding LimitSize}" Items="{Binding Units}" SelectedItem="{Binding MaximumSizeUnit}">
<ComboBox IsEnabled="{Binding LimitSize}" ItemsSource="{Binding Units}" SelectedItem="{Binding MaximumSizeUnit}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />

View File

@ -1,17 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Controls {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class AttachmentFilterPanel : UserControl {
namespace DHT.Desktop.Main.Controls;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class AttachmentFilterPanel : UserControl {
public AttachmentFilterPanel() {
InitializeComponent();
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -7,8 +7,9 @@ using DHT.Server.Database;
using DHT.Utils.Models;
using DHT.Utils.Tasks;
namespace DHT.Desktop.Main.Controls {
sealed class AttachmentFilterPanelModel : BaseModel, IDisposable {
namespace DHT.Desktop.Main.Controls;
sealed class AttachmentFilterPanelModel : BaseModel, IDisposable {
public sealed record Unit(string Name, uint Scale);
private static readonly Unit[] AllUnits = {
@ -125,5 +126,4 @@ namespace DHT.Desktop.Main.Controls {
return filter;
}
}
}

View File

@ -1,19 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Controls {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class MessageFilterPanel : UserControl {
private CalendarDatePicker StartDatePicker => this.FindControl<CalendarDatePicker>("StartDatePicker");
private CalendarDatePicker EndDatePicker => this.FindControl<CalendarDatePicker>("EndDatePicker");
namespace DHT.Desktop.Main.Controls;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class MessageFilterPanel : UserControl {
public MessageFilterPanel() {
InitializeComponent();
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
var culture = Program.Culture;
foreach (var picker in new CalendarDatePicker[] { StartDatePicker, EndDatePicker }) {
@ -30,5 +23,4 @@ namespace DHT.Desktop.Main.Controls {
model.EndDate = EndDatePicker.SelectedDate;
}
}
}
}

View File

@ -14,8 +14,9 @@ using DHT.Server.Database;
using DHT.Utils.Models;
using DHT.Utils.Tasks;
namespace DHT.Desktop.Main.Controls {
sealed class MessageFilterPanelModel : BaseModel, IDisposable {
namespace DHT.Desktop.Main.Controls;
sealed class MessageFilterPanelModel : BaseModel, IDisposable {
private static readonly HashSet<string> FilterProperties = new () {
nameof(FilterByDate),
nameof(StartDate),
@ -281,5 +282,4 @@ namespace DHT.Desktop.Main.Controls {
return result == DialogResult.OkCancel.Ok ? model.SelectedItems.Select(static item => item.Item).ToHashSet() : null;
}
}
}

View File

@ -1,16 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Controls {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class ServerConfigurationPanel : UserControl {
namespace DHT.Desktop.Main.Controls;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class ServerConfigurationPanel : UserControl {
public ServerConfigurationPanel() {
InitializeComponent();
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -6,8 +6,9 @@ using DHT.Server.Database;
using DHT.Server.Service;
using DHT.Utils.Models;
namespace DHT.Desktop.Main.Controls {
sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
namespace DHT.Desktop.Main.Controls;
sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
private string inputPort;
public string InputPort {
@ -112,5 +113,4 @@ namespace DHT.Desktop.Main.Controls {
InputPort = ServerManager.Port.ToString();
InputToken = ServerManager.Token;
}
}
}

View File

@ -1,16 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Controls {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class StatusBar : UserControl {
namespace DHT.Desktop.Main.Controls;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class StatusBar : UserControl {
public StatusBar() {
InitializeComponent();
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -2,8 +2,9 @@ using System;
using DHT.Server.Database;
using DHT.Utils.Models;
namespace DHT.Desktop.Main.Controls {
sealed class StatusBarModel : BaseModel {
namespace DHT.Desktop.Main.Controls;
sealed class StatusBarModel : BaseModel {
public DatabaseStatistics DatabaseStatistics { get; }
private Status status = Status.Stopped;
@ -41,5 +42,4 @@ namespace DHT.Desktop.Main.Controls {
Stopping,
Stopped
}
}
}

View File

@ -1,31 +1,23 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using DHT.Desktop.Main.Pages;
using JetBrains.Annotations;
namespace DHT.Desktop.Main {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class MainWindow : Window {
namespace DHT.Desktop.Main;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class MainWindow : Window {
[UsedImplicitly]
public MainWindow() {
InitializeComponent(Arguments.Empty);
InitializeComponent();
DataContext = new MainWindowModel(this, Arguments.Empty);
}
internal MainWindow(Arguments args) {
InitializeComponent(args);
}
private void InitializeComponent(Arguments args) {
AvaloniaXamlLoader.Load(this);
InitializeComponent();
DataContext = new MainWindowModel(this, args);
#if DEBUG
this.AttachDevTools();
#endif
}
public void OnClosed(object? sender, EventArgs e) {
@ -41,5 +33,4 @@ namespace DHT.Desktop.Main {
}
}
}
}
}

View File

@ -10,8 +10,9 @@ using DHT.Desktop.Server;
using DHT.Server.Database;
using DHT.Utils.Models;
namespace DHT.Desktop.Main {
sealed class MainWindowModel : BaseModel, IDisposable {
namespace DHT.Desktop.Main;
sealed class MainWindowModel : BaseModel, IDisposable {
private const string DefaultTitle = "Discord History Tracker";
public string Title { get; private set; } = DefaultTitle;
@ -112,5 +113,4 @@ namespace DHT.Desktop.Main {
db?.Dispose();
db = null;
}
}
}

View File

@ -1,16 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Pages {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class AdvancedPage : UserControl {
namespace DHT.Desktop.Main.Pages;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class AdvancedPage : UserControl {
public AdvancedPage() {
InitializeComponent();
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -6,8 +6,9 @@ using DHT.Desktop.Server;
using DHT.Server.Database;
using DHT.Utils.Models;
namespace DHT.Desktop.Main.Pages {
sealed class AdvancedPageModel : BaseModel, IDisposable {
namespace DHT.Desktop.Main.Pages;
sealed class AdvancedPageModel : BaseModel, IDisposable {
public ServerConfigurationPanelModel ServerConfigurationModel { get; }
private readonly Window window;
@ -35,5 +36,4 @@ namespace DHT.Desktop.Main.Pages {
db.Vacuum();
await Dialog.ShowOk(window, "Vacuum Database", "Done.");
}
}
}

View File

@ -38,7 +38,7 @@
<controls:AttachmentFilterPanel DataContext="{Binding FilterModel}" IsEnabled="{Binding !DataContext.IsDownloading, RelativeSource={RelativeSource AncestorType=UserControl}}" />
<StackPanel Orientation="Vertical" Spacing="12">
<Expander Header="Download Status" IsExpanded="True">
<DataGrid Items="{Binding StatisticsRows}" AutoGenerateColumns="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserSortColumns="False" IsReadOnly="True">
<DataGrid ItemsSource="{Binding StatisticsRows}" AutoGenerateColumns="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserSortColumns="False" IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="State" Binding="{Binding State}" Width="*" />
<DataGridTextColumn Header="Attachments" Binding="{Binding Items, Converter={StaticResource NumberValueConverter}}" Width="*" CellStyleClasses="right" />

View File

@ -1,16 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Pages {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class AttachmentsPage : UserControl {
namespace DHT.Desktop.Main.Pages;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class AttachmentsPage : UserControl {
public AttachmentsPage() {
InitializeComponent();
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using Avalonia.Threading;
using DHT.Desktop.Common;
using DHT.Desktop.Main.Controls;
using DHT.Server.Data;
@ -11,8 +13,9 @@ using DHT.Server.Download;
using DHT.Utils.Models;
using DHT.Utils.Tasks;
namespace DHT.Desktop.Main.Pages {
sealed class AttachmentsPageModel : BaseModel, IDisposable {
namespace DHT.Desktop.Main.Pages;
sealed class AttachmentsPageModel : BaseModel, IDisposable {
private static readonly DownloadItemFilter EnqueuedItemFilter = new() {
IncludeStatuses = new HashSet<DownloadStatus> {
DownloadStatus.Enqueued
@ -133,9 +136,9 @@ namespace DHT.Desktop.Main.Pages {
}
private void DownloadThreadOnOnItemFinished(object? sender, DownloadItem e) {
++doneItemsCount;
Interlocked.Increment(ref doneItemsCount);
UpdateDownloadMessage();
Dispatcher.UIThread.Invoke(UpdateDownloadMessage);
downloadStatisticsComputer.Recompute();
}
@ -199,5 +202,4 @@ namespace DHT.Desktop.Main.Pages {
State = state;
}
}
}
}

View File

@ -1,16 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Pages {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class DatabasePage : UserControl {
namespace DHT.Desktop.Main.Pages;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class DatabasePage : UserControl {
public DatabasePage() {
InitializeComponent();
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -7,8 +7,10 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using DHT.Desktop.Common;
using DHT.Desktop.Dialogs.File;
using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Dialogs.Progress;
using DHT.Desktop.Dialogs.TextBox;
@ -18,8 +20,9 @@ using DHT.Server.Database.Import;
using DHT.Utils.Logging;
using DHT.Utils.Models;
namespace DHT.Desktop.Main.Pages {
sealed class DatabasePageModel : BaseModel {
namespace DHT.Desktop.Main.Pages;
sealed class DatabasePageModel : BaseModel {
private static readonly Log Log = Log.ForType<DatabasePageModel>();
public IDatabaseFile Db { get; }
@ -68,12 +71,8 @@ namespace DHT.Desktop.Main.Pages {
}
public async void MergeWithDatabase() {
var fileDialog = DatabaseGui.NewOpenDatabaseFileDialog();
fileDialog.Directory = Path.GetDirectoryName(Db.Path);
fileDialog.AllowMultiple = true;
string[]? paths = await fileDialog.ShowAsync(window);
if (paths == null || paths.Length == 0) {
var paths = await DatabaseGui.NewOpenDatabaseFilesDialog(window, Path.GetDirectoryName(Db.Path));
if (paths.Length == 0) {
return;
}
@ -118,14 +117,13 @@ namespace DHT.Desktop.Main.Pages {
}
public async void ImportLegacyArchive() {
var fileDialog = new OpenFileDialog {
var paths = await window.StorageProvider.OpenFiles(new FilePickerOpenOptions {
Title = "Open Legacy DHT Archive",
Directory = Path.GetDirectoryName(Db.Path),
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, Path.GetDirectoryName(Db.Path)),
AllowMultiple = true
};
});
string[]? paths = await fileDialog.ShowAsync(window);
if (paths == null || paths.Length == 0) {
if (paths.Length == 0) {
return;
}
@ -244,5 +242,4 @@ namespace DHT.Desktop.Main.Pages {
return message.ToString();
}
}
}

View File

@ -1,16 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Pages {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class DebugPage : UserControl {
namespace DHT.Desktop.Main.Pages;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class DebugPage : UserControl {
public DebugPage() {
InitializeComponent();
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -63,7 +63,7 @@ namespace DHT.Desktop.Main.Pages {
var server = new DHT.Server.Data.Server {
Id = RandomId(rand),
Name = RandomName("s"),
Type = ServerType.Server
Type = ServerType.Server,
};
var channels = Enumerable.Range(0, channelCount).Select(i => new Channel {
@ -73,14 +73,14 @@ namespace DHT.Desktop.Main.Pages {
ParentId = null,
Position = i,
Topic = RandomText(rand, 10),
Nsfw = rand.Next(4) == 0
Nsfw = rand.Next(4) == 0,
}).ToArray();
var users = Enumerable.Range(0, userCount).Select(_ => new User {
Id = RandomId(rand),
Name = RandomName("u"),
AvatarUrl = null,
Discriminator = rand.Next(0, 9999).ToString()
Discriminator = rand.Next(0, 9999).ToString(),
}).ToArray();
db.AddServer(server);
@ -97,7 +97,7 @@ namespace DHT.Desktop.Main.Pages {
int hourOffset = batchIndex;
var messages = Enumerable.Range(0, Math.Min(messageCount, BatchSize)).Select(i => {
DateTimeOffset time = now.AddHours(hourOffset).AddMinutes((i * 60.0) / BatchSize);
DateTimeOffset time = now.AddHours(hourOffset).AddMinutes(i * 60.0 / BatchSize);
DateTimeOffset? edit = rand.Next(100) == 0 ? time.AddSeconds(rand.Next(1, 60)) : null;
var timeMillis = time.ToUnixTimeMilliseconds();
@ -113,7 +113,7 @@ namespace DHT.Desktop.Main.Pages {
RepliedToId = null,
Attachments = ImmutableArray<Attachment>.Empty,
Embeds = ImmutableArray<Embed>.Empty,
Reactions = ImmutableArray<Reaction>.Empty
Reactions = ImmutableArray<Reaction>.Empty,
};
}).ToArray();
@ -161,7 +161,7 @@ namespace DHT.Desktop.Main.Pages {
"vanilla",
"watercress", "watermelon",
"yam",
"zucchini"
"zucchini",
};
private static string RandomText(Random rand, int maxWords) {

View File

@ -3,36 +3,30 @@ using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Pages {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class TrackingPage : UserControl {
namespace DHT.Desktop.Main.Pages;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class TrackingPage : UserControl {
private bool isCopyingScript;
public TrackingPage() {
InitializeComponent();
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
public async void CopyTrackingScriptButton_OnClick(object? sender, RoutedEventArgs e) {
if (DataContext is TrackingPageModel model) {
var button = this.FindControl<Button>("CopyTrackingScript");
var originalText = button.Content;
button.MinWidth = button.Bounds.Width;
var originalText = CopyTrackingScript.Content;
CopyTrackingScript.MinWidth = CopyTrackingScript.Bounds.Width;
if (await model.OnClickCopyTrackingScript() && !isCopyingScript) {
isCopyingScript = true;
button.Content = "Script Copied!";
CopyTrackingScript.Content = "Script Copied!";
await Task.Delay(TimeSpan.FromSeconds(2));
button.Content = originalText;
CopyTrackingScript.Content = originalText;
isCopyingScript = false;
}
}
}
}
}

View File

@ -1,7 +1,6 @@
using System;
using System.Threading.Tasks;
using System.Web;
using Avalonia;
using Avalonia.Controls;
using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Discord;
@ -9,8 +8,9 @@ using DHT.Desktop.Server;
using DHT.Utils.Models;
using static DHT.Desktop.Program;
namespace DHT.Desktop.Main.Pages {
sealed class TrackingPageModel : BaseModel {
namespace DHT.Desktop.Main.Pages;
sealed class TrackingPageModel : BaseModel {
private bool areDevToolsEnabled;
private bool AreDevToolsEnabled {
@ -61,7 +61,7 @@ namespace DHT.Desktop.Main.Pages {
.Replace("/*[CSS-CONTROLLER]*/", await Resources.ReadTextAsync("Tracker/styles/controller.css"))
.Replace("/*[CSS-SETTINGS]*/", await Resources.ReadTextAsync("Tracker/styles/settings.css"));
var clipboard = Application.Current?.Clipboard;
var clipboard = window.Clipboard;
if (clipboard == null) {
await Dialog.ShowOk(window, "Copy Tracking Script", "Clipboard is not available on this system.");
return false;
@ -113,5 +113,4 @@ namespace DHT.Desktop.Main.Pages {
throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@ -1,16 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Pages {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class ViewerPage : UserControl {
namespace DHT.Desktop.Main.Pages;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class ViewerPage : UserControl {
public ViewerPage() {
InitializeComponent();
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
@ -8,7 +7,9 @@ using System.Text;
using System.Threading.Tasks;
using System.Web;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using DHT.Desktop.Common;
using DHT.Desktop.Dialogs.File;
using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Main.Controls;
using DHT.Desktop.Server;
@ -19,8 +20,9 @@ using DHT.Server.Database.Export.Strategy;
using DHT.Utils.Models;
using static DHT.Desktop.Program;
namespace DHT.Desktop.Main.Pages {
sealed class ViewerPageModel : BaseModel, IDisposable {
namespace DHT.Desktop.Main.Pages;
sealed class ViewerPageModel : BaseModel, IDisposable {
public static readonly ConcurrentBag<string> TemporaryFiles = new ();
public bool DatabaseToolFilterModeKeep { get; set; } = true;
@ -113,21 +115,19 @@ namespace DHT.Desktop.Main.Pages {
Process.Start(new ProcessStartInfo(fullPath) { UseShellExecute = true });
}
public async void OnClickSaveViewer() {
var dialog = new SaveFileDialog {
Title = "Save Viewer",
InitialFileName = Path.GetFileNameWithoutExtension(db.Path) + ".html",
Directory = Path.GetDirectoryName(db.Path),
Filters = new List<FileDialogFilter> {
new() {
Name = "Discord History Viewer",
Extensions = { "html" }
}
}
}.ShowAsync(window);
private static readonly FilePickerFileType[] ViewerFileTypes = {
FileDialogs.CreateFilter("Discord History Viewer", new string[] { "html" }),
};
string? path = await dialog;
if (!string.IsNullOrEmpty(path)) {
public async void OnClickSaveViewer() {
string? path = await window.StorageProvider.SaveFile(new FilePickerSaveOptions {
Title = "Save Viewer",
FileTypeChoices = ViewerFileTypes,
SuggestedFileName = Path.GetFileNameWithoutExtension(db.Path) + ".html",
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, Path.GetDirectoryName(db.Path)),
});
if (path != null) {
await WriteViewerFile(path, StandaloneViewerExportStrategy.Instance);
}
}
@ -146,5 +146,4 @@ namespace DHT.Desktop.Main.Pages {
}
}
}
}
}

View File

@ -1,16 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Screens {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class MainContentScreen : UserControl {
namespace DHT.Desktop.Main.Screens;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class MainContentScreen : UserControl {
public MainContentScreen() {
InitializeComponent();
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -9,8 +9,9 @@ using DHT.Server.Database;
using DHT.Server.Service;
using DHT.Utils.Logging;
namespace DHT.Desktop.Main.Screens {
sealed class MainContentScreenModel : IDisposable {
namespace DHT.Desktop.Main.Screens;
sealed class MainContentScreenModel : IDisposable {
private static readonly Log Log = Log.ForType<MainContentScreenModel>();
public DatabasePage DatabasePage { get; }
@ -115,5 +116,4 @@ namespace DHT.Desktop.Main.Screens {
Log.Error(ex);
await Dialog.ShowOk(window, "Internal Server Error", ex.Message);
}
}
}

View File

@ -1,16 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Screens {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class WelcomeScreen : UserControl {
namespace DHT.Desktop.Main.Screens;
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed partial class WelcomeScreen : UserControl {
public WelcomeScreen() {
InitializeComponent();
}
private void InitializeComponent() {
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -7,8 +7,9 @@ using DHT.Desktop.Dialogs.Message;
using DHT.Server.Database;
using DHT.Utils.Models;
namespace DHT.Desktop.Main.Screens {
sealed class WelcomeScreenModel : BaseModel, IDisposable {
namespace DHT.Desktop.Main.Screens;
sealed class WelcomeScreenModel : BaseModel, IDisposable {
public string Version => Program.Version;
public IDatabaseFile? Db { get; private set; }
@ -26,11 +27,8 @@ namespace DHT.Desktop.Main.Screens {
}
public async void OpenOrCreateDatabase() {
var dialog = DatabaseGui.NewOpenOrCreateDatabaseFileDialog();
dialog.Directory = Path.GetDirectoryName(dbFilePath);
string? path = await dialog.ShowAsync(window);
if (!string.IsNullOrWhiteSpace(path)) {
var path = await DatabaseGui.NewOpenOrCreateDatabaseFileDialog(window, Path.GetDirectoryName(dbFilePath));
if (path != null) {
await OpenOrCreateDatabaseFromPath(path);
}
}
@ -69,5 +67,4 @@ namespace DHT.Desktop.Main.Screens {
Db?.Dispose();
Db = null;
}
}
}

View File

@ -3,8 +3,9 @@ using System.Reflection;
using Avalonia;
using DHT.Utils.Resources;
namespace DHT.Desktop {
static class Program {
namespace DHT.Desktop;
static class Program {
public static string Version { get; }
public static CultureInfo Culture { get; }
public static ResourceLoader Resources { get; }
@ -33,7 +34,7 @@ namespace DHT.Desktop {
private static AppBuilder BuildAvaloniaApp() {
return AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}
}
}

View File

@ -2,8 +2,9 @@ using System;
using DHT.Server.Database;
using DHT.Server.Service;
namespace DHT.Desktop.Server {
sealed class ServerManager : IDisposable {
namespace DHT.Desktop.Server;
sealed class ServerManager : IDisposable {
public static ushort Port { get; set; } = ServerUtils.FindAvailablePort(50000, 60000);
public static string Token { get; set; } = ServerUtils.GenerateRandomToken(20);
@ -46,5 +47,4 @@ namespace DHT.Desktop.Server {
instance = null;
}
}
}
}

View File

@ -0,0 +1,8 @@
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>11</LangVersion>
</PropertyGroup>
</Project>

View File

@ -1,5 +1,6 @@
namespace DHT.Server.Data.Aggregations {
public sealed class DownloadStatusStatistics {
namespace DHT.Server.Data.Aggregations;
public sealed class DownloadStatusStatistics {
public int EnqueuedCount { get; internal set; }
public ulong EnqueuedSize { get; internal set; }
@ -11,5 +12,4 @@ namespace DHT.Server.Data.Aggregations {
public int SkippedCount { get; internal set; }
public ulong SkippedSize { get; internal set; }
}
}

View File

@ -1,5 +1,6 @@
namespace DHT.Server.Data {
public readonly struct Attachment {
namespace DHT.Server.Data;
public readonly struct Attachment {
public ulong Id { get; internal init; }
public string Name { get; internal init; }
public string? Type { get; internal init; }
@ -7,5 +8,4 @@ namespace DHT.Server.Data {
public ulong Size { get; internal init; }
public int? Width { get; internal init; }
public int? Height { get; internal init; }
}
}

View File

@ -1,5 +1,6 @@
namespace DHT.Server.Data {
public readonly struct Channel {
namespace DHT.Server.Data;
public readonly struct Channel {
public ulong Id { get; init; }
public ulong Server { get; init; }
public string Name { get; init; }
@ -7,5 +8,4 @@ namespace DHT.Server.Data {
public int? Position { get; init; }
public string? Topic { get; init; }
public bool? Nsfw { get; init; }
}
}

View File

@ -1,8 +1,9 @@
using System;
using System.Net;
namespace DHT.Server.Data {
public readonly struct Download {
namespace DHT.Server.Data;
public readonly struct Download {
internal static Download NewSuccess(string url, byte[] data) {
return new Download(url, DownloadStatus.Success, (ulong) Math.Max(data.LongLength, 0), data);
}
@ -26,5 +27,4 @@ namespace DHT.Server.Data {
internal Download WithData(byte[] data) {
return new Download(Url, Status, Size, data);
}
}
}

View File

@ -1,12 +1,12 @@
using System.Net;
namespace DHT.Server.Data {
/// <summary>
/// Extends <see cref="HttpStatusCode"/> with custom status codes in the range 0-99.
/// </summary>
public enum DownloadStatus {
namespace DHT.Server.Data;
/// <summary>
/// Extends <see cref="HttpStatusCode"/> with custom status codes in the range 0-99.
/// </summary>
public enum DownloadStatus {
Enqueued = 0,
GenericError = 1,
Success = HttpStatusCode.OK
}
}

View File

@ -1,6 +1,6 @@
namespace DHT.Server.Data {
public readonly struct DownloadedAttachment {
namespace DHT.Server.Data;
public readonly struct DownloadedAttachment {
public string? Type { get; internal init; }
public byte[] Data { get; internal init; }
}
}

View File

@ -1,5 +1,5 @@
namespace DHT.Server.Data {
public readonly struct Embed {
namespace DHT.Server.Data;
public readonly struct Embed {
public string Json { get; internal init; }
}
}

View File

@ -1,9 +1,9 @@
using System;
namespace DHT.Server.Data {
[Flags]
public enum EmojiFlags : ushort {
namespace DHT.Server.Data;
[Flags]
public enum EmojiFlags : ushort {
None = 0,
Animated = 0b1
}
}

View File

@ -1,5 +1,6 @@
namespace DHT.Server.Data.Filters {
public sealed class AttachmentFilter {
namespace DHT.Server.Data.Filters;
public sealed class AttachmentFilter {
public ulong? MaxBytes { get; set; } = null;
public DownloadItemRules? DownloadItemRule { get; set; } = null;
@ -11,5 +12,4 @@ namespace DHT.Server.Data.Filters {
OnlyNotPresent,
OnlyPresent
}
}
}

View File

@ -1,10 +1,10 @@
using System.Collections.Generic;
namespace DHT.Server.Data.Filters {
public sealed class DownloadItemFilter {
public HashSet<DownloadStatus>? IncludeStatuses { get; set; } = null;
public HashSet<DownloadStatus>? ExcludeStatuses { get; set; } = null;
namespace DHT.Server.Data.Filters;
public sealed class DownloadItemFilter {
public HashSet<DownloadStatus>? IncludeStatuses { get; init; } = null;
public HashSet<DownloadStatus>? ExcludeStatuses { get; init; } = null;
public bool IsEmpty => IncludeStatuses == null && ExcludeStatuses == null;
}
}

View File

@ -1,6 +1,6 @@
namespace DHT.Server.Data.Filters {
public enum FilterRemovalMode {
namespace DHT.Server.Data.Filters;
public enum FilterRemovalMode {
KeepMatching,
RemoveMatching
}
}

View File

@ -1,8 +1,9 @@
using System;
using System.Collections.Generic;
namespace DHT.Server.Data.Filters {
public sealed class MessageFilter {
namespace DHT.Server.Data.Filters;
public sealed class MessageFilter {
public DateTime? StartDate { get; set; } = null;
public DateTime? EndDate { get; set; } = null;
@ -15,5 +16,4 @@ namespace DHT.Server.Data.Filters {
ChannelIds == null &&
UserIds == null &&
MessageIds == null;
}
}

View File

@ -1,7 +1,8 @@
using System.Collections.Immutable;
namespace DHT.Server.Data {
public readonly struct Message {
namespace DHT.Server.Data;
public readonly struct Message {
public ulong Id { get; init; }
public ulong Sender { get; init; }
public ulong Channel { get; init; }
@ -12,5 +13,4 @@ namespace DHT.Server.Data {
public ImmutableArray<Attachment> Attachments { get; init; }
public ImmutableArray<Embed> Embeds { get; init; }
public ImmutableArray<Reaction> Reactions { get; init; }
}
}

View File

@ -1,8 +1,8 @@
namespace DHT.Server.Data {
public readonly struct Reaction {
namespace DHT.Server.Data;
public readonly struct Reaction {
public ulong? EmojiId { get; internal init; }
public string? EmojiName { get; internal init; }
public EmojiFlags EmojiFlags { get; internal init; }
public int Count { get; internal init; }
}
}

View File

@ -1,7 +1,7 @@
namespace DHT.Server.Data {
public readonly struct Server {
namespace DHT.Server.Data;
public readonly struct Server {
public ulong Id { get; init; }
public string Name { get; init; }
public ServerType? Type { get; init; }
}
}

View File

@ -1,11 +1,12 @@
namespace DHT.Server.Data {
public enum ServerType {
namespace DHT.Server.Data;
public enum ServerType {
Server,
Group,
DirectMessage
}
}
public static class ServerTypes {
public static class ServerTypes {
public static ServerType? FromString(string? str) {
return str switch {
"SERVER" => ServerType.Server,
@ -41,5 +42,4 @@ namespace DHT.Server.Data {
_ => "unknown"
};
}
}
}

View File

@ -1,8 +1,8 @@
namespace DHT.Server.Data {
public readonly struct User {
namespace DHT.Server.Data;
public readonly struct User {
public ulong Id { get; init; }
public string Name { get; init; }
public string? AvatarUrl { get; init; }
public string? Discriminator { get; init; }
}
}

View File

@ -1,8 +1,9 @@
using System.Collections.Generic;
using DHT.Server.Data;
namespace DHT.Server.Database {
public static class DatabaseExtensions {
namespace DHT.Server.Database;
public static class DatabaseExtensions {
public static void AddFrom(this IDatabaseFile target, IDatabaseFile source) {
target.AddServers(source.GetAllServers());
target.AddChannels(source.GetAllChannels());
@ -25,5 +26,4 @@ namespace DHT.Server.Database {
target.AddChannel(channel);
}
}
}
}

View File

@ -1,11 +1,12 @@
using DHT.Utils.Models;
namespace DHT.Server.Database {
/// <summary>
/// A live view of database statistics.
/// Some of the totals are computed asynchronously and may not reflect the most recent version of the database, or may not be available at all until computed for the first time.
/// </summary>
public sealed class DatabaseStatistics : BaseModel {
namespace DHT.Server.Database;
/// <summary>
/// A live view of database statistics.
/// Some of the totals are computed asynchronously and may not reflect the most recent version of the database, or may not be available at all until computed for the first time.
/// </summary>
public sealed class DatabaseStatistics : BaseModel {
private long totalServers;
private long totalChannels;
private long totalUsers;
@ -42,5 +43,4 @@ namespace DHT.Server.Database {
get => totalDownloads;
internal set => Change(ref totalDownloads, value);
}
}
}

View File

@ -1,11 +1,11 @@
namespace DHT.Server.Database {
/// <summary>
/// A complete snapshot of database statistics at a particular point in time.
/// </summary>
public readonly struct DatabaseStatisticsSnapshot {
namespace DHT.Server.Database;
/// <summary>
/// A complete snapshot of database statistics at a particular point in time.
/// </summary>
public readonly struct DatabaseStatisticsSnapshot {
public long TotalServers { get; internal init; }
public long TotalChannels { get; internal init; }
public long TotalUsers { get; internal init; }
public long TotalMessages { get; internal init; }
}
}

View File

@ -1,11 +1,14 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using DHT.Server.Data;
using DHT.Server.Data.Aggregations;
using DHT.Server.Data.Filters;
using DHT.Server.Download;
namespace DHT.Server.Database {
public sealed class DummyDatabaseFile : IDatabaseFile {
namespace DHT.Server.Database;
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public sealed class DummyDatabaseFile : IDatabaseFile {
public static DummyDatabaseFile Instance { get; } = new();
public string Path => "";
@ -84,5 +87,4 @@ namespace DHT.Server.Database {
public void Vacuum() {}
public void Dispose() {}
}
}

View File

@ -1,13 +1,13 @@
using System;
using DHT.Server.Database.Sqlite;
namespace DHT.Server.Database.Exceptions {
public sealed class DatabaseTooNewException : Exception {
namespace DHT.Server.Database.Exceptions;
public sealed class DatabaseTooNewException : Exception {
public int DatabaseVersion { get; }
public int CurrentVersion => Schema.Version;
internal DatabaseTooNewException(int databaseVersion) : base("Database is too new: " + databaseVersion + " > " + Schema.Version) {
this.DatabaseVersion = databaseVersion;
}
}
}

View File

@ -1,11 +1,11 @@
using System;
namespace DHT.Server.Database.Exceptions {
public sealed class InvalidDatabaseVersionException : Exception {
namespace DHT.Server.Database.Exceptions;
public sealed class InvalidDatabaseVersionException : Exception {
public string Version { get; }
internal InvalidDatabaseVersionException(string version) : base("Invalid database version: " + version) {
this.Version = version;
}
}
}

View File

@ -1,7 +1,7 @@
using DHT.Server.Data;
namespace DHT.Server.Database.Export.Strategy {
public interface IViewerExportStrategy {
namespace DHT.Server.Database.Export.Strategy;
public interface IViewerExportStrategy {
string GetAttachmentUrl(Attachment attachment);
}
}

View File

@ -1,8 +1,9 @@
using System.Net;
using DHT.Server.Data;
namespace DHT.Server.Database.Export.Strategy {
public sealed class LiveViewerExportStrategy : IViewerExportStrategy {
namespace DHT.Server.Database.Export.Strategy;
public sealed class LiveViewerExportStrategy : IViewerExportStrategy {
private readonly string safePort;
private readonly string safeToken;
@ -14,5 +15,4 @@ namespace DHT.Server.Database.Export.Strategy {
public string GetAttachmentUrl(Attachment attachment) {
return "http://127.0.0.1:" + safePort + "/get-attachment/" + WebUtility.UrlEncode(attachment.Url) + "?token=" + safeToken;
}
}
}

View File

@ -1,7 +1,8 @@
using DHT.Server.Data;
namespace DHT.Server.Database.Export.Strategy {
public sealed class StandaloneViewerExportStrategy : IViewerExportStrategy {
namespace DHT.Server.Database.Export.Strategy;
public sealed class StandaloneViewerExportStrategy : IViewerExportStrategy {
public static StandaloneViewerExportStrategy Instance { get; } = new ();
private StandaloneViewerExportStrategy() {}
@ -9,5 +10,4 @@ namespace DHT.Server.Database.Export.Strategy {
public string GetAttachmentUrl(Attachment attachment) {
return attachment.Url;
}
}
}

View File

@ -9,8 +9,9 @@ using DHT.Server.Data.Filters;
using DHT.Server.Database.Export.Strategy;
using DHT.Utils.Logging;
namespace DHT.Server.Database.Export {
public static class ViewerJsonExport {
namespace DHT.Server.Database.Export;
public static class ViewerJsonExport {
private static readonly Log Log = Log.ForType(typeof(ViewerJsonExport));
public static async Task Generate(Stream stream, IViewerExportStrategy strategy, IDatabaseFile db, MessageFilter? filter = null) {
@ -43,7 +44,7 @@ namespace DHT.Server.Database.Export {
var value = new {
meta = new { users, userindex, servers, channels },
data = GenerateMessageList(includedMessages, userIndices, strategy)
data = GenerateMessageList(includedMessages, userIndices, strategy),
};
perf.Step("Generate value object");
@ -57,7 +58,7 @@ namespace DHT.Server.Database.Export {
perf.End();
}
private static object GenerateUserList(IDatabaseFile db, HashSet<ulong> userIds, out List<string> userindex, out Dictionary<ulong, object> userIndices) {
private static Dictionary<string, object> GenerateUserList(IDatabaseFile db, HashSet<ulong> userIds, out List<string> userindex, out Dictionary<ulong, object> userIndices) {
var users = new Dictionary<string, object>();
userindex = new List<string>();
userIndices = new Dictionary<ulong, object>();
@ -89,7 +90,7 @@ namespace DHT.Server.Database.Export {
return users;
}
private static object GenerateServerList(IDatabaseFile db, HashSet<ulong> serverIds, out Dictionary<ulong, object> serverIndices) {
private static List<object> GenerateServerList(IDatabaseFile db, HashSet<ulong> serverIds, out Dictionary<ulong, object> serverIndices) {
var servers = new List<object>();
serverIndices = new Dictionary<ulong, object>();
@ -102,20 +103,20 @@ namespace DHT.Server.Database.Export {
serverIndices[id] = servers.Count;
servers.Add(new Dictionary<string, object> {
["name"] = server.Name,
["type"] = ServerTypes.ToJsonViewerString(server.Type)
["type"] = ServerTypes.ToJsonViewerString(server.Type),
});
}
return servers;
}
private static object GenerateChannelList(List<Channel> includedChannels, Dictionary<ulong, object> serverIndices) {
private static Dictionary<string, object> GenerateChannelList(List<Channel> includedChannels, Dictionary<ulong, object> serverIndices) {
var channels = new Dictionary<string, object>();
foreach (var channel in includedChannels) {
var obj = new Dictionary<string, object> {
["server"] = serverIndices[channel.Server],
["name"] = channel.Name
["name"] = channel.Name,
};
if (channel.ParentId != null) {
@ -140,7 +141,7 @@ namespace DHT.Server.Database.Export {
return channels;
}
private static object GenerateMessageList( List<Message> includedMessages, Dictionary<ulong, object> userIndices, IViewerExportStrategy strategy) {
private static Dictionary<string, Dictionary<string, object>> GenerateMessageList( List<Message> includedMessages, Dictionary<ulong, object> userIndices, IViewerExportStrategy strategy) {
var data = new Dictionary<string, Dictionary<string, object>>();
foreach (var grouping in includedMessages.GroupBy(static message => message.Channel)) {
@ -150,7 +151,7 @@ namespace DHT.Server.Database.Export {
foreach (var message in grouping) {
var obj = new Dictionary<string, object> {
["u"] = userIndices[message.Sender],
["t"] = message.Timestamp
["t"] = message.Timestamp,
};
if (!string.IsNullOrEmpty(message.Text)) {
@ -169,10 +170,10 @@ namespace DHT.Server.Database.Export {
obj["a"] = message.Attachments.Select(attachment => {
var a = new Dictionary<string, object> {
{ "url", strategy.GetAttachmentUrl(attachment) },
{ "name", Uri.TryCreate(attachment.Url, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.Url }
{ "name", Uri.TryCreate(attachment.Url, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.Url },
};
if (attachment.Width != null && attachment.Height != null) {
if (attachment is { Width: not null, Height: not null }) {
a["width"] = attachment.Width;
a["height"] = attachment.Height;
}
@ -211,5 +212,4 @@ namespace DHT.Server.Database.Export {
return data;
}
}
}

View File

@ -2,8 +2,9 @@ using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace DHT.Server.Database.Export {
sealed class ViewerJsonSnowflakeSerializer : JsonConverter<ulong> {
namespace DHT.Server.Database.Export;
sealed class ViewerJsonSnowflakeSerializer : JsonConverter<ulong> {
public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
return ulong.Parse(reader.GetString()!);
}
@ -11,5 +12,4 @@ namespace DHT.Server.Database.Export {
public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options) {
writer.WriteStringValue(value.ToString());
}
}
}

View File

@ -5,8 +5,9 @@ using DHT.Server.Data.Aggregations;
using DHT.Server.Data.Filters;
using DHT.Server.Download;
namespace DHT.Server.Database {
public interface IDatabaseFile : IDisposable {
namespace DHT.Server.Database;
public interface IDatabaseFile : IDisposable {
string Path { get; }
DatabaseStatistics Statistics { get; }
DatabaseStatisticsSnapshot SnapshotStatistics();
@ -39,5 +40,4 @@ namespace DHT.Server.Database {
DownloadStatusStatistics GetDownloadStatusStatistics();
void Vacuum();
}
}

View File

@ -1,10 +1,11 @@
using System;
namespace DHT.Server.Database.Import {
/// <summary>
/// https://discord.com/developers/docs/reference#snowflakes
/// </summary>
public sealed class FakeSnowflake {
namespace DHT.Server.Database.Import;
/// <summary>
/// https://discord.com/developers/docs/reference#snowflakes
/// </summary>
public sealed class FakeSnowflake {
private const ulong DiscordEpoch = 1420070400000UL;
private ulong id;
@ -17,5 +18,4 @@ namespace DHT.Server.Database.Import {
internal ulong Next() {
return id++;
}
}
}

View File

@ -12,8 +12,9 @@ using DHT.Utils.Http;
using DHT.Utils.Logging;
using Microsoft.AspNetCore.StaticFiles;
namespace DHT.Server.Database.Import {
public static class LegacyArchiveImport {
namespace DHT.Server.Database.Import;
public static class LegacyArchiveImport {
private static readonly Log Log = Log.ForType(typeof(LegacyArchiveImport));
private static readonly FileExtensionContentTypeProvider ContentTypeProvider = new ();
@ -56,11 +57,7 @@ namespace DHT.Server.Database.Import {
for (var i = 0; i < servers.Length; i++) {
var server = servers[i];
if (askedServerIds.TryGetValue(server, out var serverId)) {
servers[i] = new Data.Server {
Id = serverId,
Name = server.Name,
Type = server.Type
};
servers[i] = server with { Id = serverId };
}
}
}
@ -112,7 +109,7 @@ namespace DHT.Server.Database.Import {
Id = userId,
Name = userObj.RequireString("name", path),
AvatarUrl = userObj.HasKey("avatar") ? userObj.RequireString("avatar", path) : null,
Discriminator = userObj.HasKey("tag") ? userObj.RequireString("tag", path) : null
Discriminator = userObj.HasKey("tag") ? userObj.RequireString("tag", path) : null,
};
}
@ -125,7 +122,7 @@ namespace DHT.Server.Database.Import {
return meta.RequireArray("servers", "meta").Select(serverObj => new Data.Server {
Id = fakeSnowflake.Next(),
Name = serverObj.RequireString("name", ServersPath),
Type = ServerTypes.FromString(serverObj.RequireString("type", ServersPath))
Type = ServerTypes.FromString(serverObj.RequireString("type", ServersPath)),
}).ToArray();
}
@ -153,7 +150,7 @@ namespace DHT.Server.Database.Import {
Name = channelObj.RequireString("name", path),
Position = channelObj.HasKey("position") ? channelObj.RequireInt("position", path, min: 0) : null,
Topic = channelObj.HasKey("topic") ? channelObj.RequireString("topic", path) : null,
Nsfw = channelObj.HasKey("nsfw") ? channelObj.RequireBool("nsfw", path) : null
Nsfw = channelObj.HasKey("nsfw") ? channelObj.RequireBool("nsfw", path) : null,
};
}).ToArray();
}
@ -184,7 +181,7 @@ namespace DHT.Server.Database.Import {
RepliedToId = messageObj.HasKey("r") ? messageObj.RequireSnowflake("r", path) : null,
Attachments = messageObj.HasKey("a") ? ReadMessageAttachments(messageObj.RequireArray("a", path), fakeSnowflake, path + ".a[]").ToImmutableArray() : ImmutableArray<Attachment>.Empty,
Embeds = messageObj.HasKey("e") ? ReadMessageEmbeds(messageObj.RequireArray("e", path), path + ".e[]").ToImmutableArray() : ImmutableArray<Embed>.Empty,
Reactions = messageObj.HasKey("re") ? ReadMessageReactions(messageObj.RequireArray("re", path), path + ".re[]").ToImmutableArray() : ImmutableArray<Reaction>.Empty
Reactions = messageObj.HasKey("re") ? ReadMessageReactions(messageObj.RequireArray("re", path), path + ".re[]").ToImmutableArray() : ImmutableArray<Reaction>.Empty,
};
}).ToArray();
}
@ -201,7 +198,7 @@ namespace DHT.Server.Database.Import {
Name = name,
Type = type,
Url = url,
Size = 0 // unknown size
Size = 0, // unknown size
};
}).DistinctByKeyStable(static attachment => {
// Some Discord messages have duplicate attachments with the same id for unknown reasons.
@ -218,7 +215,7 @@ namespace DHT.Server.Database.Import {
var embedJson = new Dictionary<string, object> {
{ "url", url },
{ "type", type },
{ "dht_legacy", true }
{ "dht_legacy", true },
};
if (type == "image") {
@ -255,9 +252,8 @@ namespace DHT.Server.Database.Import {
EmojiId = id,
EmojiName = name,
EmojiFlags = reactionObj.HasKey("an") && reactionObj.RequireBool("an", path) ? EmojiFlags.Animated : EmojiFlags.None,
Count = reactionObj.RequireInt("c", path, min: 0)
Count = reactionObj.RequireInt("c", path, min: 0),
};
});
}
}
}

View File

@ -4,8 +4,9 @@ using DHT.Server.Database.Exceptions;
using DHT.Server.Database.Sqlite.Utils;
using DHT.Utils.Logging;
namespace DHT.Server.Database.Sqlite {
sealed class Schema {
namespace DHT.Server.Database.Sqlite;
sealed class Schema {
internal const int Version = 5;
private static readonly Log Log = Log.ForType<Schema>();
@ -169,5 +170,4 @@ namespace DHT.Server.Database.Sqlite {
perf.End();
}
}
}

View File

@ -13,8 +13,9 @@ using DHT.Utils.Logging;
using DHT.Utils.Tasks;
using Microsoft.Data.Sqlite;
namespace DHT.Server.Database.Sqlite {
public sealed class SqliteDatabaseFile : IDatabaseFile {
namespace DHT.Server.Database.Sqlite;
public sealed class SqliteDatabaseFile : IDatabaseFile {
private const int DefaultPoolSize = 5;
public static async Task<SqliteDatabaseFile?> OpenOrCreate(string path, Func<Task<bool>> checkCanUpgradeSchemas) {
@ -79,7 +80,7 @@ namespace DHT.Server.Database.Sqlite {
TotalServers = Statistics.TotalServers,
TotalChannels = Statistics.TotalChannels,
TotalUsers = Statistics.TotalUsers,
TotalMessages = ComputeMessageStatistics()
TotalMessages = ComputeMessageStatistics(),
};
}
@ -88,7 +89,7 @@ namespace DHT.Server.Database.Sqlite {
using var cmd = conn.Upsert("servers", new[] {
("id", SqliteType.Integer),
("name", SqliteType.Text),
("type", SqliteType.Text)
("type", SqliteType.Text),
});
cmd.Set(":id", server.Id);
@ -110,7 +111,7 @@ namespace DHT.Server.Database.Sqlite {
list.Add(new Data.Server {
Id = reader.GetUint64(0),
Name = reader.GetString(1),
Type = ServerTypes.FromString(reader.GetString(2))
Type = ServerTypes.FromString(reader.GetString(2)),
});
}
@ -127,7 +128,7 @@ namespace DHT.Server.Database.Sqlite {
("parent_id", SqliteType.Integer),
("position", SqliteType.Integer),
("topic", SqliteType.Text),
("nsfw", SqliteType.Integer)
("nsfw", SqliteType.Integer),
});
cmd.Set(":id", channel.Id);
@ -156,7 +157,7 @@ namespace DHT.Server.Database.Sqlite {
ParentId = reader.IsDBNull(3) ? null : reader.GetUint64(3),
Position = reader.IsDBNull(4) ? null : reader.GetInt32(4),
Topic = reader.IsDBNull(5) ? null : reader.GetString(5),
Nsfw = reader.IsDBNull(6) ? null : reader.GetBoolean(6)
Nsfw = reader.IsDBNull(6) ? null : reader.GetBoolean(6),
});
}
@ -170,7 +171,7 @@ namespace DHT.Server.Database.Sqlite {
("id", SqliteType.Integer),
("name", SqliteType.Text),
("avatar_url", SqliteType.Text),
("discriminator", SqliteType.Text)
("discriminator", SqliteType.Text),
});
foreach (var user in users) {
@ -198,7 +199,7 @@ namespace DHT.Server.Database.Sqlite {
Id = reader.GetUint64(0),
Name = reader.GetString(1),
AvatarUrl = reader.IsDBNull(2) ? null : reader.GetString(2),
Discriminator = reader.IsDBNull(3) ? null : reader.GetString(3)
Discriminator = reader.IsDBNull(3) ? null : reader.GetString(3),
});
}
@ -226,7 +227,7 @@ namespace DHT.Server.Database.Sqlite {
("sender_id", SqliteType.Integer),
("channel_id", SqliteType.Integer),
("text", SqliteType.Text),
("timestamp", SqliteType.Integer)
("timestamp", SqliteType.Integer),
});
using var deleteEditTimestampCmd = DeleteByMessageId(conn, "edit_timestamps");
@ -238,12 +239,12 @@ namespace DHT.Server.Database.Sqlite {
using var editTimestampCmd = conn.Insert("edit_timestamps", new [] {
("message_id", SqliteType.Integer),
("edit_timestamp", SqliteType.Integer)
("edit_timestamp", SqliteType.Integer),
});
using var repliedToCmd = conn.Insert("replied_to", new [] {
("message_id", SqliteType.Integer),
("replied_to_id", SqliteType.Integer)
("replied_to_id", SqliteType.Integer),
});
using var attachmentCmd = conn.Insert("attachments", new[] {
@ -254,12 +255,12 @@ namespace DHT.Server.Database.Sqlite {
("url", SqliteType.Text),
("size", SqliteType.Integer),
("width", SqliteType.Integer),
("height", SqliteType.Integer)
("height", SqliteType.Integer),
});
using var embedCmd = conn.Insert("embeds", new[] {
("message_id", SqliteType.Integer),
("json", SqliteType.Text)
("json", SqliteType.Text),
});
using var reactionCmd = conn.Insert("reactions", new[] {
@ -267,7 +268,7 @@ namespace DHT.Server.Database.Sqlite {
("emoji_id", SqliteType.Integer),
("emoji_name", SqliteType.Text),
("emoji_flags", SqliteType.Integer),
("count", SqliteType.Integer)
("count", SqliteType.Integer),
});
foreach (var message in messages) {
@ -382,7 +383,7 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC
RepliedToId = reader.IsDBNull(6) ? null : reader.GetUint64(6),
Attachments = attachments.GetListOrNull(id)?.ToImmutableArray() ?? ImmutableArray<Attachment>.Empty,
Embeds = embeds.GetListOrNull(id)?.ToImmutableArray() ?? ImmutableArray<Embed>.Empty,
Reactions = reactions.GetListOrNull(id)?.ToImmutableArray() ?? ImmutableArray<Reaction>.Empty
Reactions = reactions.GetListOrNull(id)?.ToImmutableArray() ?? ImmutableArray<Reaction>.Empty,
});
}
@ -429,7 +430,7 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC
("url", SqliteType.Text),
("status", SqliteType.Integer),
("size", SqliteType.Integer),
("blob", SqliteType.Blob)
("blob", SqliteType.Blob),
});
cmd.Set(":url", download.Url);
@ -492,7 +493,7 @@ WHERE d.url = :url AND d.status = :success AND d.blob IS NOT NULL");
return new DownloadedAttachment {
Type = reader.IsDBNull(0) ? null : reader.GetString(0),
Data = (byte[]) reader["blob"]
Data = (byte[]) reader["blob"],
};
}
@ -516,7 +517,7 @@ WHERE d.url = :url AND d.status = :success AND d.blob IS NOT NULL");
while (reader.Read()) {
list.Add(new DownloadItem {
Url = reader.GetString(0),
Size = reader.GetUint64(1)
Size = reader.GetUint64(1),
});
}
@ -588,7 +589,7 @@ FROM downloads");
Url = reader.GetString(4),
Size = reader.GetUint64(5),
Width = reader.IsDBNull(6) ? null : reader.GetInt32(6),
Height = reader.IsDBNull(7) ? null : reader.GetInt32(7)
Height = reader.IsDBNull(7) ? null : reader.GetInt32(7),
});
}
@ -606,7 +607,7 @@ FROM downloads");
ulong messageId = reader.GetUint64(0);
dict.Add(messageId, new Embed {
Json = reader.GetString(1)
Json = reader.GetString(1),
});
}
@ -627,7 +628,7 @@ FROM downloads");
EmojiId = reader.IsDBNull(1) ? null : reader.GetUint64(1),
EmojiName = reader.IsDBNull(2) ? null : reader.GetString(2),
EmojiFlags = (EmojiFlags) reader.GetInt16(3),
Count = reader.GetInt32(4)
Count = reader.GetInt32(4),
});
}
@ -691,5 +692,4 @@ FROM downloads");
private void UpdateDownloadStatistics(long totalDownloads) {
Statistics.TotalDownloads = totalDownloads;
}
}
}

View File

@ -5,8 +5,9 @@ using DHT.Server.Data;
using DHT.Server.Data.Filters;
using DHT.Server.Database.Sqlite.Utils;
namespace DHT.Server.Database.Sqlite {
static class SqliteFilters {
namespace DHT.Server.Database.Sqlite;
static class SqliteFilters {
public static string GenerateWhereClause(this MessageFilter? filter, string? tableAlias = null, bool invert = false) {
if (filter == null) {
return "";
@ -79,5 +80,4 @@ namespace DHT.Server.Database.Sqlite {
private static string In(this ISet<DownloadStatus> statuses) {
return string.Join(",", statuses.Select(static status => (int) status));
}
}
}

View File

@ -1,8 +1,8 @@
using System;
using Microsoft.Data.Sqlite;
namespace DHT.Server.Database.Sqlite.Utils {
interface ISqliteConnection : IDisposable {
namespace DHT.Server.Database.Sqlite.Utils;
interface ISqliteConnection : IDisposable {
SqliteConnection InnerConnection { get; }
}
}

View File

@ -5,8 +5,9 @@ using System.Threading;
using DHT.Utils.Logging;
using Microsoft.Data.Sqlite;
namespace DHT.Server.Database.Sqlite.Utils {
sealed class SqliteConnectionPool : IDisposable {
namespace DHT.Server.Database.Sqlite.Utils;
sealed class SqliteConnectionPool : IDisposable {
private static string GetConnectionString(SqliteConnectionStringBuilder connectionStringBuilder) {
connectionStringBuilder.Pooling = false;
return connectionStringBuilder.ToString();
@ -39,9 +40,7 @@ namespace DHT.Server.Database.Sqlite.Utils {
}
private void ThrowIfDisposed() {
if (isDisposed) {
throw new ObjectDisposedException(nameof(SqliteConnectionPool));
}
ObjectDisposedException.ThrowIf(isDisposed, nameof(SqliteConnectionPool));
}
public ISqliteConnection Take() {
@ -54,7 +53,7 @@ namespace DHT.Server.Database.Sqlite.Utils {
return conn;
}
else {
Log.ForType<SqliteConnectionPool>().Warn("Thread " + Thread.CurrentThread.ManagedThreadId + " is starving for connections.");
Log.ForType<SqliteConnectionPool>().Warn("Thread " + Environment.CurrentManagedThreadId + " is starving for connections.");
}
}
@ -112,5 +111,4 @@ namespace DHT.Server.Database.Sqlite.Utils {
pool.Return(this);
}
}
}
}

View File

@ -2,8 +2,9 @@ using System;
using System.Linq;
using Microsoft.Data.Sqlite;
namespace DHT.Server.Database.Sqlite.Utils {
static class SqliteExtensions {
namespace DHT.Server.Database.Sqlite.Utils;
static class SqliteExtensions {
public static SqliteCommand Command(this ISqliteConnection conn, string sql) {
var cmd = conn.InnerConnection.CreateCommand();
cmd.CommandText = sql;
@ -67,5 +68,4 @@ namespace DHT.Server.Database.Sqlite.Utils {
public static ulong GetUint64(this SqliteDataReader reader, int ordinal) {
return (ulong) reader.GetInt64(ordinal);
}
}
}

View File

@ -1,7 +1,8 @@
using System.Collections.Generic;
namespace DHT.Server.Database.Sqlite.Utils {
sealed class SqliteWhereGenerator {
namespace DHT.Server.Database.Sqlite.Utils;
sealed class SqliteWhereGenerator {
private readonly string? tableAlias;
private readonly bool invert;
private readonly List<string> conditions = new ();
@ -27,5 +28,4 @@ namespace DHT.Server.Database.Sqlite.Utils {
return " WHERE " + string.Join(" AND ", conditions);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More