mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2025-08-17 19:31:42 +02:00
Compare commits
51 Commits
Author | SHA1 | Date | |
---|---|---|---|
8f7b566db7
|
|||
70a2a01ec3
|
|||
c31155738e
|
|||
c23fac465f
|
|||
51a2ac2d66
|
|||
a5e8366f1b
|
|||
3b698dbf33
|
|||
dc2c2d7ce8
|
|||
bb5634adc4
|
|||
d26e16eadf
|
|||
8f5f6065d8
|
|||
ad299bf762
|
|||
f70bbd53d9
|
|||
ae821f738e
|
|||
ab7f5d0a41
|
|||
1bddde7ccd
|
|||
58259c0bb4
|
|||
a84a453990
|
|||
563c644f48
|
|||
f8331a571e
|
|||
1ed26a16ea
|
|||
72c13cbb58
|
|||
e420add646
|
|||
6f1149ad5e
|
|||
b9899922e0
|
|||
6a2933ea0a
|
|||
be5c76c3bd
|
|||
217c1f9e10
|
|||
725ab7accf
|
|||
9a7a2cffc2
|
|||
6d3db23f80
|
|||
4bc9626013
|
|||
8002236c1f
|
|||
c4fe6c4391
|
|||
ebfe972a98
|
|||
20aac4c47a
|
|||
35308e0995
|
|||
f7f32c3f6a
|
|||
4dc781b35c
|
|||
849ef18adb
|
|||
77aa15e557
|
|||
47b106503d
|
|||
bde4cb06f4
|
|||
d772f7ed71
|
|||
0662af9b1a
|
|||
03fd730139
|
|||
d362c96b80
|
|||
9f34c9dffa
|
|||
cacf43d1d8
|
|||
edc23d616d
|
|||
db191f87fd
|
@@ -4,18 +4,20 @@
|
||||
<option name="projectPerEditor">
|
||||
<map>
|
||||
<entry key="Desktop/App.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Dialogs/CheckBoxDialog.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Dialogs/MessageDialog.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Dialogs/ProgressDialog.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Dialogs/CheckBox/CheckBoxDialog.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Dialogs/Message/MessageDialog.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Dialogs/Progress/ProgressDialog.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Main/AboutWindow.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Main/Controls/FilterPanel.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Main/Controls/ServerConfigurationPanel.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Main/Controls/StatusBar.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Main/MainContentScreen.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Main/MainWindow.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Main/Pages/AdvancedPage.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Main/Pages/DatabasePage.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Main/Pages/TrackingPage.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Main/Pages/ViewerPage.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Main/WelcomeScreen.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Main/Screens/MainContentScreen.axaml" value="Desktop/Desktop.csproj" />
|
||||
<entry key="Desktop/Main/Screens/WelcomeScreen.axaml" value="Desktop/Desktop.csproj" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
|
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectTasksOptions">
|
||||
<enabled-global>
|
||||
<option value="UglifyJS (Tracker)" />
|
||||
</enabled-global>
|
||||
</component>
|
||||
</project>
|
@@ -1,74 +1,41 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:common="clr-namespace:DHT.Desktop.Common"
|
||||
xmlns:system="clr-namespace:System;assembly=System.Runtime"
|
||||
x:Class="DHT.Desktop.App">
|
||||
|
||||
<Application.Styles>
|
||||
|
||||
<FluentTheme Mode="Light" />
|
||||
|
||||
<Style Selector="Button, CheckBox, RadioButton, Expander /template/ ToggleButton#ExpanderHeader">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Padding" Value="10 7 10 6" />
|
||||
<Setter Property="Background" Value="#DDDDDD" />
|
||||
<Setter Property="BorderBrush" Value="#999999" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style Selector="Button /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="TextBlock.Foreground" Value="#060606" />
|
||||
<Setter Property="Background" Value="#DDDDDD" />
|
||||
<Setter Property="BorderBrush" Value="#999999" />
|
||||
</Style>
|
||||
<Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="#E7E7E7" />
|
||||
<Setter Property="BorderBrush" Value="#A2A2A2" />
|
||||
<Setter Property="CornerRadius" Value="0" />
|
||||
</Style>
|
||||
<Style Selector="Button:pressed">
|
||||
<Setter Property="RenderTransform" Value="none" />
|
||||
</Style>
|
||||
<Style Selector="Button:pressed /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="#EBEBEB" />
|
||||
<Setter Property="BorderBrush" Value="#A5A5A5" />
|
||||
<Setter Property="CornerRadius" Value="0" />
|
||||
</Style>
|
||||
<Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="TextBlock.Foreground" Value="#7A7A7A" />
|
||||
<Setter Property="Background" Value="#E9E9E9" />
|
||||
<Setter Property="BorderBrush" Value="#BFBFBF" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="BorderBrush" Value="#999999" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="0" />
|
||||
<Setter Property="SelectionBrush" Value="#72C0FF" />
|
||||
<Setter Property="Background" Value="#F4F4F4" />
|
||||
<Setter Property="Padding" Value="6 0" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style Selector="TextBox:pointerover /template/ Border#PART_BorderElement">
|
||||
<Setter Property="BorderBrush" Value="#999999" />
|
||||
<Setter Property="Background" Value="#F8F8F8" />
|
||||
</Style>
|
||||
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
|
||||
<Setter Property="BorderBrush" Value="#546A9F" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Background" Value="#FBFBFB" />
|
||||
</Style>
|
||||
<Style Selector="TextBox:disabled /template/ Border#PART_BorderElement">
|
||||
<Setter Property="BorderBrush" Value="#999999" />
|
||||
<Setter Property="Background" Value="#BBBBBB" />
|
||||
</Style>
|
||||
<Style Selector="TextBox /template/ TextBlock#PART_Watermark">
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Expander">
|
||||
<Setter Property="CornerRadius" Value="0" />
|
||||
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
</Style>
|
||||
<Style Selector="TextBox:disabled"><!-- TODO bug in Avalonia (https://github.com/AvaloniaUI/Avalonia/pull/7792) -->
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextControlForegroundDisabled}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Expander /template/ ToggleButton#ExpanderHeader">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
<Setter Property="Template">
|
||||
@@ -108,6 +75,30 @@
|
||||
</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>
|
||||
|
||||
@@ -115,9 +106,61 @@
|
||||
|
||||
<common:NumberValueConverter x:Key="NumberValueConverter" />
|
||||
|
||||
<system:Double x:Key="ControlContentThemeFontSize">14</system:Double>
|
||||
<CornerRadius x:Key="ControlCornerRadius">0</CornerRadius>
|
||||
|
||||
<Color x:Key="SystemAccentColor">#3C5F95</Color>
|
||||
<Color x:Key="SystemAccentColorDark1">#3C5F95</Color>
|
||||
<Color x:Key="SystemAccentColorDark2">#3C5F95</Color>
|
||||
<Color x:Key="SystemAccentColorDark3">#3C5F95</Color>
|
||||
<Color x:Key="SystemAccentColorLight1">#3C5F95</Color>
|
||||
<Color x:Key="SystemAccentColorLight2">#3C5F95</Color>
|
||||
<Color x:Key="SystemAccentColorLight3">#3C5F95</Color>
|
||||
|
||||
<system:Double x:Key="ScrollBarSize">14</system:Double>
|
||||
<TransformOperations x:Key="VerticalSmallScrollThumbScaleTransform">scaleX(0.5) translateX(-3px)</TransformOperations>
|
||||
<TransformOperations x:Key="HorizontalSmallScrollThumbScaleTransform">scaleY(0.5) translateY(-3px)</TransformOperations>
|
||||
<SolidColorBrush x:Key="ScrollBarPanningThumbBackground" Color="#8F8F8F" />
|
||||
|
||||
<SolidColorBrush x:Key="ButtonBorderBrush" Color="#3C5F95" />
|
||||
<SolidColorBrush x:Key="ButtonBorderBrushPointerOver" Color="#0E2B59" />
|
||||
<SolidColorBrush x:Key="ButtonBorderBrushPressed" Color="#061742" />
|
||||
<SolidColorBrush x:Key="ButtonBorderBrushDisabled" Color="#9B9B9B" />
|
||||
<SolidColorBrush x:Key="ButtonBackground" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="#F6F9FD" />
|
||||
<SolidColorBrush x:Key="ButtonBackgroundPressed" Color="#EDF3FD" />
|
||||
<SolidColorBrush x:Key="ButtonBackgroundDisabled" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="ButtonForeground" Color="#112961" />
|
||||
<SolidColorBrush x:Key="ButtonForegroundPointerOver" Color="#050E41" />
|
||||
<SolidColorBrush x:Key="ButtonForegroundPressed" Color="#010320" />
|
||||
<SolidColorBrush x:Key="ButtonForegroundDisabled" Color="#8C8C8C" />
|
||||
|
||||
<SolidColorBrush x:Key="TextControlBorderBrush" Color="#515151" />
|
||||
<SolidColorBrush x:Key="TextControlBorderBrushPointerOver" Color="#000000" />
|
||||
<SolidColorBrush x:Key="TextControlBorderBrushFocused" Color="#3C5F95" />
|
||||
<SolidColorBrush x:Key="TextControlBorderBrushDisabled" Color="#9B9B9B" />
|
||||
<SolidColorBrush x:Key="TextControlBackground" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="TextControlBackgroundPointerOver" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="TextControlBackgroundFocused" Color="#F8FCFF" />
|
||||
<SolidColorBrush x:Key="TextControlBackgroundDisabled" Color="#FFFFFF" />
|
||||
<SolidColorBrush x:Key="TextControlForeground" Color="#000000" />
|
||||
<SolidColorBrush x:Key="TextControlForegroundPointerOver" Color="#000000" />
|
||||
<SolidColorBrush x:Key="TextControlForegroundFocused" Color="#000000" />
|
||||
<SolidColorBrush x:Key="TextControlForegroundDisabled" Color="#8C8C8C" />
|
||||
<SolidColorBrush x:Key="TextControlSelectionHighlightColor" Color="#DEE9F8" />
|
||||
<SolidColorBrush x:Key="TextControlPlaceholderForeground" Color="#AAAAAA" />
|
||||
<SolidColorBrush x:Key="TextControlPlaceholderForegroundPointerOver" Color="#AAAAAA" />
|
||||
<SolidColorBrush x:Key="TextControlPlaceholderForegroundFocused" Color="#AAAAAA" />
|
||||
<SolidColorBrush x:Key="TextControlPlaceholderForegroundDisabled" Color="#AAAAAA" />
|
||||
|
||||
<Thickness x:Key="ExpanderHeaderPadding">15,0</Thickness>
|
||||
<Thickness x:Key="ExpanderContentPadding">15</Thickness>
|
||||
<SolidColorBrush x:Key="ExpanderDropDownBackground" Color="#FCFCFC" />
|
||||
<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="ExpanderDropDownBorderBrush" Color="#697DAB" />
|
||||
<SolidColorBrush x:Key="ExpanderDropDownBackground" Color="#FFFFFF" />
|
||||
|
||||
</Application.Resources>
|
||||
|
||||
|
@@ -4,7 +4,7 @@ using Avalonia.Markup.Xaml;
|
||||
using DHT.Desktop.Main;
|
||||
|
||||
namespace DHT.Desktop {
|
||||
public class App : Application {
|
||||
sealed class App : Application {
|
||||
public override void Initialize() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using DHT.Server.Logging;
|
||||
using DHT.Utils.Logging;
|
||||
|
||||
namespace DHT.Desktop {
|
||||
public class Arguments {
|
||||
sealed class Arguments {
|
||||
private static readonly Log Log = Log.ForType<Arguments>();
|
||||
|
||||
public static Arguments Empty => new(Array.Empty<string>());
|
||||
|
||||
public string? DatabaseFile { get; }
|
||||
@@ -13,12 +15,25 @@ namespace DHT.Desktop {
|
||||
for (int i = 0; i < args.Length; i++) {
|
||||
string key = args[i];
|
||||
|
||||
if (i >= args.Length - 1) {
|
||||
switch (key) {
|
||||
case "-debug":
|
||||
Log.IsDebugEnabled = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
string value;
|
||||
|
||||
if (i == 0 && !key.StartsWith('-')) {
|
||||
value = key;
|
||||
key = "-db";
|
||||
}
|
||||
else if (i >= args.Length - 1) {
|
||||
Log.Warn("Missing value for command line argument: " + key);
|
||||
continue;
|
||||
}
|
||||
|
||||
string value = args[++i];
|
||||
else {
|
||||
value = args[++i];
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "-db":
|
||||
|
@@ -3,14 +3,16 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using DHT.Desktop.Dialogs;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Database.Exceptions;
|
||||
using DHT.Server.Database.Sqlite;
|
||||
using DHT.Server.Logging;
|
||||
using DHT.Utils.Logging;
|
||||
|
||||
namespace DHT.Desktop.Common {
|
||||
public static class DatabaseGui {
|
||||
static class DatabaseGui {
|
||||
private static readonly Log Log = Log.ForType(typeof(DatabaseGui));
|
||||
|
||||
private const string DatabaseFileInitialName = "archive.dht";
|
||||
|
||||
private static readonly List<FileDialogFilter> DatabaseFileDialogFilter = new() {
|
||||
@@ -54,11 +56,11 @@ namespace DHT.Desktop.Common {
|
||||
}
|
||||
|
||||
public static async Task<DialogResult.YesNo> ShowCanUpgradeDatabaseDialog(Window window) {
|
||||
return await Dialog.ShowYesNo(window, "Database Upgrade", "This database was created with an older version of DHT. If you proceed, the database will be upgraded and will no longer open in previous versions of DHT. Do you want to proceed with the upgrade?");
|
||||
return await Dialog.ShowYesNo(window, "Database Upgrade", "This database was created with an older version of DHT. If you proceed, the database will be upgraded and will no longer open in previous versions of DHT.\n\nPlease ensure you have a backup of the database. Do you want to proceed with the upgrade?");
|
||||
}
|
||||
|
||||
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. 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?");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,12 +3,12 @@ using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace DHT.Desktop.Common {
|
||||
public class NumberValueConverter : IValueConverter {
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
sealed class NumberValueConverter : IValueConverter {
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
|
||||
return string.Format(Program.Culture, "{0:n0}", value);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
namespace DHT.Desktop.Common {
|
||||
public static class TextFormat {
|
||||
static class TextFormat {
|
||||
public static string Format(this int number) {
|
||||
return number.ToString("N0", Program.Culture);
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>DiscordHistoryTracker</AssemblyName>
|
||||
<RootNamespace>DHT.Desktop</RootNamespace>
|
||||
<PackageId>DiscordHistoryTracker</PackageId>
|
||||
<Authors>chylex</Authors>
|
||||
@@ -11,56 +12,58 @@
|
||||
<ApplicationIcon>./Resources/icon.ico</ApplicationIcon>
|
||||
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<AssemblyName>DiscordHistoryTracker</AssemblyName>
|
||||
<Version>33.0.0.0</Version>
|
||||
<AssemblyVersion>$(Version)</AssemblyVersion>
|
||||
<FileVersion>$(Version)</FileVersion>
|
||||
<PackageVersion>$(Version)</PackageVersion>
|
||||
<GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
|
||||
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
|
||||
<GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>none</DebugType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="0.10.10" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.10" />
|
||||
<PackageReference Include="Avalonia" Version="0.10.13" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.13" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.13" Condition=" '$(Configuration)' == 'Debug' " />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Server\Server.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.10" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Windows\MainWindow.axaml.cs">
|
||||
<DependentUpon>MainWindow.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Dialogs\MessageDialog.axaml.cs">
|
||||
<DependentUpon>MessageDialog.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<UpToDateCheckInput Remove="Pages\DatabasePage.axaml" />
|
||||
<UpToDateCheckInput Remove="Pages\TrackingPage.axaml" />
|
||||
<UpToDateCheckInput Remove="Pages\ViewerPage.axaml" />
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Version.cs" Link="Version.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Resources/icon.ico" />
|
||||
<EmbeddedResource Include="../Resources/Tracker/bootstrap.js">
|
||||
<LogicalName>Tracker\%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||
<Link>Resources/Tracker/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<Visible>false</Visible>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="../Resources/Tracker/scripts.min/**">
|
||||
<EmbeddedResource Include="../Resources/Tracker/scripts/**" Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<LogicalName>Tracker\scripts\%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||
<Link>Resources/Tracker/scripts/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<Visible>false</Visible>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="$(ProjectDir)bin/.res/scripts/**" Condition=" '$(Configuration)' == 'Release' ">
|
||||
<LogicalName>Tracker\scripts\%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||
<Link>Resources/Tracker/scripts/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<Visible>false</Visible>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="../Resources/Tracker/styles/**">
|
||||
<LogicalName>Tracker\styles\%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||
<Link>Resources/Tracker/styles/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<Visible>false</Visible>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="../Resources/Viewer/**">
|
||||
<LogicalName>Viewer\%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||
<Link>Resources/Viewer/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<Visible>false</Visible>
|
||||
</EmbeddedResource>
|
||||
<AvaloniaResource Include="Resources/icon.ico" />
|
||||
</ItemGroup>
|
||||
<Target Name="MinifyResources" BeforeTargets="PrepareForBuild" Condition=" '$(Configuration)' == 'Release' ">
|
||||
<ItemGroup>
|
||||
<UpToDateCheckInput Include="$(ProjectDir)../Resources/Tracker/scripts/**" Visible="false" />
|
||||
</ItemGroup>
|
||||
<RemoveDir Directories="$(ProjectDir)bin/.res/scripts" />
|
||||
<Exec Command="$(ProjectDir)../Resources/minify.py" WorkingDirectory="$(ProjectDir)../Resources" IgnoreExitCode="false" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
@@ -2,16 +2,16 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:dialogs="clr-namespace:DHT.Desktop.Dialogs"
|
||||
xmlns:namespace="clr-namespace:DHT.Desktop.Dialogs.CheckBox"
|
||||
mc:Ignorable="d" d:DesignWidth="500"
|
||||
x:Class="DHT.Desktop.Dialogs.CheckBoxDialog"
|
||||
x:Class="DHT.Desktop.Dialogs.CheckBox.CheckBoxDialog"
|
||||
Title="{Binding Title}"
|
||||
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||
Width="500" SizeToContent="Height" CanResize="False"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Window.DataContext>
|
||||
<dialogs:CheckBoxDialogModel />
|
||||
<namespace:CheckBoxDialogModel />
|
||||
</Window.DataContext>
|
||||
|
||||
<Window.Styles>
|
@@ -1,10 +1,13 @@
|
||||
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 {
|
||||
public class CheckBoxDialog : Window {
|
||||
namespace DHT.Desktop.Dialogs.CheckBox {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed class CheckBoxDialog : Window {
|
||||
public CheckBoxDialog() {
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
@@ -2,10 +2,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using DHT.Desktop.Models;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Dialogs {
|
||||
public class CheckBoxDialogModel : BaseModel {
|
||||
namespace DHT.Desktop.Dialogs.CheckBox {
|
||||
class CheckBoxDialogModel : BaseModel {
|
||||
public string Title { get; init; } = "";
|
||||
|
||||
private IReadOnlyList<CheckBoxItem> items = Array.Empty<CheckBoxItem>();
|
||||
@@ -28,8 +28,8 @@ namespace DHT.Desktop.Dialogs {
|
||||
|
||||
private bool pauseCheckEvents = false;
|
||||
|
||||
public bool AreAllSelected => Items.All(item => item.Checked);
|
||||
public bool AreNoneSelected => Items.All(item => !item.Checked);
|
||||
public bool AreAllSelected => Items.All(static item => item.Checked);
|
||||
public bool AreNoneSelected => Items.All(static item => !item.Checked);
|
||||
|
||||
public void SelectAll() => SetAllChecked(true);
|
||||
public void SelectNone() => SetAllChecked(false);
|
||||
@@ -57,10 +57,10 @@ namespace DHT.Desktop.Dialogs {
|
||||
}
|
||||
}
|
||||
|
||||
public class CheckBoxDialogModel<T> : CheckBoxDialogModel {
|
||||
sealed class CheckBoxDialogModel<T> : CheckBoxDialogModel {
|
||||
public new IReadOnlyList<CheckBoxItem<T>> Items { get; }
|
||||
|
||||
public IEnumerable<CheckBoxItem<T>> SelectedItems => Items.Where(item => item.Checked);
|
||||
public IEnumerable<CheckBoxItem<T>> SelectedItems => Items.Where(static item => item.Checked);
|
||||
|
||||
public CheckBoxDialogModel(IEnumerable<CheckBoxItem<T>> items) {
|
||||
this.Items = new List<CheckBoxItem<T>>(items);
|
@@ -1,7 +1,7 @@
|
||||
using DHT.Desktop.Models;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Dialogs {
|
||||
public class CheckBoxItem : BaseModel {
|
||||
namespace DHT.Desktop.Dialogs.CheckBox {
|
||||
class CheckBoxItem : BaseModel {
|
||||
public string Title { get; init; } = "";
|
||||
public object? Item { get; init; } = null;
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace DHT.Desktop.Dialogs {
|
||||
}
|
||||
}
|
||||
|
||||
public class CheckBoxItem<T> : CheckBoxItem {
|
||||
sealed class CheckBoxItem<T> : CheckBoxItem {
|
||||
public new T Item { get; }
|
||||
|
||||
public CheckBoxItem(T item) {
|
@@ -2,8 +2,8 @@ using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace DHT.Desktop.Dialogs {
|
||||
public static class Dialog {
|
||||
namespace DHT.Desktop.Dialogs.Message {
|
||||
static class Dialog {
|
||||
public static async Task ShowOk(Window owner, string title, string message) {
|
||||
if (!Dispatcher.UIThread.CheckAccess()) {
|
||||
await Dispatcher.UIThread.InvokeAsync(() => ShowOk(owner, title, message));
|
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
|
||||
namespace DHT.Desktop.Dialogs {
|
||||
public static class DialogResult {
|
||||
namespace DHT.Desktop.Dialogs.Message {
|
||||
static class DialogResult {
|
||||
public enum All {
|
||||
Ok,
|
||||
Yes,
|
@@ -2,16 +2,16 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:dialogs="clr-namespace:DHT.Desktop.Dialogs"
|
||||
xmlns:namespace="clr-namespace:DHT.Desktop.Dialogs.Message"
|
||||
mc:Ignorable="d" d:DesignWidth="500"
|
||||
x:Class="DHT.Desktop.Dialogs.MessageDialog"
|
||||
x:Class="DHT.Desktop.Dialogs.Message.MessageDialog"
|
||||
Title="{Binding Title}"
|
||||
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||
Width="500" SizeToContent="Height" CanResize="False"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Window.DataContext>
|
||||
<dialogs:MessageDialogModel />
|
||||
<namespace:MessageDialogModel />
|
||||
</Window.DataContext>
|
||||
|
||||
<Window.Styles>
|
@@ -1,10 +1,12 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Dialogs {
|
||||
public class MessageDialog : Window {
|
||||
namespace DHT.Desktop.Dialogs.Message {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed class MessageDialog : Window {
|
||||
public MessageDialog() {
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
@@ -1,5 +1,5 @@
|
||||
namespace DHT.Desktop.Dialogs {
|
||||
public class MessageDialogModel {
|
||||
namespace DHT.Desktop.Dialogs.Message {
|
||||
sealed class MessageDialogModel {
|
||||
public string Title { get; init; } = "";
|
||||
public string Message { get; init; } = "";
|
||||
|
@@ -1,7 +1,7 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DHT.Desktop.Dialogs {
|
||||
public interface IProgressCallback {
|
||||
namespace DHT.Desktop.Dialogs.Progress {
|
||||
interface IProgressCallback {
|
||||
Task Update(string message, int finishedItems, int totalItems);
|
||||
}
|
||||
}
|
@@ -2,9 +2,9 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:dialogs="clr-namespace:DHT.Desktop.Dialogs"
|
||||
xmlns:namespace="clr-namespace:DHT.Desktop.Dialogs.Progress"
|
||||
mc:Ignorable="d" d:DesignWidth="500"
|
||||
x:Class="DHT.Desktop.Dialogs.ProgressDialog"
|
||||
x:Class="DHT.Desktop.Dialogs.Progress.ProgressDialog"
|
||||
Title="{Binding Title}"
|
||||
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||
Opened="Loaded"
|
||||
@@ -13,7 +13,7 @@
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Window.DataContext>
|
||||
<dialogs:ProgressDialogModel />
|
||||
<namespace:ProgressDialogModel />
|
||||
</Window.DataContext>
|
||||
|
||||
<Window.Styles>
|
@@ -1,12 +1,14 @@
|
||||
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 {
|
||||
public class ProgressDialog : Window {
|
||||
namespace DHT.Desktop.Dialogs.Progress {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed class ProgressDialog : Window {
|
||||
private bool isFinished = false;
|
||||
|
||||
public ProgressDialog() {
|
@@ -2,10 +2,10 @@ using System;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Threading;
|
||||
using DHT.Desktop.Common;
|
||||
using DHT.Desktop.Models;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Dialogs {
|
||||
public class ProgressDialogModel : BaseModel {
|
||||
namespace DHT.Desktop.Dialogs.Progress {
|
||||
sealed class ProgressDialogModel : BaseModel {
|
||||
public string Title { get; init; } = "";
|
||||
|
||||
private string message = "";
|
||||
@@ -46,7 +46,7 @@ namespace DHT.Desktop.Dialogs {
|
||||
|
||||
public delegate Task TaskRunner(IProgressCallback callback);
|
||||
|
||||
private class Callback : IProgressCallback {
|
||||
private sealed class Callback : IProgressCallback {
|
||||
private readonly ProgressDialogModel model;
|
||||
|
||||
public Callback(ProgressDialogModel model) {
|
119
app/Desktop/Discord/DiscordAppSettings.cs
Normal file
119
app/Desktop/Discord/DiscordAppSettings.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Utils.Logging;
|
||||
using static System.Environment.SpecialFolder;
|
||||
using static System.Environment.SpecialFolderOption;
|
||||
|
||||
namespace DHT.Desktop.Discord {
|
||||
static class DiscordAppSettings {
|
||||
private static readonly Log Log = Log.ForType(typeof(DiscordAppSettings));
|
||||
|
||||
private const string JsonKeyDevTools = "DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING";
|
||||
|
||||
public static string JsonFilePath { get; }
|
||||
private static string JsonBackupFilePath { get; }
|
||||
|
||||
[SuppressMessage("ReSharper", "ConvertIfStatementToConditionalTernaryExpression")]
|
||||
static DiscordAppSettings() {
|
||||
string rootFolder;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
||||
rootFolder = Path.Combine(Environment.GetFolderPath(ApplicationData, DoNotVerify), "Discord");
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
|
||||
rootFolder = Path.Combine(Environment.GetFolderPath(UserProfile, DoNotVerify), "Library", "Application Support", "Discord");
|
||||
}
|
||||
else {
|
||||
rootFolder = Path.Combine(Environment.GetFolderPath(ApplicationData, DoNotVerify), "discord");
|
||||
}
|
||||
|
||||
JsonFilePath = Path.Combine(rootFolder, "settings.json");
|
||||
JsonBackupFilePath = JsonFilePath + ".bak";
|
||||
}
|
||||
|
||||
public static async Task<bool?> AreDevToolsEnabled() {
|
||||
try {
|
||||
return AreDevToolsEnabled(await ReadSettingsJson());
|
||||
} catch (Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AreDevToolsEnabled(Dictionary<string, object?> json) {
|
||||
return json.TryGetValue(JsonKeyDevTools, out var value) && value is JsonElement { ValueKind: JsonValueKind.True };
|
||||
}
|
||||
|
||||
public static async Task<SettingsJsonResult> ConfigureDevTools(bool enable) {
|
||||
Dictionary<string, object?> json;
|
||||
|
||||
try {
|
||||
json = await ReadSettingsJson();
|
||||
} catch (FileNotFoundException) {
|
||||
return SettingsJsonResult.FileNotFound;
|
||||
} catch (JsonException) {
|
||||
return SettingsJsonResult.InvalidJson;
|
||||
} catch (Exception e) {
|
||||
Log.Error(e);
|
||||
return SettingsJsonResult.ReadError;
|
||||
}
|
||||
|
||||
if (enable == AreDevToolsEnabled(json)) {
|
||||
return SettingsJsonResult.AlreadySet;
|
||||
}
|
||||
|
||||
if (enable) {
|
||||
json[JsonKeyDevTools] = true;
|
||||
}
|
||||
else {
|
||||
json.Remove(JsonKeyDevTools);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!File.Exists(JsonBackupFilePath)) {
|
||||
File.Copy(JsonFilePath, JsonBackupFilePath);
|
||||
}
|
||||
|
||||
await WriteSettingsJson(json);
|
||||
} catch (Exception e) {
|
||||
Log.Error("An error occurred when writing settings file.");
|
||||
Log.Error(e);
|
||||
|
||||
if (File.Exists(JsonBackupFilePath)) {
|
||||
try {
|
||||
File.Move(JsonBackupFilePath, JsonFilePath, true);
|
||||
Log.Info("Restored settings file from backup.");
|
||||
} catch (Exception e2) {
|
||||
Log.Error("Cannot restore settings file from backup.");
|
||||
Log.Error(e2);
|
||||
}
|
||||
}
|
||||
|
||||
return SettingsJsonResult.WriteError;
|
||||
}
|
||||
|
||||
try {
|
||||
File.Delete(JsonBackupFilePath);
|
||||
} catch (Exception e) {
|
||||
Log.Error("Cannot delete backup file.");
|
||||
Log.Error(e);
|
||||
}
|
||||
|
||||
return SettingsJsonResult.Success;
|
||||
}
|
||||
|
||||
private static async Task<Dictionary<string, object?>> ReadSettingsJson() {
|
||||
await using var stream = new FileStream(JsonFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
return await JsonSerializer.DeserializeAsync<Dictionary<string, object?>?>(stream) ?? throw new JsonException();
|
||||
}
|
||||
|
||||
private static async Task WriteSettingsJson(Dictionary<string, object?> json) {
|
||||
await using var stream = new FileStream(JsonFilePath, FileMode.Truncate, FileAccess.Write, FileShare.None);
|
||||
await JsonSerializer.SerializeAsync(stream, json, new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
}
|
||||
}
|
10
app/Desktop/Discord/SettingsJsonResult.cs
Normal file
10
app/Desktop/Discord/SettingsJsonResult.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace DHT.Desktop.Discord {
|
||||
enum SettingsJsonResult {
|
||||
Success,
|
||||
AlreadySet,
|
||||
FileNotFound,
|
||||
ReadError,
|
||||
InvalidJson,
|
||||
WriteError
|
||||
}
|
||||
}
|
@@ -1,9 +1,11 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main {
|
||||
public class AboutWindow : Window {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed class AboutWindow : Window {
|
||||
public AboutWindow() {
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
|
@@ -1,7 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace DHT.Desktop.Main {
|
||||
public class AboutWindowModel {
|
||||
sealed class AboutWindowModel {
|
||||
public void ShowOfficialWebsite() {
|
||||
OpenUrl("https://dht.chylex.com");
|
||||
}
|
||||
|
@@ -15,6 +15,9 @@
|
||||
<Setter Property="Margin" Value="0 20 40 0" />
|
||||
<Setter Property="Spacing" Value="4" />
|
||||
</Style>
|
||||
<Style Selector="WrapPanel > StackPanel:nth-last-child(1)">
|
||||
<Setter Property="Margin" Value="0 20 0 0" />
|
||||
</Style>
|
||||
<Style Selector="Grid > Label">
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
@@ -1,8 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
public class FilterPanel : UserControl {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed class FilterPanel : UserControl {
|
||||
private CalendarDatePicker StartDatePicker => this.FindControl<CalendarDatePicker>("StartDatePicker");
|
||||
private CalendarDatePicker EndDatePicker => this.FindControl<CalendarDatePicker>("EndDatePicker");
|
||||
|
||||
|
@@ -6,14 +6,15 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using DHT.Desktop.Common;
|
||||
using DHT.Desktop.Dialogs;
|
||||
using DHT.Desktop.Models;
|
||||
using DHT.Desktop.Dialogs.CheckBox;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Data.Filters;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
public class FilterPanelModel : BaseModel {
|
||||
sealed class FilterPanelModel : BaseModel, IDisposable {
|
||||
private static readonly HashSet<string> FilterProperties = new () {
|
||||
nameof(FilterByDate),
|
||||
nameof(StartDate),
|
||||
@@ -57,7 +58,7 @@ namespace DHT.Desktop.Main.Controls {
|
||||
}
|
||||
|
||||
public HashSet<ulong> IncludedChannels {
|
||||
get => includedChannels ?? db.GetAllChannels().Select(channel => channel.Id).ToHashSet();
|
||||
get => includedChannels ?? db.GetAllChannels().Select(static channel => channel.Id).ToHashSet();
|
||||
set => Change(ref includedChannels, value);
|
||||
}
|
||||
|
||||
@@ -67,7 +68,7 @@ namespace DHT.Desktop.Main.Controls {
|
||||
}
|
||||
|
||||
public HashSet<ulong> IncludedUsers {
|
||||
get => includedUsers ?? db.GetAllUsers().Select(user => user.Id).ToHashSet();
|
||||
get => includedUsers ?? db.GetAllUsers().Select(static user => user.Id).ToHashSet();
|
||||
set => Change(ref includedUsers, value);
|
||||
}
|
||||
|
||||
@@ -102,6 +103,10 @@ namespace DHT.Desktop.Main.Controls {
|
||||
db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
||||
}
|
||||
|
||||
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||
if (e.PropertyName != null && FilterProperties.Contains(e.PropertyName)) {
|
||||
FilterPropertyChanged?.Invoke(sender, e);
|
||||
@@ -125,7 +130,7 @@ namespace DHT.Desktop.Main.Controls {
|
||||
}
|
||||
|
||||
public async void OpenChannelFilterDialog() {
|
||||
var servers = db.GetAllServers().ToDictionary(server => server.Id);
|
||||
var servers = db.GetAllServers().ToDictionary(static server => server.Id);
|
||||
var items = new List<CheckBoxItem<ulong>>();
|
||||
var included = IncludedChannels;
|
||||
|
||||
@@ -221,7 +226,7 @@ namespace DHT.Desktop.Main.Controls {
|
||||
}
|
||||
|
||||
private static async Task<HashSet<ulong>?> OpenIdFilterDialog(Window window, string title, List<CheckBoxItem<ulong>> items) {
|
||||
items.Sort((item1, item2) => item1.Title.CompareTo(item2.Title));
|
||||
items.Sort(static (item1, item2) => item1.Title.CompareTo(item2.Title));
|
||||
|
||||
var model = new CheckBoxDialogModel<ulong>(items) {
|
||||
Title = title
|
||||
@@ -230,7 +235,7 @@ namespace DHT.Desktop.Main.Controls {
|
||||
var dialog = new CheckBoxDialog { DataContext = model };
|
||||
var result = await dialog.ShowDialog<DialogResult.OkCancel>(window);
|
||||
|
||||
return result == DialogResult.OkCancel.Ok ? model.SelectedItems.Select(item => item.Item).ToHashSet() : null;
|
||||
return result == DialogResult.OkCancel.Ok ? model.SelectedItems.Select(static item => item.Item).ToHashSet() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
50
app/Desktop/Main/Controls/ServerConfigurationPanel.axaml
Normal file
50
app/Desktop/Main/Controls/ServerConfigurationPanel.axaml
Normal file
@@ -0,0 +1,50 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="clr-namespace:DHT.Desktop.Main.Controls"
|
||||
mc:Ignorable="d"
|
||||
x:Class="DHT.Desktop.Main.Controls.ServerConfigurationPanel">
|
||||
|
||||
<Design.DataContext>
|
||||
<controls:ServerConfigurationPanelModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<UserControl.Styles>
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="FontFamily" Value="Consolas,Courier" />
|
||||
<Setter Property="FontSize" Value="15" />
|
||||
</Style>
|
||||
<Style Selector="WrapPanel > StackPanel">
|
||||
<Setter Property="Orientation" Value="Vertical" />
|
||||
<Setter Property="Margin" Value="0 0 10 10" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<StackPanel>
|
||||
<Button Command="{Binding OnClickToggleServerButton}" Content="{Binding ToggleServerButtonText}" IsEnabled="{Binding IsToggleServerButtonEnabled}" />
|
||||
<TextBlock TextWrapping="Wrap" Margin="0 15">
|
||||
The following settings determine how the tracking script communicates with this application. If you change them, you will have to copy and apply the tracking script again.
|
||||
</TextBlock>
|
||||
<WrapPanel>
|
||||
<StackPanel>
|
||||
<Label Target="Port">Port</Label>
|
||||
<TextBox x:Name="Port" Width="70" Text="{Binding InputPort}" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<Label Target="Token">Token</Label>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBox x:Name="Token" Width="200" Text="{Binding InputToken}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel VerticalAlignment="Bottom">
|
||||
<Button Command="{Binding OnClickRandomizeToken}">Randomize Token</Button>
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Button IsEnabled="{Binding HasMadeChanges}" Command="{Binding OnClickApplyChanges}">Apply & Restart</Button>
|
||||
<Button IsEnabled="{Binding HasMadeChanges}" Command="{Binding OnClickCancelChanges}">Cancel</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
</UserControl>
|
16
app/Desktop/Main/Controls/ServerConfigurationPanel.axaml.cs
Normal file
16
app/Desktop/Main/Controls/ServerConfigurationPanel.axaml.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed class ServerConfigurationPanel : UserControl {
|
||||
public ServerConfigurationPanel() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
116
app/Desktop/Main/Controls/ServerConfigurationPanelModel.cs
Normal file
116
app/Desktop/Main/Controls/ServerConfigurationPanelModel.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Desktop.Server;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Service;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
|
||||
private string inputPort;
|
||||
|
||||
public string InputPort {
|
||||
get => inputPort;
|
||||
set {
|
||||
Change(ref inputPort, value);
|
||||
OnPropertyChanged(nameof(HasMadeChanges));
|
||||
}
|
||||
}
|
||||
|
||||
private string inputToken;
|
||||
|
||||
public string InputToken {
|
||||
get => inputToken;
|
||||
set {
|
||||
Change(ref inputToken, value);
|
||||
OnPropertyChanged(nameof(HasMadeChanges));
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasMadeChanges => ServerManager.Port.ToString() != InputPort || ServerManager.Token != InputToken;
|
||||
|
||||
private bool isToggleServerButtonEnabled = true;
|
||||
|
||||
public bool IsToggleServerButtonEnabled {
|
||||
get => isToggleServerButtonEnabled;
|
||||
set => Change(ref isToggleServerButtonEnabled, value);
|
||||
}
|
||||
|
||||
public string ToggleServerButtonText => serverManager.IsRunning ? "Stop Server" : "Start Server";
|
||||
|
||||
public event EventHandler<StatusBarModel.Status>? ServerStatusChanged;
|
||||
|
||||
private readonly Window window;
|
||||
private readonly ServerManager serverManager;
|
||||
|
||||
[Obsolete("Designer")]
|
||||
public ServerConfigurationPanelModel() : this(null!, new ServerManager(DummyDatabaseFile.Instance)) {}
|
||||
|
||||
public ServerConfigurationPanelModel(Window window, ServerManager serverManager) {
|
||||
this.window = window;
|
||||
this.serverManager = serverManager;
|
||||
this.inputPort = ServerManager.Port.ToString();
|
||||
this.inputToken = ServerManager.Token;
|
||||
}
|
||||
|
||||
public void Initialize() {
|
||||
ServerLauncher.ServerStatusChanged += ServerLauncherOnServerStatusChanged;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
ServerLauncher.ServerStatusChanged -= ServerLauncherOnServerStatusChanged;
|
||||
}
|
||||
|
||||
private void ServerLauncherOnServerStatusChanged(object? sender, EventArgs e) {
|
||||
ServerStatusChanged?.Invoke(this, serverManager.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped);
|
||||
OnPropertyChanged(nameof(ToggleServerButtonText));
|
||||
IsToggleServerButtonEnabled = true;
|
||||
}
|
||||
|
||||
private void BeforeServerStart() {
|
||||
IsToggleServerButtonEnabled = false;
|
||||
ServerStatusChanged?.Invoke(this, StatusBarModel.Status.Starting);
|
||||
}
|
||||
|
||||
private void StartServer() {
|
||||
BeforeServerStart();
|
||||
serverManager.Launch();
|
||||
}
|
||||
|
||||
private void StopServer() {
|
||||
IsToggleServerButtonEnabled = false;
|
||||
ServerStatusChanged?.Invoke(this, StatusBarModel.Status.Stopping);
|
||||
serverManager.Stop();
|
||||
}
|
||||
|
||||
public void OnClickToggleServerButton() {
|
||||
if (serverManager.IsRunning) {
|
||||
StopServer();
|
||||
}
|
||||
else {
|
||||
StartServer();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnClickRandomizeToken() {
|
||||
InputToken = ServerUtils.GenerateRandomToken(20);
|
||||
}
|
||||
|
||||
public async void OnClickApplyChanges() {
|
||||
if (!ushort.TryParse(InputPort, out ushort port)) {
|
||||
await Dialog.ShowOk(window, "Invalid Port", "Port must be a number between 0 and 65535.");
|
||||
return;
|
||||
}
|
||||
|
||||
BeforeServerStart();
|
||||
serverManager.Relaunch(port, InputToken);
|
||||
OnPropertyChanged(nameof(HasMadeChanges));
|
||||
}
|
||||
|
||||
public void OnClickCancelChanges() {
|
||||
InputPort = ServerManager.Port.ToString();
|
||||
InputToken = ServerManager.Token;
|
||||
}
|
||||
}
|
||||
}
|
@@ -11,32 +11,35 @@
|
||||
</Design.DataContext>
|
||||
|
||||
<UserControl.Background>
|
||||
<SolidColorBrush>#546A9F</SolidColorBrush>
|
||||
<SolidColorBrush>#3C4F79</SolidColorBrush>
|
||||
</UserControl.Background>
|
||||
|
||||
<UserControl.Styles>
|
||||
<Style Selector="StackPanel > TextBlock">
|
||||
<Setter Property="Foreground" Value="#E0E0E0" />
|
||||
<Setter Property="Foreground" Value="#E7E7E7" />
|
||||
</Style>
|
||||
<Style Selector="StackPanel > TextBlock.label">
|
||||
<Setter Property="FontSize" Value="15" />
|
||||
<Setter Property="FontWeight" Value="SemiLight" />
|
||||
</Style>
|
||||
<Style Selector="StackPanel > TextBlock.value">
|
||||
<Setter Property="FontSize" Value="16" />
|
||||
<Setter Property="FontWeight" Value="SemiLight" />
|
||||
<Setter Property="TextAlignment" Value="Right" />
|
||||
<Setter Property="Margin" Value="0 1" />
|
||||
</Style>
|
||||
<Style Selector="StackPanel > Rectangle">
|
||||
<Setter Property="Margin" Value="14 0" />
|
||||
<Setter Property="Stroke" Value="#3B5287" />
|
||||
<Setter Property="Margin" Value="14 1" />
|
||||
<Setter Property="Stroke" Value="#697899" />
|
||||
<Setter Property="StrokeThickness" Value="2" />
|
||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="4 3">
|
||||
<StackPanel Orientation="Horizontal" Margin="6 3">
|
||||
<StackPanel Orientation="Vertical" Width="65">
|
||||
<TextBlock Classes="label">Status</TextBlock>
|
||||
<TextBlock FontSize="12" Margin="0 2 0 0" Text="{Binding StatusText}" />
|
||||
<TextBlock FontSize="12" Margin="0 3 0 0" Text="{Binding StatusText}" />
|
||||
</StackPanel>
|
||||
<Rectangle />
|
||||
<StackPanel Orientation="Vertical">
|
||||
|
@@ -1,8 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
public class StatusBar : UserControl {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed class StatusBar : UserControl {
|
||||
public StatusBar() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using DHT.Desktop.Models;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
public class StatusBarModel : BaseModel {
|
||||
sealed class StatusBarModel : BaseModel {
|
||||
public DatabaseStatistics DatabaseStatistics { get; }
|
||||
|
||||
private Status status = Status.Stopped;
|
||||
|
@@ -1,57 +0,0 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using DHT.Desktop.Main.Controls;
|
||||
using DHT.Desktop.Main.Pages;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Service;
|
||||
|
||||
namespace DHT.Desktop.Main {
|
||||
public class MainContentScreenModel : IDisposable {
|
||||
public DatabasePage DatabasePage { get; }
|
||||
private DatabasePageModel DatabasePageModel { get; }
|
||||
|
||||
public TrackingPage TrackingPage { get; }
|
||||
private TrackingPageModel TrackingPageModel { get; }
|
||||
|
||||
public ViewerPage ViewerPage { get; }
|
||||
private ViewerPageModel ViewerPageModel { get; }
|
||||
|
||||
public StatusBarModel StatusBarModel { get; }
|
||||
|
||||
public event EventHandler? DatabaseClosed {
|
||||
add { DatabasePageModel.DatabaseClosed += value; }
|
||||
remove { DatabasePageModel.DatabaseClosed -= value; }
|
||||
}
|
||||
|
||||
[Obsolete("Designer")]
|
||||
public MainContentScreenModel() : this(null!, DummyDatabaseFile.Instance) {}
|
||||
|
||||
public MainContentScreenModel(Window window, IDatabaseFile db) {
|
||||
DatabasePageModel = new DatabasePageModel(window, db);
|
||||
DatabasePage = new DatabasePage { DataContext = DatabasePageModel };
|
||||
|
||||
TrackingPageModel = new TrackingPageModel(window, db);
|
||||
TrackingPage = new TrackingPage { DataContext = TrackingPageModel };
|
||||
|
||||
ViewerPageModel = new ViewerPageModel(window, db);
|
||||
ViewerPage = new ViewerPage { DataContext = ViewerPageModel };
|
||||
|
||||
StatusBarModel = new StatusBarModel(db.Statistics);
|
||||
TrackingPageModel.ServerStatusChanged += TrackingPageModelOnServerStatusChanged;
|
||||
StatusBarModel.CurrentStatus = ServerLauncher.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped;
|
||||
}
|
||||
|
||||
public void Initialize() {
|
||||
TrackingPageModel.Initialize();
|
||||
}
|
||||
|
||||
private void TrackingPageModelOnServerStatusChanged(object? sender, StatusBarModel.Status e) {
|
||||
StatusBarModel.CurrentStatus = e;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
TrackingPageModel.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,18 +5,18 @@
|
||||
xmlns:main="clr-namespace:DHT.Desktop.Main"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="DHT.Desktop.Main.MainWindow"
|
||||
Title="Discord History Tracker"
|
||||
Title="{Binding Title}"
|
||||
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||
Width="800" Height="500"
|
||||
MinWidth="480" MinHeight="240"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
MinWidth="500" MinHeight="275"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Closed="OnClosed">
|
||||
|
||||
<Design.DataContext>
|
||||
<main:MainWindowModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Panel>
|
||||
<ContentPresenter Content="{Binding WelcomeScreen}" IsVisible="{Binding ShowWelcomeScreen}" />
|
||||
<ContentPresenter Content="{Binding MainContentScreen}" IsVisible="{Binding ShowMainContentScreen}" />
|
||||
<ContentPresenter Content="{Binding CurrentScreen}" />
|
||||
</Panel>
|
||||
</Window>
|
||||
|
@@ -1,16 +1,19 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace DHT.Desktop.Main {
|
||||
public class MainWindow : Window {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed class MainWindow : Window {
|
||||
[UsedImplicitly]
|
||||
public MainWindow() {
|
||||
InitializeComponent(Arguments.Empty);
|
||||
}
|
||||
|
||||
public MainWindow(Arguments args) {
|
||||
internal MainWindow(Arguments args) {
|
||||
InitializeComponent(args);
|
||||
}
|
||||
|
||||
@@ -22,5 +25,11 @@ namespace DHT.Desktop.Main {
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
}
|
||||
|
||||
public void OnClosed(object? sender, EventArgs e) {
|
||||
if (DataContext is IDisposable disposable) {
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,21 +4,25 @@ using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using DHT.Desktop.Dialogs;
|
||||
using DHT.Desktop.Main.Pages;
|
||||
using DHT.Desktop.Models;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Desktop.Main.Screens;
|
||||
using DHT.Desktop.Server;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main {
|
||||
public class MainWindowModel : BaseModel {
|
||||
public WelcomeScreen WelcomeScreen { get; }
|
||||
private WelcomeScreenModel WelcomeScreenModel { get; }
|
||||
sealed class MainWindowModel : BaseModel, IDisposable {
|
||||
private const string DefaultTitle = "Discord History Tracker";
|
||||
|
||||
public MainContentScreen? MainContentScreen { get; private set; }
|
||||
private MainContentScreenModel? MainContentScreenModel { get; set; }
|
||||
public string Title { get; private set; } = DefaultTitle;
|
||||
|
||||
public bool ShowWelcomeScreen => db == null;
|
||||
public bool ShowMainContentScreen => db != null;
|
||||
public UserControl CurrentScreen { get; private set; }
|
||||
|
||||
private readonly WelcomeScreen welcomeScreen;
|
||||
private readonly WelcomeScreenModel welcomeScreenModel;
|
||||
|
||||
private MainContentScreen? mainContentScreen;
|
||||
private MainContentScreenModel? mainContentScreenModel;
|
||||
|
||||
private readonly Window window;
|
||||
|
||||
@@ -30,10 +34,11 @@ namespace DHT.Desktop.Main {
|
||||
public MainWindowModel(Window window, Arguments args) {
|
||||
this.window = window;
|
||||
|
||||
WelcomeScreenModel = new WelcomeScreenModel(window);
|
||||
WelcomeScreen = new WelcomeScreen { DataContext = WelcomeScreenModel };
|
||||
welcomeScreenModel = new WelcomeScreenModel(window);
|
||||
welcomeScreen = new WelcomeScreen { DataContext = welcomeScreenModel };
|
||||
CurrentScreen = welcomeScreen;
|
||||
|
||||
WelcomeScreenModel.PropertyChanged += WelcomeScreenModelOnPropertyChanged;
|
||||
welcomeScreenModel.PropertyChanged += WelcomeScreenModelOnPropertyChanged;
|
||||
|
||||
var dbFile = args.DatabaseFile;
|
||||
if (!string.IsNullOrWhiteSpace(dbFile)) {
|
||||
@@ -46,7 +51,7 @@ namespace DHT.Desktop.Main {
|
||||
}
|
||||
|
||||
if (File.Exists(dbFile)) {
|
||||
await WelcomeScreenModel.OpenOrCreateDatabaseFromPath(dbFile);
|
||||
await welcomeScreenModel.OpenOrCreateDatabaseFromPath(dbFile);
|
||||
}
|
||||
else {
|
||||
await Dialog.ShowOk(window, "Database Error", "Database file not found:\n" + dbFile);
|
||||
@@ -57,45 +62,55 @@ namespace DHT.Desktop.Main {
|
||||
}
|
||||
|
||||
if (args.ServerPort != null) {
|
||||
TrackingPageModel.ServerPort = args.ServerPort.ToString()!;
|
||||
ServerManager.Port = args.ServerPort.Value;
|
||||
}
|
||||
|
||||
if (args.ServerToken != null) {
|
||||
TrackingPageModel.ServerToken = args.ServerToken;
|
||||
ServerManager.Token = args.ServerToken;
|
||||
}
|
||||
}
|
||||
|
||||
private void WelcomeScreenModelOnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||
if (e.PropertyName == nameof(WelcomeScreenModel.Db)) {
|
||||
if (MainContentScreenModel != null) {
|
||||
MainContentScreenModel.DatabaseClosed -= MainContentScreenModelOnDatabaseClosed;
|
||||
MainContentScreenModel.Dispose();
|
||||
private async void WelcomeScreenModelOnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||
if (e.PropertyName == nameof(welcomeScreenModel.Db)) {
|
||||
if (mainContentScreenModel != null) {
|
||||
mainContentScreenModel.DatabaseClosed -= MainContentScreenModelOnDatabaseClosed;
|
||||
mainContentScreenModel.Dispose();
|
||||
}
|
||||
|
||||
db?.Dispose();
|
||||
db = WelcomeScreenModel.Db;
|
||||
db = welcomeScreenModel.Db;
|
||||
|
||||
if (db == null) {
|
||||
MainContentScreenModel = null;
|
||||
MainContentScreen = null;
|
||||
Title = DefaultTitle;
|
||||
mainContentScreenModel = null;
|
||||
mainContentScreen = null;
|
||||
CurrentScreen = welcomeScreen;
|
||||
}
|
||||
else {
|
||||
MainContentScreenModel = new MainContentScreenModel(window, db);
|
||||
MainContentScreenModel.Initialize();
|
||||
MainContentScreenModel.DatabaseClosed += MainContentScreenModelOnDatabaseClosed;
|
||||
MainContentScreen = new MainContentScreen { DataContext = MainContentScreenModel };
|
||||
OnPropertyChanged(nameof(MainContentScreen));
|
||||
Title = Path.GetFileName(db.Path) + " - " + DefaultTitle;
|
||||
mainContentScreenModel = new MainContentScreenModel(window, db);
|
||||
await mainContentScreenModel.Initialize();
|
||||
mainContentScreenModel.DatabaseClosed += MainContentScreenModelOnDatabaseClosed;
|
||||
mainContentScreen = new MainContentScreen { DataContext = mainContentScreenModel };
|
||||
CurrentScreen = mainContentScreen;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(ShowWelcomeScreen));
|
||||
OnPropertyChanged(nameof(ShowMainContentScreen));
|
||||
OnPropertyChanged(nameof(CurrentScreen));
|
||||
OnPropertyChanged(nameof(Title));
|
||||
|
||||
window.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
private void MainContentScreenModelOnDatabaseClosed(object? sender, EventArgs e) {
|
||||
WelcomeScreenModel.CloseDatabase();
|
||||
welcomeScreenModel.CloseDatabase();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
welcomeScreenModel.Dispose();
|
||||
mainContentScreenModel?.Dispose();
|
||||
db?.Dispose();
|
||||
db = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
19
app/Desktop/Main/Pages/AdvancedPage.axaml
Normal file
19
app/Desktop/Main/Pages/AdvancedPage.axaml
Normal file
@@ -0,0 +1,19 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:pages="clr-namespace:DHT.Desktop.Main.Pages"
|
||||
xmlns:controls="clr-namespace:DHT.Desktop.Main.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="DHT.Desktop.Main.Pages.AdvancedPage">
|
||||
|
||||
<Design.DataContext>
|
||||
<pages:AdvancedPageModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<StackPanel Orientation="Vertical" Spacing="10">
|
||||
<Expander Header="Internal Server Configuration" IsExpanded="True">
|
||||
<controls:ServerConfigurationPanel DataContext="{Binding ServerConfigurationModel}" />
|
||||
</Expander>
|
||||
</StackPanel>
|
||||
</UserControl>
|
16
app/Desktop/Main/Pages/AdvancedPage.axaml.cs
Normal file
16
app/Desktop/Main/Pages/AdvancedPage.axaml.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed class AdvancedPage : UserControl {
|
||||
public AdvancedPage() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() {
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
27
app/Desktop/Main/Pages/AdvancedPageModel.cs
Normal file
27
app/Desktop/Main/Pages/AdvancedPageModel.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using DHT.Desktop.Main.Controls;
|
||||
using DHT.Desktop.Server;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
sealed class AdvancedPageModel : BaseModel, IDisposable {
|
||||
public ServerConfigurationPanelModel ServerConfigurationModel { get; }
|
||||
|
||||
[Obsolete("Designer")]
|
||||
public AdvancedPageModel() : this(null!, new ServerManager(DummyDatabaseFile.Instance)) {}
|
||||
|
||||
public AdvancedPageModel(Window window, ServerManager serverManager) {
|
||||
ServerConfigurationModel = new ServerConfigurationPanelModel(window, serverManager);
|
||||
}
|
||||
|
||||
public void Initialize() {
|
||||
ServerConfigurationModel.Initialize();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
ServerConfigurationModel.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@@ -19,7 +19,7 @@
|
||||
<StackPanel Spacing="10">
|
||||
<DockPanel>
|
||||
<Button Command="{Binding CloseDatabase}" DockPanel.Dock="Right">Close Database</Button>
|
||||
<TextBox Text="{Binding Db.Path}" Width="NaN" Margin="0 0 10 0" IsEnabled="True" />
|
||||
<TextBox Text="{Binding Db.Path}" Width="NaN" Margin="0 0 10 0" IsReadOnly="True" />
|
||||
</DockPanel>
|
||||
<WrapPanel>
|
||||
<Button Command="{Binding OpenDatabaseFolder}">Open Database Folder</Button>
|
||||
|
@@ -1,8 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
public class DatabasePage : UserControl {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed class DatabasePage : UserControl {
|
||||
public DatabasePage() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
@@ -5,14 +5,16 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using DHT.Desktop.Common;
|
||||
using DHT.Desktop.Dialogs;
|
||||
using DHT.Desktop.Models;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Desktop.Dialogs.Progress;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Logging;
|
||||
using DHT.Server.Service;
|
||||
using DHT.Utils.Logging;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
public class DatabasePageModel : BaseModel {
|
||||
sealed class DatabasePageModel : BaseModel {
|
||||
private static readonly Log Log = Log.ForType<DatabasePageModel>();
|
||||
|
||||
public IDatabaseFile Db { get; }
|
||||
|
||||
public event EventHandler? DatabaseClosed;
|
||||
@@ -73,7 +75,6 @@ namespace DHT.Desktop.Main.Pages {
|
||||
}
|
||||
|
||||
public void CloseDatabase() {
|
||||
ServerLauncher.Stop();
|
||||
DatabaseClosed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
|
@@ -10,51 +10,17 @@
|
||||
<pages:TrackingPageModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<UserControl.Styles>
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="FontFamily" Value="Consolas,Courier" />
|
||||
<Setter Property="FontSize" Value="15" />
|
||||
</Style>
|
||||
<Style Selector="WrapPanel > StackPanel">
|
||||
<Setter Property="Orientation" Value="Vertical" />
|
||||
<Setter Property="Margin" Value="0 0 10 10" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
To start tracking messages, copy the tracking script and paste it into the console of either the Discord app (Ctrl+Shift+I), or your browser with Discord open.
|
||||
To start tracking messages, copy the tracking script and paste it into the console of either the Discord app, or your browser. The console is usually opened by pressing Ctrl+Shift+I.
|
||||
</TextBlock>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal" Spacing="10">
|
||||
<Button x:Name="CopyTrackingScript" Click="CopyTrackingScriptButton_OnClick">Copy Tracking Script</Button>
|
||||
<Button Command="{Binding OnClickToggleButton}" Content="{Binding ToggleButtonText}" IsEnabled="{Binding IsToggleButtonEnabled}" />
|
||||
</StackPanel>
|
||||
<Expander Header="Advanced Settings">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
The following settings determine how the tracking script communicates with this application. If you change them, you will have to copy and apply the tracking script again.
|
||||
</TextBlock>
|
||||
<WrapPanel>
|
||||
<StackPanel>
|
||||
<Label Target="Port">Port</Label>
|
||||
<TextBox x:Name="Port" Width="70" Text="{Binding InputPort}" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<Label Target="Token">Token</Label>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBox x:Name="Token" Width="200" Text="{Binding InputToken}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel VerticalAlignment="Bottom">
|
||||
<Button Command="{Binding OnClickRandomizeToken}">Randomize Token</Button>
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Button IsEnabled="{Binding HasMadeChanges}" Command="{Binding OnClickApplyChanges}">Apply & Restart</Button>
|
||||
<Button IsEnabled="{Binding HasMadeChanges}" Command="{Binding OnClickCancelChanges}">Cancel</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Expander>
|
||||
<TextBlock TextWrapping="Wrap" Margin="0 5 0 0">
|
||||
By default, the Discord app does not allow opening the console. The button below will change a hidden setting in the Discord app that controls whether the Ctrl+Shift+I shortcut is enabled.
|
||||
</TextBlock>
|
||||
<Button DockPanel.Dock="Right" Command="{Binding OnClickToggleAppDevTools}" Content="{Binding ToggleAppDevToolsButtonText}" IsEnabled="{Binding IsToggleAppDevToolsButtonEnabled}" />
|
||||
</StackPanel>
|
||||
|
||||
</UserControl>
|
||||
|
@@ -1,11 +1,13 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
public class TrackingPage : UserControl {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed class TrackingPage : UserControl {
|
||||
private bool isCopyingScript;
|
||||
|
||||
public TrackingPage() {
|
||||
@@ -22,9 +24,7 @@ namespace DHT.Desktop.Main.Pages {
|
||||
var originalText = button.Content;
|
||||
button.MinWidth = button.Bounds.Width;
|
||||
|
||||
await model.OnClickCopyTrackingScript();
|
||||
|
||||
if (!isCopyingScript) {
|
||||
if (await model.OnClickCopyTrackingScript() && !isCopyingScript) {
|
||||
isCopyingScript = true;
|
||||
button.Content = "Script Copied!";
|
||||
|
||||
|
@@ -3,145 +3,115 @@ using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using DHT.Desktop.Dialogs;
|
||||
using DHT.Desktop.Main.Controls;
|
||||
using DHT.Desktop.Models;
|
||||
using DHT.Desktop.Resources;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Logging;
|
||||
using DHT.Server.Service;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Desktop.Discord;
|
||||
using DHT.Desktop.Server;
|
||||
using DHT.Utils.Models;
|
||||
using static DHT.Desktop.Program;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
public class TrackingPageModel : BaseModel, IDisposable {
|
||||
internal static string ServerPort { get; set; } = ServerUtils.FindAvailablePort(50000, 60000).ToString();
|
||||
internal static string ServerToken { get; set; } = ServerUtils.GenerateRandomToken(20);
|
||||
sealed class TrackingPageModel : BaseModel {
|
||||
private bool areDevToolsEnabled;
|
||||
|
||||
private string inputPort = ServerPort;
|
||||
|
||||
public string InputPort {
|
||||
get => inputPort;
|
||||
private bool AreDevToolsEnabled {
|
||||
get => areDevToolsEnabled;
|
||||
set {
|
||||
Change(ref inputPort, value);
|
||||
OnPropertyChanged(nameof(HasMadeChanges));
|
||||
Change(ref areDevToolsEnabled, value);
|
||||
OnPropertyChanged(nameof(ToggleAppDevToolsButtonText));
|
||||
}
|
||||
}
|
||||
|
||||
private string inputToken = ServerToken;
|
||||
public bool IsToggleAppDevToolsButtonEnabled { get; private set; } = true;
|
||||
|
||||
public string InputToken {
|
||||
get => inputToken;
|
||||
set {
|
||||
Change(ref inputToken, value);
|
||||
OnPropertyChanged(nameof(HasMadeChanges));
|
||||
public string ToggleAppDevToolsButtonText {
|
||||
get {
|
||||
if (!IsToggleAppDevToolsButtonEnabled) {
|
||||
return "Unavailable";
|
||||
}
|
||||
|
||||
return AreDevToolsEnabled ? "Disable Ctrl+Shift+I" : "Enable Ctrl+Shift+I";
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasMadeChanges => ServerPort != InputPort || ServerToken != InputToken;
|
||||
|
||||
private bool isToggleButtonEnabled = true;
|
||||
|
||||
public bool IsToggleButtonEnabled {
|
||||
get => isToggleButtonEnabled;
|
||||
set => Change(ref isToggleButtonEnabled, value);
|
||||
}
|
||||
|
||||
public string ToggleButtonText => ServerLauncher.IsRunning ? "Pause Tracking" : "Resume Tracking";
|
||||
|
||||
public event EventHandler<StatusBarModel.Status>? ServerStatusChanged;
|
||||
|
||||
private readonly Window window;
|
||||
private readonly IDatabaseFile db;
|
||||
|
||||
[Obsolete("Designer")]
|
||||
public TrackingPageModel() : this(null!, DummyDatabaseFile.Instance) {}
|
||||
public TrackingPageModel() : this(null!) {}
|
||||
|
||||
public TrackingPageModel(Window window, IDatabaseFile db) {
|
||||
public TrackingPageModel(Window window) {
|
||||
this.window = window;
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public void Initialize() {
|
||||
ServerLauncher.ServerStatusChanged += ServerLauncherOnServerStatusChanged;
|
||||
ServerLauncher.ServerManagementExceptionCaught += ServerLauncherOnServerManagementExceptionCaught;
|
||||
|
||||
if (int.TryParse(ServerPort, out int port)) {
|
||||
string token = ServerToken;
|
||||
ServerLauncher.Relaunch(port, token, db);
|
||||
public async Task Initialize() {
|
||||
bool? devToolsEnabled = await DiscordAppSettings.AreDevToolsEnabled();
|
||||
if (devToolsEnabled.HasValue) {
|
||||
AreDevToolsEnabled = devToolsEnabled.Value;
|
||||
}
|
||||
else {
|
||||
IsToggleAppDevToolsButtonEnabled = false;
|
||||
OnPropertyChanged(nameof(IsToggleAppDevToolsButtonEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
ServerLauncher.ServerManagementExceptionCaught -= ServerLauncherOnServerManagementExceptionCaught;
|
||||
ServerLauncher.ServerStatusChanged -= ServerLauncherOnServerStatusChanged;
|
||||
ServerLauncher.Stop();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
public async Task<bool> OnClickCopyTrackingScript() {
|
||||
string bootstrap = await Resources.ReadTextAsync("Tracker/bootstrap.js");
|
||||
string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + ServerManager.Port + ";")
|
||||
.Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(ServerManager.Token))
|
||||
.Replace("/*[IMPORTS]*/", await Resources.ReadJoinedAsync("Tracker/scripts/", '\n'))
|
||||
.Replace("/*[CSS-CONTROLLER]*/", await Resources.ReadTextAsync("Tracker/styles/controller.css"))
|
||||
.Replace("/*[CSS-SETTINGS]*/", await Resources.ReadTextAsync("Tracker/styles/settings.css"));
|
||||
|
||||
private async Task<bool> StartServer() {
|
||||
if (!int.TryParse(InputPort, out int port) || port is < 0 or > 65535) {
|
||||
await Dialog.ShowOk(window, "Invalid Port", "Port must be a number between 0 and 65535.");
|
||||
var clipboard = Application.Current?.Clipboard;
|
||||
if (clipboard == null) {
|
||||
await Dialog.ShowOk(window, "Copy Tracking Script", "Clipboard is not available on this system.");
|
||||
return false;
|
||||
}
|
||||
|
||||
IsToggleButtonEnabled = false;
|
||||
ServerStatusChanged?.Invoke(this, StatusBarModel.Status.Starting);
|
||||
ServerLauncher.Relaunch(port, InputToken, db);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void StopServer() {
|
||||
IsToggleButtonEnabled = false;
|
||||
ServerStatusChanged?.Invoke(this, StatusBarModel.Status.Stopping);
|
||||
ServerLauncher.Stop();
|
||||
}
|
||||
|
||||
public async Task<bool> OnClickToggleButton() {
|
||||
if (ServerLauncher.IsRunning) {
|
||||
StopServer();
|
||||
try {
|
||||
await clipboard.SetTextAsync(script);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return await StartServer();
|
||||
} catch {
|
||||
await Dialog.ShowOk(window, "Copy Tracking Script", "An error occurred while copying to clipboard.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnClickCopyTrackingScript() {
|
||||
string bootstrap = await ResourceLoader.ReadTextAsync("Tracker/bootstrap.js");
|
||||
string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + ServerPort + ";")
|
||||
.Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(ServerToken))
|
||||
.Replace("/*[IMPORTS]*/", await ResourceLoader.ReadJoinedAsync("Tracker/scripts/", '\n'))
|
||||
.Replace("/*[CSS-CONTROLLER]*/", await ResourceLoader.ReadTextAsync("Tracker/styles/controller.css"))
|
||||
.Replace("/*[CSS-SETTINGS]*/", await ResourceLoader.ReadTextAsync("Tracker/styles/settings.css"));
|
||||
public async void OnClickToggleAppDevTools() {
|
||||
const string DialogTitle = "Discord App Settings File";
|
||||
|
||||
await Application.Current.Clipboard.SetTextAsync(script);
|
||||
}
|
||||
bool oldState = AreDevToolsEnabled;
|
||||
bool newState = !oldState;
|
||||
|
||||
public void OnClickRandomizeToken() {
|
||||
InputToken = ServerUtils.GenerateRandomToken(20);
|
||||
}
|
||||
switch (await DiscordAppSettings.ConfigureDevTools(newState)) {
|
||||
case SettingsJsonResult.Success:
|
||||
AreDevToolsEnabled = newState;
|
||||
await Dialog.ShowOk(window, DialogTitle, "Ctrl+Shift+I was " + (newState ? "enabled." : "disabled.") + " Restart the Discord app for the change to take effect.");
|
||||
break;
|
||||
|
||||
public async void OnClickApplyChanges() {
|
||||
if (await StartServer()) {
|
||||
ServerPort = InputPort;
|
||||
ServerToken = InputToken;
|
||||
OnPropertyChanged(nameof(HasMadeChanges));
|
||||
case SettingsJsonResult.AlreadySet:
|
||||
await Dialog.ShowOk(window, DialogTitle, "Ctrl+Shift+I is already " + (newState ? "enabled." : "disabled."));
|
||||
AreDevToolsEnabled = newState;
|
||||
break;
|
||||
|
||||
case SettingsJsonResult.FileNotFound:
|
||||
await Dialog.ShowOk(window, DialogTitle, "Cannot find the settings file:\n" + DiscordAppSettings.JsonFilePath);
|
||||
break;
|
||||
|
||||
case SettingsJsonResult.ReadError:
|
||||
await Dialog.ShowOk(window, DialogTitle, "Cannot read the settings file:\n" + DiscordAppSettings.JsonFilePath);
|
||||
break;
|
||||
|
||||
case SettingsJsonResult.InvalidJson:
|
||||
await Dialog.ShowOk(window, DialogTitle, "Unknown format of the settings file:\n" + DiscordAppSettings.JsonFilePath);
|
||||
break;
|
||||
|
||||
case SettingsJsonResult.WriteError:
|
||||
await Dialog.ShowOk(window, DialogTitle, "Cannot save the settings file:\n" + DiscordAppSettings.JsonFilePath);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnClickCancelChanges() {
|
||||
InputPort = ServerPort;
|
||||
InputToken = ServerToken;
|
||||
}
|
||||
|
||||
private void ServerLauncherOnServerStatusChanged(object? sender, EventArgs e) {
|
||||
ServerStatusChanged?.Invoke(this, ServerLauncher.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped);
|
||||
OnPropertyChanged(nameof(ToggleButtonText));
|
||||
IsToggleButtonEnabled = true;
|
||||
}
|
||||
|
||||
private async void ServerLauncherOnServerManagementExceptionCaught(object? sender, Exception ex) {
|
||||
Log.Error(ex);
|
||||
await Dialog.ShowOk(window, "Server Error", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
public class ViewerPage : UserControl {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed class ViewerPage : UserControl {
|
||||
public ViewerPage() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
@@ -7,16 +7,16 @@ using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Avalonia.Controls;
|
||||
using DHT.Desktop.Common;
|
||||
using DHT.Desktop.Dialogs;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Desktop.Main.Controls;
|
||||
using DHT.Desktop.Models;
|
||||
using DHT.Desktop.Resources;
|
||||
using DHT.Server.Data.Filters;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Database.Export;
|
||||
using DHT.Utils.Models;
|
||||
using static DHT.Desktop.Program;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
public class ViewerPageModel : BaseModel {
|
||||
sealed class ViewerPageModel : BaseModel, IDisposable {
|
||||
public string ExportedMessageText { get; private set; } = "";
|
||||
|
||||
public bool DatabaseToolFilterModeKeep { get; set; } = true;
|
||||
@@ -41,12 +41,17 @@ namespace DHT.Desktop.Main.Pages {
|
||||
this.window = window;
|
||||
this.db = db;
|
||||
|
||||
this.FilterModel = new FilterPanelModel(window, db);
|
||||
this.FilterModel.FilterPropertyChanged += OnFilterPropertyChanged;
|
||||
this.db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
||||
FilterModel = new FilterPanelModel(window, db);
|
||||
FilterModel.FilterPropertyChanged += OnFilterPropertyChanged;
|
||||
db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
||||
UpdateStatistics();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
||||
FilterModel.Dispose();
|
||||
}
|
||||
|
||||
private void OnFilterPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||
UpdateStatistics();
|
||||
HasFilters = FilterModel.HasAnyFilters;
|
||||
@@ -66,9 +71,9 @@ namespace DHT.Desktop.Main.Pages {
|
||||
private async Task<string> GenerateViewerContents() {
|
||||
string json = ViewerJsonExport.Generate(db, FilterModel.CreateFilter());
|
||||
|
||||
string index = await ResourceLoader.ReadTextAsync("Viewer/index.html");
|
||||
string viewer = index.Replace("/*[JS]*/", await ResourceLoader.ReadJoinedAsync("Viewer/scripts/", '\n'))
|
||||
.Replace("/*[CSS]*/", await ResourceLoader.ReadJoinedAsync("Viewer/styles/", '\n'))
|
||||
string index = await Resources.ReadTextAsync("Viewer/index.html");
|
||||
string viewer = index.Replace("/*[JS]*/", await Resources.ReadJoinedAsync("Viewer/scripts/", '\n'))
|
||||
.Replace("/*[CSS]*/", await Resources.ReadJoinedAsync("Viewer/styles/", '\n'))
|
||||
.Replace("/*[ARCHIVE]*/", HttpUtility.JavaScriptStringEncode(json));
|
||||
return viewer;
|
||||
}
|
||||
|
@@ -2,20 +2,30 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:main="clr-namespace:DHT.Desktop.Main"
|
||||
xmlns:controls="clr-namespace:DHT.Desktop.Main.Controls"
|
||||
xmlns:screens="clr-namespace:DHT.Desktop.Main.Screens"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="DHT.Desktop.Main.MainContentScreen">
|
||||
x:Class="DHT.Desktop.Main.Screens.MainContentScreen">
|
||||
|
||||
<Design.DataContext>
|
||||
<main:MainContentScreenModel />
|
||||
<screens:MainContentScreenModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<UserControl.Styles>
|
||||
<Style Selector="TabControl ItemsPresenter > WrapPanel">
|
||||
<Style Selector="Border.statusBar">
|
||||
<Setter Property="Background" Value="#3C4F79" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.invisibleTabItem">
|
||||
<Setter Property="FontSize" Value="20" />
|
||||
<Setter Property="FontWeight" Value="SemiLight" />
|
||||
<Setter Property="Margin" Value="17 0" />
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
<Style Selector="TabControl ItemsPresenter > Grid">
|
||||
<Setter Property="Background" Value="#546A9F" />
|
||||
</Style>
|
||||
<Style Selector="TabItem">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Foreground" Value="#E9E9E9" />
|
||||
<Setter Property="FontSize" Value="20" />
|
||||
</Style>
|
||||
@@ -23,10 +33,11 @@
|
||||
<Setter Property="Margin" Value="5 0" />
|
||||
</Style>
|
||||
<Style Selector="TabItem:pointerover">
|
||||
<Setter Property="Background" Value="#1F2E45" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Background" Value="#455785" />
|
||||
</Style>
|
||||
<Style Selector="TabItem:pointerover /template/ Border">
|
||||
<Setter Property="Background" Value="#1F2E45" />
|
||||
<Setter Property="Background" Value="#455785" />
|
||||
</Style>
|
||||
<Style Selector="TabItem:pointerover > TextBlock">
|
||||
<Setter Property="Foreground" Value="#E9E9E9" />
|
||||
@@ -55,35 +66,42 @@
|
||||
<Setter Property="Padding" Value="0" />
|
||||
</Style>
|
||||
<Style Selector="ContentPresenter.page">
|
||||
<Setter Property="Margin" Value="15 21" />
|
||||
<Setter Property="Margin" Value="15 21 20 21" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<DockPanel>
|
||||
<TabControl x:Name="TabControl" TabStripPlacement="Left">
|
||||
<TabItem x:Name="TabDatabase" Header="Database" Classes="first">
|
||||
<DockPanel>
|
||||
<controls:StatusBar DataContext="{Binding StatusBarModel}" DockPanel.Dock="Bottom" />
|
||||
<ScrollViewer>
|
||||
<ContentPresenter Content="{Binding DatabasePage}" Classes="page" />
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
<Border Classes="statusBar" DockPanel.Dock="Bottom">
|
||||
<DockPanel>
|
||||
<TextBlock Classes="invisibleTabItem" DockPanel.Dock="Left">Advanced</TextBlock>
|
||||
<controls:StatusBar DataContext="{Binding StatusBarModel}" DockPanel.Dock="Right" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<TabControl x:Name="TabControl" TabStripPlacement="Left" DockPanel.Dock="Top">
|
||||
<TabControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Grid ColumnDefinitions="Auto" RowDefinitions="Auto,Auto,Auto,*,Auto" />
|
||||
</ItemsPanelTemplate>
|
||||
</TabControl.ItemsPanel>
|
||||
<TabItem x:Name="TabDatabase" Header="Database" Classes="first" Grid.Row="0">
|
||||
<ScrollViewer>
|
||||
<ContentPresenter Content="{Binding DatabasePage}" Classes="page" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem x:Name="TabTracking" Header="Tracking">
|
||||
<DockPanel>
|
||||
<controls:StatusBar DataContext="{Binding StatusBarModel}" DockPanel.Dock="Bottom" />
|
||||
<ScrollViewer>
|
||||
<ContentPresenter Content="{Binding TrackingPage}" Classes="page" />
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
<TabItem x:Name="TabTracking" Header="Tracking" DockPanel.Dock="Top" Grid.Row="1">
|
||||
<ScrollViewer>
|
||||
<ContentPresenter Content="{Binding TrackingPage}" Classes="page" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem x:Name="TabViewer" Header="Viewer">
|
||||
<DockPanel>
|
||||
<controls:StatusBar DataContext="{Binding StatusBarModel}" DockPanel.Dock="Bottom" />
|
||||
<ScrollViewer>
|
||||
<ContentPresenter Content="{Binding ViewerPage}" Classes="page" />
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
<TabItem x:Name="TabViewer" Header="Viewer" DockPanel.Dock="Top" Grid.Row="2">
|
||||
<ScrollViewer>
|
||||
<ContentPresenter Content="{Binding ViewerPage}" Classes="page" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem x:Name="TabAdvanced" Header="Advanced" DockPanel.Dock="Bottom" Grid.Row="4">
|
||||
<ScrollViewer>
|
||||
<ContentPresenter Content="{Binding AdvancedPage}" Classes="page" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</DockPanel>
|
@@ -1,8 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main {
|
||||
public class MainContentScreen : UserControl {
|
||||
namespace DHT.Desktop.Main.Screens {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed class MainContentScreen : UserControl {
|
||||
public MainContentScreen() {
|
||||
InitializeComponent();
|
||||
}
|
96
app/Desktop/Main/Screens/MainContentScreenModel.cs
Normal file
96
app/Desktop/Main/Screens/MainContentScreenModel.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Desktop.Main.Controls;
|
||||
using DHT.Desktop.Main.Pages;
|
||||
using DHT.Desktop.Server;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Service;
|
||||
using DHT.Utils.Logging;
|
||||
|
||||
namespace DHT.Desktop.Main.Screens {
|
||||
sealed class MainContentScreenModel : IDisposable {
|
||||
private static readonly Log Log = Log.ForType<MainContentScreenModel>();
|
||||
|
||||
public DatabasePage DatabasePage { get; }
|
||||
private DatabasePageModel DatabasePageModel { get; }
|
||||
|
||||
public TrackingPage TrackingPage { get; }
|
||||
private TrackingPageModel TrackingPageModel { get; }
|
||||
|
||||
public ViewerPage ViewerPage { get; }
|
||||
private ViewerPageModel ViewerPageModel { get; }
|
||||
|
||||
public AdvancedPage AdvancedPage { get; }
|
||||
private AdvancedPageModel AdvancedPageModel { get; }
|
||||
|
||||
public StatusBarModel StatusBarModel { get; }
|
||||
|
||||
public event EventHandler? DatabaseClosed {
|
||||
add {
|
||||
DatabasePageModel.DatabaseClosed += value;
|
||||
}
|
||||
remove {
|
||||
DatabasePageModel.DatabaseClosed -= value;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Window window;
|
||||
private readonly ServerManager serverManager;
|
||||
|
||||
[Obsolete("Designer")]
|
||||
public MainContentScreenModel() : this(null!, DummyDatabaseFile.Instance) {}
|
||||
|
||||
public MainContentScreenModel(Window window, IDatabaseFile db) {
|
||||
this.window = window;
|
||||
this.serverManager = new ServerManager(db);
|
||||
|
||||
ServerLauncher.ServerManagementExceptionCaught += ServerLauncherOnServerManagementExceptionCaught;
|
||||
|
||||
DatabasePageModel = new DatabasePageModel(window, db);
|
||||
DatabasePage = new DatabasePage { DataContext = DatabasePageModel };
|
||||
|
||||
TrackingPageModel = new TrackingPageModel(window);
|
||||
TrackingPage = new TrackingPage { DataContext = TrackingPageModel };
|
||||
|
||||
ViewerPageModel = new ViewerPageModel(window, db);
|
||||
ViewerPage = new ViewerPage { DataContext = ViewerPageModel };
|
||||
|
||||
AdvancedPageModel = new AdvancedPageModel(window, serverManager);
|
||||
AdvancedPage = new AdvancedPage { DataContext = AdvancedPageModel };
|
||||
|
||||
StatusBarModel = new StatusBarModel(db.Statistics);
|
||||
|
||||
AdvancedPageModel.ServerConfigurationModel.ServerStatusChanged += OnServerStatusChanged;
|
||||
DatabaseClosed += OnDatabaseClosed;
|
||||
|
||||
StatusBarModel.CurrentStatus = serverManager.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped;
|
||||
}
|
||||
|
||||
public async Task Initialize() {
|
||||
await TrackingPageModel.Initialize();
|
||||
AdvancedPageModel.Initialize();
|
||||
serverManager.Launch();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
ServerLauncher.ServerManagementExceptionCaught -= ServerLauncherOnServerManagementExceptionCaught;
|
||||
ViewerPageModel.Dispose();
|
||||
serverManager.Dispose();
|
||||
}
|
||||
|
||||
private void OnServerStatusChanged(object? sender, StatusBarModel.Status e) {
|
||||
StatusBarModel.CurrentStatus = e;
|
||||
}
|
||||
|
||||
private void OnDatabaseClosed(object? sender, EventArgs e) {
|
||||
serverManager.Stop();
|
||||
}
|
||||
|
||||
private async void ServerLauncherOnServerManagementExceptionCaught(object? sender, Exception ex) {
|
||||
Log.Error(ex);
|
||||
await Dialog.ShowOk(window, "Internal Server Error", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,12 +2,12 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:main="clr-namespace:DHT.Desktop.Main"
|
||||
xmlns:screens="clr-namespace:DHT.Desktop.Main.Screens"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="DHT.Desktop.Main.WelcomeScreen">
|
||||
x:Class="DHT.Desktop.Main.Screens.WelcomeScreen">
|
||||
|
||||
<Design.DataContext>
|
||||
<main:WelcomeScreenModel />
|
||||
<screens:WelcomeScreenModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<UserControl.Background>
|
@@ -1,8 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DHT.Desktop.Main {
|
||||
public class WelcomeScreen : UserControl {
|
||||
namespace DHT.Desktop.Main.Screens {
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public sealed class WelcomeScreen : UserControl {
|
||||
public WelcomeScreen() {
|
||||
InitializeComponent();
|
||||
}
|
@@ -3,12 +3,12 @@ using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using DHT.Desktop.Common;
|
||||
using DHT.Desktop.Dialogs;
|
||||
using DHT.Desktop.Models;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main {
|
||||
public class WelcomeScreenModel : BaseModel {
|
||||
namespace DHT.Desktop.Main.Screens {
|
||||
sealed class WelcomeScreenModel : BaseModel, IDisposable {
|
||||
public string Version => Program.Version;
|
||||
|
||||
public IDatabaseFile? Db { get; private set; }
|
||||
@@ -52,8 +52,7 @@ namespace DHT.Desktop.Main {
|
||||
}
|
||||
|
||||
public void CloseDatabase() {
|
||||
Db = null;
|
||||
|
||||
Dispose();
|
||||
OnPropertyChanged(nameof(Db));
|
||||
OnPropertyChanged(nameof(HasDatabase));
|
||||
}
|
||||
@@ -65,5 +64,10 @@ namespace DHT.Desktop.Main {
|
||||
public void Exit() {
|
||||
window.Close();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
Db?.Dispose();
|
||||
Db = null;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,14 +1,18 @@
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using Avalonia;
|
||||
using DHT.Utils.Resources;
|
||||
|
||||
namespace DHT.Desktop {
|
||||
internal static class Program {
|
||||
static class Program {
|
||||
public static string Version { get; }
|
||||
public static CultureInfo Culture { get; }
|
||||
public static ResourceLoader Resources { get; }
|
||||
|
||||
static Program() {
|
||||
Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "";
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
Version = assembly.GetName().Version?.ToString() ?? "";
|
||||
while (Version.EndsWith(".0")) {
|
||||
Version = Version[..^2];
|
||||
}
|
||||
@@ -18,6 +22,8 @@ namespace DHT.Desktop {
|
||||
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
|
||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
|
||||
|
||||
Resources = new ResourceLoader(assembly);
|
||||
}
|
||||
|
||||
public static void Main(string[] args) {
|
||||
|
50
app/Desktop/Server/ServerManager.cs
Normal file
50
app/Desktop/Server/ServerManager.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Service;
|
||||
|
||||
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);
|
||||
|
||||
private static ServerManager? instance;
|
||||
|
||||
public bool IsRunning => ServerLauncher.IsRunning;
|
||||
|
||||
private readonly IDatabaseFile db;
|
||||
|
||||
public ServerManager(IDatabaseFile db) {
|
||||
if (db != DummyDatabaseFile.Instance) {
|
||||
if (instance != null) {
|
||||
throw new InvalidOperationException("Only one instance of ServerManager can exist at the same time!");
|
||||
}
|
||||
|
||||
instance = this;
|
||||
}
|
||||
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public void Launch() {
|
||||
ServerLauncher.Relaunch(Port, Token, db);
|
||||
}
|
||||
|
||||
public void Relaunch(ushort port, string token) {
|
||||
Port = port;
|
||||
Token = token;
|
||||
Launch();
|
||||
}
|
||||
|
||||
public void Stop() {
|
||||
ServerLauncher.Stop();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
Stop();
|
||||
|
||||
if (instance == this) {
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Desktop", "Desktop\Desktop.
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{7F94B470-B06F-43C0-9655-6592A9AE2D92}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utils", "Utils\Utils.csproj", "{7EEC1C05-1D21-4B39-B1D6-CF51F3795629}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -18,5 +20,9 @@ Global
|
||||
{7F94B470-B06F-43C0-9655-6592A9AE2D92}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7F94B470-B06F-43C0-9655-6592A9AE2D92}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7F94B470-B06F-43C0-9655-6592A9AE2D92}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7EEC1C05-1D21-4B39-B1D6-CF51F3795629}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7EEC1C05-1D21-4B39-B1D6-CF51F3795629}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7EEC1C05-1D21-4B39-B1D6-CF51F3795629}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7EEC1C05-1D21-4B39-B1D6-CF51F3795629}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
19
app/Resources/Tracker/bootstrap.js
vendored
19
app/Resources/Tracker/bootstrap.js
vendored
@@ -42,6 +42,10 @@
|
||||
stopTrackingDelayed(() => isSending = false);
|
||||
};
|
||||
|
||||
const isNoAction = function(action) {
|
||||
return action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING;
|
||||
};
|
||||
|
||||
const onTrackingContinued = function(anyNewMessages) {
|
||||
if (!STATE.isTracking()) {
|
||||
return;
|
||||
@@ -59,14 +63,14 @@
|
||||
if (SETTINGS.autoscroll) {
|
||||
let action = null;
|
||||
|
||||
if (!anyNewMessages) {
|
||||
action = SETTINGS.afterSavedMsg;
|
||||
}
|
||||
else if (!DISCORD.hasMoreMessages()) {
|
||||
if (!DISCORD.hasMoreMessages()) {
|
||||
action = SETTINGS.afterFirstMsg;
|
||||
}
|
||||
if (isNoAction(action) && !anyNewMessages) {
|
||||
action = SETTINGS.afterSavedMsg;
|
||||
}
|
||||
|
||||
if (action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING) {
|
||||
if (isNoAction(action)) {
|
||||
DISCORD.loadOlderMessages();
|
||||
}
|
||||
else if (action === CONSTANTS.AUTOSCROLL_ACTION_PAUSE || (action === CONSTANTS.AUTOSCROLL_ACTION_SWITCH && !DISCORD.selectNextTextChannel())) {
|
||||
@@ -113,8 +117,8 @@
|
||||
|
||||
try {
|
||||
if (!messages.length) {
|
||||
DISCORD.loadOlderMessages();
|
||||
isSending = false;
|
||||
onTrackingContinued(false);
|
||||
}
|
||||
else {
|
||||
const anyNewMessages = await STATE.addDiscordMessages(info.id, messages);
|
||||
@@ -125,8 +129,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
const callbackTimer = DISCORD.setupMessageCallback(onMessagesUpdated);
|
||||
window.DHT_ON_UNLOAD.push(() => window.clearTimeout(callbackTimer));
|
||||
DISCORD.setupMessageCallback(onMessagesUpdated);
|
||||
|
||||
STATE.onTrackingStateChanged(enabled => {
|
||||
if (enabled) {
|
||||
|
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
class DOM{static id(id,parent){return(parent||document).getElementById(id)}static queryReactClass(cls,parent){return(parent||document).querySelector(`[class*="${cls}-"]`)}static createElement(tag,parent,id,html){const ele=document.createElement(tag);ele.id=id||"";ele.innerHTML=html||"";parent.appendChild(ele);return ele}static removeElement(ele){return ele.parentNode.removeChild(ele)}static createStyle(styles){return this.createElement("style",document.head,"",styles)}static saveToCookie(name,obj,expiresInSeconds){const expires=new Date(Date.now()+1e3*expiresInSeconds).toUTCString();document.cookie=name+"="+encodeURIComponent(JSON.stringify(obj))+";path=/;expires="+expires}static loadFromCookie(name){const value=document.cookie.replace(new RegExp("(?:(?:^|.*;\\s*)"+name+"\\s*\\=\\s*([^;]*).*$)|^.*$"),"$1");return value.length?JSON.parse(decodeURIComponent(value)):null}static getReactProps(ele){const keys=Object.keys(ele||{});let key=keys.find(key=>key.startsWith("__reactInternalInstance"));if(key){return ele[key].memoizedProps}key=keys.find(key=>key.startsWith("__reactProps$"));return key?ele[key]:null}}
|
@@ -1,17 +0,0 @@
|
||||
const GUI=function(){let controller=null;let settings=null;const stateChangedEvent=()=>{if(settings){settings.ui.cbAutoscroll.checked=SETTINGS.autoscroll;settings.ui.optsAfterFirstMsg[SETTINGS.afterFirstMsg].checked=true;settings.ui.optsAfterSavedMsg[SETTINGS.afterSavedMsg].checked=true;const autoscrollDisabled=!SETTINGS.autoscroll;Object.values(settings.ui.optsAfterFirstMsg).forEach(ele=>ele.disabled=autoscrollDisabled);Object.values(settings.ui.optsAfterSavedMsg).forEach(ele=>ele.disabled=autoscrollDisabled)}};return{showController(){if(controller){return}const html=`
|
||||
<button id='dht-ctrl-close'>X</button>
|
||||
<button id='dht-ctrl-settings'>Settings</button>
|
||||
<button id='dht-ctrl-track'></button>
|
||||
<p id='dht-ctrl-status'>Waiting</p>`;controller={styles:DOM.createStyle(`/*[CSS-CONTROLLER]*/`),ele:DOM.createElement("div",document.body,"dht-ctrl",html)};controller.ui={btnSettings:DOM.id("dht-ctrl-settings"),btnTrack:DOM.id("dht-ctrl-track"),btnClose:DOM.id("dht-ctrl-close"),textStatus:DOM.id("dht-ctrl-status")};controller.ui.btnSettings.addEventListener("click",()=>{this.showSettings()});controller.ui.btnTrack.addEventListener("click",()=>{const isTracking=!STATE.isTracking();STATE.setIsTracking(isTracking);if(!isTracking){controller.ui.textStatus.innerText="Stopped"}});controller.ui.btnClose.addEventListener("click",()=>{this.hideController();window.DHT_ON_UNLOAD.forEach(f=>f());window.DHT_LOADED=false});STATE.onTrackingStateChanged(isTracking=>{controller.ui.btnTrack.innerText=isTracking?"Pause Tracking":"Start Tracking";controller.ui.btnSettings.disabled=isTracking});SETTINGS.onSettingsChanged(stateChangedEvent);stateChangedEvent()},hideController(){if(controller){DOM.removeElement(controller.ele);DOM.removeElement(controller.styles);controller=null}},showSettings(){if(settings){return}const radio=(type,id,label)=>"<label><input id='dht-cfg-"+type+"-"+id+"' name='dht-"+type+"' type='radio'> "+label+"</label><br>";const html=`
|
||||
<label><input id='dht-cfg-autoscroll' type='checkbox'> Autoscroll</label><br>
|
||||
<br>
|
||||
<label>After reaching the first message in channel...</label><br>
|
||||
${radio("afm","nothing","Do Nothing")}
|
||||
${radio("afm","pause","Pause Tracking")}
|
||||
${radio("afm","switch","Switch to Next Channel")}
|
||||
<br>
|
||||
<label>After reaching a previously saved message...</label><br>
|
||||
${radio("asm","nothing","Do Nothing")}
|
||||
${radio("asm","pause","Pause Tracking")}
|
||||
${radio("asm","switch","Switch to Next Channel")}
|
||||
<p id='dht-cfg-note'>It is recommended to disable link and image previews to avoid putting unnecessary strain on your browser.</p>`;settings={styles:DOM.createStyle(`/*[CSS-SETTINGS]*/`),overlay:DOM.createElement("div",document.body,"dht-cfg-overlay"),ele:DOM.createElement("div",document.body,"dht-cfg",html)};settings.overlay.addEventListener("click",()=>{this.hideSettings()});settings.ui={cbAutoscroll:DOM.id("dht-cfg-autoscroll"),optsAfterFirstMsg:{},optsAfterSavedMsg:{}};settings.ui.optsAfterFirstMsg[CONSTANTS.AUTOSCROLL_ACTION_NOTHING]=DOM.id("dht-cfg-afm-nothing");settings.ui.optsAfterFirstMsg[CONSTANTS.AUTOSCROLL_ACTION_PAUSE]=DOM.id("dht-cfg-afm-pause");settings.ui.optsAfterFirstMsg[CONSTANTS.AUTOSCROLL_ACTION_SWITCH]=DOM.id("dht-cfg-afm-switch");settings.ui.optsAfterSavedMsg[CONSTANTS.AUTOSCROLL_ACTION_NOTHING]=DOM.id("dht-cfg-asm-nothing");settings.ui.optsAfterSavedMsg[CONSTANTS.AUTOSCROLL_ACTION_PAUSE]=DOM.id("dht-cfg-asm-pause");settings.ui.optsAfterSavedMsg[CONSTANTS.AUTOSCROLL_ACTION_SWITCH]=DOM.id("dht-cfg-asm-switch");settings.ui.cbAutoscroll.addEventListener("change",()=>{SETTINGS.autoscroll=settings.ui.cbAutoscroll.checked});Object.keys(settings.ui.optsAfterFirstMsg).forEach(key=>{settings.ui.optsAfterFirstMsg[key].addEventListener("click",()=>{SETTINGS.afterFirstMsg=key})});Object.keys(settings.ui.optsAfterSavedMsg).forEach(key=>{settings.ui.optsAfterSavedMsg[key].addEventListener("click",()=>{SETTINGS.afterSavedMsg=key})});stateChangedEvent()},hideSettings(){if(settings){DOM.removeElement(settings.overlay);DOM.removeElement(settings.ele);DOM.removeElement(settings.styles);settings=null}},setStatus(state){if(controller){controller.ui.textStatus.innerText=state}}}}();
|
@@ -1 +0,0 @@
|
||||
const CONSTANTS={AUTOSCROLL_ACTION_NOTHING:"optNothing",AUTOSCROLL_ACTION_PAUSE:"optPause",AUTOSCROLL_ACTION_SWITCH:"optSwitch"};let IS_FIRST_RUN=false;const SETTINGS=function(){const settingsChangedEvents=[];const saveSettings=function(){DOM.saveToCookie("DHT_SETTINGS",root,60*60*24*365*5)};const triggerSettingsChanged=function(property){for(const callback of settingsChangedEvents){callback(property)}saveSettings()};const defineTriggeringProperty=function(obj,property,value){const name="_"+property;Object.defineProperty(obj,property,{get:()=>obj[name],set:value=>{obj[name]=value;triggerSettingsChanged(property)}});obj[name]=value};let loaded=DOM.loadFromCookie("DHT_SETTINGS");if(!loaded){loaded={_autoscroll:true,_afterFirstMsg:CONSTANTS.AUTOSCROLL_ACTION_PAUSE,_afterSavedMsg:CONSTANTS.AUTOSCROLL_ACTION_PAUSE};IS_FIRST_RUN=true}const root={onSettingsChanged(callback){settingsChangedEvents.push(callback)}};defineTriggeringProperty(root,"autoscroll",loaded._autoscroll);defineTriggeringProperty(root,"afterFirstMsg",loaded._afterFirstMsg);defineTriggeringProperty(root,"afterSavedMsg",loaded._afterSavedMsg);if(IS_FIRST_RUN){saveSettings()}return root}();
|
@@ -1 +0,0 @@
|
||||
const STATE=function(){let serverPort=-1;let serverToken="";const post=function(endpoint,json){const aborter=new AbortController;const timeout=window.setTimeout(()=>aborter.abort(),5e3);return new Promise(async(resolve,reject)=>{let r;try{r=await fetch("http://127.0.0.1:"+serverPort+endpoint,{method:"POST",headers:{"Content-Type":"application/json","X-DHT-Token":serverToken},credentials:"omit",redirect:"error",body:JSON.stringify(json),signal:aborter.signal})}catch(e){if(e.name==="AbortError"){reject({status:"DISCONNECTED"});return}else{reject({status:"ERROR",message:e.message});return}}finally{window.clearTimeout(timeout)}if(r.status===200){resolve(r);return}try{const message=await r.text();reject({status:"ERROR",message:message})}catch(e){reject({status:"ERROR",message:e.message})}})};const trackingStateChangedListeners=[];let isTracking=false;const addedChannels=new Set;const addedUsers=new Set;return{setup(port,token){serverPort=port;serverToken=token},onTrackingStateChanged(callback){trackingStateChangedListeners.push(callback);callback(isTracking)},isTracking(){return isTracking},setIsTracking(state){if(isTracking!==state){isTracking=state;if(isTracking){addedChannels.clear();addedUsers.clear()}for(const callback of trackingStateChangedListeners){callback(isTracking)}}},async addDiscordChannel(serverInfo,channelInfo){if(addedChannels.has(channelInfo.id)){return}const server={id:serverInfo.id,name:serverInfo.name,type:serverInfo.type};const channel={id:channelInfo.id,name:channelInfo.name};if("extra"in channelInfo){const extra=channelInfo.extra;if("parent"in extra){channel.parent=extra.parent}channel.position=extra.position;channel.topic=extra.topic;channel.nsfw=extra.nsfw}await post("/track-channel",{server:server,channel:channel});addedChannels.add(channelInfo.id)},async addDiscordMessages(channelId,discordMessageArray){discordMessageArray=discordMessageArray.filter(msg=>(msg.type===0||msg.type===19||msg.type===21)&&msg.state==="SENT");if(discordMessageArray.length===0){return false}const userInfo={};let hasNewUsers=false;for(const msg of discordMessageArray){const user=msg.author;if(!addedUsers.has(user.id)){const obj={id:user.id,name:user.username};if(user.avatar){obj.avatar=user.avatar}if(!user.bot){obj.discriminator=user.discriminator}userInfo[user.id]=obj;hasNewUsers=true}}if(hasNewUsers){await post("/track-users",Object.values(userInfo));for(const id of Object.keys(userInfo)){addedUsers.add(id)}}const response=await post("/track-messages",discordMessageArray.map(msg=>{const obj={id:msg.id,sender:msg.author.id,channel:msg.channel_id,text:msg.content,timestamp:msg.timestamp.toDate().getTime()};if(msg.editedTimestamp!==null){obj.editTimestamp=msg.editedTimestamp.toDate().getTime()}if(msg.messageReference!==null){obj.repliedToId=msg.messageReference.message_id}if(msg.attachments.length>0){obj.attachments=msg.attachments.map(attachment=>{const mapped={id:attachment.id,name:attachment.filename,size:attachment.size,url:attachment.url};if(attachment.content_type){mapped.type=attachment.content_type}return mapped})}if(msg.embeds.length>0){obj.embeds=msg.embeds.map(embed=>{const mapped={};for(const key of Object.keys(embed)){if(key==="id"){continue}if(key==="rawTitle"){mapped["title"]=embed[key]}else if(key==="rawDescription"){mapped["description"]=embed[key]}else{mapped[key]=embed[key]}}return JSON.stringify(mapped)})}if(msg.reactions.length>0){obj.reactions=msg.reactions.map(reaction=>{const emoji=reaction.emoji;const mapped={count:reaction.count};if(emoji.id){mapped.id=emoji.id}if(emoji.name){mapped.name=emoji.name}if(emoji.animated){mapped.isAnimated=emoji.animated}return mapped})}return obj}));const anyNewMessages=await response.text();return anyNewMessages==="1"}}}();
|
@@ -26,14 +26,13 @@ class DISCORD {
|
||||
|
||||
/**
|
||||
* Calls the provided function with a list of messages whenever the currently loaded messages change.
|
||||
* Returns a setInterval handle.
|
||||
*/
|
||||
static setupMessageCallback(callback) {
|
||||
let skipsLeft = 0;
|
||||
let waitForCleanup = false;
|
||||
const previousMessages = new Set();
|
||||
|
||||
return window.setInterval(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
if (skipsLeft > 0) {
|
||||
--skipsLeft;
|
||||
return;
|
||||
@@ -68,14 +67,7 @@ class DISCORD {
|
||||
}
|
||||
|
||||
const messages = this.getMessages();
|
||||
let hasChanged = false;
|
||||
|
||||
for (const message of messages) {
|
||||
if (!previousMessages.has(message.id)) {
|
||||
hasChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const hasChanged = messages.some(message => !previousMessages.has(message.id)) || !this.hasMoreMessages();
|
||||
|
||||
if (!hasChanged) {
|
||||
return;
|
||||
@@ -88,6 +80,8 @@ class DISCORD {
|
||||
|
||||
callback(messages);
|
||||
}, 200);
|
||||
|
||||
window.DHT_ON_UNLOAD.push(() => window.clearInterval(timer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,16 +133,27 @@ class DISCORD {
|
||||
try {
|
||||
let obj;
|
||||
|
||||
for (const ele of this.getMessageElements()) {
|
||||
const props = this.getMessageElementProps(ele);
|
||||
try {
|
||||
for (const child of DOM.getReactProps(DOM.queryReactClass("chatContent")).children) {
|
||||
if (child && child.props && child.props.channel) {
|
||||
obj = child.props.channel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error retrieving selected channel from 'chatContent' element.", e);
|
||||
|
||||
if (props != null) {
|
||||
obj = props.channel;
|
||||
break;
|
||||
for (const ele of this.getMessageElements()) {
|
||||
const props = this.getMessageElementProps(ele);
|
||||
|
||||
if (props != null) {
|
||||
obj = props.channel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!obj) {
|
||||
if (!obj || typeof obj.id !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -157,7 +162,7 @@ class DISCORD {
|
||||
if (dms) {
|
||||
let name;
|
||||
|
||||
for (const ele of dms.querySelectorAll("[class*='channel-'][class*='selected-'] [class^='name-'] *")) {
|
||||
for (const ele of dms.querySelectorAll("[class*='channel-'] [class*='selected-'] [class^='name-'] *, [class*='channel-'][class*='selected-'] [class^='name-'] *")) {
|
||||
const node = Array.prototype.find.call(ele.childNodes, node => node.nodeType === Node.TEXT_NODE);
|
||||
|
||||
if (node) {
|
||||
@@ -214,7 +219,7 @@ class DISCORD {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error("[DHT] Error retrieving selected channel.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -227,42 +232,38 @@ class DISCORD {
|
||||
|
||||
if (dms) {
|
||||
const currentChannel = DOM.queryReactClass("selected", dms);
|
||||
const nextChannel = currentChannel && currentChannel.nextElementSibling;
|
||||
const currentChannelContainer = currentChannel && currentChannel.closest("[class*='channel-']");
|
||||
const nextChannel = currentChannelContainer && currentChannelContainer.nextElementSibling;
|
||||
|
||||
if (!nextChannel || !nextChannel.getAttribute("class").includes("channel-") || !("href" in nextChannel) || !nextChannel.href.includes("/@me/")) {
|
||||
if (!nextChannel || !nextChannel.getAttribute("class").includes("channel-")) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
nextChannel.click();
|
||||
nextChannel.scrollIntoView(true);
|
||||
return true;
|
||||
|
||||
const nextChannelLink = nextChannel.querySelector("a[href*='/@me/']");
|
||||
if (!nextChannelLink) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nextChannelLink.click();
|
||||
nextChannelLink.scrollIntoView(true);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
const channelIcons = [
|
||||
/* normal */ "M5.88657 21C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H15.76L16.3968 3.41262C16.4391 3.17391 16.6466 3 16.8891 3H17.8734C18.1845 3 18.4201 3.28107 18.3657 3.58738L17.76 7H21.1649C21.4755 7 21.711 7.28023 21.6574 7.58619L21.4824 8.58619C21.4406 8.82544 21.2328 9 20.9899 9H17.41L16.35 15H19.7549C20.0655 15 20.301 15.2802 20.2474 15.5862L20.0724 16.5862C20.0306 16.8254 19.8228 17 19.5799 17H16L15.3632 20.5874C15.3209 20.8261 15.1134 21 14.8709 21H13.8866C13.5755 21 13.3399 20.7189 13.3943 20.4126L14 17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657ZM9.41045 9L8.35045 15H14.3504L15.4104 9H9.41045Z",
|
||||
/* normal + thread */ "M5.43309 21C5.35842 21 5.30189 20.9325 5.31494 20.859L5.99991 17H2.14274C2.06819 17 2.01168 16.9327 2.02453 16.8593L2.33253 15.0993C2.34258 15.0419 2.39244 15 2.45074 15H6.34991L7.40991 9H3.55274C3.47819 9 3.42168 8.93274 3.43453 8.85931L3.74253 7.09931C3.75258 7.04189 3.80244 7 3.86074 7H7.75991L8.45234 3.09903C8.46251 3.04174 8.51231 3 8.57049 3H10.3267C10.4014 3 10.4579 3.06746 10.4449 3.14097L9.75991 7H15.7599L16.4523 3.09903C16.4625 3.04174 16.5123 3 16.5705 3H18.3267C18.4014 3 18.4579 3.06746 18.4449 3.14097L17.7599 7H21.6171C21.6916 7 21.7481 7.06725 21.7353 7.14069L21.4273 8.90069C21.4172 8.95811 21.3674 9 21.3091 9H17.4099L17.0495 11.04H15.05L15.4104 9H9.41035L8.35035 15H10.5599V17H7.99991L7.30749 20.901C7.29732 20.9583 7.24752 21 7.18934 21H5.43309Z",
|
||||
/* nsfw or private */ "M14 8C14 7.44772 13.5523 7 13 7H9.76001L10.3657 3.58738C10.4201 3.28107 10.1845 3 9.87344 3H8.88907C8.64664 3 8.43914 3.17391 8.39677 3.41262L7.76001 7H4.18011C3.93722 7 3.72946 7.17456 3.68759 7.41381L3.51259 8.41381C3.45905 8.71977 3.69449 9 4.00511 9H7.41001L6.35001 15H2.77011C2.52722 15 2.31946 15.1746 2.27759 15.4138L2.10259 16.4138C2.04905 16.7198 2.28449 17 2.59511 17H6.00001L5.39427 20.4126C5.3399 20.7189 5.57547 21 5.88657 21H6.87094C7.11337 21 7.32088 20.8261 7.36325 20.5874L8.00001 17H14L13.3943 20.4126C13.3399 20.7189 13.5755 21 13.8866 21H14.8709C15.1134 21 15.3209 20.8261 15.3632 20.5874L16 17H19.5799C19.8228 17 20.0306 16.8254 20.0724 16.5862L20.2474 15.5862C20.301 15.2802 20.0655 15 19.7549 15H16.35L16.6758 13.1558C16.7823 12.5529 16.3186 12 15.7063 12C15.2286 12 14.8199 12.3429 14.7368 12.8133L14.3504 15H8.35045L9.41045 9H13C13.5523 9 14 8.55228 14 8Z",
|
||||
/* nsfw + thread */ "M14.4 7C14.5326 7 14.64 7.10745 14.64 7.24V8.76C14.64 8.89255 14.5326 9 14.4 9H9.41045L8.35045 15H10.56V17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H14.4Z",
|
||||
/* private + thread */ "M15.44 6.99992C15.5725 6.99992 15.68 7.10737 15.68 7.23992V8.75992C15.68 8.89247 15.5725 8.99992 15.44 8.99992H9.41045L8.35045 14.9999H10.56V16.9999H8.00001L7.36325 20.5873C7.32088 20.826 7.11337 20.9999 6.87094 20.9999H5.88657C5.57547 20.9999 5.3399 20.7189 5.39427 20.4125L6.00001 16.9999H2.59511C2.28449 16.9999 2.04905 16.7197 2.10259 16.4137L2.27759 15.4137C2.31946 15.1745 2.52722 14.9999 2.77011 14.9999H6.35001L7.41001 8.99992H4.00511C3.69449 8.99992 3.45905 8.71969 3.51259 8.41373L3.68759 7.41373C3.72946 7.17448 3.93722 6.99992 4.18011 6.99992H7.76001L8.39677 3.41254C8.43914 3.17384 8.64664 2.99992 8.88907 2.99992H9.87344C10.1845 2.99992 10.4201 3.28099 10.3657 3.58731L9.76001 6.99992H15.44Z"
|
||||
];
|
||||
|
||||
const isValidChannelClass = cls => cls.includes("wrapper-") && !cls.includes("clickable-");
|
||||
const isValidChannelType = ele => channelIcons.some(icon => !!ele.querySelector("path[d=\"" + icon + "\"]"));
|
||||
const isValidChannel = ele => ele.childElementCount > 0 && isValidChannelClass(ele.children[0].className) && isValidChannelType(ele);
|
||||
|
||||
const channelListEle = document.querySelector("div[class*='sidebar'] > nav[class*='container'] > div[class*='scroller']");
|
||||
|
||||
const channelListEle = document.getElementById("channels");
|
||||
if (!channelListEle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const allChannels = Array.prototype.filter.call(channelListEle.querySelectorAll("[class*='containerDefault']"), isValidChannel);
|
||||
function getLinkElement(channel) {
|
||||
return channel.querySelector("a[href^='/channels/'][role='link']");
|
||||
}
|
||||
|
||||
const allTextChannels = Array.prototype.filter.call(channelListEle.querySelectorAll("[class*='containerDefault']"), ele => getLinkElement(ele) !== null);
|
||||
let nextChannel = null;
|
||||
|
||||
for (let index = 0; index < allChannels.length - 1; index++) {
|
||||
if (allChannels[index].children[0].className.includes("modeSelected")) {
|
||||
nextChannel = allChannels[index + 1];
|
||||
for (let index = 0; index < allTextChannels.length - 1; index++) {
|
||||
if (allTextChannels[index].className.includes("selected-")) {
|
||||
nextChannel = allTextChannels[index + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -271,7 +272,7 @@ class DISCORD {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nextChannelLink = nextChannel.querySelector("a[href^='/channels/']");
|
||||
const nextChannelLink = getLinkElement(nextChannel);
|
||||
if (!nextChannelLink) {
|
||||
return false;
|
||||
}
|
||||
|
@@ -55,7 +55,8 @@ const GUI = (function() {
|
||||
controller.ui.btnClose.addEventListener("click", () => {
|
||||
this.hideController();
|
||||
window.DHT_ON_UNLOAD.forEach(f => f());
|
||||
window.DHT_LOADED = false;
|
||||
delete window.DHT_ON_UNLOAD;
|
||||
delete window.DHT_LOADED;
|
||||
});
|
||||
|
||||
STATE.onTrackingStateChanged(isTracking => {
|
||||
|
27
app/Resources/minify.py
Normal file
27
app/Resources/minify.py
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import glob
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
if os.name == "nt":
|
||||
uglifyjs = os.path.abspath("../../lib/uglifyjs.cmd")
|
||||
else:
|
||||
uglifyjs = "uglifyjs"
|
||||
|
||||
if shutil.which(uglifyjs) is None:
|
||||
print("Cannot find executable: {0}".format(uglifyjs))
|
||||
sys.exit(1)
|
||||
|
||||
input_dir = os.path.abspath("./Tracker/scripts")
|
||||
output_dir = os.path.abspath("../Desktop/bin/.res/scripts")
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
for file in glob.glob(input_dir + "/*.js"):
|
||||
name = os.path.basename(file)
|
||||
print("Minifying {0}...".format(name))
|
||||
os.system("{0} {1} -o {2}/{3}".format(uglifyjs, file, output_dir, name))
|
||||
|
||||
print("Done!")
|
@@ -1,9 +1,9 @@
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct Attachment {
|
||||
public ulong Id { get; init; }
|
||||
public string Name { get; init; }
|
||||
public string? Type { get; init; }
|
||||
public string Url { get; init; }
|
||||
public ulong Size { get; init; }
|
||||
public ulong Id { get; internal init; }
|
||||
public string Name { get; internal init; }
|
||||
public string? Type { get; internal init; }
|
||||
public string Url { get; internal init; }
|
||||
public ulong Size { get; internal init; }
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,11 @@
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct Channel {
|
||||
public ulong Id { get; init; }
|
||||
public ulong Server { get; init; }
|
||||
public string Name { get; init; }
|
||||
public ulong? ParentId { get; init; }
|
||||
public int? Position { get; init; }
|
||||
public string? Topic { get; init; }
|
||||
public bool? Nsfw { get; init; }
|
||||
public ulong Id { get; internal init; }
|
||||
public ulong Server { get; internal init; }
|
||||
public string Name { get; internal init; }
|
||||
public ulong? ParentId { get; internal init; }
|
||||
public int? Position { get; internal init; }
|
||||
public string? Topic { get; internal init; }
|
||||
public bool? Nsfw { get; internal init; }
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct Embed {
|
||||
public string Json { get; init; }
|
||||
public string Json { get; internal init; }
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DHT.Server.Data.Filters {
|
||||
public class MessageFilter {
|
||||
public sealed class MessageFilter {
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
|
@@ -2,15 +2,15 @@ using System.Collections.Immutable;
|
||||
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct Message {
|
||||
public ulong Id { get; init; }
|
||||
public ulong Sender { get; init; }
|
||||
public ulong Channel { get; init; }
|
||||
public string Text { get; init; }
|
||||
public long Timestamp { get; init; }
|
||||
public long? EditTimestamp { get; init; }
|
||||
public ulong? RepliedToId { get; init; }
|
||||
public ImmutableArray<Attachment> Attachments { get; init; }
|
||||
public ImmutableArray<Embed> Embeds { get; init; }
|
||||
public ImmutableArray<Reaction> Reactions { get; init; }
|
||||
public ulong Id { get; internal init; }
|
||||
public ulong Sender { get; internal init; }
|
||||
public ulong Channel { get; internal init; }
|
||||
public string Text { get; internal init; }
|
||||
public long Timestamp { get; internal init; }
|
||||
public long? EditTimestamp { get; internal init; }
|
||||
public ulong? RepliedToId { get; internal init; }
|
||||
public ImmutableArray<Attachment> Attachments { get; internal init; }
|
||||
public ImmutableArray<Embed> Embeds { get; internal init; }
|
||||
public ImmutableArray<Reaction> Reactions { get; internal init; }
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct Reaction {
|
||||
public ulong? EmojiId { get; init; }
|
||||
public string? EmojiName { get; init; }
|
||||
public EmojiFlags EmojiFlags { get; init; }
|
||||
public int Count { get; init; }
|
||||
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 {
|
||||
public readonly struct Server {
|
||||
public ulong Id { get; init; }
|
||||
public string Name { get; init; }
|
||||
public ServerType? Type { get; init; }
|
||||
public ulong Id { get; internal init; }
|
||||
public string Name { get; internal init; }
|
||||
public ServerType? Type { get; internal init; }
|
||||
}
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ namespace DHT.Server.Data {
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToJsonViewerString(ServerType? type) {
|
||||
internal static string ToJsonViewerString(ServerType? type) {
|
||||
return type switch {
|
||||
ServerType.Server => "server",
|
||||
ServerType.Group => "group",
|
||||
|
@@ -1,8 +1,8 @@
|
||||
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; }
|
||||
public ulong Id { get; internal init; }
|
||||
public string Name { get; internal init; }
|
||||
public string? AvatarUrl { get; internal init; }
|
||||
public string? Discriminator { get; internal init; }
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,7 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Server.Database {
|
||||
public class DatabaseStatistics : INotifyPropertyChanged {
|
||||
public sealed class DatabaseStatistics : BaseModel {
|
||||
private long totalServers;
|
||||
private long totalChannels;
|
||||
private long totalUsers;
|
||||
@@ -10,29 +9,22 @@ namespace DHT.Server.Database {
|
||||
|
||||
public long TotalServers {
|
||||
get => totalServers;
|
||||
internal set => Change(out totalServers, value);
|
||||
internal set => Change(ref totalServers, value);
|
||||
}
|
||||
|
||||
public long TotalChannels {
|
||||
get => totalChannels;
|
||||
internal set => Change(out totalChannels, value);
|
||||
internal set => Change(ref totalChannels, value);
|
||||
}
|
||||
|
||||
public long TotalUsers {
|
||||
get => totalUsers;
|
||||
internal set => Change(out totalUsers, value);
|
||||
internal set => Change(ref totalUsers, value);
|
||||
}
|
||||
|
||||
public long TotalMessages {
|
||||
get => totalMessages;
|
||||
internal set => Change(out totalMessages, value);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void Change<T>(out T field, T value, [CallerMemberName] string? propertyName = null) {
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
internal set => Change(ref totalMessages, value);
|
||||
}
|
||||
|
||||
public DatabaseStatistics Clone() {
|
||||
|
@@ -1,10 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Data.Filters;
|
||||
|
||||
namespace DHT.Server.Database {
|
||||
public class DummyDatabaseFile : IDatabaseFile {
|
||||
public sealed class DummyDatabaseFile : IDatabaseFile {
|
||||
public static DummyDatabaseFile Instance { get; } = new();
|
||||
|
||||
public string Path => "";
|
||||
@@ -42,8 +41,6 @@ namespace DHT.Server.Database {
|
||||
|
||||
public void RemoveMessages(MessageFilter filter, MessageFilterRemovalMode mode) {}
|
||||
|
||||
public void Dispose() {
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
public void Dispose() {}
|
||||
}
|
||||
}
|
||||
|
@@ -2,11 +2,11 @@ using System;
|
||||
using DHT.Server.Database.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Exceptions {
|
||||
public class DatabaseTooNewException : Exception {
|
||||
public sealed class DatabaseTooNewException : Exception {
|
||||
public int DatabaseVersion { get; }
|
||||
public int CurrentVersion => Schema.Version;
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace DHT.Server.Database.Exceptions {
|
||||
public class InvalidDatabaseVersionException : Exception {
|
||||
public sealed class InvalidDatabaseVersionException : Exception {
|
||||
public string Version { get; }
|
||||
|
||||
public InvalidDatabaseVersionException(string version) : base("Invalid database version: " + version) {
|
||||
internal InvalidDatabaseVersionException(string version) : base("Invalid database version: " + version) {
|
||||
this.Version = version;
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Data.Filters;
|
||||
using DHT.Utils.Logging;
|
||||
|
||||
namespace DHT.Server.Database.Export {
|
||||
public static class ViewerJsonExport {
|
||||
private static readonly Log Log = Log.ForType(typeof(ViewerJsonExport));
|
||||
|
||||
public static string Generate(IDatabaseFile db, MessageFilter? filter = null) {
|
||||
var perf = Log.Start();
|
||||
|
||||
var includedUserIds = new HashSet<ulong>();
|
||||
var includedChannelIds = new HashSet<ulong>();
|
||||
var includedServerIds = new HashSet<ulong>();
|
||||
@@ -27,23 +31,29 @@ namespace DHT.Server.Database.Export {
|
||||
}
|
||||
}
|
||||
|
||||
var opts = new JsonSerializerOptions();
|
||||
opts.Converters.Add(new ViewerJsonSnowflakeSerializer());
|
||||
|
||||
var users = GenerateUserList(db, includedUserIds, out var userindex, out var userIndices);
|
||||
var servers = GenerateServerList(db, includedServerIds, out var serverindex);
|
||||
var channels = GenerateChannelList(includedChannels, serverindex);
|
||||
|
||||
return JsonSerializer.Serialize(new {
|
||||
perf.Step("Collect database data");
|
||||
|
||||
var opts = new JsonSerializerOptions();
|
||||
opts.Converters.Add(new ViewerJsonSnowflakeSerializer());
|
||||
|
||||
var json = JsonSerializer.Serialize(new {
|
||||
meta = new { users, userindex, servers, channels },
|
||||
data = GenerateMessageList(includedMessages, userIndices)
|
||||
}, opts);
|
||||
|
||||
perf.Step("Serialize to JSON");
|
||||
perf.End();
|
||||
return json;
|
||||
}
|
||||
|
||||
private static dynamic GenerateUserList(IDatabaseFile db, HashSet<ulong> userIds, out List<string> userindex, out Dictionary<ulong, int> userIndices) {
|
||||
var users = new Dictionary<string, dynamic>();
|
||||
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, int>();
|
||||
userIndices = new Dictionary<ulong, object>();
|
||||
|
||||
foreach (var user in db.GetAllUsers()) {
|
||||
var id = user.Id;
|
||||
@@ -51,15 +61,16 @@ namespace DHT.Server.Database.Export {
|
||||
continue;
|
||||
}
|
||||
|
||||
dynamic obj = new ExpandoObject();
|
||||
obj.name = user.Name;
|
||||
var obj = new Dictionary<string, object> {
|
||||
["name"] = user.Name
|
||||
};
|
||||
|
||||
if (user.AvatarUrl != null) {
|
||||
obj.avatar = user.AvatarUrl;
|
||||
obj["avatar"] = user.AvatarUrl;
|
||||
}
|
||||
|
||||
if (user.Discriminator != null) {
|
||||
obj.tag = user.Discriminator;
|
||||
obj["tag"] = user.Discriminator;
|
||||
}
|
||||
|
||||
var idStr = id.ToString();
|
||||
@@ -71,9 +82,9 @@ namespace DHT.Server.Database.Export {
|
||||
return users;
|
||||
}
|
||||
|
||||
private static dynamic GenerateServerList(IDatabaseFile db, HashSet<ulong> serverIds, out Dictionary<ulong, int> serverIndices) {
|
||||
var servers = new List<dynamic>();
|
||||
serverIndices = new Dictionary<ulong, int>();
|
||||
private static object GenerateServerList(IDatabaseFile db, HashSet<ulong> serverIds, out Dictionary<ulong, object> serverIndices) {
|
||||
var servers = new List<object>();
|
||||
serverIndices = new Dictionary<ulong, object>();
|
||||
|
||||
foreach (var server in db.GetAllServers()) {
|
||||
var id = server.Id;
|
||||
@@ -82,37 +93,38 @@ namespace DHT.Server.Database.Export {
|
||||
}
|
||||
|
||||
serverIndices[id] = servers.Count;
|
||||
servers.Add(new {
|
||||
name = server.Name,
|
||||
type = ServerTypes.ToJsonViewerString(server.Type)
|
||||
servers.Add(new Dictionary<string, object> {
|
||||
["name"] = server.Name,
|
||||
["type"] = ServerTypes.ToJsonViewerString(server.Type)
|
||||
});
|
||||
}
|
||||
|
||||
return servers;
|
||||
}
|
||||
|
||||
private static dynamic GenerateChannelList(List<Channel> includedChannels, Dictionary<ulong, int> serverIndices) {
|
||||
var channels = new Dictionary<string, dynamic>();
|
||||
private static object GenerateChannelList(List<Channel> includedChannels, Dictionary<ulong, object> serverIndices) {
|
||||
var channels = new Dictionary<string, object>();
|
||||
|
||||
foreach (var channel in includedChannels) {
|
||||
dynamic obj = new ExpandoObject();
|
||||
obj.server = serverIndices[channel.Server];
|
||||
obj.name = channel.Name;
|
||||
var obj = new Dictionary<string, object> {
|
||||
["server"] = serverIndices[channel.Server],
|
||||
["name"] = channel.Name
|
||||
};
|
||||
|
||||
if (channel.ParentId != null) {
|
||||
obj.parent = channel.ParentId;
|
||||
obj["parent"] = channel.ParentId;
|
||||
}
|
||||
|
||||
if (channel.Position != null) {
|
||||
obj.position = channel.Position;
|
||||
obj["position"] = channel.Position;
|
||||
}
|
||||
|
||||
if (channel.Topic != null) {
|
||||
obj.topic = channel.Topic;
|
||||
obj["topic"] = channel.Topic;
|
||||
}
|
||||
|
||||
if (channel.Nsfw != null) {
|
||||
obj.nsfw = channel.Nsfw;
|
||||
obj["nsfw"] = channel.Nsfw;
|
||||
}
|
||||
|
||||
channels[channel.Id.ToString()] = obj;
|
||||
@@ -121,54 +133,55 @@ namespace DHT.Server.Database.Export {
|
||||
return channels;
|
||||
}
|
||||
|
||||
private static dynamic GenerateMessageList(List<Message> includedMessages, Dictionary<ulong, int> userIndices) {
|
||||
var data = new Dictionary<string, Dictionary<string, dynamic>>();
|
||||
private static object GenerateMessageList(List<Message> includedMessages, Dictionary<ulong, object> userIndices) {
|
||||
var data = new Dictionary<string, Dictionary<string, object>>();
|
||||
|
||||
foreach (var grouping in includedMessages.GroupBy(message => message.Channel)) {
|
||||
foreach (var grouping in includedMessages.GroupBy(static message => message.Channel)) {
|
||||
var channel = grouping.Key.ToString();
|
||||
var channelData = new Dictionary<string, dynamic>();
|
||||
var channelData = new Dictionary<string, object>();
|
||||
|
||||
foreach (var message in grouping) {
|
||||
dynamic obj = new ExpandoObject();
|
||||
obj.u = userIndices[message.Sender];
|
||||
obj.t = message.Timestamp;
|
||||
var obj = new Dictionary<string, object> {
|
||||
["u"] = userIndices[message.Sender],
|
||||
["t"] = message.Timestamp
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(message.Text)) {
|
||||
obj.m = message.Text;
|
||||
obj["m"] = message.Text;
|
||||
}
|
||||
|
||||
if (message.EditTimestamp != null) {
|
||||
obj.te = message.EditTimestamp;
|
||||
obj["te"] = message.EditTimestamp;
|
||||
}
|
||||
|
||||
if (message.RepliedToId != null) {
|
||||
obj.r = message.RepliedToId.Value;
|
||||
obj["r"] = message.RepliedToId.Value;
|
||||
}
|
||||
|
||||
if (!message.Attachments.IsEmpty) {
|
||||
obj.a = message.Attachments.Select(attachment => new {
|
||||
obj["a"] = message.Attachments.Select(static attachment => new {
|
||||
url = attachment.Url
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
if (!message.Embeds.IsEmpty) {
|
||||
obj.e = message.Embeds.Select(embed => embed.Json).ToArray();
|
||||
obj["e"] = message.Embeds.Select(static embed => embed.Json).ToArray();
|
||||
}
|
||||
|
||||
if (!message.Reactions.IsEmpty) {
|
||||
obj.re = message.Reactions.Select(reaction => {
|
||||
dynamic r = new ExpandoObject();
|
||||
obj["re"] = message.Reactions.Select(static reaction => {
|
||||
var r = new Dictionary<string, object>();
|
||||
|
||||
if (reaction.EmojiId != null) {
|
||||
r.id = reaction.EmojiId.Value;
|
||||
r["id"] = reaction.EmojiId.Value;
|
||||
}
|
||||
|
||||
if (reaction.EmojiName != null) {
|
||||
r.n = reaction.EmojiName;
|
||||
r["n"] = reaction.EmojiName;
|
||||
}
|
||||
|
||||
r.a = reaction.EmojiFlags.HasFlag(EmojiFlags.Animated);
|
||||
r.c = reaction.Count;
|
||||
r["a"] = reaction.EmojiFlags.HasFlag(EmojiFlags.Animated);
|
||||
r["c"] = reaction.Count;
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DHT.Server.Database.Export {
|
||||
public class ViewerJsonSnowflakeSerializer : JsonConverter<ulong> {
|
||||
sealed class ViewerJsonSnowflakeSerializer : JsonConverter<ulong> {
|
||||
public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
|
||||
return ulong.Parse(reader.GetString()!);
|
||||
}
|
||||
|
@@ -1,32 +1,29 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Database.Exceptions;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using DHT.Server.Database.Sqlite.Utils;
|
||||
using DHT.Utils.Logging;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite {
|
||||
internal class Schema {
|
||||
internal const int Version = 2;
|
||||
sealed class Schema {
|
||||
internal const int Version = 3;
|
||||
|
||||
private readonly SqliteConnection conn;
|
||||
private static readonly Log Log = Log.ForType<Schema>();
|
||||
|
||||
public Schema(SqliteConnection conn) {
|
||||
private readonly ISqliteConnection conn;
|
||||
|
||||
public Schema(ISqliteConnection conn) {
|
||||
this.conn = conn;
|
||||
}
|
||||
|
||||
private SqliteCommand Sql(string sql) {
|
||||
var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private void Execute(string sql) {
|
||||
Sql(sql).ExecuteNonQuery();
|
||||
conn.Command(sql).ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public async Task<bool> Setup(Func<Task<bool>> checkCanUpgradeSchemas) {
|
||||
Execute(@"CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT)");
|
||||
|
||||
var dbVersionStr = Sql("SELECT value FROM metadata WHERE key = 'version'").ExecuteScalar();
|
||||
var dbVersionStr = conn.SelectScalar("SELECT value FROM metadata WHERE key = 'version'");
|
||||
if (dbVersionStr == null) {
|
||||
InitializeSchemas();
|
||||
}
|
||||
@@ -74,9 +71,7 @@ namespace DHT.Server.Database.Sqlite {
|
||||
sender_id INTEGER NOT NULL,
|
||||
channel_id INTEGER NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
edit_timestamp INTEGER,
|
||||
replied_to_id INTEGER)");
|
||||
timestamp INTEGER NOT NULL)");
|
||||
|
||||
Execute(@"CREATE TABLE attachments (
|
||||
message_id INTEGER NOT NULL,
|
||||
@@ -97,6 +92,9 @@ namespace DHT.Server.Database.Sqlite {
|
||||
emoji_flags INTEGER NOT NULL,
|
||||
count INTEGER NOT NULL)");
|
||||
|
||||
CreateMessageEditTimestampTable();
|
||||
CreateMessageRepliedToTable();
|
||||
|
||||
Execute("CREATE INDEX attachments_message_ix ON attachments(message_id)");
|
||||
Execute("CREATE INDEX embeds_message_ix ON embeds(message_id)");
|
||||
Execute("CREATE INDEX reactions_message_ix ON reactions(message_id)");
|
||||
@@ -104,12 +102,50 @@ namespace DHT.Server.Database.Sqlite {
|
||||
Execute("INSERT INTO metadata (key, value) VALUES ('version', " + Version + ")");
|
||||
}
|
||||
|
||||
private void CreateMessageEditTimestampTable() {
|
||||
Execute(@"CREATE TABLE edit_timestamps (
|
||||
message_id INTEGER PRIMARY KEY NOT NULL,
|
||||
edit_timestamp INTEGER NOT NULL)");
|
||||
}
|
||||
|
||||
private void CreateMessageRepliedToTable() {
|
||||
Execute(@"CREATE TABLE replied_to (
|
||||
message_id INTEGER PRIMARY KEY NOT NULL,
|
||||
replied_to_id INTEGER NOT NULL)");
|
||||
}
|
||||
|
||||
private void UpgradeSchemas(int dbVersion) {
|
||||
var perf = Log.Start("from version " + dbVersion);
|
||||
|
||||
Execute("UPDATE metadata SET value = " + Version + " WHERE key = 'version'");
|
||||
|
||||
if (dbVersion <= 1) {
|
||||
Execute("ALTER TABLE channels ADD parent_id INTEGER");
|
||||
perf.Step("Upgrade to version 2");
|
||||
}
|
||||
|
||||
if (dbVersion <= 2) {
|
||||
CreateMessageEditTimestampTable();
|
||||
CreateMessageRepliedToTable();
|
||||
|
||||
Execute(@"INSERT INTO edit_timestamps (message_id, edit_timestamp)
|
||||
SELECT message_id, edit_timestamp FROM messages
|
||||
WHERE edit_timestamp IS NOT NULL");
|
||||
|
||||
Execute(@"INSERT INTO replied_to (message_id, replied_to_id)
|
||||
SELECT message_id, replied_to_id FROM messages
|
||||
WHERE replied_to_id IS NOT NULL");
|
||||
|
||||
Execute("ALTER TABLE messages DROP COLUMN replied_to_id");
|
||||
Execute("ALTER TABLE messages DROP COLUMN edit_timestamp");
|
||||
|
||||
perf.Step("Upgrade to version 3");
|
||||
|
||||
Execute("VACUUM");
|
||||
perf.Step("Vacuum");
|
||||
}
|
||||
|
||||
perf.End();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,104 +3,128 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Collections;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Data.Filters;
|
||||
using DHT.Server.Database.Sqlite.Utils;
|
||||
using DHT.Utils.Collections;
|
||||
using DHT.Utils.Logging;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite {
|
||||
public class SqliteDatabaseFile : IDatabaseFile {
|
||||
public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
private const int DefaultPoolSize = 5;
|
||||
|
||||
public static async Task<SqliteDatabaseFile?> OpenOrCreate(string path, Func<Task<bool>> checkCanUpgradeSchemas) {
|
||||
string connectionString = new SqliteConnectionStringBuilder {
|
||||
var connectionString = new SqliteConnectionStringBuilder {
|
||||
DataSource = path,
|
||||
Mode = SqliteOpenMode.ReadWriteCreate
|
||||
}.ToString();
|
||||
Mode = SqliteOpenMode.ReadWriteCreate,
|
||||
};
|
||||
|
||||
var conn = new SqliteConnection(connectionString);
|
||||
conn.Open();
|
||||
var pool = new SqliteConnectionPool(connectionString, DefaultPoolSize);
|
||||
|
||||
return await new Schema(conn).Setup(checkCanUpgradeSchemas) ? new SqliteDatabaseFile(path, conn) : null;
|
||||
using (var conn = pool.Take()) {
|
||||
if (!await new Schema(conn).Setup(checkCanUpgradeSchemas)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return new SqliteDatabaseFile(path, pool);
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
public DatabaseStatistics Statistics { get; }
|
||||
|
||||
private readonly SqliteConnection conn;
|
||||
private readonly Log log;
|
||||
private readonly SqliteConnectionPool pool;
|
||||
|
||||
private SqliteDatabaseFile(string path, SqliteConnection conn) {
|
||||
this.conn = conn;
|
||||
private SqliteDatabaseFile(string path, SqliteConnectionPool pool) {
|
||||
this.log = Log.ForType(typeof(SqliteDatabaseFile), System.IO.Path.GetFileName(path));
|
||||
this.pool = pool;
|
||||
this.Path = path;
|
||||
this.Statistics = new DatabaseStatistics();
|
||||
UpdateServerStatistics();
|
||||
UpdateChannelStatistics();
|
||||
UpdateUserStatistics();
|
||||
UpdateMessageStatistics();
|
||||
|
||||
using var conn = pool.Take();
|
||||
UpdateServerStatistics(conn);
|
||||
UpdateChannelStatistics(conn);
|
||||
UpdateUserStatistics(conn);
|
||||
UpdateMessageStatistics(conn);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
conn.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
pool.Dispose();
|
||||
}
|
||||
|
||||
public void AddServer(Data.Server server) {
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Upsert("servers", new[] {
|
||||
"id", "name", "type"
|
||||
("id", SqliteType.Integer),
|
||||
("name", SqliteType.Text),
|
||||
("type", SqliteType.Text)
|
||||
});
|
||||
|
||||
var serverParams = cmd.Parameters;
|
||||
serverParams.AddAndSet(":id", server.Id);
|
||||
serverParams.AddAndSet(":name", server.Name);
|
||||
serverParams.AddAndSet(":type", ServerTypes.ToString(server.Type));
|
||||
cmd.Set(":id", server.Id);
|
||||
cmd.Set(":name", server.Name);
|
||||
cmd.Set(":type", ServerTypes.ToString(server.Type));
|
||||
cmd.ExecuteNonQuery();
|
||||
UpdateServerStatistics();
|
||||
UpdateServerStatistics(conn);
|
||||
}
|
||||
|
||||
public List<Data.Server> GetAllServers() {
|
||||
var perf = log.Start();
|
||||
var list = new List<Data.Server>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT id, name, type FROM servers");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read()) {
|
||||
list.Add(new Data.Server {
|
||||
Id = (ulong) reader.GetInt64(0),
|
||||
Id = reader.GetUint64(0),
|
||||
Name = reader.GetString(1),
|
||||
Type = ServerTypes.FromString(reader.GetString(2))
|
||||
});
|
||||
}
|
||||
|
||||
perf.End();
|
||||
return list;
|
||||
}
|
||||
|
||||
public void AddChannel(Channel channel) {
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Upsert("channels", new[] {
|
||||
"id", "server", "name", "parent_id", "position", "topic", "nsfw"
|
||||
("id", SqliteType.Integer),
|
||||
("server", SqliteType.Integer),
|
||||
("name", SqliteType.Text),
|
||||
("parent_id", SqliteType.Integer),
|
||||
("position", SqliteType.Integer),
|
||||
("topic", SqliteType.Text),
|
||||
("nsfw", SqliteType.Integer)
|
||||
});
|
||||
|
||||
var channelParams = cmd.Parameters;
|
||||
channelParams.AddAndSet(":id", channel.Id);
|
||||
channelParams.AddAndSet(":server", channel.Server);
|
||||
channelParams.AddAndSet(":name", channel.Name);
|
||||
channelParams.AddAndSet(":parent_id", channel.ParentId);
|
||||
channelParams.AddAndSet(":position", channel.Position);
|
||||
channelParams.AddAndSet(":topic", channel.Topic);
|
||||
channelParams.AddAndSet(":nsfw", channel.Nsfw);
|
||||
cmd.Set(":id", channel.Id);
|
||||
cmd.Set(":server", channel.Server);
|
||||
cmd.Set(":name", channel.Name);
|
||||
cmd.Set(":parent_id", channel.ParentId);
|
||||
cmd.Set(":position", channel.Position);
|
||||
cmd.Set(":topic", channel.Topic);
|
||||
cmd.Set(":nsfw", channel.Nsfw);
|
||||
cmd.ExecuteNonQuery();
|
||||
UpdateChannelStatistics();
|
||||
UpdateChannelStatistics(conn);
|
||||
}
|
||||
|
||||
public List<Channel> GetAllChannels() {
|
||||
var list = new List<Channel>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT id, server, name, parent_id, position, topic, nsfw FROM channels");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read()) {
|
||||
list.Add(new Channel {
|
||||
Id = (ulong) reader.GetInt64(0),
|
||||
Server = (ulong) reader.GetInt64(1),
|
||||
Id = reader.GetUint64(0),
|
||||
Server = reader.GetUint64(1),
|
||||
Name = reader.GetString(2),
|
||||
ParentId = reader.IsDBNull(3) ? null : (ulong) reader.GetInt64(3),
|
||||
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)
|
||||
@@ -111,163 +135,175 @@ namespace DHT.Server.Database.Sqlite {
|
||||
}
|
||||
|
||||
public void AddUsers(User[] users) {
|
||||
using var conn = pool.Take();
|
||||
using var tx = conn.BeginTransaction();
|
||||
using var cmd = conn.Upsert("users", new[] {
|
||||
"id", "name", "avatar_url", "discriminator"
|
||||
("id", SqliteType.Integer),
|
||||
("name", SqliteType.Text),
|
||||
("avatar_url", SqliteType.Text),
|
||||
("discriminator", SqliteType.Text)
|
||||
});
|
||||
|
||||
var userParams = cmd.Parameters;
|
||||
userParams.Add(":id", SqliteType.Integer);
|
||||
userParams.Add(":name", SqliteType.Text);
|
||||
userParams.Add(":avatar_url", SqliteType.Text);
|
||||
userParams.Add(":discriminator", SqliteType.Text);
|
||||
|
||||
foreach (var user in users) {
|
||||
userParams.Set(":id", user.Id);
|
||||
userParams.Set(":name", user.Name);
|
||||
userParams.Set(":avatar_url", user.AvatarUrl);
|
||||
userParams.Set(":discriminator", user.Discriminator);
|
||||
cmd.Set(":id", user.Id);
|
||||
cmd.Set(":name", user.Name);
|
||||
cmd.Set(":avatar_url", user.AvatarUrl);
|
||||
cmd.Set(":discriminator", user.Discriminator);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
UpdateUserStatistics();
|
||||
UpdateUserStatistics(conn);
|
||||
}
|
||||
|
||||
public List<User> GetAllUsers() {
|
||||
var perf = log.Start();
|
||||
var list = new List<User>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT id, name, avatar_url, discriminator FROM users");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read()) {
|
||||
list.Add(new User {
|
||||
Id = (ulong) reader.GetInt64(0),
|
||||
Id = reader.GetUint64(0),
|
||||
Name = reader.GetString(1),
|
||||
AvatarUrl = reader.IsDBNull(2) ? null : reader.GetString(2),
|
||||
Discriminator = reader.IsDBNull(3) ? null : reader.GetString(3)
|
||||
});
|
||||
}
|
||||
|
||||
perf.End();
|
||||
return list;
|
||||
}
|
||||
|
||||
public void AddMessages(Message[] messages) {
|
||||
static SqliteCommand DeleteByMessageId(ISqliteConnection conn, string tableName) {
|
||||
return conn.Delete(tableName, ("message_id", SqliteType.Integer));
|
||||
}
|
||||
|
||||
static void ExecuteDeleteByMessageId(SqliteCommand cmd, object id) {
|
||||
cmd.Set(":message_id", id);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var tx = conn.BeginTransaction();
|
||||
|
||||
using var messageCmd = conn.Upsert("messages", new[] {
|
||||
"message_id", "sender_id", "channel_id", "text", "timestamp", "edit_timestamp", "replied_to_id"
|
||||
("message_id", SqliteType.Integer),
|
||||
("sender_id", SqliteType.Integer),
|
||||
("channel_id", SqliteType.Integer),
|
||||
("text", SqliteType.Text),
|
||||
("timestamp", SqliteType.Integer)
|
||||
});
|
||||
|
||||
using var deleteEditTimestampCmd = DeleteByMessageId(conn, "edit_timestamps");
|
||||
using var deleteRepliedToCmd = DeleteByMessageId(conn, "replied_to");
|
||||
|
||||
using var deleteAttachmentsCmd = DeleteByMessageId(conn, "attachments");
|
||||
using var deleteEmbedsCmd = DeleteByMessageId(conn, "embeds");
|
||||
using var deleteReactionsCmd = DeleteByMessageId(conn, "reactions");
|
||||
|
||||
using var editTimestampCmd = conn.Insert("edit_timestamps", new [] {
|
||||
("message_id", SqliteType.Integer),
|
||||
("edit_timestamp", SqliteType.Integer)
|
||||
});
|
||||
|
||||
using var repliedToCmd = conn.Insert("replied_to", new [] {
|
||||
("message_id", SqliteType.Integer),
|
||||
("replied_to_id", SqliteType.Integer)
|
||||
});
|
||||
|
||||
using var deleteAttachmentsCmd = conn.Command("DELETE FROM attachments WHERE message_id = :message_id");
|
||||
using var attachmentCmd = conn.Insert("attachments", new[] {
|
||||
"message_id", "attachment_id", "name", "type", "url", "size"
|
||||
("message_id", SqliteType.Integer),
|
||||
("attachment_id", SqliteType.Integer),
|
||||
("name", SqliteType.Text),
|
||||
("type", SqliteType.Text),
|
||||
("url", SqliteType.Text),
|
||||
("size", SqliteType.Integer)
|
||||
});
|
||||
|
||||
using var deleteEmbedsCmd = conn.Command("DELETE FROM embeds WHERE message_id = :message_id");
|
||||
using var embedCmd = conn.Insert("embeds", new[] {
|
||||
"message_id", "json"
|
||||
("message_id", SqliteType.Integer),
|
||||
("json", SqliteType.Text)
|
||||
});
|
||||
|
||||
using var deleteReactionsCmd = conn.Command("DELETE FROM reactions WHERE message_id = :message_id");
|
||||
using var reactionCmd = conn.Insert("reactions", new[] {
|
||||
"message_id", "emoji_id", "emoji_name", "emoji_flags", "count"
|
||||
("message_id", SqliteType.Integer),
|
||||
("emoji_id", SqliteType.Integer),
|
||||
("emoji_name", SqliteType.Text),
|
||||
("emoji_flags", SqliteType.Integer),
|
||||
("count", SqliteType.Integer)
|
||||
});
|
||||
|
||||
var messageParams = messageCmd.Parameters;
|
||||
messageParams.Add(":message_id", SqliteType.Integer);
|
||||
messageParams.Add(":sender_id", SqliteType.Integer);
|
||||
messageParams.Add(":channel_id", SqliteType.Integer);
|
||||
messageParams.Add(":text", SqliteType.Text);
|
||||
messageParams.Add(":timestamp", SqliteType.Integer);
|
||||
messageParams.Add(":edit_timestamp", SqliteType.Integer);
|
||||
messageParams.Add(":replied_to_id", SqliteType.Integer);
|
||||
|
||||
var deleteAttachmentsParams = deleteAttachmentsCmd.Parameters;
|
||||
deleteAttachmentsParams.Add(":message_id", SqliteType.Integer);
|
||||
|
||||
var attachmentParams = attachmentCmd.Parameters;
|
||||
attachmentParams.Add(":message_id", SqliteType.Integer);
|
||||
attachmentParams.Add(":attachment_id", SqliteType.Integer);
|
||||
attachmentParams.Add(":name", SqliteType.Text);
|
||||
attachmentParams.Add(":type", SqliteType.Text);
|
||||
attachmentParams.Add(":url", SqliteType.Text);
|
||||
attachmentParams.Add(":size", SqliteType.Integer);
|
||||
|
||||
var deleteEmbedsParams = deleteEmbedsCmd.Parameters;
|
||||
deleteEmbedsParams.Add(":message_id", SqliteType.Integer);
|
||||
|
||||
var embedParams = embedCmd.Parameters;
|
||||
embedParams.Add(":message_id", SqliteType.Integer);
|
||||
embedParams.Add(":json", SqliteType.Text);
|
||||
|
||||
var deleteReactionsParams = deleteReactionsCmd.Parameters;
|
||||
deleteReactionsParams.Add(":message_id", SqliteType.Integer);
|
||||
|
||||
var reactionParams = reactionCmd.Parameters;
|
||||
reactionParams.Add(":message_id", SqliteType.Integer);
|
||||
reactionParams.Add(":emoji_id", SqliteType.Integer);
|
||||
reactionParams.Add(":emoji_name", SqliteType.Text);
|
||||
reactionParams.Add(":emoji_flags", SqliteType.Integer);
|
||||
reactionParams.Add(":count", SqliteType.Integer);
|
||||
|
||||
foreach (var message in messages) {
|
||||
object messageId = message.Id;
|
||||
|
||||
messageParams.Set(":message_id", messageId);
|
||||
messageParams.Set(":sender_id", message.Sender);
|
||||
messageParams.Set(":channel_id", message.Channel);
|
||||
messageParams.Set(":text", message.Text);
|
||||
messageParams.Set(":timestamp", message.Timestamp);
|
||||
messageParams.Set(":edit_timestamp", message.EditTimestamp);
|
||||
messageParams.Set(":replied_to_id", message.RepliedToId);
|
||||
messageCmd.Set(":message_id", messageId);
|
||||
messageCmd.Set(":sender_id", message.Sender);
|
||||
messageCmd.Set(":channel_id", message.Channel);
|
||||
messageCmd.Set(":text", message.Text);
|
||||
messageCmd.Set(":timestamp", message.Timestamp);
|
||||
messageCmd.ExecuteNonQuery();
|
||||
|
||||
deleteAttachmentsParams.Set(":message_id", messageId);
|
||||
deleteAttachmentsCmd.ExecuteNonQuery();
|
||||
ExecuteDeleteByMessageId(deleteEditTimestampCmd, messageId);
|
||||
ExecuteDeleteByMessageId(deleteRepliedToCmd, messageId);
|
||||
|
||||
deleteEmbedsParams.Set(":message_id", messageId);
|
||||
deleteEmbedsCmd.ExecuteNonQuery();
|
||||
ExecuteDeleteByMessageId(deleteAttachmentsCmd, messageId);
|
||||
ExecuteDeleteByMessageId(deleteEmbedsCmd, messageId);
|
||||
ExecuteDeleteByMessageId(deleteReactionsCmd, messageId);
|
||||
|
||||
deleteReactionsParams.Set(":message_id", messageId);
|
||||
deleteReactionsCmd.ExecuteNonQuery();
|
||||
if (message.EditTimestamp is {} timestamp) {
|
||||
editTimestampCmd.Set(":message_id", messageId);
|
||||
editTimestampCmd.Set(":edit_timestamp", timestamp);
|
||||
editTimestampCmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
if (message.RepliedToId is {} repliedToId) {
|
||||
repliedToCmd.Set(":message_id", messageId);
|
||||
repliedToCmd.Set(":replied_to_id", repliedToId);
|
||||
repliedToCmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
if (!message.Attachments.IsEmpty) {
|
||||
foreach (var attachment in message.Attachments) {
|
||||
attachmentParams.Set(":message_id", messageId);
|
||||
attachmentParams.Set(":attachment_id", attachment.Id);
|
||||
attachmentParams.Set(":name", attachment.Name);
|
||||
attachmentParams.Set(":type", attachment.Type);
|
||||
attachmentParams.Set(":url", attachment.Url);
|
||||
attachmentParams.Set(":size", attachment.Size);
|
||||
attachmentCmd.Set(":message_id", messageId);
|
||||
attachmentCmd.Set(":attachment_id", attachment.Id);
|
||||
attachmentCmd.Set(":name", attachment.Name);
|
||||
attachmentCmd.Set(":type", attachment.Type);
|
||||
attachmentCmd.Set(":url", attachment.Url);
|
||||
attachmentCmd.Set(":size", attachment.Size);
|
||||
attachmentCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
if (!message.Embeds.IsEmpty) {
|
||||
foreach (var embed in message.Embeds) {
|
||||
embedParams.Set(":message_id", messageId);
|
||||
embedParams.Set(":json", embed.Json);
|
||||
embedCmd.Set(":message_id", messageId);
|
||||
embedCmd.Set(":json", embed.Json);
|
||||
embedCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
if (!message.Reactions.IsEmpty) {
|
||||
foreach (var reaction in message.Reactions) {
|
||||
reactionParams.Set(":message_id", messageId);
|
||||
reactionParams.Set(":emoji_id", reaction.EmojiId);
|
||||
reactionParams.Set(":emoji_name", reaction.EmojiName);
|
||||
reactionParams.Set(":emoji_flags", (int) reaction.EmojiFlags);
|
||||
reactionParams.Set(":count", reaction.Count);
|
||||
reactionCmd.Set(":message_id", messageId);
|
||||
reactionCmd.Set(":emoji_id", reaction.EmojiId);
|
||||
reactionCmd.Set(":emoji_name", reaction.EmojiName);
|
||||
reactionCmd.Set(":emoji_flags", (int) reaction.EmojiFlags);
|
||||
reactionCmd.Set(":count", reaction.Count);
|
||||
reactionCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
UpdateMessageStatistics();
|
||||
UpdateMessageStatistics(conn);
|
||||
}
|
||||
|
||||
public int CountMessages(MessageFilter? filter = null) {
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT COUNT(*) FROM messages" + filter.GenerateWhereClause());
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
@@ -275,32 +311,39 @@ namespace DHT.Server.Database.Sqlite {
|
||||
}
|
||||
|
||||
public List<Message> GetMessages(MessageFilter? filter = null) {
|
||||
var perf = log.Start();
|
||||
var list = new List<Message>();
|
||||
|
||||
var attachments = GetAllAttachments();
|
||||
var embeds = GetAllEmbeds();
|
||||
var reactions = GetAllReactions();
|
||||
|
||||
var list = new List<Message>();
|
||||
|
||||
using var cmd = conn.Command("SELECT message_id, sender_id, channel_id, text, timestamp, edit_timestamp, replied_to_id FROM messages" + filter.GenerateWhereClause());
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command(@"
|
||||
SELECT m.message_id, m.sender_id, m.channel_id, m.text, m.timestamp, et.edit_timestamp, rt.replied_to_id
|
||||
FROM messages m
|
||||
LEFT JOIN edit_timestamps et ON m.message_id = et.message_id
|
||||
LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereClause("m"));
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read()) {
|
||||
ulong id = (ulong) reader.GetInt64(0);
|
||||
ulong id = reader.GetUint64(0);
|
||||
|
||||
list.Add(new Message {
|
||||
Id = id,
|
||||
Sender = (ulong) reader.GetInt64(1),
|
||||
Channel = (ulong) reader.GetInt64(2),
|
||||
Sender = reader.GetUint64(1),
|
||||
Channel = reader.GetUint64(2),
|
||||
Text = reader.GetString(3),
|
||||
Timestamp = reader.GetInt64(4),
|
||||
EditTimestamp = reader.IsDBNull(5) ? null : reader.GetInt64(5),
|
||||
RepliedToId = reader.IsDBNull(6) ? null : (ulong) reader.GetInt64(6),
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
perf.End();
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -310,33 +353,38 @@ namespace DHT.Server.Database.Sqlite {
|
||||
return;
|
||||
}
|
||||
|
||||
var perf = log.Start();
|
||||
|
||||
// Rider is being stupid...
|
||||
StringBuilder build = new StringBuilder()
|
||||
.Append("DELETE ")
|
||||
.Append("FROM messages")
|
||||
.Append(whereClause);
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command(build.ToString());
|
||||
cmd.ExecuteNonQuery();
|
||||
|
||||
UpdateMessageStatistics();
|
||||
UpdateMessageStatistics(conn);
|
||||
perf.End();
|
||||
}
|
||||
|
||||
private MultiDictionary<ulong, Attachment> GetAllAttachments() {
|
||||
var dict = new MultiDictionary<ulong, Attachment>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT message_id, attachment_id, name, type, url, size FROM attachments");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read()) {
|
||||
ulong messageId = (ulong) reader.GetInt64(0);
|
||||
ulong messageId = reader.GetUint64(0);
|
||||
|
||||
dict.Add(messageId, new Attachment {
|
||||
Id = (ulong) reader.GetInt64(1),
|
||||
Id = reader.GetUint64(1),
|
||||
Name = reader.GetString(2),
|
||||
Type = reader.IsDBNull(3) ? null : reader.GetString(3),
|
||||
Url = reader.GetString(4),
|
||||
Size = (ulong) reader.GetInt64(5)
|
||||
Size = reader.GetUint64(5)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -346,11 +394,12 @@ namespace DHT.Server.Database.Sqlite {
|
||||
private MultiDictionary<ulong, Embed> GetAllEmbeds() {
|
||||
var dict = new MultiDictionary<ulong, Embed>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT message_id, json FROM embeds");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read()) {
|
||||
ulong messageId = (ulong) reader.GetInt64(0);
|
||||
ulong messageId = reader.GetUint64(0);
|
||||
|
||||
dict.Add(messageId, new Embed {
|
||||
Json = reader.GetString(1)
|
||||
@@ -363,14 +412,15 @@ namespace DHT.Server.Database.Sqlite {
|
||||
private MultiDictionary<ulong, Reaction> GetAllReactions() {
|
||||
var dict = new MultiDictionary<ulong, Reaction>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT message_id, emoji_id, emoji_name, emoji_flags, count FROM reactions");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read()) {
|
||||
ulong messageId = (ulong) reader.GetInt64(0);
|
||||
ulong messageId = reader.GetUint64(0);
|
||||
|
||||
dict.Add(messageId, new Reaction {
|
||||
EmojiId = reader.IsDBNull(1) ? null : (ulong) reader.GetInt64(1),
|
||||
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)
|
||||
@@ -380,24 +430,20 @@ namespace DHT.Server.Database.Sqlite {
|
||||
return dict;
|
||||
}
|
||||
|
||||
private void UpdateServerStatistics() {
|
||||
using var cmd = conn.Command("SELECT COUNT(*) FROM servers");
|
||||
Statistics.TotalServers = cmd.ExecuteScalar() as long? ?? 0;
|
||||
private void UpdateServerStatistics(ISqliteConnection conn) {
|
||||
Statistics.TotalServers = conn.SelectScalar("SELECT COUNT(*) FROM servers") as long? ?? 0;
|
||||
}
|
||||
|
||||
private void UpdateChannelStatistics() {
|
||||
using var cmd = conn.Command("SELECT COUNT(*) FROM channels");
|
||||
Statistics.TotalChannels = cmd.ExecuteScalar() as long? ?? 0;
|
||||
private void UpdateChannelStatistics(ISqliteConnection conn) {
|
||||
Statistics.TotalChannels = conn.SelectScalar("SELECT COUNT(*) FROM channels") as long? ?? 0;
|
||||
}
|
||||
|
||||
private void UpdateUserStatistics() {
|
||||
using var cmd = conn.Command("SELECT COUNT(*) FROM users");
|
||||
Statistics.TotalUsers = cmd.ExecuteScalar() as long? ?? 0;
|
||||
private void UpdateUserStatistics(ISqliteConnection conn) {
|
||||
Statistics.TotalUsers = conn.SelectScalar("SELECT COUNT(*) FROM users") as long? ?? 0;
|
||||
}
|
||||
|
||||
private void UpdateMessageStatistics() {
|
||||
using var cmd = conn.Command("SELECT COUNT(*) FROM messages");
|
||||
Statistics.TotalMessages = cmd.ExecuteScalar() as long? ?? 0L;
|
||||
private void UpdateMessageStatistics(ISqliteConnection conn) {
|
||||
Statistics.TotalMessages = conn.SelectScalar("SELECT COUNT(*) FROM messages") as long? ?? 0L;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,32 +3,36 @@ using System.Collections.Generic;
|
||||
using DHT.Server.Data.Filters;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite {
|
||||
public static class SqliteMessageFilter {
|
||||
public static string GenerateWhereClause(this MessageFilter? filter, bool invert = false) {
|
||||
static class SqliteMessageFilter {
|
||||
public static string GenerateWhereClause(this MessageFilter? filter, string? tableAlias = null, bool invert = false) {
|
||||
if (filter == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (tableAlias != null) {
|
||||
tableAlias += ".";
|
||||
}
|
||||
|
||||
List<string> conditions = new();
|
||||
|
||||
if (filter.StartDate != null) {
|
||||
conditions.Add("timestamp >= " + new DateTimeOffset(filter.StartDate.Value).ToUnixTimeMilliseconds());
|
||||
conditions.Add(tableAlias + "timestamp >= " + new DateTimeOffset(filter.StartDate.Value).ToUnixTimeMilliseconds());
|
||||
}
|
||||
|
||||
if (filter.EndDate != null) {
|
||||
conditions.Add("timestamp <= " + new DateTimeOffset(filter.EndDate.Value).ToUnixTimeMilliseconds());
|
||||
conditions.Add(tableAlias + "timestamp <= " + new DateTimeOffset(filter.EndDate.Value).ToUnixTimeMilliseconds());
|
||||
}
|
||||
|
||||
if (filter.ChannelIds != null) {
|
||||
conditions.Add("channel_id IN (" + string.Join(",", filter.ChannelIds) + ")");
|
||||
conditions.Add(tableAlias + "channel_id IN (" + string.Join(",", filter.ChannelIds) + ")");
|
||||
}
|
||||
|
||||
if (filter.UserIds != null) {
|
||||
conditions.Add("sender_id IN (" + string.Join(",", filter.UserIds) + ")");
|
||||
conditions.Add(tableAlias + "sender_id IN (" + string.Join(",", filter.UserIds) + ")");
|
||||
}
|
||||
|
||||
if (filter.MessageIds != null) {
|
||||
conditions.Add("message_id IN (" + string.Join(",", filter.MessageIds) + ")");
|
||||
conditions.Add(tableAlias + "message_id IN (" + string.Join(",", filter.MessageIds) + ")");
|
||||
}
|
||||
|
||||
if (conditions.Count == 0) {
|
||||
|
@@ -1,40 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite {
|
||||
public static class SqliteUtils {
|
||||
public static SqliteCommand Command(this SqliteConnection conn, string sql) {
|
||||
var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static SqliteCommand Insert(this SqliteConnection conn, string tableName, string[] columns) {
|
||||
string columnNames = string.Join(',', columns);
|
||||
string columnParams = string.Join(',', columns.Select(c => ':' + c));
|
||||
|
||||
return conn.Command("INSERT INTO " + tableName + " (" + columnNames + ")" +
|
||||
"VALUES (" + columnParams + ")");
|
||||
}
|
||||
|
||||
public static SqliteCommand Upsert(this SqliteConnection conn, string tableName, string[] columns) {
|
||||
string columnNames = string.Join(',', columns);
|
||||
string columnParams = string.Join(',', columns.Select(c => ':' + c));
|
||||
string columnUpdates = string.Join(',', columns.Skip(1).Select(c => c + " = excluded." + c));
|
||||
|
||||
return conn.Command("INSERT INTO " + tableName + " (" + columnNames + ")" +
|
||||
"VALUES (" + columnParams + ")" +
|
||||
"ON CONFLICT (" + columns[0] + ")" +
|
||||
"DO UPDATE SET " + columnUpdates);
|
||||
}
|
||||
|
||||
public static void AddAndSet(this SqliteParameterCollection parameters, string key, object? value) {
|
||||
parameters.AddWithValue(key, value ?? DBNull.Value);
|
||||
}
|
||||
|
||||
public static void Set(this SqliteParameterCollection parameters, string key, object? value) {
|
||||
parameters[key].Value = value ?? DBNull.Value;
|
||||
}
|
||||
}
|
||||
}
|
8
app/Server/Database/Sqlite/Utils/ISqliteConnection.cs
Normal file
8
app/Server/Database/Sqlite/Utils/ISqliteConnection.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite.Utils {
|
||||
interface ISqliteConnection : IDisposable {
|
||||
SqliteConnection InnerConnection { get; }
|
||||
}
|
||||
}
|
109
app/Server/Database/Sqlite/Utils/SqliteConnectionPool.cs
Normal file
109
app/Server/Database/Sqlite/Utils/SqliteConnectionPool.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using DHT.Utils.Logging;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite.Utils {
|
||||
sealed class SqliteConnectionPool : IDisposable {
|
||||
private static string GetConnectionString(SqliteConnectionStringBuilder connectionStringBuilder) {
|
||||
connectionStringBuilder.Pooling = false;
|
||||
return connectionStringBuilder.ToString();
|
||||
}
|
||||
|
||||
private readonly object monitor = new ();
|
||||
private volatile bool isDisposed;
|
||||
|
||||
private readonly BlockingCollection<PooledConnection> free = new (new ConcurrentStack<PooledConnection>());
|
||||
private readonly List<PooledConnection> used;
|
||||
|
||||
public SqliteConnectionPool(SqliteConnectionStringBuilder connectionStringBuilder, int poolSize) {
|
||||
var connectionString = GetConnectionString(connectionStringBuilder);
|
||||
|
||||
for (int i = 0; i < poolSize; i++) {
|
||||
var conn = new SqliteConnection(connectionString);
|
||||
conn.Open();
|
||||
free.Add(new PooledConnection(this, conn));
|
||||
}
|
||||
|
||||
used = new List<PooledConnection>(poolSize);
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed() {
|
||||
if (isDisposed) {
|
||||
throw new ObjectDisposedException(nameof(SqliteConnectionPool));
|
||||
}
|
||||
}
|
||||
|
||||
public ISqliteConnection Take() {
|
||||
PooledConnection? conn = null;
|
||||
|
||||
while (conn == null) {
|
||||
ThrowIfDisposed();
|
||||
lock (monitor) {
|
||||
if (free.TryTake(out conn, TimeSpan.FromMilliseconds(100))) {
|
||||
used.Add(conn);
|
||||
break;
|
||||
}
|
||||
else {
|
||||
Log.ForType<SqliteConnectionPool>().Warn("Thread " + Thread.CurrentThread.ManagedThreadId + " is starving for connections.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
private void Return(PooledConnection conn) {
|
||||
ThrowIfDisposed();
|
||||
|
||||
lock (monitor) {
|
||||
if (used.Remove(conn)) {
|
||||
free.Add(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
isDisposed = true;
|
||||
|
||||
lock (monitor) {
|
||||
while (free.TryTake(out var conn)) {
|
||||
Close(conn.InnerConnection);
|
||||
}
|
||||
|
||||
foreach (var conn in used) {
|
||||
Close(conn.InnerConnection);
|
||||
}
|
||||
|
||||
free.Dispose();
|
||||
used.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static void Close(SqliteConnection conn) {
|
||||
conn.Close();
|
||||
conn.Dispose();
|
||||
}
|
||||
|
||||
private sealed class PooledConnection : ISqliteConnection {
|
||||
public SqliteConnection InnerConnection { get; }
|
||||
|
||||
private readonly SqliteConnectionPool pool;
|
||||
|
||||
public PooledConnection(SqliteConnectionPool pool, SqliteConnection conn) {
|
||||
this.pool = pool;
|
||||
this.InnerConnection = conn;
|
||||
}
|
||||
|
||||
void IDisposable.Dispose() {
|
||||
pool.Return(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
67
app/Server/Database/Sqlite/Utils/SqliteExtensions.cs
Normal file
67
app/Server/Database/Sqlite/Utils/SqliteExtensions.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite.Utils {
|
||||
static class SqliteExtensions {
|
||||
public static SqliteCommand Command(this ISqliteConnection conn, string sql) {
|
||||
var cmd = conn.InnerConnection.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static SqliteTransaction BeginTransaction(this ISqliteConnection conn) {
|
||||
return conn.InnerConnection.BeginTransaction();
|
||||
}
|
||||
|
||||
public static object? SelectScalar(this ISqliteConnection conn, string sql) {
|
||||
using var cmd = conn.Command(sql);
|
||||
return cmd.ExecuteScalar();
|
||||
}
|
||||
|
||||
public static SqliteCommand Insert(this ISqliteConnection conn, string tableName, (string Name, SqliteType Type)[] columns) {
|
||||
string columnNames = string.Join(',', columns.Select(static c => c.Name));
|
||||
string columnParams = string.Join(',', columns.Select(static c => ':' + c.Name));
|
||||
|
||||
var cmd = conn.Command("INSERT INTO " + tableName + " (" + columnNames + ")" +
|
||||
"VALUES (" + columnParams + ")");
|
||||
|
||||
CreateParameters(cmd, columns);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static SqliteCommand Upsert(this ISqliteConnection conn, string tableName, (string Name, SqliteType Type)[] columns) {
|
||||
string columnNames = string.Join(',', columns.Select(static c => c.Name));
|
||||
string columnParams = string.Join(',', columns.Select(static c => ':' + c.Name));
|
||||
string columnUpdates = string.Join(',', columns.Skip(1).Select(static c => c.Name + " = excluded." + c.Name));
|
||||
|
||||
var cmd = conn.Command("INSERT INTO " + tableName + " (" + columnNames + ")" +
|
||||
"VALUES (" + columnParams + ")" +
|
||||
"ON CONFLICT (" + columns[0].Name + ")" +
|
||||
"DO UPDATE SET " + columnUpdates);
|
||||
|
||||
CreateParameters(cmd, columns);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static SqliteCommand Delete(this ISqliteConnection conn, string tableName, (string Name, SqliteType Type) column) {
|
||||
var cmd = conn.Command("DELETE FROM " + tableName + " WHERE " + column.Name + " = :" + column.Name);
|
||||
CreateParameters(cmd, new [] { column });
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private static void CreateParameters(SqliteCommand cmd, (string Name, SqliteType Type)[] columns) {
|
||||
foreach (var (name, type) in columns) {
|
||||
cmd.Parameters.Add(":" + name, type);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Set(this SqliteCommand cmd, string key, object? value) {
|
||||
cmd.Parameters[key].Value = value ?? DBNull.Value;
|
||||
}
|
||||
|
||||
public static ulong GetUint64(this SqliteDataReader reader, int ordinal) {
|
||||
return (ulong) reader.GetInt64(ordinal);
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,13 +3,16 @@ using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Logging;
|
||||
using DHT.Server.Service;
|
||||
using DHT.Utils.Http;
|
||||
using DHT.Utils.Logging;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
|
||||
namespace DHT.Server.Endpoints {
|
||||
public abstract class BaseEndpoint {
|
||||
abstract class BaseEndpoint {
|
||||
private static readonly Log Log = Log.ForType<BaseEndpoint>();
|
||||
|
||||
protected IDatabaseFile Db { get; }
|
||||
private readonly ServerParameters parameters;
|
||||
|
||||
|
@@ -3,12 +3,12 @@ using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Json;
|
||||
using DHT.Server.Service;
|
||||
using DHT.Utils.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DHT.Server.Endpoints {
|
||||
public class TrackChannelEndpoint : BaseEndpoint {
|
||||
sealed class TrackChannelEndpoint : BaseEndpoint {
|
||||
public TrackChannelEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
|
||||
|
||||
protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) {
|
||||
|
@@ -7,12 +7,12 @@ using System.Threading.Tasks;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Data.Filters;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Json;
|
||||
using DHT.Server.Service;
|
||||
using DHT.Utils.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DHT.Server.Endpoints {
|
||||
public class TrackMessagesEndpoint : BaseEndpoint {
|
||||
sealed class TrackMessagesEndpoint : BaseEndpoint {
|
||||
public TrackMessagesEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
|
||||
|
||||
protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) {
|
||||
|
@@ -3,12 +3,12 @@ using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Json;
|
||||
using DHT.Server.Service;
|
||||
using DHT.Utils.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DHT.Server.Endpoints {
|
||||
public class TrackUsersEndpoint : BaseEndpoint {
|
||||
sealed class TrackUsersEndpoint : BaseEndpoint {
|
||||
public TrackUsersEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
|
||||
|
||||
protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) {
|
||||
|
@@ -1,31 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace DHT.Server.Logging {
|
||||
public static class Log {
|
||||
private static void LogLevel(ConsoleColor color, string level, string text) {
|
||||
Console.ForegroundColor = color;
|
||||
foreach (string line in text.Replace("\r", "").Split('\n')) {
|
||||
string formatted = $"[{level}] {line}";
|
||||
Console.WriteLine(formatted);
|
||||
Trace.WriteLine(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Info(string message) {
|
||||
LogLevel(ConsoleColor.Blue, "INFO", message);
|
||||
}
|
||||
|
||||
public static void Warn(string message) {
|
||||
LogLevel(ConsoleColor.Yellow, "WARN", message);
|
||||
}
|
||||
|
||||
public static void Error(string message) {
|
||||
LogLevel(ConsoleColor.Red, "ERROR", message);
|
||||
}
|
||||
|
||||
public static void Error(Exception e) {
|
||||
LogLevel(ConsoleColor.Red, "ERROR", e.ToString());
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,19 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>DHT.Server</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>DiscordHistoryTracker.Server</AssemblyName>
|
||||
<PackageId>DiscordHistoryTrackerServer</PackageId>
|
||||
<Authors>chylex</Authors>
|
||||
<Company>DiscordHistoryTracker</Company>
|
||||
<Product>DiscordHistoryTrackerServer</Product>
|
||||
<Version>33.0.0.0</Version>
|
||||
<AssemblyVersion>$(Version)</AssemblyVersion>
|
||||
<FileVersion>$(Version)</FileVersion>
|
||||
<PackageVersion>$(Version)</PackageVersion>
|
||||
<GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
|
||||
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
|
||||
<GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>none</DebugType>
|
||||
@@ -22,6 +20,12 @@
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Utils\Utils.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Version.cs" Link="Version.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@@ -3,7 +3,7 @@ using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Logging;
|
||||
using DHT.Utils.Logging;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
@@ -11,6 +11,8 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DHT.Server.Service {
|
||||
public static class ServerLauncher {
|
||||
private static readonly Log Log = Log.ForType(typeof(ServerLauncher));
|
||||
|
||||
private static IWebHost? Server { get; set; } = null;
|
||||
|
||||
public static bool IsRunning { get; private set; }
|
||||
@@ -27,6 +29,7 @@ namespace DHT.Server.Service {
|
||||
try {
|
||||
if (ManagementThread == null) {
|
||||
ManagementThread = new Thread(RunManagementThread) {
|
||||
Name = "DHT server management thread",
|
||||
IsBackground = true
|
||||
};
|
||||
ManagementThread.Start();
|
||||
@@ -70,7 +73,7 @@ namespace DHT.Server.Service {
|
||||
void SetKestrelOptions(KestrelServerOptions options) {
|
||||
options.Limits.MaxRequestBodySize = null;
|
||||
options.Limits.MinResponseDataRate = null;
|
||||
options.ListenLocalhost(port, listenOptions => listenOptions.Protocols = HttpProtocols.Http1);
|
||||
options.ListenLocalhost(port, static listenOptions => listenOptions.Protocols = HttpProtocols.Http1);
|
||||
}
|
||||
|
||||
Server = WebHost.CreateDefaultBuilder()
|
||||
@@ -90,11 +93,12 @@ namespace DHT.Server.Service {
|
||||
if (Server != null) {
|
||||
Log.Info("Stopping server...");
|
||||
Server.StopAsync().Wait();
|
||||
Server.Dispose();
|
||||
Server = null;
|
||||
|
||||
Log.Info("Server stopped");
|
||||
IsRunning = false;
|
||||
ServerStatusChanged?.Invoke(null, EventArgs.Empty);
|
||||
Server = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
namespace DHT.Server.Service {
|
||||
public struct ServerParameters {
|
||||
readonly struct ServerParameters {
|
||||
public string Token { get; init; }
|
||||
}
|
||||
}
|
||||
|
@@ -8,14 +8,14 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace DHT.Server.Service {
|
||||
public class Startup {
|
||||
sealed class Startup {
|
||||
public void ConfigureServices(IServiceCollection services) {
|
||||
services.Configure<JsonOptions>(options => {
|
||||
services.Configure<JsonOptions>(static options => {
|
||||
options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict;
|
||||
});
|
||||
|
||||
services.AddCors(cors => {
|
||||
cors.AddDefaultPolicy(builder => {
|
||||
services.AddCors(static cors => {
|
||||
cors.AddDefaultPolicy(static builder => {
|
||||
builder.WithOrigins("https://discord.com", "https://discordapp.com").AllowCredentials().AllowAnyMethod().AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
@@ -7,15 +7,15 @@ using System.Text.RegularExpressions;
|
||||
|
||||
namespace DHT.Server.Service {
|
||||
public static class ServerUtils {
|
||||
public static int FindAvailablePort(int min, int max) {
|
||||
public static ushort FindAvailablePort(ushort min, ushort max) {
|
||||
var properties = IPGlobalProperties.GetIPGlobalProperties();
|
||||
var occupied = new HashSet<int>();
|
||||
occupied.UnionWith(properties.GetActiveTcpListeners().Select(tcp => tcp.Port));
|
||||
occupied.UnionWith(properties.GetActiveTcpConnections().Select(tcp => tcp.LocalEndPoint.Port));
|
||||
occupied.UnionWith(properties.GetActiveTcpListeners().Select(static tcp => tcp.Port));
|
||||
occupied.UnionWith(properties.GetActiveTcpConnections().Select(static tcp => tcp.LocalEndPoint.Port));
|
||||
|
||||
for (int port = min; port < max; port++) {
|
||||
if (!occupied.Contains(port)) {
|
||||
return port;
|
||||
return (ushort) port;
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user