1
0
mirror of https://github.com/chylex/Discord-History-Tracker.git synced 2024-11-26 07:42:50 +01:00

Compare commits

..

No commits in common. "2424a8ac8db863bbcee55d384d6268ce00ed6016" and "22958536e7de0747080bcc33873adc7a03cd47c9" have entirely different histories.

130 changed files with 4957 additions and 4827 deletions

View File

@ -19,7 +19,7 @@ Folder organization:
* `lib/` contains utilities required to build the project * `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 * `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 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/). 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/).
### Building ### Building
@ -50,6 +50,6 @@ Run the `app/build.sh` script, and read the [Distribution](#distribution) sectio
#### Distribution #### 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 8 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 5 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. 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> <Application.Styles>
<FluentTheme /> <FluentTheme Mode="Light" />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml" /> <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Default.xaml" />
<Style Selector="Button, CheckBox, RadioButton, Expander /template/ ToggleButton#ExpanderHeader"> <Style Selector="Button, CheckBox, RadioButton, Expander /template/ ToggleButton#ExpanderHeader">
<Setter Property="Cursor" Value="Hand" /> <Setter Property="Cursor" Value="Hand" />
@ -48,9 +48,68 @@
<Setter Property="Template" Value="{StaticResource InlineDataValidationContentTemplate}" /> <Setter Property="Template" Value="{StaticResource InlineDataValidationContentTemplate}" />
</Style> </Style>
<Style Selector="Expander"> <Style Selector="Expander /template/ ToggleButton#ExpanderHeader">
<Setter Property="MinHeight" Value="40" /> <Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="HorizontalAlignment" Value="Stretch" /> <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> </Style>
</Application.Styles> </Application.Styles>
@ -109,26 +168,12 @@
<Thickness x:Key="ExpanderHeaderPadding">15,0</Thickness> <Thickness x:Key="ExpanderHeaderPadding">15,0</Thickness>
<Thickness x:Key="ExpanderContentPadding">12</Thickness> <Thickness x:Key="ExpanderContentPadding">12</Thickness>
<SolidColorBrush x:Key="ExpanderHeaderBorderBrush" Color="#697DAB" /> <SolidColorBrush x:Key="ExpanderBorderBrush" Color="#697DAB" />
<SolidColorBrush x:Key="ExpanderHeaderBorderBrushPointerOver" Color="#697DAB" /> <SolidColorBrush x:Key="ExpanderBackground" Color="#697DAB" />
<SolidColorBrush x:Key="ExpanderHeaderBorderBrushPressed" Color="#697DAB" /> <SolidColorBrush x:Key="ExpanderForeground" Color="#FFFFFF" />
<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="ExpanderChevronForeground" Color="#FFFFFF" />
<SolidColorBrush x:Key="ExpanderChevronForegroundPointerOver" Color="#FFFFFF" /> <SolidColorBrush x:Key="ExpanderDropDownBorderBrush" Color="#697DAB" />
<SolidColorBrush x:Key="ExpanderChevronForegroundPressed" Color="#FFFFFF" /> <SolidColorBrush x:Key="ExpanderDropDownBackground" Color="#FFFFFF" />
<SolidColorBrush x:Key="ExpanderChevronForegroundDisabled" Color="#FFFFFF" />
</Application.Resources> </Application.Resources>

View File

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

View File

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

View File

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

View File

@ -3,42 +3,39 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Platform.Storage;
using DHT.Desktop.Dialogs.File;
using DHT.Desktop.Dialogs.Message; using DHT.Desktop.Dialogs.Message;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Server.Database.Exceptions; using DHT.Server.Database.Exceptions;
using DHT.Server.Database.Sqlite; using DHT.Server.Database.Sqlite;
using DHT.Utils.Logging; using DHT.Utils.Logging;
namespace DHT.Desktop.Common; namespace DHT.Desktop.Common {
static class DatabaseGui {
static class DatabaseGui {
private static readonly Log Log = Log.ForType(typeof(DatabaseGui)); private static readonly Log Log = Log.ForType(typeof(DatabaseGui));
private const string DatabaseFileInitialName = "archive.dht"; private const string DatabaseFileInitialName = "archive.dht";
private static readonly IReadOnlyList<FilePickerFileType> DatabaseFileDialogFilter = new List<FilePickerFileType> { private static readonly List<FileDialogFilter> DatabaseFileDialogFilter = new() {
FileDialogs.CreateFilter("Discord History Tracker Database", new [] { "dht" }) new FileDialogFilter {
Name = "Discord History Tracker Database",
Extensions = { "dht" }
}
}; };
public static async Task<string[]> NewOpenDatabaseFilesDialog(Window window, string? suggestedDirectory) { public static OpenFileDialog NewOpenDatabaseFileDialog() {
return await window.StorageProvider.OpenFiles(new FilePickerOpenOptions { return new OpenFileDialog {
Title = "Open Database File", Title = "Open Database File",
FileTypeFilter = DatabaseFileDialogFilter, InitialFileName = DatabaseFileInitialName,
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, suggestedDirectory), Filters = DatabaseFileDialogFilter
AllowMultiple = true };
});
} }
public static async Task<string?> NewOpenOrCreateDatabaseFileDialog(Window window, string? suggestedDirectory) { public static SaveFileDialog NewOpenOrCreateDatabaseFileDialog() {
return await window.StorageProvider.SaveFile(new FilePickerSaveOptions { return new SaveFileDialog {
Title = "Open or Create Database File", Title = "Open or Create Database File",
FileTypeChoices = DatabaseFileDialogFilter, InitialFileName = DatabaseFileInitialName,
SuggestedFileName = DatabaseFileInitialName, Filters = DatabaseFileDialogFilter
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, suggestedDirectory), };
ShowOverwritePrompt = false
});
} }
public static async Task<IDatabaseFile?> TryOpenOrCreateDatabaseFromPath(string path, Window window, Func<Task<bool>> checkCanUpgradeDatabase) { public static async Task<IDatabaseFile?> TryOpenOrCreateDatabaseFromPath(string path, Window window, Func<Task<bool>> checkCanUpgradeDatabase) {
@ -65,4 +62,5 @@ static class DatabaseGui {
public static async Task<DialogResult.YesNo> ShowCanUpgradeMultipleDatabaseDialog(Window window) { 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?"); 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,9 +2,8 @@ using System;
using System.Globalization; using System.Globalization;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
namespace DHT.Desktop.Common; namespace DHT.Desktop.Common {
sealed class NumberValueConverter : IValueConverter {
sealed class NumberValueConverter : IValueConverter {
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
return value == null ? "-" : string.Format(Program.Culture, "{0:n0}", value); return value == null ? "-" : string.Format(Program.Culture, "{0:n0}", value);
} }
@ -12,4 +11,5 @@ sealed class NumberValueConverter : IValueConverter {
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotSupportedException(); throw new NotSupportedException();
} }
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,9 +6,8 @@ using DHT.Desktop.Server;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Utils.Models; using DHT.Utils.Models;
namespace DHT.Desktop.Main.Pages; namespace DHT.Desktop.Main.Pages {
sealed class AdvancedPageModel : BaseModel, IDisposable {
sealed class AdvancedPageModel : BaseModel, IDisposable {
public ServerConfigurationPanelModel ServerConfigurationModel { get; } public ServerConfigurationPanelModel ServerConfigurationModel { get; }
private readonly Window window; private readonly Window window;
@ -36,4 +35,5 @@ sealed class AdvancedPageModel : BaseModel, IDisposable {
db.Vacuum(); db.Vacuum();
await Dialog.ShowOk(window, "Vacuum Database", "Done."); 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}}" /> <controls:AttachmentFilterPanel DataContext="{Binding FilterModel}" IsEnabled="{Binding !DataContext.IsDownloading, RelativeSource={RelativeSource AncestorType=UserControl}}" />
<StackPanel Orientation="Vertical" Spacing="12"> <StackPanel Orientation="Vertical" Spacing="12">
<Expander Header="Download Status" IsExpanded="True"> <Expander Header="Download Status" IsExpanded="True">
<DataGrid ItemsSource="{Binding StatisticsRows}" AutoGenerateColumns="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserSortColumns="False" IsReadOnly="True"> <DataGrid Items="{Binding StatisticsRows}" AutoGenerateColumns="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserSortColumns="False" IsReadOnly="True">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="State" Binding="{Binding State}" Width="*" /> <DataGridTextColumn Header="State" Binding="{Binding State}" Width="*" />
<DataGridTextColumn Header="Attachments" Binding="{Binding Items, Converter={StaticResource NumberValueConverter}}" Width="*" CellStyleClasses="right" /> <DataGridTextColumn Header="Attachments" Binding="{Binding Items, Converter={StaticResource NumberValueConverter}}" Width="*" CellStyleClasses="right" />

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,16 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Pages; namespace DHT.Desktop.Main.Pages {
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
[SuppressMessage("ReSharper", "MemberCanBeInternal")] public sealed class DebugPage : UserControl {
public sealed partial class DebugPage : UserControl {
public DebugPage() { public DebugPage() {
InitializeComponent(); 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 { var server = new DHT.Server.Data.Server {
Id = RandomId(rand), Id = RandomId(rand),
Name = RandomName("s"), Name = RandomName("s"),
Type = ServerType.Server, Type = ServerType.Server
}; };
var channels = Enumerable.Range(0, channelCount).Select(i => new Channel { var channels = Enumerable.Range(0, channelCount).Select(i => new Channel {
@ -73,14 +73,14 @@ namespace DHT.Desktop.Main.Pages {
ParentId = null, ParentId = null,
Position = i, Position = i,
Topic = RandomText(rand, 10), Topic = RandomText(rand, 10),
Nsfw = rand.Next(4) == 0, Nsfw = rand.Next(4) == 0
}).ToArray(); }).ToArray();
var users = Enumerable.Range(0, userCount).Select(_ => new User { var users = Enumerable.Range(0, userCount).Select(_ => new User {
Id = RandomId(rand), Id = RandomId(rand),
Name = RandomName("u"), Name = RandomName("u"),
AvatarUrl = null, AvatarUrl = null,
Discriminator = rand.Next(0, 9999).ToString(), Discriminator = rand.Next(0, 9999).ToString()
}).ToArray(); }).ToArray();
db.AddServer(server); db.AddServer(server);
@ -97,7 +97,7 @@ namespace DHT.Desktop.Main.Pages {
int hourOffset = batchIndex; int hourOffset = batchIndex;
var messages = Enumerable.Range(0, Math.Min(messageCount, BatchSize)).Select(i => { 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; DateTimeOffset? edit = rand.Next(100) == 0 ? time.AddSeconds(rand.Next(1, 60)) : null;
var timeMillis = time.ToUnixTimeMilliseconds(); var timeMillis = time.ToUnixTimeMilliseconds();
@ -113,7 +113,7 @@ namespace DHT.Desktop.Main.Pages {
RepliedToId = null, RepliedToId = null,
Attachments = ImmutableArray<Attachment>.Empty, Attachments = ImmutableArray<Attachment>.Empty,
Embeds = ImmutableArray<Embed>.Empty, Embeds = ImmutableArray<Embed>.Empty,
Reactions = ImmutableArray<Reaction>.Empty, Reactions = ImmutableArray<Reaction>.Empty
}; };
}).ToArray(); }).ToArray();
@ -161,7 +161,7 @@ namespace DHT.Desktop.Main.Pages {
"vanilla", "vanilla",
"watercress", "watermelon", "watercress", "watermelon",
"yam", "yam",
"zucchini", "zucchini"
}; };
private static string RandomText(Random rand, int maxWords) { private static string RandomText(Random rand, int maxWords) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,11 @@
using DHT.Utils.Models; using DHT.Utils.Models;
namespace DHT.Server.Database; namespace DHT.Server.Database {
/// <summary>
/// <summary> /// A live view of database statistics.
/// 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.
/// 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>
/// </summary> public sealed class DatabaseStatistics : BaseModel {
public sealed class DatabaseStatistics : BaseModel {
private long totalServers; private long totalServers;
private long totalChannels; private long totalChannels;
private long totalUsers; private long totalUsers;
@ -43,4 +42,5 @@ public sealed class DatabaseStatistics : BaseModel {
get => totalDownloads; get => totalDownloads;
internal set => Change(ref totalDownloads, value); internal set => Change(ref totalDownloads, value);
} }
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,9 +12,8 @@ using DHT.Utils.Http;
using DHT.Utils.Logging; using DHT.Utils.Logging;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
namespace DHT.Server.Database.Import; namespace DHT.Server.Database.Import {
public static class LegacyArchiveImport {
public static class LegacyArchiveImport {
private static readonly Log Log = Log.ForType(typeof(LegacyArchiveImport)); private static readonly Log Log = Log.ForType(typeof(LegacyArchiveImport));
private static readonly FileExtensionContentTypeProvider ContentTypeProvider = new (); private static readonly FileExtensionContentTypeProvider ContentTypeProvider = new ();
@ -57,7 +56,11 @@ public static class LegacyArchiveImport {
for (var i = 0; i < servers.Length; i++) { for (var i = 0; i < servers.Length; i++) {
var server = servers[i]; var server = servers[i];
if (askedServerIds.TryGetValue(server, out var serverId)) { if (askedServerIds.TryGetValue(server, out var serverId)) {
servers[i] = server with { Id = serverId }; servers[i] = new Data.Server {
Id = serverId,
Name = server.Name,
Type = server.Type
};
} }
} }
} }
@ -109,7 +112,7 @@ public static class LegacyArchiveImport {
Id = userId, Id = userId,
Name = userObj.RequireString("name", path), Name = userObj.RequireString("name", path),
AvatarUrl = userObj.HasKey("avatar") ? userObj.RequireString("avatar", path) : null, 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
}; };
} }
@ -122,7 +125,7 @@ public static class LegacyArchiveImport {
return meta.RequireArray("servers", "meta").Select(serverObj => new Data.Server { return meta.RequireArray("servers", "meta").Select(serverObj => new Data.Server {
Id = fakeSnowflake.Next(), Id = fakeSnowflake.Next(),
Name = serverObj.RequireString("name", ServersPath), Name = serverObj.RequireString("name", ServersPath),
Type = ServerTypes.FromString(serverObj.RequireString("type", ServersPath)), Type = ServerTypes.FromString(serverObj.RequireString("type", ServersPath))
}).ToArray(); }).ToArray();
} }
@ -150,7 +153,7 @@ public static class LegacyArchiveImport {
Name = channelObj.RequireString("name", path), Name = channelObj.RequireString("name", path),
Position = channelObj.HasKey("position") ? channelObj.RequireInt("position", path, min: 0) : null, Position = channelObj.HasKey("position") ? channelObj.RequireInt("position", path, min: 0) : null,
Topic = channelObj.HasKey("topic") ? channelObj.RequireString("topic", path) : 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(); }).ToArray();
} }
@ -181,7 +184,7 @@ public static class LegacyArchiveImport {
RepliedToId = messageObj.HasKey("r") ? messageObj.RequireSnowflake("r", path) : null, 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, 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, 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(); }).ToArray();
} }
@ -198,7 +201,7 @@ public static class LegacyArchiveImport {
Name = name, Name = name,
Type = type, Type = type,
Url = url, Url = url,
Size = 0, // unknown size Size = 0 // unknown size
}; };
}).DistinctByKeyStable(static attachment => { }).DistinctByKeyStable(static attachment => {
// Some Discord messages have duplicate attachments with the same id for unknown reasons. // Some Discord messages have duplicate attachments with the same id for unknown reasons.
@ -215,7 +218,7 @@ public static class LegacyArchiveImport {
var embedJson = new Dictionary<string, object> { var embedJson = new Dictionary<string, object> {
{ "url", url }, { "url", url },
{ "type", type }, { "type", type },
{ "dht_legacy", true }, { "dht_legacy", true }
}; };
if (type == "image") { if (type == "image") {
@ -252,8 +255,9 @@ public static class LegacyArchiveImport {
EmojiId = id, EmojiId = id,
EmojiName = name, EmojiName = name,
EmojiFlags = reactionObj.HasKey("an") && reactionObj.RequireBool("an", path) ? EmojiFlags.Animated : EmojiFlags.None, 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,9 +4,8 @@ using DHT.Server.Database.Exceptions;
using DHT.Server.Database.Sqlite.Utils; using DHT.Server.Database.Sqlite.Utils;
using DHT.Utils.Logging; using DHT.Utils.Logging;
namespace DHT.Server.Database.Sqlite; namespace DHT.Server.Database.Sqlite {
sealed class Schema {
sealed class Schema {
internal const int Version = 5; internal const int Version = 5;
private static readonly Log Log = Log.ForType<Schema>(); private static readonly Log Log = Log.ForType<Schema>();
@ -170,4 +169,5 @@ sealed class Schema {
perf.End(); perf.End();
} }
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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