mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2024-11-25 22:42:51 +01:00
Compare commits
No commits in common. "2424a8ac8db863bbcee55d384d6268ce00ed6016" and "22958536e7de0747080bcc33873adc7a03cd47c9" have entirely different histories.
2424a8ac8d
...
22958536e7
@ -19,7 +19,7 @@ Folder organization:
|
||||
* `lib/` contains utilities required to build the project
|
||||
* `web/` contains source code of the [official website](https://dht.chylex.com), which can be used as a template when making your own website
|
||||
|
||||
To start editing source code for the desktop app, install the [.NET 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
|
||||
|
||||
@ -50,6 +50,6 @@ Run the `app/build.sh` script, and read the [Distribution](#distribution) sectio
|
||||
|
||||
#### Distribution
|
||||
|
||||
The mentioned build scripts will prepare `Release` builds ready for distribution. Once the script finishes, the `app/bin` folder will contain self-contained executables for each major operating system, and a portable version that works on all other systems but requires .NET 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.
|
||||
|
@ -6,8 +6,8 @@
|
||||
|
||||
<Application.Styles>
|
||||
|
||||
<FluentTheme />
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml" />
|
||||
<FluentTheme Mode="Light" />
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Default.xaml" />
|
||||
|
||||
<Style Selector="Button, CheckBox, RadioButton, Expander /template/ ToggleButton#ExpanderHeader">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
@ -48,9 +48,68 @@
|
||||
<Setter Property="Template" Value="{StaticResource InlineDataValidationContentTemplate}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Expander">
|
||||
<Setter Property="MinHeight" Value="40" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Style Selector="Expander /template/ ToggleButton#ExpanderHeader">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="ToggleButtonBackground">
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="35">
|
||||
<Border x:Name="ExpandCollapseChevronBorder"
|
||||
Grid.Column="0"
|
||||
Width="35"
|
||||
Height="35"
|
||||
Margin="2,0"
|
||||
RenderTransformOrigin="50%,50%">
|
||||
<Path x:Name="ExpandCollapseChevron"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
RenderTransformOrigin="50%,50%"
|
||||
Stretch="None"
|
||||
Stroke="{DynamicResource ExpanderChevronForeground}"
|
||||
StrokeThickness="1" />
|
||||
<Border.RenderTransform>
|
||||
<RotateTransform />
|
||||
</Border.RenderTransform>
|
||||
</Border>
|
||||
<ContentPresenter x:Name="PART_ContentPresenter"
|
||||
Grid.Column="1"
|
||||
Margin="0"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="Center"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
TextBlock.Foreground="{DynamicResource ExpanderForeground}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="Expander:expanded /template/ ToggleButton#ExpanderHeader /template/ Border#ExpandCollapseChevronBorder">
|
||||
<Style.Animations>
|
||||
<Animation FillMode="Both" Duration="0:0:0.0625">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="RotateTransform.Angle" Value="180" />
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="RotateTransform.Angle" Value="180" />
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
<Style Selector="Expander:not(:expanded) /template/ ToggleButton#ExpanderHeader /template/ Border#ExpandCollapseChevronBorder">
|
||||
<Style.Animations>
|
||||
<Animation FillMode="Both" Duration="0:0:0.0625">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="RotateTransform.Angle" Value="0" />
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="RotateTransform.Angle" Value="0" />
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
|
||||
</Application.Styles>
|
||||
@ -109,26 +168,12 @@
|
||||
|
||||
<Thickness x:Key="ExpanderHeaderPadding">15,0</Thickness>
|
||||
<Thickness x:Key="ExpanderContentPadding">12</Thickness>
|
||||
<SolidColorBrush x:Key="ExpanderHeaderBorderBrush" Color="#697DAB" />
|
||||
<SolidColorBrush x:Key="ExpanderHeaderBorderBrushPointerOver" Color="#697DAB" />
|
||||
<SolidColorBrush x:Key="ExpanderHeaderBorderBrushPressed" Color="#697DAB" />
|
||||
<SolidColorBrush x:Key="ExpanderHeaderBorderBrushDisabled" Color="#697DAB" />
|
||||
<SolidColorBrush x:Key="ExpanderHeaderBackground" Color="#697DAB" />
|
||||
<SolidColorBrush x:Key="ExpanderHeaderBackgroundPointerOver" Color="#536794" />
|
||||
<SolidColorBrush x:Key="ExpanderHeaderBackgroundPressed" Color="#47587F" />
|
||||
<SolidColorBrush x:Key="ExpanderHeaderBackgroundDisabled" Color="#697DAB" />
|
||||
<SolidColorBrush x:Key="ExpanderHeaderForeground" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="ExpanderHeaderForegroundPointerOver" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="ExpanderHeaderForegroundPressed" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="ExpanderHeaderForegroundDisabled" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="ExpanderChevronBackground" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="ExpanderChevronBackgroundPointerOver" Color="#536794" />
|
||||
<SolidColorBrush x:Key="ExpanderChevronBackgroundPressed" Color="#47587F" />
|
||||
<SolidColorBrush x:Key="ExpanderChevronBackgroundDisabled" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="ExpanderBorderBrush" Color="#697DAB" />
|
||||
<SolidColorBrush x:Key="ExpanderBackground" Color="#697DAB" />
|
||||
<SolidColorBrush x:Key="ExpanderForeground" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="ExpanderChevronForeground" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="ExpanderChevronForegroundPointerOver" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="ExpanderChevronForegroundPressed" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="ExpanderChevronForegroundDisabled" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="ExpanderDropDownBorderBrush" Color="#697DAB" />
|
||||
<SolidColorBrush x:Key="ExpanderDropDownBackground" Color="#FFFFFF" />
|
||||
|
||||
</Application.Resources>
|
||||
|
||||
|
@ -1,11 +1,9 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using DHT.Desktop.Main;
|
||||
|
||||
namespace DHT.Desktop;
|
||||
|
||||
namespace DHT.Desktop {
|
||||
sealed class App : Application {
|
||||
public override void Initialize() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
@ -13,9 +11,10 @@ sealed class App : Application {
|
||||
|
||||
public override void OnFrameworkInitializationCompleted() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
using System;
|
||||
using DHT.Utils.Logging;
|
||||
|
||||
namespace DHT.Desktop;
|
||||
|
||||
namespace DHT.Desktop {
|
||||
sealed class Arguments {
|
||||
private static readonly Log Log = Log.ForType<Arguments>();
|
||||
|
||||
@ -63,3 +62,4 @@ sealed class Arguments {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,7 @@ using System;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace DHT.Desktop.Common;
|
||||
|
||||
namespace DHT.Desktop.Common {
|
||||
sealed class BytesValueConverter : IValueConverter {
|
||||
private sealed class Unit {
|
||||
private readonly string label;
|
||||
@ -51,3 +50,4 @@ sealed class BytesValueConverter : IValueConverter {
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,42 +3,39 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using DHT.Desktop.Dialogs.File;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Database.Exceptions;
|
||||
using DHT.Server.Database.Sqlite;
|
||||
using DHT.Utils.Logging;
|
||||
|
||||
namespace DHT.Desktop.Common;
|
||||
|
||||
namespace DHT.Desktop.Common {
|
||||
static class DatabaseGui {
|
||||
private static readonly Log Log = Log.ForType(typeof(DatabaseGui));
|
||||
|
||||
private const string DatabaseFileInitialName = "archive.dht";
|
||||
|
||||
private static readonly IReadOnlyList<FilePickerFileType> DatabaseFileDialogFilter = new List<FilePickerFileType> {
|
||||
FileDialogs.CreateFilter("Discord History Tracker Database", new [] { "dht" })
|
||||
private static readonly List<FileDialogFilter> DatabaseFileDialogFilter = new() {
|
||||
new FileDialogFilter {
|
||||
Name = "Discord History Tracker Database",
|
||||
Extensions = { "dht" }
|
||||
}
|
||||
};
|
||||
|
||||
public static async Task<string[]> NewOpenDatabaseFilesDialog(Window window, string? suggestedDirectory) {
|
||||
return await window.StorageProvider.OpenFiles(new FilePickerOpenOptions {
|
||||
public static OpenFileDialog NewOpenDatabaseFileDialog() {
|
||||
return new OpenFileDialog {
|
||||
Title = "Open Database File",
|
||||
FileTypeFilter = DatabaseFileDialogFilter,
|
||||
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, suggestedDirectory),
|
||||
AllowMultiple = true
|
||||
});
|
||||
InitialFileName = DatabaseFileInitialName,
|
||||
Filters = DatabaseFileDialogFilter
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<string?> NewOpenOrCreateDatabaseFileDialog(Window window, string? suggestedDirectory) {
|
||||
return await window.StorageProvider.SaveFile(new FilePickerSaveOptions {
|
||||
public static SaveFileDialog NewOpenOrCreateDatabaseFileDialog() {
|
||||
return new SaveFileDialog {
|
||||
Title = "Open or Create Database File",
|
||||
FileTypeChoices = DatabaseFileDialogFilter,
|
||||
SuggestedFileName = DatabaseFileInitialName,
|
||||
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, suggestedDirectory),
|
||||
ShowOverwritePrompt = false
|
||||
});
|
||||
InitialFileName = DatabaseFileInitialName,
|
||||
Filters = DatabaseFileDialogFilter
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<IDatabaseFile?> TryOpenOrCreateDatabaseFromPath(string path, Window window, Func<Task<bool>> checkCanUpgradeDatabase) {
|
||||
@ -66,3 +63,4 @@ static class DatabaseGui {
|
||||
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?");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,7 @@ using System;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace DHT.Desktop.Common;
|
||||
|
||||
namespace DHT.Desktop.Common {
|
||||
sealed class NumberValueConverter : IValueConverter {
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
|
||||
return value == null ? "-" : string.Format(Program.Culture, "{0:n0}", value);
|
||||
@ -13,3 +12,4 @@ sealed class NumberValueConverter : IValueConverter {
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
namespace DHT.Desktop.Common;
|
||||
|
||||
namespace DHT.Desktop.Common {
|
||||
static class TextFormat {
|
||||
public static string Format(this int number) {
|
||||
return number.ToString("N0", Program.Culture);
|
||||
@ -17,3 +16,4 @@ static class TextFormat {
|
||||
return number.Format() + "\u00A0" + (number == 1 ? singular : singular + "s");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>DiscordHistoryTracker</AssemblyName>
|
||||
<RootNamespace>DHT.Desktop</RootNamespace>
|
||||
@ -20,13 +21,10 @@
|
||||
<DebugType>none</DebugType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0" Condition=" '$(Configuration)' == 'Debug' " />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.18" Condition=" '$(Configuration)' == 'Debug' " />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Server\Server.csproj" />
|
||||
|
@ -34,7 +34,7 @@
|
||||
|
||||
<StackPanel Margin="20">
|
||||
<ScrollViewer MaxHeight="400">
|
||||
<ItemsRepeater ItemsSource="{Binding Items}">
|
||||
<ItemsRepeater Items="{Binding Items}">
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox IsChecked="{Binding Checked}">
|
||||
|
@ -1,14 +1,22 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.CheckBox;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.CheckBox {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class CheckBoxDialog : Window {
|
||||
public sealed class CheckBoxDialog : Window {
|
||||
public CheckBoxDialog() {
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void ClickOk(object? sender, RoutedEventArgs e) {
|
||||
@ -19,3 +27,5 @@ public sealed partial class CheckBoxDialog : Window {
|
||||
Close(DialogResult.OkCancel.Cancel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,7 @@ using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.CheckBox;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.CheckBox {
|
||||
class CheckBoxDialogModel : BaseModel {
|
||||
public string Title { get; init; } = "";
|
||||
|
||||
@ -68,3 +67,4 @@ sealed class CheckBoxDialogModel<T> : CheckBoxDialogModel {
|
||||
base.Items = this.Items;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.CheckBox;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.CheckBox {
|
||||
class CheckBoxItem : BaseModel {
|
||||
public string Title { get; init; } = "";
|
||||
public object? Item { get; init; } = null;
|
||||
@ -22,3 +21,4 @@ sealed class CheckBoxItem<T> : CheckBoxItem {
|
||||
base.Item = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -2,8 +2,7 @@ using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.Message;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.Message {
|
||||
static class Dialog {
|
||||
public static async Task ShowOk(Window owner, string title, string message) {
|
||||
if (!Dispatcher.UIThread.CheckAccess()) {
|
||||
@ -72,3 +71,4 @@ static class Dialog {
|
||||
return result.ToYesNoCancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.Message;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.Message {
|
||||
static class DialogResult {
|
||||
public enum All {
|
||||
Ok,
|
||||
@ -57,3 +56,4 @@ static class DialogResult {
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,21 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.Message;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.Message {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class MessageDialog : Window {
|
||||
public sealed class MessageDialog : Window {
|
||||
public MessageDialog() {
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void ClickOk(object? sender, RoutedEventArgs e) {
|
||||
@ -26,3 +34,4 @@ public sealed partial class MessageDialog : Window {
|
||||
Close(DialogResult.All.Cancel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
namespace DHT.Desktop.Dialogs.Message;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.Message {
|
||||
sealed class MessageDialogModel {
|
||||
public string Title { get; init; } = "";
|
||||
public string Message { get; init; } = "";
|
||||
@ -9,3 +8,4 @@ sealed class MessageDialogModel {
|
||||
public bool IsNoVisible { get; init; } = false;
|
||||
public bool IsCancelVisible { get; init; } = false;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.Progress;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.Progress {
|
||||
interface IProgressCallback {
|
||||
Task Update(string message, int finishedItems, int totalItems);
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
x:Class="DHT.Desktop.Dialogs.Progress.ProgressDialog"
|
||||
Title="{Binding Title}"
|
||||
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||
Opened="OnOpened"
|
||||
Opened="Loaded"
|
||||
Closing="OnClosing"
|
||||
Width="500" SizeToContent="Height" CanResize="False"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
@ -1,30 +1,40 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.Progress;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.Progress {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class ProgressDialog : Window {
|
||||
public sealed class ProgressDialog : Window {
|
||||
private bool isFinished = false;
|
||||
|
||||
public ProgressDialog() {
|
||||
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) {
|
||||
Task.Run(model.StartTask).ContinueWith(OnFinished, TaskScheduler.FromCurrentSynchronizationContext());
|
||||
}
|
||||
}
|
||||
|
||||
public void OnClosing(object? sender, WindowClosingEventArgs e) {
|
||||
e.Cancel = !isFinished;
|
||||
}
|
||||
|
||||
private void OnFinished(Task task) {
|
||||
isFinished = true;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,7 @@ using Avalonia.Threading;
|
||||
using DHT.Desktop.Common;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.Progress;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.Progress {
|
||||
sealed class ProgressDialogModel : BaseModel {
|
||||
public string Title { get; init; } = "";
|
||||
|
||||
@ -63,3 +62,4 @@ sealed class ProgressDialogModel : BaseModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@
|
||||
<ScrollViewer MaxHeight="400">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" />
|
||||
<ItemsRepeater ItemsSource="{Binding Items}">
|
||||
<ItemsRepeater Items="{Binding Items}">
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<DockPanel Margin="0 5 25 0">
|
||||
|
@ -1,14 +1,22 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.TextBox;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.TextBox {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class TextBoxDialog : Window {
|
||||
public sealed class TextBoxDialog : Window {
|
||||
public TextBoxDialog() {
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void ClickOk(object? sender, RoutedEventArgs e) {
|
||||
@ -19,3 +27,5 @@ public sealed partial class TextBoxDialog : Window {
|
||||
Close(DialogResult.OkCancel.Cancel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,7 @@ using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.TextBox;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.TextBox {
|
||||
class TextBoxDialogModel : BaseModel {
|
||||
public string Title { get; init; } = "";
|
||||
public string Description { get; init; } = "";
|
||||
@ -45,3 +44,4 @@ sealed class TextBoxDialogModel<T> : TextBoxDialogModel {
|
||||
base.Items = this.Items;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,7 @@ using System.Collections;
|
||||
using System.ComponentModel;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.TextBox;
|
||||
|
||||
namespace DHT.Desktop.Dialogs.TextBox {
|
||||
class TextBoxItem : BaseModel, INotifyDataErrorInfo {
|
||||
public string Title { get; init; } = "";
|
||||
public object? Item { get; init; } = null;
|
||||
@ -40,3 +39,4 @@ sealed class TextBoxItem<T> : TextBoxItem {
|
||||
base.Item = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,7 @@ using DHT.Utils.Logging;
|
||||
using static System.Environment.SpecialFolder;
|
||||
using static System.Environment.SpecialFolderOption;
|
||||
|
||||
namespace DHT.Desktop.Discord;
|
||||
|
||||
namespace DHT.Desktop.Discord {
|
||||
static class DiscordAppSettings {
|
||||
private static readonly Log Log = Log.ForType(typeof(DiscordAppSettings));
|
||||
|
||||
@ -117,3 +116,4 @@ static class DiscordAppSettings {
|
||||
await JsonSerializer.SerializeAsync(stream, json, new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
namespace DHT.Desktop.Discord;
|
||||
|
||||
namespace DHT.Desktop.Discord {
|
||||
enum SettingsJsonResult {
|
||||
Success,
|
||||
AlreadySet,
|
||||
@ -8,3 +7,4 @@ enum SettingsJsonResult {
|
||||
InvalidJson,
|
||||
WriteError
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" FontWeight="Bold">License</TextBlock>
|
||||
<TextBlock Grid.Row="0" Grid.Column="2" FontWeight="Bold">Link</TextBlock>
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0">.NET 8</TextBlock>
|
||||
<TextBlock Grid.Row="2" Grid.Column="0">.NET 5</TextBlock>
|
||||
<TextBlock Grid.Row="2" Grid.Column="1">MIT</TextBlock>
|
||||
<Button Grid.Row="2" Grid.Column="2" Command="{Binding ShowLibraryNetCore}">GitHub</Button>
|
||||
|
||||
|
@ -1,11 +1,20 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main;
|
||||
|
||||
namespace DHT.Desktop.Main {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class AboutWindow : Window {
|
||||
public sealed class AboutWindow : Window {
|
||||
public AboutWindow() {
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace DHT.Desktop.Main;
|
||||
|
||||
namespace DHT.Desktop.Main {
|
||||
sealed class AboutWindowModel {
|
||||
public void ShowOfficialWebsite() {
|
||||
OpenUrl("https://dht.chylex.com");
|
||||
@ -31,3 +30,4 @@ sealed class AboutWindowModel {
|
||||
Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@
|
||||
<CheckBox IsChecked="{Binding LimitSize}">Limit Size</CheckBox>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBox Text="{Binding MaximumSize}" IsEnabled="{Binding LimitSize}" HorizontalContentAlignment="Right" />
|
||||
<ComboBox IsEnabled="{Binding LimitSize}" ItemsSource="{Binding Units}" SelectedItem="{Binding MaximumSizeUnit}">
|
||||
<ComboBox IsEnabled="{Binding LimitSize}" Items="{Binding Units}" SelectedItem="{Binding MaximumSizeUnit}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
|
@ -1,11 +1,17 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class AttachmentFilterPanel : UserControl {
|
||||
public sealed class AttachmentFilterPanel : UserControl {
|
||||
public AttachmentFilterPanel() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,8 +7,7 @@ using DHT.Server.Database;
|
||||
using DHT.Utils.Models;
|
||||
using DHT.Utils.Tasks;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
sealed class AttachmentFilterPanelModel : BaseModel, IDisposable {
|
||||
public sealed record Unit(string Name, uint Scale);
|
||||
|
||||
@ -127,3 +126,4 @@ sealed class AttachmentFilterPanelModel : BaseModel, IDisposable {
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,19 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class MessageFilterPanel : UserControl {
|
||||
public sealed class MessageFilterPanel : UserControl {
|
||||
private CalendarDatePicker StartDatePicker => this.FindControl<CalendarDatePicker>("StartDatePicker");
|
||||
private CalendarDatePicker EndDatePicker => this.FindControl<CalendarDatePicker>("EndDatePicker");
|
||||
|
||||
public MessageFilterPanel() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
|
||||
var culture = Program.Culture;
|
||||
foreach (var picker in new CalendarDatePicker[] { StartDatePicker, EndDatePicker }) {
|
||||
@ -24,3 +31,4 @@ public sealed partial class MessageFilterPanel : UserControl {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,7 @@ using DHT.Server.Database;
|
||||
using DHT.Utils.Models;
|
||||
using DHT.Utils.Tasks;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
sealed class MessageFilterPanelModel : BaseModel, IDisposable {
|
||||
private static readonly HashSet<string> FilterProperties = new () {
|
||||
nameof(FilterByDate),
|
||||
@ -283,3 +282,4 @@ sealed class MessageFilterPanelModel : BaseModel, IDisposable {
|
||||
return result == DialogResult.OkCancel.Ok ? model.SelectedItems.Select(static item => item.Item).ToHashSet() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class ServerConfigurationPanel : UserControl {
|
||||
public sealed class ServerConfigurationPanel : UserControl {
|
||||
public ServerConfigurationPanel() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,7 @@ using DHT.Server.Database;
|
||||
using DHT.Server.Service;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
|
||||
private string inputPort;
|
||||
|
||||
@ -114,3 +113,4 @@ sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
|
||||
InputToken = ServerManager.Token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class StatusBar : UserControl {
|
||||
public sealed class StatusBar : UserControl {
|
||||
public StatusBar() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,7 @@ using System;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
sealed class StatusBarModel : BaseModel {
|
||||
public DatabaseStatistics DatabaseStatistics { get; }
|
||||
|
||||
@ -43,3 +42,4 @@ sealed class StatusBarModel : BaseModel {
|
||||
Stopped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,31 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using DHT.Desktop.Main.Pages;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace DHT.Desktop.Main;
|
||||
|
||||
namespace DHT.Desktop.Main {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class MainWindow : Window {
|
||||
public sealed class MainWindow : Window {
|
||||
[UsedImplicitly]
|
||||
public MainWindow() {
|
||||
InitializeComponent();
|
||||
DataContext = new MainWindowModel(this, Arguments.Empty);
|
||||
InitializeComponent(Arguments.Empty);
|
||||
}
|
||||
|
||||
internal MainWindow(Arguments args) {
|
||||
InitializeComponent();
|
||||
InitializeComponent(args);
|
||||
}
|
||||
|
||||
private void InitializeComponent(Arguments args) {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
DataContext = new MainWindowModel(this, args);
|
||||
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
}
|
||||
|
||||
public void OnClosed(object? sender, EventArgs e) {
|
||||
@ -34,3 +42,4 @@ public sealed partial class MainWindow : Window {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,8 +10,7 @@ using DHT.Desktop.Server;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main;
|
||||
|
||||
namespace DHT.Desktop.Main {
|
||||
sealed class MainWindowModel : BaseModel, IDisposable {
|
||||
private const string DefaultTitle = "Discord History Tracker";
|
||||
|
||||
@ -114,3 +113,4 @@ sealed class MainWindowModel : BaseModel, IDisposable {
|
||||
db = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class AdvancedPage : UserControl {
|
||||
public sealed class AdvancedPage : UserControl {
|
||||
public AdvancedPage() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,7 @@ using DHT.Desktop.Server;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
sealed class AdvancedPageModel : BaseModel, IDisposable {
|
||||
public ServerConfigurationPanelModel ServerConfigurationModel { get; }
|
||||
|
||||
@ -37,3 +36,4 @@ sealed class AdvancedPageModel : BaseModel, IDisposable {
|
||||
await Dialog.ShowOk(window, "Vacuum Database", "Done.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@
|
||||
<controls:AttachmentFilterPanel DataContext="{Binding FilterModel}" IsEnabled="{Binding !DataContext.IsDownloading, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||
<StackPanel Orientation="Vertical" Spacing="12">
|
||||
<Expander Header="Download Status" IsExpanded="True">
|
||||
<DataGrid 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>
|
||||
<DataGridTextColumn Header="State" Binding="{Binding State}" Width="*" />
|
||||
<DataGridTextColumn Header="Attachments" Binding="{Binding Items, Converter={StaticResource NumberValueConverter}}" Width="*" CellStyleClasses="right" />
|
||||
|
@ -1,11 +1,16 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class AttachmentsPage : UserControl {
|
||||
public sealed class AttachmentsPage : UserControl {
|
||||
public AttachmentsPage() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using Avalonia.Threading;
|
||||
using DHT.Desktop.Common;
|
||||
using DHT.Desktop.Main.Controls;
|
||||
using DHT.Server.Data;
|
||||
@ -13,8 +11,7 @@ using DHT.Server.Download;
|
||||
using DHT.Utils.Models;
|
||||
using DHT.Utils.Tasks;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
||||
private static readonly DownloadItemFilter EnqueuedItemFilter = new() {
|
||||
IncludeStatuses = new HashSet<DownloadStatus> {
|
||||
@ -136,9 +133,9 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
||||
}
|
||||
|
||||
private void DownloadThreadOnOnItemFinished(object? sender, DownloadItem e) {
|
||||
Interlocked.Increment(ref doneItemsCount);
|
||||
++doneItemsCount;
|
||||
|
||||
Dispatcher.UIThread.Invoke(UpdateDownloadMessage);
|
||||
UpdateDownloadMessage();
|
||||
downloadStatisticsComputer.Recompute();
|
||||
}
|
||||
|
||||
@ -203,3 +200,4 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class DatabasePage : UserControl {
|
||||
public sealed class DatabasePage : UserControl {
|
||||
public DatabasePage() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,8 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using DHT.Desktop.Common;
|
||||
using DHT.Desktop.Dialogs.File;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Desktop.Dialogs.Progress;
|
||||
using DHT.Desktop.Dialogs.TextBox;
|
||||
@ -20,8 +18,7 @@ using DHT.Server.Database.Import;
|
||||
using DHT.Utils.Logging;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
sealed class DatabasePageModel : BaseModel {
|
||||
private static readonly Log Log = Log.ForType<DatabasePageModel>();
|
||||
|
||||
@ -71,8 +68,12 @@ sealed class DatabasePageModel : BaseModel {
|
||||
}
|
||||
|
||||
public async void MergeWithDatabase() {
|
||||
var paths = await DatabaseGui.NewOpenDatabaseFilesDialog(window, Path.GetDirectoryName(Db.Path));
|
||||
if (paths.Length == 0) {
|
||||
var fileDialog = DatabaseGui.NewOpenDatabaseFileDialog();
|
||||
fileDialog.Directory = Path.GetDirectoryName(Db.Path);
|
||||
fileDialog.AllowMultiple = true;
|
||||
|
||||
string[]? paths = await fileDialog.ShowAsync(window);
|
||||
if (paths == null || paths.Length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -117,13 +118,14 @@ sealed class DatabasePageModel : BaseModel {
|
||||
}
|
||||
|
||||
public async void ImportLegacyArchive() {
|
||||
var paths = await window.StorageProvider.OpenFiles(new FilePickerOpenOptions {
|
||||
var fileDialog = new OpenFileDialog {
|
||||
Title = "Open Legacy DHT Archive",
|
||||
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, Path.GetDirectoryName(Db.Path)),
|
||||
Directory = Path.GetDirectoryName(Db.Path),
|
||||
AllowMultiple = true
|
||||
});
|
||||
};
|
||||
|
||||
if (paths.Length == 0) {
|
||||
string[]? paths = await fileDialog.ShowAsync(window);
|
||||
if (paths == null || paths.Length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -243,3 +245,4 @@ sealed class DatabasePageModel : BaseModel {
|
||||
return message.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class DebugPage : UserControl {
|
||||
public sealed class DebugPage : UserControl {
|
||||
public DebugPage() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ namespace DHT.Desktop.Main.Pages {
|
||||
var server = new DHT.Server.Data.Server {
|
||||
Id = RandomId(rand),
|
||||
Name = RandomName("s"),
|
||||
Type = ServerType.Server,
|
||||
Type = ServerType.Server
|
||||
};
|
||||
|
||||
var channels = Enumerable.Range(0, channelCount).Select(i => new Channel {
|
||||
@ -73,14 +73,14 @@ namespace DHT.Desktop.Main.Pages {
|
||||
ParentId = null,
|
||||
Position = i,
|
||||
Topic = RandomText(rand, 10),
|
||||
Nsfw = rand.Next(4) == 0,
|
||||
Nsfw = rand.Next(4) == 0
|
||||
}).ToArray();
|
||||
|
||||
var users = Enumerable.Range(0, userCount).Select(_ => new User {
|
||||
Id = RandomId(rand),
|
||||
Name = RandomName("u"),
|
||||
AvatarUrl = null,
|
||||
Discriminator = rand.Next(0, 9999).ToString(),
|
||||
Discriminator = rand.Next(0, 9999).ToString()
|
||||
}).ToArray();
|
||||
|
||||
db.AddServer(server);
|
||||
@ -97,7 +97,7 @@ namespace DHT.Desktop.Main.Pages {
|
||||
int hourOffset = batchIndex;
|
||||
|
||||
var messages = Enumerable.Range(0, Math.Min(messageCount, BatchSize)).Select(i => {
|
||||
DateTimeOffset time = now.AddHours(hourOffset).AddMinutes(i * 60.0 / BatchSize);
|
||||
DateTimeOffset time = now.AddHours(hourOffset).AddMinutes((i * 60.0) / BatchSize);
|
||||
DateTimeOffset? edit = rand.Next(100) == 0 ? time.AddSeconds(rand.Next(1, 60)) : null;
|
||||
|
||||
var timeMillis = time.ToUnixTimeMilliseconds();
|
||||
@ -113,7 +113,7 @@ namespace DHT.Desktop.Main.Pages {
|
||||
RepliedToId = null,
|
||||
Attachments = ImmutableArray<Attachment>.Empty,
|
||||
Embeds = ImmutableArray<Embed>.Empty,
|
||||
Reactions = ImmutableArray<Reaction>.Empty,
|
||||
Reactions = ImmutableArray<Reaction>.Empty
|
||||
};
|
||||
}).ToArray();
|
||||
|
||||
@ -161,7 +161,7 @@ namespace DHT.Desktop.Main.Pages {
|
||||
"vanilla",
|
||||
"watercress", "watermelon",
|
||||
"yam",
|
||||
"zucchini",
|
||||
"zucchini"
|
||||
};
|
||||
|
||||
private static string RandomText(Random rand, int maxWords) {
|
||||
|
@ -3,30 +3,36 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class TrackingPage : UserControl {
|
||||
public sealed class TrackingPage : UserControl {
|
||||
private bool isCopyingScript;
|
||||
|
||||
public TrackingPage() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public async void CopyTrackingScriptButton_OnClick(object? sender, RoutedEventArgs e) {
|
||||
if (DataContext is TrackingPageModel model) {
|
||||
var originalText = CopyTrackingScript.Content;
|
||||
CopyTrackingScript.MinWidth = CopyTrackingScript.Bounds.Width;
|
||||
var button = this.FindControl<Button>("CopyTrackingScript");
|
||||
var originalText = button.Content;
|
||||
button.MinWidth = button.Bounds.Width;
|
||||
|
||||
if (await model.OnClickCopyTrackingScript() && !isCopyingScript) {
|
||||
isCopyingScript = true;
|
||||
CopyTrackingScript.Content = "Script Copied!";
|
||||
button.Content = "Script Copied!";
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2));
|
||||
CopyTrackingScript.Content = originalText;
|
||||
button.Content = originalText;
|
||||
isCopyingScript = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Desktop.Discord;
|
||||
@ -8,8 +9,7 @@ using DHT.Desktop.Server;
|
||||
using DHT.Utils.Models;
|
||||
using static DHT.Desktop.Program;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
sealed class TrackingPageModel : BaseModel {
|
||||
private bool areDevToolsEnabled;
|
||||
|
||||
@ -61,7 +61,7 @@ sealed class TrackingPageModel : BaseModel {
|
||||
.Replace("/*[CSS-CONTROLLER]*/", await Resources.ReadTextAsync("Tracker/styles/controller.css"))
|
||||
.Replace("/*[CSS-SETTINGS]*/", await Resources.ReadTextAsync("Tracker/styles/settings.css"));
|
||||
|
||||
var clipboard = window.Clipboard;
|
||||
var clipboard = Application.Current?.Clipboard;
|
||||
if (clipboard == null) {
|
||||
await Dialog.ShowOk(window, "Copy Tracking Script", "Clipboard is not available on this system.");
|
||||
return false;
|
||||
@ -114,3 +114,4 @@ sealed class TrackingPageModel : BaseModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class ViewerPage : UserControl {
|
||||
public sealed class ViewerPage : UserControl {
|
||||
public ViewerPage() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
@ -7,9 +8,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using DHT.Desktop.Common;
|
||||
using DHT.Desktop.Dialogs.File;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Desktop.Main.Controls;
|
||||
using DHT.Desktop.Server;
|
||||
@ -20,8 +19,7 @@ using DHT.Server.Database.Export.Strategy;
|
||||
using DHT.Utils.Models;
|
||||
using static DHT.Desktop.Program;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
sealed class ViewerPageModel : BaseModel, IDisposable {
|
||||
public static readonly ConcurrentBag<string> TemporaryFiles = new ();
|
||||
|
||||
@ -115,19 +113,21 @@ sealed class ViewerPageModel : BaseModel, IDisposable {
|
||||
Process.Start(new ProcessStartInfo(fullPath) { UseShellExecute = true });
|
||||
}
|
||||
|
||||
private static readonly FilePickerFileType[] ViewerFileTypes = {
|
||||
FileDialogs.CreateFilter("Discord History Viewer", new string[] { "html" }),
|
||||
};
|
||||
|
||||
public async void OnClickSaveViewer() {
|
||||
string? path = await window.StorageProvider.SaveFile(new FilePickerSaveOptions {
|
||||
var dialog = new SaveFileDialog {
|
||||
Title = "Save Viewer",
|
||||
FileTypeChoices = ViewerFileTypes,
|
||||
SuggestedFileName = Path.GetFileNameWithoutExtension(db.Path) + ".html",
|
||||
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, Path.GetDirectoryName(db.Path)),
|
||||
});
|
||||
InitialFileName = Path.GetFileNameWithoutExtension(db.Path) + ".html",
|
||||
Directory = 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);
|
||||
}
|
||||
}
|
||||
@ -147,3 +147,4 @@ sealed class ViewerPageModel : BaseModel, IDisposable {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Screens;
|
||||
|
||||
namespace DHT.Desktop.Main.Screens {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class MainContentScreen : UserControl {
|
||||
public sealed class MainContentScreen : UserControl {
|
||||
public MainContentScreen() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,7 @@ using DHT.Server.Database;
|
||||
using DHT.Server.Service;
|
||||
using DHT.Utils.Logging;
|
||||
|
||||
namespace DHT.Desktop.Main.Screens;
|
||||
|
||||
namespace DHT.Desktop.Main.Screens {
|
||||
sealed class MainContentScreenModel : IDisposable {
|
||||
private static readonly Log Log = Log.ForType<MainContentScreenModel>();
|
||||
|
||||
@ -117,3 +116,4 @@ sealed class MainContentScreenModel : IDisposable {
|
||||
await Dialog.ShowOk(window, "Internal Server Error", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Screens;
|
||||
|
||||
namespace DHT.Desktop.Main.Screens {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed partial class WelcomeScreen : UserControl {
|
||||
public sealed class WelcomeScreen : UserControl {
|
||||
public WelcomeScreen() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,7 @@ using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main.Screens;
|
||||
|
||||
namespace DHT.Desktop.Main.Screens {
|
||||
sealed class WelcomeScreenModel : BaseModel, IDisposable {
|
||||
public string Version => Program.Version;
|
||||
|
||||
@ -27,8 +26,11 @@ sealed class WelcomeScreenModel : BaseModel, IDisposable {
|
||||
}
|
||||
|
||||
public async void OpenOrCreateDatabase() {
|
||||
var path = await DatabaseGui.NewOpenOrCreateDatabaseFileDialog(window, Path.GetDirectoryName(dbFilePath));
|
||||
if (path != null) {
|
||||
var dialog = DatabaseGui.NewOpenOrCreateDatabaseFileDialog();
|
||||
dialog.Directory = Path.GetDirectoryName(dbFilePath);
|
||||
|
||||
string? path = await dialog.ShowAsync(window);
|
||||
if (!string.IsNullOrWhiteSpace(path)) {
|
||||
await OpenOrCreateDatabaseFromPath(path);
|
||||
}
|
||||
}
|
||||
@ -68,3 +70,4 @@ sealed class WelcomeScreenModel : BaseModel, IDisposable {
|
||||
Db = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,7 @@ using System.Reflection;
|
||||
using Avalonia;
|
||||
using DHT.Utils.Resources;
|
||||
|
||||
namespace DHT.Desktop;
|
||||
|
||||
namespace DHT.Desktop {
|
||||
static class Program {
|
||||
public static string Version { get; }
|
||||
public static CultureInfo Culture { get; }
|
||||
@ -34,7 +33,7 @@ static class Program {
|
||||
private static AppBuilder BuildAvaloniaApp() {
|
||||
return AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,7 @@ using System;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Service;
|
||||
|
||||
namespace DHT.Desktop.Server;
|
||||
|
||||
namespace DHT.Desktop.Server {
|
||||
sealed class ServerManager : IDisposable {
|
||||
public static ushort Port { get; set; } = ServerUtils.FindAvailablePort(50000, 60000);
|
||||
public static string Token { get; set; } = ServerUtils.GenerateRandomToken(20);
|
||||
@ -48,3 +47,4 @@ sealed class ServerManager : IDisposable {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>11</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
@ -1,5 +1,4 @@
|
||||
namespace DHT.Server.Data.Aggregations;
|
||||
|
||||
namespace DHT.Server.Data.Aggregations {
|
||||
public sealed class DownloadStatusStatistics {
|
||||
public int EnqueuedCount { get; internal set; }
|
||||
public ulong EnqueuedSize { get; internal set; }
|
||||
@ -13,3 +12,4 @@ public sealed class DownloadStatusStatistics {
|
||||
public int SkippedCount { get; internal set; }
|
||||
public ulong SkippedSize { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
namespace DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct Attachment {
|
||||
public ulong Id { get; internal init; }
|
||||
public string Name { get; internal init; }
|
||||
@ -9,3 +8,4 @@ public readonly struct Attachment {
|
||||
public int? Width { get; internal init; }
|
||||
public int? Height { get; internal init; }
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
namespace DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct Channel {
|
||||
public ulong Id { get; init; }
|
||||
public ulong Server { get; init; }
|
||||
@ -9,3 +8,4 @@ public readonly struct Channel {
|
||||
public string? Topic { get; init; }
|
||||
public bool? Nsfw { get; init; }
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct Download {
|
||||
internal static Download NewSuccess(string url, byte[] data) {
|
||||
return new Download(url, DownloadStatus.Success, (ulong) Math.Max(data.LongLength, 0), data);
|
||||
@ -28,3 +27,4 @@ public readonly struct Download {
|
||||
return new Download(Url, Status, Size, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System.Net;
|
||||
|
||||
namespace DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Data {
|
||||
/// <summary>
|
||||
/// Extends <see cref="HttpStatusCode"/> with custom status codes in the range 0-99.
|
||||
/// </summary>
|
||||
@ -10,3 +9,4 @@ public enum DownloadStatus {
|
||||
GenericError = 1,
|
||||
Success = HttpStatusCode.OK
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
namespace DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct DownloadedAttachment {
|
||||
public string? Type { get; internal init; }
|
||||
public byte[] Data { get; internal init; }
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
namespace DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct Embed {
|
||||
public string Json { get; internal init; }
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Data {
|
||||
[Flags]
|
||||
public enum EmojiFlags : ushort {
|
||||
None = 0,
|
||||
Animated = 0b1
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
namespace DHT.Server.Data.Filters;
|
||||
|
||||
namespace DHT.Server.Data.Filters {
|
||||
public sealed class AttachmentFilter {
|
||||
public ulong? MaxBytes { get; set; } = null;
|
||||
|
||||
@ -13,3 +12,4 @@ public sealed class AttachmentFilter {
|
||||
OnlyPresent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DHT.Server.Data.Filters;
|
||||
|
||||
namespace DHT.Server.Data.Filters {
|
||||
public sealed class DownloadItemFilter {
|
||||
public HashSet<DownloadStatus>? IncludeStatuses { get; init; } = null;
|
||||
public HashSet<DownloadStatus>? ExcludeStatuses { get; init; } = null;
|
||||
public HashSet<DownloadStatus>? IncludeStatuses { get; set; } = null;
|
||||
public HashSet<DownloadStatus>? ExcludeStatuses { get; set; } = null;
|
||||
|
||||
public bool IsEmpty => IncludeStatuses == null && ExcludeStatuses == null;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
namespace DHT.Server.Data.Filters;
|
||||
|
||||
namespace DHT.Server.Data.Filters {
|
||||
public enum FilterRemovalMode {
|
||||
KeepMatching,
|
||||
RemoveMatching
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DHT.Server.Data.Filters;
|
||||
|
||||
namespace DHT.Server.Data.Filters {
|
||||
public sealed class MessageFilter {
|
||||
public DateTime? StartDate { get; set; } = null;
|
||||
public DateTime? EndDate { get; set; } = null;
|
||||
@ -17,3 +16,4 @@ public sealed class MessageFilter {
|
||||
UserIds == null &&
|
||||
MessageIds == null;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct Message {
|
||||
public ulong Id { get; init; }
|
||||
public ulong Sender { get; init; }
|
||||
@ -14,3 +13,4 @@ public readonly struct Message {
|
||||
public ImmutableArray<Embed> Embeds { get; init; }
|
||||
public ImmutableArray<Reaction> Reactions { get; init; }
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
namespace DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct Reaction {
|
||||
public ulong? EmojiId { get; internal init; }
|
||||
public string? EmojiName { get; internal init; }
|
||||
public EmojiFlags EmojiFlags { get; internal init; }
|
||||
public int Count { get; internal init; }
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
namespace DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct Server {
|
||||
public ulong Id { get; init; }
|
||||
public string Name { get; init; }
|
||||
public ServerType? Type { get; init; }
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
namespace DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Data {
|
||||
public enum ServerType {
|
||||
Server,
|
||||
Group,
|
||||
@ -43,3 +42,4 @@ public static class ServerTypes {
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
namespace DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct User {
|
||||
public ulong Id { get; init; }
|
||||
public string Name { get; init; }
|
||||
public string? AvatarUrl { get; init; }
|
||||
public string? Discriminator { get; init; }
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Database;
|
||||
|
||||
namespace DHT.Server.Database {
|
||||
public static class DatabaseExtensions {
|
||||
public static void AddFrom(this IDatabaseFile target, IDatabaseFile source) {
|
||||
target.AddServers(source.GetAllServers());
|
||||
@ -27,3 +26,4 @@ public static class DatabaseExtensions {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Server.Database;
|
||||
|
||||
namespace DHT.Server.Database {
|
||||
/// <summary>
|
||||
/// A live view of database statistics.
|
||||
/// Some of the totals are computed asynchronously and may not reflect the most recent version of the database, or may not be available at all until computed for the first time.
|
||||
@ -44,3 +43,4 @@ public sealed class DatabaseStatistics : BaseModel {
|
||||
internal set => Change(ref totalDownloads, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
namespace DHT.Server.Database;
|
||||
|
||||
namespace DHT.Server.Database {
|
||||
/// <summary>
|
||||
/// A complete snapshot of database statistics at a particular point in time.
|
||||
/// </summary>
|
||||
@ -9,3 +8,4 @@ public readonly struct DatabaseStatisticsSnapshot {
|
||||
public long TotalUsers { get; internal init; }
|
||||
public long TotalMessages { get; internal init; }
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Data.Aggregations;
|
||||
using DHT.Server.Data.Filters;
|
||||
using DHT.Server.Download;
|
||||
|
||||
namespace DHT.Server.Database;
|
||||
|
||||
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
|
||||
namespace DHT.Server.Database {
|
||||
public sealed class DummyDatabaseFile : IDatabaseFile {
|
||||
public static DummyDatabaseFile Instance { get; } = new();
|
||||
|
||||
@ -88,3 +85,4 @@ public sealed class DummyDatabaseFile : IDatabaseFile {
|
||||
|
||||
public void Dispose() {}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
using System;
|
||||
using DHT.Server.Database.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Exceptions;
|
||||
|
||||
namespace DHT.Server.Database.Exceptions {
|
||||
public sealed class DatabaseTooNewException : Exception {
|
||||
public int DatabaseVersion { get; }
|
||||
public int CurrentVersion => Schema.Version;
|
||||
@ -11,3 +10,4 @@ public sealed class DatabaseTooNewException : Exception {
|
||||
this.DatabaseVersion = databaseVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace DHT.Server.Database.Exceptions;
|
||||
|
||||
namespace DHT.Server.Database.Exceptions {
|
||||
public sealed class InvalidDatabaseVersionException : Exception {
|
||||
public string Version { get; }
|
||||
|
||||
@ -9,3 +8,4 @@ public sealed class InvalidDatabaseVersionException : Exception {
|
||||
this.Version = version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
using DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Database.Export.Strategy;
|
||||
|
||||
namespace DHT.Server.Database.Export.Strategy {
|
||||
public interface IViewerExportStrategy {
|
||||
string GetAttachmentUrl(Attachment attachment);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
using System.Net;
|
||||
using DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Database.Export.Strategy;
|
||||
|
||||
namespace DHT.Server.Database.Export.Strategy {
|
||||
public sealed class LiveViewerExportStrategy : IViewerExportStrategy {
|
||||
private readonly string safePort;
|
||||
private readonly string safeToken;
|
||||
@ -16,3 +15,4 @@ public sealed class LiveViewerExportStrategy : IViewerExportStrategy {
|
||||
return "http://127.0.0.1:" + safePort + "/get-attachment/" + WebUtility.UrlEncode(attachment.Url) + "?token=" + safeToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
using DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Database.Export.Strategy;
|
||||
|
||||
namespace DHT.Server.Database.Export.Strategy {
|
||||
public sealed class StandaloneViewerExportStrategy : IViewerExportStrategy {
|
||||
public static StandaloneViewerExportStrategy Instance { get; } = new ();
|
||||
|
||||
@ -11,3 +10,4 @@ public sealed class StandaloneViewerExportStrategy : IViewerExportStrategy {
|
||||
return attachment.Url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,7 @@ using DHT.Server.Data.Filters;
|
||||
using DHT.Server.Database.Export.Strategy;
|
||||
using DHT.Utils.Logging;
|
||||
|
||||
namespace DHT.Server.Database.Export;
|
||||
|
||||
namespace DHT.Server.Database.Export {
|
||||
public static class ViewerJsonExport {
|
||||
private static readonly Log Log = Log.ForType(typeof(ViewerJsonExport));
|
||||
|
||||
@ -44,7 +43,7 @@ public static class ViewerJsonExport {
|
||||
|
||||
var value = new {
|
||||
meta = new { users, userindex, servers, channels },
|
||||
data = GenerateMessageList(includedMessages, userIndices, strategy),
|
||||
data = GenerateMessageList(includedMessages, userIndices, strategy)
|
||||
};
|
||||
|
||||
perf.Step("Generate value object");
|
||||
@ -58,7 +57,7 @@ public static class ViewerJsonExport {
|
||||
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>();
|
||||
userindex = new List<string>();
|
||||
userIndices = new Dictionary<ulong, object>();
|
||||
@ -90,7 +89,7 @@ public static class ViewerJsonExport {
|
||||
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>();
|
||||
serverIndices = new Dictionary<ulong, object>();
|
||||
|
||||
@ -103,20 +102,20 @@ public static class ViewerJsonExport {
|
||||
serverIndices[id] = servers.Count;
|
||||
servers.Add(new Dictionary<string, object> {
|
||||
["name"] = server.Name,
|
||||
["type"] = ServerTypes.ToJsonViewerString(server.Type),
|
||||
["type"] = ServerTypes.ToJsonViewerString(server.Type)
|
||||
});
|
||||
}
|
||||
|
||||
return servers;
|
||||
}
|
||||
|
||||
private static 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>();
|
||||
|
||||
foreach (var channel in includedChannels) {
|
||||
var obj = new Dictionary<string, object> {
|
||||
["server"] = serverIndices[channel.Server],
|
||||
["name"] = channel.Name,
|
||||
["name"] = channel.Name
|
||||
};
|
||||
|
||||
if (channel.ParentId != null) {
|
||||
@ -141,7 +140,7 @@ public static class ViewerJsonExport {
|
||||
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>>();
|
||||
|
||||
foreach (var grouping in includedMessages.GroupBy(static message => message.Channel)) {
|
||||
@ -151,7 +150,7 @@ public static class ViewerJsonExport {
|
||||
foreach (var message in grouping) {
|
||||
var obj = new Dictionary<string, object> {
|
||||
["u"] = userIndices[message.Sender],
|
||||
["t"] = message.Timestamp,
|
||||
["t"] = message.Timestamp
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(message.Text)) {
|
||||
@ -170,10 +169,10 @@ public static class ViewerJsonExport {
|
||||
obj["a"] = message.Attachments.Select(attachment => {
|
||||
var a = new Dictionary<string, object> {
|
||||
{ "url", strategy.GetAttachmentUrl(attachment) },
|
||||
{ "name", Uri.TryCreate(attachment.Url, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.Url },
|
||||
{ "name", Uri.TryCreate(attachment.Url, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.Url }
|
||||
};
|
||||
|
||||
if (attachment is { Width: not null, Height: not null }) {
|
||||
if (attachment.Width != null && attachment.Height != null) {
|
||||
a["width"] = attachment.Width;
|
||||
a["height"] = attachment.Height;
|
||||
}
|
||||
@ -213,3 +212,4 @@ public static class ViewerJsonExport {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,7 @@ using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DHT.Server.Database.Export;
|
||||
|
||||
namespace DHT.Server.Database.Export {
|
||||
sealed class ViewerJsonSnowflakeSerializer : JsonConverter<ulong> {
|
||||
public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
|
||||
return ulong.Parse(reader.GetString()!);
|
||||
@ -13,3 +12,4 @@ sealed class ViewerJsonSnowflakeSerializer : JsonConverter<ulong> {
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,7 @@ using DHT.Server.Data.Aggregations;
|
||||
using DHT.Server.Data.Filters;
|
||||
using DHT.Server.Download;
|
||||
|
||||
namespace DHT.Server.Database;
|
||||
|
||||
namespace DHT.Server.Database {
|
||||
public interface IDatabaseFile : IDisposable {
|
||||
string Path { get; }
|
||||
DatabaseStatistics Statistics { get; }
|
||||
@ -41,3 +40,4 @@ public interface IDatabaseFile : IDisposable {
|
||||
|
||||
void Vacuum();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace DHT.Server.Database.Import;
|
||||
|
||||
namespace DHT.Server.Database.Import {
|
||||
/// <summary>
|
||||
/// https://discord.com/developers/docs/reference#snowflakes
|
||||
/// </summary>
|
||||
@ -19,3 +18,4 @@ public sealed class FakeSnowflake {
|
||||
return id++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,8 +12,7 @@ using DHT.Utils.Http;
|
||||
using DHT.Utils.Logging;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
|
||||
namespace DHT.Server.Database.Import;
|
||||
|
||||
namespace DHT.Server.Database.Import {
|
||||
public static class LegacyArchiveImport {
|
||||
private static readonly Log Log = Log.ForType(typeof(LegacyArchiveImport));
|
||||
|
||||
@ -57,7 +56,11 @@ public static class LegacyArchiveImport {
|
||||
for (var i = 0; i < servers.Length; i++) {
|
||||
var server = servers[i];
|
||||
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,
|
||||
Name = userObj.RequireString("name", path),
|
||||
AvatarUrl = userObj.HasKey("avatar") ? userObj.RequireString("avatar", path) : null,
|
||||
Discriminator = userObj.HasKey("tag") ? userObj.RequireString("tag", path) : null,
|
||||
Discriminator = userObj.HasKey("tag") ? userObj.RequireString("tag", path) : null
|
||||
};
|
||||
}
|
||||
|
||||
@ -122,7 +125,7 @@ public static class LegacyArchiveImport {
|
||||
return meta.RequireArray("servers", "meta").Select(serverObj => new Data.Server {
|
||||
Id = fakeSnowflake.Next(),
|
||||
Name = serverObj.RequireString("name", ServersPath),
|
||||
Type = ServerTypes.FromString(serverObj.RequireString("type", ServersPath)),
|
||||
Type = ServerTypes.FromString(serverObj.RequireString("type", ServersPath))
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
@ -150,7 +153,7 @@ public static class LegacyArchiveImport {
|
||||
Name = channelObj.RequireString("name", path),
|
||||
Position = channelObj.HasKey("position") ? channelObj.RequireInt("position", path, min: 0) : null,
|
||||
Topic = channelObj.HasKey("topic") ? channelObj.RequireString("topic", path) : null,
|
||||
Nsfw = channelObj.HasKey("nsfw") ? channelObj.RequireBool("nsfw", path) : null,
|
||||
Nsfw = channelObj.HasKey("nsfw") ? channelObj.RequireBool("nsfw", path) : null
|
||||
};
|
||||
}).ToArray();
|
||||
}
|
||||
@ -181,7 +184,7 @@ public static class LegacyArchiveImport {
|
||||
RepliedToId = messageObj.HasKey("r") ? messageObj.RequireSnowflake("r", path) : null,
|
||||
Attachments = messageObj.HasKey("a") ? ReadMessageAttachments(messageObj.RequireArray("a", path), fakeSnowflake, path + ".a[]").ToImmutableArray() : ImmutableArray<Attachment>.Empty,
|
||||
Embeds = messageObj.HasKey("e") ? ReadMessageEmbeds(messageObj.RequireArray("e", path), path + ".e[]").ToImmutableArray() : ImmutableArray<Embed>.Empty,
|
||||
Reactions = messageObj.HasKey("re") ? ReadMessageReactions(messageObj.RequireArray("re", path), path + ".re[]").ToImmutableArray() : ImmutableArray<Reaction>.Empty,
|
||||
Reactions = messageObj.HasKey("re") ? ReadMessageReactions(messageObj.RequireArray("re", path), path + ".re[]").ToImmutableArray() : ImmutableArray<Reaction>.Empty
|
||||
};
|
||||
}).ToArray();
|
||||
}
|
||||
@ -198,7 +201,7 @@ public static class LegacyArchiveImport {
|
||||
Name = name,
|
||||
Type = type,
|
||||
Url = url,
|
||||
Size = 0, // unknown size
|
||||
Size = 0 // unknown size
|
||||
};
|
||||
}).DistinctByKeyStable(static attachment => {
|
||||
// Some Discord messages have duplicate attachments with the same id for unknown reasons.
|
||||
@ -215,7 +218,7 @@ public static class LegacyArchiveImport {
|
||||
var embedJson = new Dictionary<string, object> {
|
||||
{ "url", url },
|
||||
{ "type", type },
|
||||
{ "dht_legacy", true },
|
||||
{ "dht_legacy", true }
|
||||
};
|
||||
|
||||
if (type == "image") {
|
||||
@ -252,8 +255,9 @@ public static class LegacyArchiveImport {
|
||||
EmojiId = id,
|
||||
EmojiName = name,
|
||||
EmojiFlags = reactionObj.HasKey("an") && reactionObj.RequireBool("an", path) ? EmojiFlags.Animated : EmojiFlags.None,
|
||||
Count = reactionObj.RequireInt("c", path, min: 0),
|
||||
Count = reactionObj.RequireInt("c", path, min: 0)
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,7 @@ using DHT.Server.Database.Exceptions;
|
||||
using DHT.Server.Database.Sqlite.Utils;
|
||||
using DHT.Utils.Logging;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite {
|
||||
sealed class Schema {
|
||||
internal const int Version = 5;
|
||||
|
||||
@ -171,3 +170,4 @@ sealed class Schema {
|
||||
perf.End();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,7 @@ using DHT.Utils.Logging;
|
||||
using DHT.Utils.Tasks;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite {
|
||||
public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
private const int DefaultPoolSize = 5;
|
||||
|
||||
@ -80,7 +79,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
TotalServers = Statistics.TotalServers,
|
||||
TotalChannels = Statistics.TotalChannels,
|
||||
TotalUsers = Statistics.TotalUsers,
|
||||
TotalMessages = ComputeMessageStatistics(),
|
||||
TotalMessages = ComputeMessageStatistics()
|
||||
};
|
||||
}
|
||||
|
||||
@ -89,7 +88,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
using var cmd = conn.Upsert("servers", new[] {
|
||||
("id", SqliteType.Integer),
|
||||
("name", SqliteType.Text),
|
||||
("type", SqliteType.Text),
|
||||
("type", SqliteType.Text)
|
||||
});
|
||||
|
||||
cmd.Set(":id", server.Id);
|
||||
@ -111,7 +110,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
list.Add(new Data.Server {
|
||||
Id = reader.GetUint64(0),
|
||||
Name = reader.GetString(1),
|
||||
Type = ServerTypes.FromString(reader.GetString(2)),
|
||||
Type = ServerTypes.FromString(reader.GetString(2))
|
||||
});
|
||||
}
|
||||
|
||||
@ -128,7 +127,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
("parent_id", SqliteType.Integer),
|
||||
("position", SqliteType.Integer),
|
||||
("topic", SqliteType.Text),
|
||||
("nsfw", SqliteType.Integer),
|
||||
("nsfw", SqliteType.Integer)
|
||||
});
|
||||
|
||||
cmd.Set(":id", channel.Id);
|
||||
@ -157,7 +156,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
ParentId = reader.IsDBNull(3) ? null : reader.GetUint64(3),
|
||||
Position = reader.IsDBNull(4) ? null : reader.GetInt32(4),
|
||||
Topic = reader.IsDBNull(5) ? null : reader.GetString(5),
|
||||
Nsfw = reader.IsDBNull(6) ? null : reader.GetBoolean(6),
|
||||
Nsfw = reader.IsDBNull(6) ? null : reader.GetBoolean(6)
|
||||
});
|
||||
}
|
||||
|
||||
@ -171,7 +170,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
("id", SqliteType.Integer),
|
||||
("name", SqliteType.Text),
|
||||
("avatar_url", SqliteType.Text),
|
||||
("discriminator", SqliteType.Text),
|
||||
("discriminator", SqliteType.Text)
|
||||
});
|
||||
|
||||
foreach (var user in users) {
|
||||
@ -199,7 +198,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
Id = reader.GetUint64(0),
|
||||
Name = reader.GetString(1),
|
||||
AvatarUrl = reader.IsDBNull(2) ? null : reader.GetString(2),
|
||||
Discriminator = reader.IsDBNull(3) ? null : reader.GetString(3),
|
||||
Discriminator = reader.IsDBNull(3) ? null : reader.GetString(3)
|
||||
});
|
||||
}
|
||||
|
||||
@ -227,7 +226,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
("sender_id", SqliteType.Integer),
|
||||
("channel_id", SqliteType.Integer),
|
||||
("text", SqliteType.Text),
|
||||
("timestamp", SqliteType.Integer),
|
||||
("timestamp", SqliteType.Integer)
|
||||
});
|
||||
|
||||
using var deleteEditTimestampCmd = DeleteByMessageId(conn, "edit_timestamps");
|
||||
@ -239,12 +238,12 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
|
||||
using var editTimestampCmd = conn.Insert("edit_timestamps", new [] {
|
||||
("message_id", SqliteType.Integer),
|
||||
("edit_timestamp", SqliteType.Integer),
|
||||
("edit_timestamp", SqliteType.Integer)
|
||||
});
|
||||
|
||||
using var repliedToCmd = conn.Insert("replied_to", new [] {
|
||||
("message_id", SqliteType.Integer),
|
||||
("replied_to_id", SqliteType.Integer),
|
||||
("replied_to_id", SqliteType.Integer)
|
||||
});
|
||||
|
||||
using var attachmentCmd = conn.Insert("attachments", new[] {
|
||||
@ -255,12 +254,12 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
("url", SqliteType.Text),
|
||||
("size", SqliteType.Integer),
|
||||
("width", SqliteType.Integer),
|
||||
("height", SqliteType.Integer),
|
||||
("height", SqliteType.Integer)
|
||||
});
|
||||
|
||||
using var embedCmd = conn.Insert("embeds", new[] {
|
||||
("message_id", SqliteType.Integer),
|
||||
("json", SqliteType.Text),
|
||||
("json", SqliteType.Text)
|
||||
});
|
||||
|
||||
using var reactionCmd = conn.Insert("reactions", new[] {
|
||||
@ -268,7 +267,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
("emoji_id", SqliteType.Integer),
|
||||
("emoji_name", SqliteType.Text),
|
||||
("emoji_flags", SqliteType.Integer),
|
||||
("count", SqliteType.Integer),
|
||||
("count", SqliteType.Integer)
|
||||
});
|
||||
|
||||
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),
|
||||
Attachments = attachments.GetListOrNull(id)?.ToImmutableArray() ?? ImmutableArray<Attachment>.Empty,
|
||||
Embeds = embeds.GetListOrNull(id)?.ToImmutableArray() ?? ImmutableArray<Embed>.Empty,
|
||||
Reactions = reactions.GetListOrNull(id)?.ToImmutableArray() ?? ImmutableArray<Reaction>.Empty,
|
||||
Reactions = reactions.GetListOrNull(id)?.ToImmutableArray() ?? ImmutableArray<Reaction>.Empty
|
||||
});
|
||||
}
|
||||
|
||||
@ -430,7 +429,7 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC
|
||||
("url", SqliteType.Text),
|
||||
("status", SqliteType.Integer),
|
||||
("size", SqliteType.Integer),
|
||||
("blob", SqliteType.Blob),
|
||||
("blob", SqliteType.Blob)
|
||||
});
|
||||
|
||||
cmd.Set(":url", download.Url);
|
||||
@ -493,7 +492,7 @@ WHERE d.url = :url AND d.status = :success AND d.blob IS NOT NULL");
|
||||
|
||||
return new DownloadedAttachment {
|
||||
Type = reader.IsDBNull(0) ? null : reader.GetString(0),
|
||||
Data = (byte[]) reader["blob"],
|
||||
Data = (byte[]) reader["blob"]
|
||||
};
|
||||
}
|
||||
|
||||
@ -517,7 +516,7 @@ WHERE d.url = :url AND d.status = :success AND d.blob IS NOT NULL");
|
||||
while (reader.Read()) {
|
||||
list.Add(new DownloadItem {
|
||||
Url = reader.GetString(0),
|
||||
Size = reader.GetUint64(1),
|
||||
Size = reader.GetUint64(1)
|
||||
});
|
||||
}
|
||||
|
||||
@ -589,7 +588,7 @@ FROM downloads");
|
||||
Url = reader.GetString(4),
|
||||
Size = reader.GetUint64(5),
|
||||
Width = reader.IsDBNull(6) ? null : reader.GetInt32(6),
|
||||
Height = reader.IsDBNull(7) ? null : reader.GetInt32(7),
|
||||
Height = reader.IsDBNull(7) ? null : reader.GetInt32(7)
|
||||
});
|
||||
}
|
||||
|
||||
@ -607,7 +606,7 @@ FROM downloads");
|
||||
ulong messageId = reader.GetUint64(0);
|
||||
|
||||
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),
|
||||
EmojiName = reader.IsDBNull(2) ? null : reader.GetString(2),
|
||||
EmojiFlags = (EmojiFlags) reader.GetInt16(3),
|
||||
Count = reader.GetInt32(4),
|
||||
Count = reader.GetInt32(4)
|
||||
});
|
||||
}
|
||||
|
||||
@ -693,3 +692,4 @@ FROM downloads");
|
||||
Statistics.TotalDownloads = totalDownloads;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,7 @@ using DHT.Server.Data;
|
||||
using DHT.Server.Data.Filters;
|
||||
using DHT.Server.Database.Sqlite.Utils;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite {
|
||||
static class SqliteFilters {
|
||||
public static string GenerateWhereClause(this MessageFilter? filter, string? tableAlias = null, bool invert = false) {
|
||||
if (filter == null) {
|
||||
@ -81,3 +80,4 @@ static class SqliteFilters {
|
||||
return string.Join(",", statuses.Select(static status => (int) status));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite.Utils;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite.Utils {
|
||||
interface ISqliteConnection : IDisposable {
|
||||
SqliteConnection InnerConnection { get; }
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,7 @@ using System.Threading;
|
||||
using DHT.Utils.Logging;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite.Utils;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite.Utils {
|
||||
sealed class SqliteConnectionPool : IDisposable {
|
||||
private static string GetConnectionString(SqliteConnectionStringBuilder connectionStringBuilder) {
|
||||
connectionStringBuilder.Pooling = false;
|
||||
@ -40,7 +39,9 @@ sealed class SqliteConnectionPool : IDisposable {
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed() {
|
||||
ObjectDisposedException.ThrowIf(isDisposed, nameof(SqliteConnectionPool));
|
||||
if (isDisposed) {
|
||||
throw new ObjectDisposedException(nameof(SqliteConnectionPool));
|
||||
}
|
||||
}
|
||||
|
||||
public ISqliteConnection Take() {
|
||||
@ -53,7 +54,7 @@ sealed class SqliteConnectionPool : IDisposable {
|
||||
return conn;
|
||||
}
|
||||
else {
|
||||
Log.ForType<SqliteConnectionPool>().Warn("Thread " + Environment.CurrentManagedThreadId + " is starving for connections.");
|
||||
Log.ForType<SqliteConnectionPool>().Warn("Thread " + Thread.CurrentThread.ManagedThreadId + " is starving for connections.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,3 +113,4 @@ sealed class SqliteConnectionPool : IDisposable {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,7 @@ using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite.Utils;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite.Utils {
|
||||
static class SqliteExtensions {
|
||||
public static SqliteCommand Command(this ISqliteConnection conn, string sql) {
|
||||
var cmd = conn.InnerConnection.CreateCommand();
|
||||
@ -69,3 +68,4 @@ static class SqliteExtensions {
|
||||
return (ulong) reader.GetInt64(ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite.Utils;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite.Utils {
|
||||
sealed class SqliteWhereGenerator {
|
||||
private readonly string? tableAlias;
|
||||
private readonly bool invert;
|
||||
@ -29,3 +28,4 @@ sealed class SqliteWhereGenerator {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user