1
0
mirror of https://github.com/chylex/Discord-History-Tracker.git synced 2025-08-17 19:31:42 +02:00

19 Commits
v33 ... v33.2

Author SHA1 Message Date
8002236c1f Release v33.2 (app) 2022-02-27 17:09:51 +01:00
c4fe6c4391 Move app version info out of .csproj and into a single linked .cs file 2022-02-27 16:20:39 +01:00
ebfe972a98 Update uses of Avalonia APIs & safeguard clipboard code 2022-02-27 15:29:54 +01:00
20aac4c47a Update Avalonia to 0.10.12 2022-02-27 15:21:38 +01:00
35308e0995 Add option to re-enable Ctrl+Shift+I in the Discord app 2022-02-27 15:08:56 +01:00
f7f32c3f6a Fix Avalonia designer not seeing custom windows and controls 2022-02-27 11:37:08 +01:00
4dc781b35c Address Rider inspections 2022-02-21 22:27:29 +01:00
849ef18adb Reorganize namespaces and move some classes to a separate Utils project 2022-02-21 22:27:01 +01:00
77aa15e557 Add database file name to the app title
References #165
2022-02-20 20:03:59 +01:00
47b106503d Fix database path being editable in the Database tab in the app 2022-02-20 19:55:32 +01:00
bde4cb06f4 Dispose of all window properties when the DHT panel is closed (app) 2022-02-12 20:43:35 +01:00
d772f7ed71 Fix calling clearTimeout instead of clearInterval in app script (no difference according to spec, but cleaner) 2022-02-12 20:40:57 +01:00
0662af9b1a Update website (put desktop app on top, add anchor links) 2022-02-12 19:17:37 +01:00
03fd730139 Release v.31a (browser script) 2022-02-12 18:37:39 +01:00
d362c96b80 Fix skipping to next channel not working after a Discord update (browser script) 2022-02-12 18:35:47 +01:00
9f34c9dffa Fix occasional skipping of messages when autoscrolling in unfocused browser (browser script) 2022-02-12 18:34:43 +01:00
cacf43d1d8 Fix broken channel detection after a Discord update (browser script)
#161
2022-02-12 17:38:45 +01:00
edc23d616d Release v33.1 (app) 2022-02-12 11:23:24 +01:00
db191f87fd Fix not finding selected DM channel after a Discord update
Closes #161
2022-02-12 11:15:33 +01:00
94 changed files with 1023 additions and 604 deletions

View File

@@ -4,9 +4,9 @@
<option name="projectPerEditor"> <option name="projectPerEditor">
<map> <map>
<entry key="Desktop/App.axaml" value="Desktop/Desktop.csproj" /> <entry key="Desktop/App.axaml" value="Desktop/Desktop.csproj" />
<entry key="Desktop/Dialogs/CheckBoxDialog.axaml" value="Desktop/Desktop.csproj" /> <entry key="Desktop/Dialogs/CheckBox/CheckBoxDialog.axaml" value="Desktop/Desktop.csproj" />
<entry key="Desktop/Dialogs/MessageDialog.axaml" value="Desktop/Desktop.csproj" /> <entry key="Desktop/Dialogs/Message/MessageDialog.axaml" value="Desktop/Desktop.csproj" />
<entry key="Desktop/Dialogs/ProgressDialog.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/AboutWindow.axaml" value="Desktop/Desktop.csproj" />
<entry key="Desktop/Main/Controls/FilterPanel.axaml" value="Desktop/Desktop.csproj" /> <entry key="Desktop/Main/Controls/FilterPanel.axaml" value="Desktop/Desktop.csproj" />
<entry key="Desktop/Main/Controls/StatusBar.axaml" value="Desktop/Desktop.csproj" /> <entry key="Desktop/Main/Controls/StatusBar.axaml" value="Desktop/Desktop.csproj" />

View File

@@ -4,7 +4,7 @@ using Avalonia.Markup.Xaml;
using DHT.Desktop.Main; using DHT.Desktop.Main;
namespace DHT.Desktop { namespace DHT.Desktop {
public class App : Application { sealed class App : Application {
public override void Initialize() { public override void Initialize() {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }

View File

@@ -1,8 +1,10 @@
using System; using System;
using DHT.Server.Logging; using DHT.Utils.Logging;
namespace DHT.Desktop { 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 static Arguments Empty => new(Array.Empty<string>());
public string? DatabaseFile { get; } public string? DatabaseFile { get; }

View File

@@ -3,14 +3,16 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using DHT.Desktop.Dialogs; using DHT.Desktop.Dialogs.Message;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Server.Database.Exceptions; using DHT.Server.Database.Exceptions;
using DHT.Server.Database.Sqlite; using DHT.Server.Database.Sqlite;
using DHT.Server.Logging; using DHT.Utils.Logging;
namespace DHT.Desktop.Common { 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 const string DatabaseFileInitialName = "archive.dht";
private static readonly List<FileDialogFilter> DatabaseFileDialogFilter = new() { private static readonly List<FileDialogFilter> DatabaseFileDialogFilter = new() {

View File

@@ -3,12 +3,12 @@ using System.Globalization;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
namespace DHT.Desktop.Common { namespace DHT.Desktop.Common {
public class NumberValueConverter : IValueConverter { sealed class NumberValueConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
return string.Format(Program.Culture, "{0:n0}", value); 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(); throw new NotSupportedException();
} }
} }

View File

@@ -1,5 +1,5 @@
namespace DHT.Desktop.Common { namespace DHT.Desktop.Common {
public static class TextFormat { static class TextFormat {
public static string Format(this int number) { public static string Format(this int number) {
return number.ToString("N0", Program.Culture); return number.ToString("N0", Program.Culture);
} }

View File

@@ -12,29 +12,39 @@
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<AssemblyName>DiscordHistoryTracker</AssemblyName> <AssemblyName>DiscordHistoryTracker</AssemblyName>
<Version>33.0.0.0</Version> <GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
<AssemblyVersion>$(Version)</AssemblyVersion> <GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
<FileVersion>$(Version)</FileVersion> <GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
<PackageVersion>$(Version)</PackageVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' "> <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
<DebugType>none</DebugType> <DebugType>none</DebugType>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.10" /> <PackageReference Include="Avalonia" Version="0.10.12" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.10" /> <PackageReference Include="Avalonia.Desktop" Version="0.10.12" />
<ProjectReference Include="..\Server\Server.csproj" /> <ProjectReference Include="..\Server\Server.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.10" /> <PackageReference Include="Avalonia.Diagnostics" Version="0.10.12" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Version.cs" Link="Version.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Windows\MainWindow.axaml.cs"> <Compile Update="Windows\MainWindow.axaml.cs">
<DependentUpon>MainWindow.axaml</DependentUpon> <DependentUpon>MainWindow.axaml</DependentUpon>
<SubType>Code</SubType> <SubType>Code</SubType>
</Compile> </Compile>
<Compile Update="Dialogs\MessageDialog.axaml.cs"> <Compile Update="Dialogs\CheckBox\CheckBoxDialog.axaml.cs">
<DependentUpon>CheckBoxDialog.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Dialogs\Progress\ProgressDialog.axaml.cs">
<DependentUpon>ProgressDialog.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Dialogs\Message\MessageDialog.axaml.cs">
<DependentUpon>MessageDialog.axaml</DependentUpon> <DependentUpon>MessageDialog.axaml</DependentUpon>
<SubType>Code</SubType> <SubType>Code</SubType>
</Compile> </Compile>

View File

@@ -2,16 +2,16 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 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" mc:Ignorable="d" d:DesignWidth="500"
x:Class="DHT.Desktop.Dialogs.CheckBoxDialog" x:Class="DHT.Desktop.Dialogs.CheckBox.CheckBoxDialog"
Title="{Binding Title}" Title="{Binding Title}"
Icon="avares://DiscordHistoryTracker/Resources/icon.ico" Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
Width="500" SizeToContent="Height" CanResize="False" Width="500" SizeToContent="Height" CanResize="False"
WindowStartupLocation="CenterOwner"> WindowStartupLocation="CenterOwner">
<Window.DataContext> <Window.DataContext>
<dialogs:CheckBoxDialogModel /> <namespace:CheckBoxDialogModel />
</Window.DataContext> </Window.DataContext>
<Window.Styles> <Window.Styles>

View File

@@ -1,10 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using DHT.Desktop.Dialogs.Message;
namespace DHT.Desktop.Dialogs { namespace DHT.Desktop.Dialogs.CheckBox {
public class CheckBoxDialog : Window { [SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class CheckBoxDialog : Window {
public CheckBoxDialog() { public CheckBoxDialog() {
InitializeComponent(); InitializeComponent();
#if DEBUG #if DEBUG

View File

@@ -2,10 +2,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using DHT.Desktop.Models; using DHT.Utils.Models;
namespace DHT.Desktop.Dialogs { namespace DHT.Desktop.Dialogs.CheckBox {
public class CheckBoxDialogModel : BaseModel { class CheckBoxDialogModel : BaseModel {
public string Title { get; init; } = ""; public string Title { get; init; } = "";
private IReadOnlyList<CheckBoxItem> items = Array.Empty<CheckBoxItem>(); private IReadOnlyList<CheckBoxItem> items = Array.Empty<CheckBoxItem>();
@@ -28,8 +28,8 @@ namespace DHT.Desktop.Dialogs {
private bool pauseCheckEvents = false; private bool pauseCheckEvents = false;
public bool AreAllSelected => Items.All(item => item.Checked); public bool AreAllSelected => Items.All(static item => item.Checked);
public bool AreNoneSelected => Items.All(item => !item.Checked); public bool AreNoneSelected => Items.All(static item => !item.Checked);
public void SelectAll() => SetAllChecked(true); public void SelectAll() => SetAllChecked(true);
public void SelectNone() => SetAllChecked(false); 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 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) { public CheckBoxDialogModel(IEnumerable<CheckBoxItem<T>> items) {
this.Items = new List<CheckBoxItem<T>>(items); this.Items = new List<CheckBoxItem<T>>(items);

View File

@@ -1,7 +1,7 @@
using DHT.Desktop.Models; using DHT.Utils.Models;
namespace DHT.Desktop.Dialogs { namespace DHT.Desktop.Dialogs.CheckBox {
public class CheckBoxItem : BaseModel { class CheckBoxItem : BaseModel {
public string Title { get; init; } = ""; public string Title { get; init; } = "";
public object? Item { get; init; } = null; public object? Item { get; init; } = null;
@@ -13,7 +13,7 @@ namespace DHT.Desktop.Dialogs {
} }
} }
public class CheckBoxItem<T> : CheckBoxItem { sealed class CheckBoxItem<T> : CheckBoxItem {
public new T Item { get; } public new T Item { get; }
public CheckBoxItem(T item) { public CheckBoxItem(T item) {

View File

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

View File

@@ -1,7 +1,7 @@
using System; using System;
namespace DHT.Desktop.Dialogs { namespace DHT.Desktop.Dialogs.Message {
public static class DialogResult { static class DialogResult {
public enum All { public enum All {
Ok, Ok,
Yes, Yes,

View File

@@ -2,16 +2,16 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 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" mc:Ignorable="d" d:DesignWidth="500"
x:Class="DHT.Desktop.Dialogs.MessageDialog" x:Class="DHT.Desktop.Dialogs.Message.MessageDialog"
Title="{Binding Title}" Title="{Binding Title}"
Icon="avares://DiscordHistoryTracker/Resources/icon.ico" Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
Width="500" SizeToContent="Height" CanResize="False" Width="500" SizeToContent="Height" CanResize="False"
WindowStartupLocation="CenterOwner"> WindowStartupLocation="CenterOwner">
<Window.DataContext> <Window.DataContext>
<dialogs:MessageDialogModel /> <namespace:MessageDialogModel />
</Window.DataContext> </Window.DataContext>
<Window.Styles> <Window.Styles>

View File

@@ -1,10 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Dialogs { namespace DHT.Desktop.Dialogs.Message {
public class MessageDialog : Window { [SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class MessageDialog : Window {
public MessageDialog() { public MessageDialog() {
InitializeComponent(); InitializeComponent();
#if DEBUG #if DEBUG

View File

@@ -1,5 +1,5 @@
namespace DHT.Desktop.Dialogs { namespace DHT.Desktop.Dialogs.Message {
public class MessageDialogModel { sealed class MessageDialogModel {
public string Title { get; init; } = ""; public string Title { get; init; } = "";
public string Message { get; init; } = ""; public string Message { get; init; } = "";

View File

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

View File

@@ -2,9 +2,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 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" mc:Ignorable="d" d:DesignWidth="500"
x:Class="DHT.Desktop.Dialogs.ProgressDialog" x:Class="DHT.Desktop.Dialogs.Progress.ProgressDialog"
Title="{Binding Title}" Title="{Binding Title}"
Icon="avares://DiscordHistoryTracker/Resources/icon.ico" Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
Opened="Loaded" Opened="Loaded"
@@ -13,7 +13,7 @@
WindowStartupLocation="CenterOwner"> WindowStartupLocation="CenterOwner">
<Window.DataContext> <Window.DataContext>
<dialogs:ProgressDialogModel /> <namespace:ProgressDialogModel />
</Window.DataContext> </Window.DataContext>
<Window.Styles> <Window.Styles>

View File

@@ -1,12 +1,14 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Dialogs { namespace DHT.Desktop.Dialogs.Progress {
public class ProgressDialog : Window { [SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class ProgressDialog : Window {
private bool isFinished = false; private bool isFinished = false;
public ProgressDialog() { public ProgressDialog() {

View File

@@ -2,10 +2,10 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Threading; using Avalonia.Threading;
using DHT.Desktop.Common; using DHT.Desktop.Common;
using DHT.Desktop.Models; using DHT.Utils.Models;
namespace DHT.Desktop.Dialogs { namespace DHT.Desktop.Dialogs.Progress {
public class ProgressDialogModel : BaseModel { sealed class ProgressDialogModel : BaseModel {
public string Title { get; init; } = ""; public string Title { get; init; } = "";
private string message = ""; private string message = "";
@@ -46,7 +46,7 @@ namespace DHT.Desktop.Dialogs {
public delegate Task TaskRunner(IProgressCallback callback); public delegate Task TaskRunner(IProgressCallback callback);
private class Callback : IProgressCallback { private sealed class Callback : IProgressCallback {
private readonly ProgressDialogModel model; private readonly ProgressDialogModel model;
public Callback(ProgressDialogModel model) { public Callback(ProgressDialogModel model) {

View 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 });
}
}
}

View File

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

View File

@@ -1,9 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main { namespace DHT.Desktop.Main {
public class AboutWindow : Window { [SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class AboutWindow : Window {
public AboutWindow() { public AboutWindow() {
InitializeComponent(); InitializeComponent();
#if DEBUG #if DEBUG

View File

@@ -1,7 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
namespace DHT.Desktop.Main { namespace DHT.Desktop.Main {
public class AboutWindowModel { sealed class AboutWindowModel {
public void ShowOfficialWebsite() { public void ShowOfficialWebsite() {
OpenUrl("https://dht.chylex.com"); OpenUrl("https://dht.chylex.com");
} }

View File

@@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Controls { 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 StartDatePicker => this.FindControl<CalendarDatePicker>("StartDatePicker");
private CalendarDatePicker EndDatePicker => this.FindControl<CalendarDatePicker>("EndDatePicker"); private CalendarDatePicker EndDatePicker => this.FindControl<CalendarDatePicker>("EndDatePicker");

View File

@@ -6,14 +6,15 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using DHT.Desktop.Common; using DHT.Desktop.Common;
using DHT.Desktop.Dialogs; using DHT.Desktop.Dialogs.CheckBox;
using DHT.Desktop.Models; using DHT.Desktop.Dialogs.Message;
using DHT.Server.Data; using DHT.Server.Data;
using DHT.Server.Data.Filters; using DHT.Server.Data.Filters;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Utils.Models;
namespace DHT.Desktop.Main.Controls { namespace DHT.Desktop.Main.Controls {
public class FilterPanelModel : BaseModel { sealed class FilterPanelModel : BaseModel {
private static readonly HashSet<string> FilterProperties = new () { private static readonly HashSet<string> FilterProperties = new () {
nameof(FilterByDate), nameof(FilterByDate),
nameof(StartDate), nameof(StartDate),
@@ -57,7 +58,7 @@ namespace DHT.Desktop.Main.Controls {
} }
public HashSet<ulong> IncludedChannels { 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); set => Change(ref includedChannels, value);
} }
@@ -67,7 +68,7 @@ namespace DHT.Desktop.Main.Controls {
} }
public HashSet<ulong> IncludedUsers { 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); set => Change(ref includedUsers, value);
} }
@@ -125,7 +126,7 @@ namespace DHT.Desktop.Main.Controls {
} }
public async void OpenChannelFilterDialog() { 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 items = new List<CheckBoxItem<ulong>>();
var included = IncludedChannels; var included = IncludedChannels;
@@ -221,7 +222,7 @@ namespace DHT.Desktop.Main.Controls {
} }
private static async Task<HashSet<ulong>?> OpenIdFilterDialog(Window window, string title, List<CheckBoxItem<ulong>> items) { 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) { var model = new CheckBoxDialogModel<ulong>(items) {
Title = title Title = title
@@ -230,7 +231,7 @@ namespace DHT.Desktop.Main.Controls {
var dialog = new CheckBoxDialog { DataContext = model }; var dialog = new CheckBoxDialog { DataContext = model };
var result = await dialog.ShowDialog<DialogResult.OkCancel>(window); 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;
} }
} }
} }

View File

@@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Controls { namespace DHT.Desktop.Main.Controls {
public class StatusBar : UserControl { [SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class StatusBar : UserControl {
public StatusBar() { public StatusBar() {
InitializeComponent(); InitializeComponent();
} }

View File

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

View File

@@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main { namespace DHT.Desktop.Main {
public class MainContentScreen : UserControl { [SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class MainContentScreen : UserControl {
public MainContentScreen() { public MainContentScreen() {
InitializeComponent(); InitializeComponent();
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using DHT.Desktop.Main.Controls; using DHT.Desktop.Main.Controls;
using DHT.Desktop.Main.Pages; using DHT.Desktop.Main.Pages;
@@ -6,7 +7,7 @@ using DHT.Server.Database;
using DHT.Server.Service; using DHT.Server.Service;
namespace DHT.Desktop.Main { namespace DHT.Desktop.Main {
public class MainContentScreenModel : IDisposable { sealed class MainContentScreenModel : IDisposable {
public DatabasePage DatabasePage { get; } public DatabasePage DatabasePage { get; }
private DatabasePageModel DatabasePageModel { get; } private DatabasePageModel DatabasePageModel { get; }
@@ -19,8 +20,12 @@ namespace DHT.Desktop.Main {
public StatusBarModel StatusBarModel { get; } public StatusBarModel StatusBarModel { get; }
public event EventHandler? DatabaseClosed { public event EventHandler? DatabaseClosed {
add { DatabasePageModel.DatabaseClosed += value; } add {
remove { DatabasePageModel.DatabaseClosed -= value; } DatabasePageModel.DatabaseClosed += value;
}
remove {
DatabasePageModel.DatabaseClosed -= value;
}
} }
[Obsolete("Designer")] [Obsolete("Designer")]
@@ -41,8 +46,8 @@ namespace DHT.Desktop.Main {
StatusBarModel.CurrentStatus = ServerLauncher.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped; StatusBarModel.CurrentStatus = ServerLauncher.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped;
} }
public void Initialize() { public async Task Initialize() {
TrackingPageModel.Initialize(); await TrackingPageModel.Initialize();
} }
private void TrackingPageModelOnServerStatusChanged(object? sender, StatusBarModel.Status e) { private void TrackingPageModelOnServerStatusChanged(object? sender, StatusBarModel.Status e) {
@@ -51,7 +56,6 @@ namespace DHT.Desktop.Main {
public void Dispose() { public void Dispose() {
TrackingPageModel.Dispose(); TrackingPageModel.Dispose();
GC.SuppressFinalize(this);
} }
} }
} }

View File

@@ -5,7 +5,7 @@
xmlns:main="clr-namespace:DHT.Desktop.Main" xmlns:main="clr-namespace:DHT.Desktop.Main"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="DHT.Desktop.Main.MainWindow" x:Class="DHT.Desktop.Main.MainWindow"
Title="Discord History Tracker" Title="{Binding Title}"
Icon="avares://DiscordHistoryTracker/Resources/icon.ico" Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
Width="800" Height="500" Width="800" Height="500"
MinWidth="480" MinHeight="240" MinWidth="480" MinHeight="240"

View File

@@ -1,16 +1,18 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using JetBrains.Annotations; using JetBrains.Annotations;
namespace DHT.Desktop.Main { namespace DHT.Desktop.Main {
public class MainWindow : Window { [SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class MainWindow : Window {
[UsedImplicitly] [UsedImplicitly]
public MainWindow() { public MainWindow() {
InitializeComponent(Arguments.Empty); InitializeComponent(Arguments.Empty);
} }
public MainWindow(Arguments args) { internal MainWindow(Arguments args) {
InitializeComponent(args); InitializeComponent(args);
} }

View File

@@ -4,13 +4,17 @@ using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using DHT.Desktop.Dialogs; using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Main.Pages; using DHT.Desktop.Main.Pages;
using DHT.Desktop.Models;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Utils.Models;
namespace DHT.Desktop.Main { namespace DHT.Desktop.Main {
public class MainWindowModel : BaseModel { sealed class MainWindowModel : BaseModel {
private const string DefaultTitle = "Discord History Tracker";
public string Title { get; private set; } = DefaultTitle;
public WelcomeScreen WelcomeScreen { get; } public WelcomeScreen WelcomeScreen { get; }
private WelcomeScreenModel WelcomeScreenModel { get; } private WelcomeScreenModel WelcomeScreenModel { get; }
@@ -65,7 +69,7 @@ namespace DHT.Desktop.Main {
} }
} }
private void WelcomeScreenModelOnPropertyChanged(object? sender, PropertyChangedEventArgs e) { private async void WelcomeScreenModelOnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
if (e.PropertyName == nameof(WelcomeScreenModel.Db)) { if (e.PropertyName == nameof(WelcomeScreenModel.Db)) {
if (MainContentScreenModel != null) { if (MainContentScreenModel != null) {
MainContentScreenModel.DatabaseClosed -= MainContentScreenModelOnDatabaseClosed; MainContentScreenModel.DatabaseClosed -= MainContentScreenModelOnDatabaseClosed;
@@ -76,12 +80,14 @@ namespace DHT.Desktop.Main {
db = WelcomeScreenModel.Db; db = WelcomeScreenModel.Db;
if (db == null) { if (db == null) {
Title = DefaultTitle;
MainContentScreenModel = null; MainContentScreenModel = null;
MainContentScreen = null; MainContentScreen = null;
} }
else { else {
Title = Path.GetFileName(db.Path) + " - " + DefaultTitle;
MainContentScreenModel = new MainContentScreenModel(window, db); MainContentScreenModel = new MainContentScreenModel(window, db);
MainContentScreenModel.Initialize(); await MainContentScreenModel.Initialize();
MainContentScreenModel.DatabaseClosed += MainContentScreenModelOnDatabaseClosed; MainContentScreenModel.DatabaseClosed += MainContentScreenModelOnDatabaseClosed;
MainContentScreen = new MainContentScreen { DataContext = MainContentScreenModel }; MainContentScreen = new MainContentScreen { DataContext = MainContentScreenModel };
OnPropertyChanged(nameof(MainContentScreen)); OnPropertyChanged(nameof(MainContentScreen));
@@ -89,6 +95,7 @@ namespace DHT.Desktop.Main {
OnPropertyChanged(nameof(ShowWelcomeScreen)); OnPropertyChanged(nameof(ShowWelcomeScreen));
OnPropertyChanged(nameof(ShowMainContentScreen)); OnPropertyChanged(nameof(ShowMainContentScreen));
OnPropertyChanged(nameof(Title));
window.Focus(); window.Focus();
} }

View File

@@ -19,7 +19,7 @@
<StackPanel Spacing="10"> <StackPanel Spacing="10">
<DockPanel> <DockPanel>
<Button Command="{Binding CloseDatabase}" DockPanel.Dock="Right">Close Database</Button> <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> </DockPanel>
<WrapPanel> <WrapPanel>
<Button Command="{Binding OpenDatabaseFolder}">Open Database Folder</Button> <Button Command="{Binding OpenDatabaseFolder}">Open Database Folder</Button>

View File

@@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Pages { namespace DHT.Desktop.Main.Pages {
public class DatabasePage : UserControl { [SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class DatabasePage : UserControl {
public DatabasePage() { public DatabasePage() {
InitializeComponent(); InitializeComponent();
} }

View File

@@ -5,14 +5,17 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using DHT.Desktop.Common; using DHT.Desktop.Common;
using DHT.Desktop.Dialogs; using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Models; using DHT.Desktop.Dialogs.Progress;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Server.Logging;
using DHT.Server.Service; using DHT.Server.Service;
using DHT.Utils.Logging;
using DHT.Utils.Models;
namespace DHT.Desktop.Main.Pages { 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 IDatabaseFile Db { get; }
public event EventHandler? DatabaseClosed; public event EventHandler? DatabaseClosed;

View File

@@ -23,13 +23,13 @@
<StackPanel Spacing="10"> <StackPanel Spacing="10">
<TextBlock TextWrapping="Wrap"> <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> </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 x:Name="CopyTrackingScript" Click="CopyTrackingScriptButton_OnClick">Copy Tracking Script</Button>
<Button Command="{Binding OnClickToggleButton}" Content="{Binding ToggleButtonText}" IsEnabled="{Binding IsToggleButtonEnabled}" /> <Button Command="{Binding OnClickToggleTrackingButton}" Content="{Binding ToggleTrackingButtonText}" IsEnabled="{Binding IsToggleButtonEnabled}" />
</StackPanel> </StackPanel>
<Expander Header="Advanced Settings"> <Expander Header="Advanced Tracking Settings">
<StackPanel Spacing="10"> <StackPanel Spacing="10">
<TextBlock TextWrapping="Wrap"> <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. 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.
@@ -55,6 +55,10 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</Expander> </Expander>
<TextBlock TextWrapping="Wrap" Margin="0 15 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> </StackPanel>
</UserControl> </UserControl>

View File

@@ -1,11 +1,13 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Pages { namespace DHT.Desktop.Main.Pages {
public class TrackingPage : UserControl { [SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class TrackingPage : UserControl {
private bool isCopyingScript; private bool isCopyingScript;
public TrackingPage() { public TrackingPage() {
@@ -22,9 +24,7 @@ namespace DHT.Desktop.Main.Pages {
var originalText = button.Content; var originalText = button.Content;
button.MinWidth = button.Bounds.Width; button.MinWidth = button.Bounds.Width;
await model.OnClickCopyTrackingScript(); if (await model.OnClickCopyTrackingScript() && !isCopyingScript) {
if (!isCopyingScript) {
isCopyingScript = true; isCopyingScript = true;
button.Content = "Script Copied!"; button.Content = "Script Copied!";

View File

@@ -3,16 +3,19 @@ using System.Threading.Tasks;
using System.Web; using System.Web;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using DHT.Desktop.Dialogs; using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Discord;
using DHT.Desktop.Main.Controls; using DHT.Desktop.Main.Controls;
using DHT.Desktop.Models;
using DHT.Desktop.Resources;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Server.Logging;
using DHT.Server.Service; using DHT.Server.Service;
using DHT.Utils.Logging;
using DHT.Utils.Models;
using static DHT.Desktop.Program;
namespace DHT.Desktop.Main.Pages { namespace DHT.Desktop.Main.Pages {
public class TrackingPageModel : BaseModel, IDisposable { sealed class TrackingPageModel : BaseModel, IDisposable {
private static readonly Log Log = Log.ForType<TrackingPageModel>();
internal static string ServerPort { get; set; } = ServerUtils.FindAvailablePort(50000, 60000).ToString(); internal static string ServerPort { get; set; } = ServerUtils.FindAvailablePort(50000, 60000).ToString();
internal static string ServerToken { get; set; } = ServerUtils.GenerateRandomToken(20); internal static string ServerToken { get; set; } = ServerUtils.GenerateRandomToken(20);
@@ -38,14 +41,36 @@ namespace DHT.Desktop.Main.Pages {
public bool HasMadeChanges => ServerPort != InputPort || ServerToken != InputToken; public bool HasMadeChanges => ServerPort != InputPort || ServerToken != InputToken;
private bool isToggleButtonEnabled = true; private bool isToggleTrackingButtonEnabled = true;
public bool IsToggleButtonEnabled { public bool IsToggleButtonEnabled {
get => isToggleButtonEnabled; get => isToggleTrackingButtonEnabled;
set => Change(ref isToggleButtonEnabled, value); set => Change(ref isToggleTrackingButtonEnabled, value);
} }
public string ToggleButtonText => ServerLauncher.IsRunning ? "Pause Tracking" : "Resume Tracking"; public string ToggleTrackingButtonText => ServerLauncher.IsRunning ? "Pause Tracking" : "Resume Tracking";
private bool areDevToolsEnabled;
private bool AreDevToolsEnabled {
get => areDevToolsEnabled;
set {
Change(ref areDevToolsEnabled, value);
OnPropertyChanged(nameof(ToggleAppDevToolsButtonText));
}
}
public bool IsToggleAppDevToolsButtonEnabled { get; private set; } = true;
public string ToggleAppDevToolsButtonText {
get {
if (!IsToggleAppDevToolsButtonEnabled) {
return "Unavailable";
}
return AreDevToolsEnabled ? "Disable Ctrl+Shift+I" : "Enable Ctrl+Shift+I";
}
}
public event EventHandler<StatusBarModel.Status>? ServerStatusChanged; public event EventHandler<StatusBarModel.Status>? ServerStatusChanged;
@@ -60,7 +85,7 @@ namespace DHT.Desktop.Main.Pages {
this.db = db; this.db = db;
} }
public void Initialize() { public async Task Initialize() {
ServerLauncher.ServerStatusChanged += ServerLauncherOnServerStatusChanged; ServerLauncher.ServerStatusChanged += ServerLauncherOnServerStatusChanged;
ServerLauncher.ServerManagementExceptionCaught += ServerLauncherOnServerManagementExceptionCaught; ServerLauncher.ServerManagementExceptionCaught += ServerLauncherOnServerManagementExceptionCaught;
@@ -68,13 +93,32 @@ namespace DHT.Desktop.Main.Pages {
string token = ServerToken; string token = ServerToken;
ServerLauncher.Relaunch(port, token, db); ServerLauncher.Relaunch(port, token, db);
} }
bool? devToolsEnabled = await DiscordAppSettings.AreDevToolsEnabled();
if (devToolsEnabled.HasValue) {
AreDevToolsEnabled = devToolsEnabled.Value;
}
else {
IsToggleAppDevToolsButtonEnabled = false;
OnPropertyChanged(nameof(IsToggleAppDevToolsButtonEnabled));
}
} }
public void Dispose() { public void Dispose() {
ServerLauncher.ServerManagementExceptionCaught -= ServerLauncherOnServerManagementExceptionCaught; ServerLauncher.ServerManagementExceptionCaught -= ServerLauncherOnServerManagementExceptionCaught;
ServerLauncher.ServerStatusChanged -= ServerLauncherOnServerStatusChanged; ServerLauncher.ServerStatusChanged -= ServerLauncherOnServerStatusChanged;
ServerLauncher.Stop(); ServerLauncher.Stop();
GC.SuppressFinalize(this); }
private void ServerLauncherOnServerStatusChanged(object? sender, EventArgs e) {
ServerStatusChanged?.Invoke(this, ServerLauncher.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped);
OnPropertyChanged(nameof(ToggleTrackingButtonText));
IsToggleButtonEnabled = true;
}
private async void ServerLauncherOnServerManagementExceptionCaught(object? sender, Exception ex) {
Log.Error(ex);
await Dialog.ShowOk(window, "Server Error", ex.Message);
} }
private async Task<bool> StartServer() { private async Task<bool> StartServer() {
@@ -95,7 +139,7 @@ namespace DHT.Desktop.Main.Pages {
ServerLauncher.Stop(); ServerLauncher.Stop();
} }
public async Task<bool> OnClickToggleButton() { public async Task<bool> OnClickToggleTrackingButton() {
if (ServerLauncher.IsRunning) { if (ServerLauncher.IsRunning) {
StopServer(); StopServer();
return true; return true;
@@ -105,15 +149,27 @@ namespace DHT.Desktop.Main.Pages {
} }
} }
public async Task OnClickCopyTrackingScript() { public async Task<bool> OnClickCopyTrackingScript() {
string bootstrap = await ResourceLoader.ReadTextAsync("Tracker/bootstrap.js"); string bootstrap = await Resources.ReadTextAsync("Tracker/bootstrap.js");
string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + ServerPort + ";") string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + ServerPort + ";")
.Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(ServerToken)) .Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(ServerToken))
.Replace("/*[IMPORTS]*/", await ResourceLoader.ReadJoinedAsync("Tracker/scripts/", '\n')) .Replace("/*[IMPORTS]*/", await Resources.ReadJoinedAsync("Tracker/scripts/", '\n'))
.Replace("/*[CSS-CONTROLLER]*/", await ResourceLoader.ReadTextAsync("Tracker/styles/controller.css")) .Replace("/*[CSS-CONTROLLER]*/", await Resources.ReadTextAsync("Tracker/styles/controller.css"))
.Replace("/*[CSS-SETTINGS]*/", await ResourceLoader.ReadTextAsync("Tracker/styles/settings.css")); .Replace("/*[CSS-SETTINGS]*/", await Resources.ReadTextAsync("Tracker/styles/settings.css"));
await Application.Current.Clipboard.SetTextAsync(script); var clipboard = Application.Current?.Clipboard;
if (clipboard == null) {
await Dialog.ShowOk(window, "Copy Tracking Script", "Clipboard is not available on this system.");
return false;
}
try {
await clipboard.SetTextAsync(script);
return true;
} catch {
await Dialog.ShowOk(window, "Copy Tracking Script", "An error occurred while copying to clipboard.");
return false;
}
} }
public void OnClickRandomizeToken() { public void OnClickRandomizeToken() {
@@ -133,15 +189,42 @@ namespace DHT.Desktop.Main.Pages {
InputToken = ServerToken; InputToken = ServerToken;
} }
private void ServerLauncherOnServerStatusChanged(object? sender, EventArgs e) { public async void OnClickToggleAppDevTools() {
ServerStatusChanged?.Invoke(this, ServerLauncher.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped); const string DialogTitle = "Discord App Settings File";
OnPropertyChanged(nameof(ToggleButtonText));
IsToggleButtonEnabled = true;
}
private async void ServerLauncherOnServerManagementExceptionCaught(object? sender, Exception ex) { bool oldState = AreDevToolsEnabled;
Log.Error(ex); bool newState = !oldState;
await Dialog.ShowOk(window, "Server Error", ex.Message);
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;
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();
}
} }
} }
} }

View File

@@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main.Pages { namespace DHT.Desktop.Main.Pages {
public class ViewerPage : UserControl { [SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class ViewerPage : UserControl {
public ViewerPage() { public ViewerPage() {
InitializeComponent(); InitializeComponent();
} }

View File

@@ -7,16 +7,16 @@ using System.Threading.Tasks;
using System.Web; using System.Web;
using Avalonia.Controls; using Avalonia.Controls;
using DHT.Desktop.Common; using DHT.Desktop.Common;
using DHT.Desktop.Dialogs; using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Main.Controls; using DHT.Desktop.Main.Controls;
using DHT.Desktop.Models;
using DHT.Desktop.Resources;
using DHT.Server.Data.Filters; using DHT.Server.Data.Filters;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Server.Database.Export; using DHT.Server.Database.Export;
using DHT.Utils.Models;
using static DHT.Desktop.Program;
namespace DHT.Desktop.Main.Pages { namespace DHT.Desktop.Main.Pages {
public class ViewerPageModel : BaseModel { sealed class ViewerPageModel : BaseModel {
public string ExportedMessageText { get; private set; } = ""; public string ExportedMessageText { get; private set; } = "";
public bool DatabaseToolFilterModeKeep { get; set; } = true; public bool DatabaseToolFilterModeKeep { get; set; } = true;
@@ -65,10 +65,10 @@ namespace DHT.Desktop.Main.Pages {
private async Task<string> GenerateViewerContents() { private async Task<string> GenerateViewerContents() {
string json = ViewerJsonExport.Generate(db, FilterModel.CreateFilter()); string json = ViewerJsonExport.Generate(db, FilterModel.CreateFilter());
string index = await ResourceLoader.ReadTextAsync("Viewer/index.html"); string index = await Resources.ReadTextAsync("Viewer/index.html");
string viewer = index.Replace("/*[JS]*/", await ResourceLoader.ReadJoinedAsync("Viewer/scripts/", '\n')) string viewer = index.Replace("/*[JS]*/", await Resources.ReadJoinedAsync("Viewer/scripts/", '\n'))
.Replace("/*[CSS]*/", await ResourceLoader.ReadJoinedAsync("Viewer/styles/", '\n')) .Replace("/*[CSS]*/", await Resources.ReadJoinedAsync("Viewer/styles/", '\n'))
.Replace("/*[ARCHIVE]*/", HttpUtility.JavaScriptStringEncode(json)); .Replace("/*[ARCHIVE]*/", HttpUtility.JavaScriptStringEncode(json));
return viewer; return viewer;
} }

View File

@@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace DHT.Desktop.Main { namespace DHT.Desktop.Main {
public class WelcomeScreen : UserControl { [SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class WelcomeScreen : UserControl {
public WelcomeScreen() { public WelcomeScreen() {
InitializeComponent(); InitializeComponent();
} }

View File

@@ -3,12 +3,12 @@ using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using DHT.Desktop.Common; using DHT.Desktop.Common;
using DHT.Desktop.Dialogs; using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Models;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Utils.Models;
namespace DHT.Desktop.Main { namespace DHT.Desktop.Main {
public class WelcomeScreenModel : BaseModel { sealed class WelcomeScreenModel : BaseModel {
public string Version => Program.Version; public string Version => Program.Version;
public IDatabaseFile? Db { get; private set; } public IDatabaseFile? Db { get; private set; }

View File

@@ -1,14 +1,18 @@
using System.Globalization; using System.Globalization;
using System.Reflection; using System.Reflection;
using Avalonia; using Avalonia;
using DHT.Utils.Resources;
namespace DHT.Desktop { namespace DHT.Desktop {
internal static class Program { static class Program {
public static string Version { get; } public static string Version { get; }
public static CultureInfo Culture { get; } public static CultureInfo Culture { get; }
public static ResourceLoader Resources { get; }
static Program() { static Program() {
Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? ""; var assembly = Assembly.GetExecutingAssembly();
Version = assembly.GetName().Version?.ToString() ?? "";
while (Version.EndsWith(".0")) { while (Version.EndsWith(".0")) {
Version = Version[..^2]; Version = Version[..^2];
} }
@@ -18,6 +22,8 @@ namespace DHT.Desktop {
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture; CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
Resources = new ResourceLoader(assembly);
} }
public static void Main(string[] args) { public static void Main(string[] args) {

View File

@@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Desktop", "Desktop\Desktop.
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{7F94B470-B06F-43C0-9655-6592A9AE2D92}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{7F94B470-B06F-43C0-9655-6592A9AE2D92}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utils", "Utils\Utils.csproj", "{7EEC1C05-1D21-4B39-B1D6-CF51F3795629}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{7F94B470-B06F-43C0-9655-6592A9AE2D92}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal

View File

@@ -125,8 +125,7 @@
} }
}; };
const callbackTimer = DISCORD.setupMessageCallback(onMessagesUpdated); DISCORD.setupMessageCallback(onMessagesUpdated);
window.DHT_ON_UNLOAD.push(() => window.clearTimeout(callbackTimer));
STATE.onTrackingStateChanged(enabled => { STATE.onTrackingStateChanged(enabled => {
if (enabled) { if (enabled) {

File diff suppressed because one or more lines are too long

View File

@@ -26,14 +26,13 @@ class DISCORD {
/** /**
* Calls the provided function with a list of messages whenever the currently loaded messages change. * Calls the provided function with a list of messages whenever the currently loaded messages change.
* Returns a setInterval handle.
*/ */
static setupMessageCallback(callback) { static setupMessageCallback(callback) {
let skipsLeft = 0; let skipsLeft = 0;
let waitForCleanup = false; let waitForCleanup = false;
const previousMessages = new Set(); const previousMessages = new Set();
return window.setInterval(() => { const timer = window.setInterval(() => {
if (skipsLeft > 0) { if (skipsLeft > 0) {
--skipsLeft; --skipsLeft;
return; return;
@@ -88,6 +87,8 @@ class DISCORD {
callback(messages); callback(messages);
}, 200); }, 200);
window.DHT_ON_UNLOAD.push(() => window.clearInterval(timer));
} }
/** /**
@@ -157,7 +158,7 @@ class DISCORD {
if (dms) { if (dms) {
let name; 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); const node = Array.prototype.find.call(ele.childNodes, node => node.nodeType === Node.TEXT_NODE);
if (node) { if (node) {

View File

@@ -55,7 +55,8 @@ const GUI = (function() {
controller.ui.btnClose.addEventListener("click", () => { controller.ui.btnClose.addEventListener("click", () => {
this.hideController(); this.hideController();
window.DHT_ON_UNLOAD.forEach(f => f()); window.DHT_ON_UNLOAD.forEach(f => f());
window.DHT_LOADED = false; delete window.DHT_ON_UNLOAD;
delete window.DHT_LOADED;
}); });
STATE.onTrackingStateChanged(isTracking => { STATE.onTrackingStateChanged(isTracking => {

View File

@@ -1,9 +1,9 @@
namespace DHT.Server.Data { namespace DHT.Server.Data {
public readonly struct Attachment { public readonly struct Attachment {
public ulong Id { get; init; } public ulong Id { get; internal init; }
public string Name { get; init; } public string Name { get; internal init; }
public string? Type { get; init; } public string? Type { get; internal init; }
public string Url { get; init; } public string Url { get; internal init; }
public ulong Size { get; init; } public ulong Size { get; internal init; }
} }
} }

View File

@@ -1,11 +1,11 @@
namespace DHT.Server.Data { namespace DHT.Server.Data {
public readonly struct Channel { public readonly struct Channel {
public ulong Id { get; init; } public ulong Id { get; internal init; }
public ulong Server { get; init; } public ulong Server { get; internal init; }
public string Name { get; init; } public string Name { get; internal init; }
public ulong? ParentId { get; init; } public ulong? ParentId { get; internal init; }
public int? Position { get; init; } public int? Position { get; internal init; }
public string? Topic { get; init; } public string? Topic { get; internal init; }
public bool? Nsfw { get; init; } public bool? Nsfw { get; internal init; }
} }
} }

View File

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

View File

@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace DHT.Server.Data.Filters { namespace DHT.Server.Data.Filters {
public class MessageFilter { public sealed class MessageFilter {
public DateTime? StartDate { get; set; } public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; } public DateTime? EndDate { get; set; }

View File

@@ -2,15 +2,15 @@ using System.Collections.Immutable;
namespace DHT.Server.Data { namespace DHT.Server.Data {
public readonly struct Message { public readonly struct Message {
public ulong Id { get; init; } public ulong Id { get; internal init; }
public ulong Sender { get; init; } public ulong Sender { get; internal init; }
public ulong Channel { get; init; } public ulong Channel { get; internal init; }
public string Text { get; init; } public string Text { get; internal init; }
public long Timestamp { get; init; } public long Timestamp { get; internal init; }
public long? EditTimestamp { get; init; } public long? EditTimestamp { get; internal init; }
public ulong? RepliedToId { get; init; } public ulong? RepliedToId { get; internal init; }
public ImmutableArray<Attachment> Attachments { get; init; } public ImmutableArray<Attachment> Attachments { get; internal init; }
public ImmutableArray<Embed> Embeds { get; init; } public ImmutableArray<Embed> Embeds { get; internal init; }
public ImmutableArray<Reaction> Reactions { get; init; } public ImmutableArray<Reaction> Reactions { get; internal init; }
} }
} }

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ namespace DHT.Server.Data {
}; };
} }
public static string ToJsonViewerString(ServerType? type) { internal static string ToJsonViewerString(ServerType? type) {
return type switch { return type switch {
ServerType.Server => "server", ServerType.Server => "server",
ServerType.Group => "group", ServerType.Group => "group",

View File

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

View File

@@ -2,7 +2,7 @@ using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace DHT.Server.Database { namespace DHT.Server.Database {
public class DatabaseStatistics : INotifyPropertyChanged { public sealed class DatabaseStatistics : INotifyPropertyChanged {
private long totalServers; private long totalServers;
private long totalChannels; private long totalChannels;
private long totalUsers; private long totalUsers;

View File

@@ -1,10 +1,9 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using DHT.Server.Data; using DHT.Server.Data;
using DHT.Server.Data.Filters; using DHT.Server.Data.Filters;
namespace DHT.Server.Database { namespace DHT.Server.Database {
public class DummyDatabaseFile : IDatabaseFile { public sealed class DummyDatabaseFile : IDatabaseFile {
public static DummyDatabaseFile Instance { get; } = new(); public static DummyDatabaseFile Instance { get; } = new();
public string Path => ""; public string Path => "";
@@ -42,8 +41,6 @@ namespace DHT.Server.Database {
public void RemoveMessages(MessageFilter filter, MessageFilterRemovalMode mode) {} public void RemoveMessages(MessageFilter filter, MessageFilterRemovalMode mode) {}
public void Dispose() { public void Dispose() {}
GC.SuppressFinalize(this);
}
} }
} }

View File

@@ -2,11 +2,11 @@ using System;
using DHT.Server.Database.Sqlite; using DHT.Server.Database.Sqlite;
namespace DHT.Server.Database.Exceptions { namespace DHT.Server.Database.Exceptions {
public class DatabaseTooNewException : Exception { public sealed class DatabaseTooNewException : Exception {
public int DatabaseVersion { get; } public int DatabaseVersion { get; }
public int CurrentVersion => Schema.Version; public int CurrentVersion => Schema.Version;
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; this.DatabaseVersion = databaseVersion;
} }
} }

View File

@@ -1,10 +1,10 @@
using System; using System;
namespace DHT.Server.Database.Exceptions { namespace DHT.Server.Database.Exceptions {
public class InvalidDatabaseVersionException : Exception { public sealed class InvalidDatabaseVersionException : Exception {
public string Version { get; } 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; this.Version = version;
} }
} }

View File

@@ -124,7 +124,7 @@ namespace DHT.Server.Database.Export {
private static dynamic GenerateMessageList(List<Message> includedMessages, Dictionary<ulong, int> userIndices) { private static dynamic GenerateMessageList(List<Message> includedMessages, Dictionary<ulong, int> userIndices) {
var data = new Dictionary<string, Dictionary<string, dynamic>>(); var data = new Dictionary<string, Dictionary<string, dynamic>>();
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 channel = grouping.Key.ToString();
var channelData = new Dictionary<string, dynamic>(); var channelData = new Dictionary<string, dynamic>();
@@ -146,17 +146,17 @@ namespace DHT.Server.Database.Export {
} }
if (!message.Attachments.IsEmpty) { if (!message.Attachments.IsEmpty) {
obj.a = message.Attachments.Select(attachment => new { obj.a = message.Attachments.Select(static attachment => new {
url = attachment.Url url = attachment.Url
}).ToArray(); }).ToArray();
} }
if (!message.Embeds.IsEmpty) { 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) { if (!message.Reactions.IsEmpty) {
obj.re = message.Reactions.Select(reaction => { obj.re = message.Reactions.Select(static reaction => {
dynamic r = new ExpandoObject(); dynamic r = new ExpandoObject();
if (reaction.EmojiId != null) { if (reaction.EmojiId != null) {

View File

@@ -3,7 +3,7 @@ using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace DHT.Server.Database.Export { 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) { public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
return ulong.Parse(reader.GetString()!); return ulong.Parse(reader.GetString()!);
} }

View File

@@ -4,7 +4,7 @@ using DHT.Server.Database.Exceptions;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
namespace DHT.Server.Database.Sqlite { namespace DHT.Server.Database.Sqlite {
internal class Schema { sealed class Schema {
internal const int Version = 2; internal const int Version = 2;
private readonly SqliteConnection conn; private readonly SqliteConnection conn;

View File

@@ -3,13 +3,13 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using DHT.Server.Collections;
using DHT.Server.Data; using DHT.Server.Data;
using DHT.Server.Data.Filters; using DHT.Server.Data.Filters;
using DHT.Utils.Collections;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
namespace DHT.Server.Database.Sqlite { namespace DHT.Server.Database.Sqlite {
public class SqliteDatabaseFile : IDatabaseFile { public sealed class SqliteDatabaseFile : IDatabaseFile {
public static async Task<SqliteDatabaseFile?> OpenOrCreate(string path, Func<Task<bool>> checkCanUpgradeSchemas) { public static async Task<SqliteDatabaseFile?> OpenOrCreate(string path, Func<Task<bool>> checkCanUpgradeSchemas) {
string connectionString = new SqliteConnectionStringBuilder { string connectionString = new SqliteConnectionStringBuilder {
DataSource = path, DataSource = path,
@@ -39,7 +39,6 @@ namespace DHT.Server.Database.Sqlite {
public void Dispose() { public void Dispose() {
conn.Dispose(); conn.Dispose();
GC.SuppressFinalize(this);
} }
public void AddServer(Data.Server server) { public void AddServer(Data.Server server) {

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using DHT.Server.Data.Filters; using DHT.Server.Data.Filters;
namespace DHT.Server.Database.Sqlite { namespace DHT.Server.Database.Sqlite {
public static class SqliteMessageFilter { static class SqliteMessageFilter {
public static string GenerateWhereClause(this MessageFilter? filter, bool invert = false) { public static string GenerateWhereClause(this MessageFilter? filter, bool invert = false) {
if (filter == null) { if (filter == null) {
return ""; return "";

View File

@@ -3,7 +3,7 @@ using System.Linq;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
namespace DHT.Server.Database.Sqlite { namespace DHT.Server.Database.Sqlite {
public static class SqliteUtils { static class SqliteUtils {
public static SqliteCommand Command(this SqliteConnection conn, string sql) { public static SqliteCommand Command(this SqliteConnection conn, string sql) {
var cmd = conn.CreateCommand(); var cmd = conn.CreateCommand();
cmd.CommandText = sql; cmd.CommandText = sql;
@@ -12,7 +12,7 @@ namespace DHT.Server.Database.Sqlite {
public static SqliteCommand Insert(this SqliteConnection conn, string tableName, string[] columns) { public static SqliteCommand Insert(this SqliteConnection conn, string tableName, string[] columns) {
string columnNames = string.Join(',', columns); string columnNames = string.Join(',', columns);
string columnParams = string.Join(',', columns.Select(c => ':' + c)); string columnParams = string.Join(',', columns.Select(static c => ':' + c));
return conn.Command("INSERT INTO " + tableName + " (" + columnNames + ")" + return conn.Command("INSERT INTO " + tableName + " (" + columnNames + ")" +
"VALUES (" + columnParams + ")"); "VALUES (" + columnParams + ")");
@@ -20,8 +20,8 @@ namespace DHT.Server.Database.Sqlite {
public static SqliteCommand Upsert(this SqliteConnection conn, string tableName, string[] columns) { public static SqliteCommand Upsert(this SqliteConnection conn, string tableName, string[] columns) {
string columnNames = string.Join(',', columns); string columnNames = string.Join(',', columns);
string columnParams = string.Join(',', columns.Select(c => ':' + c)); string columnParams = string.Join(',', columns.Select(static c => ':' + c));
string columnUpdates = string.Join(',', columns.Skip(1).Select(c => c + " = excluded." + c)); string columnUpdates = string.Join(',', columns.Skip(1).Select(static c => c + " = excluded." + c));
return conn.Command("INSERT INTO " + tableName + " (" + columnNames + ")" + return conn.Command("INSERT INTO " + tableName + " (" + columnNames + ")" +
"VALUES (" + columnParams + ")" + "VALUES (" + columnParams + ")" +

View File

@@ -3,13 +3,16 @@ using System.Net;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Server.Logging;
using DHT.Server.Service; using DHT.Server.Service;
using DHT.Utils.Http;
using DHT.Utils.Logging;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Extensions;
namespace DHT.Server.Endpoints { namespace DHT.Server.Endpoints {
public abstract class BaseEndpoint { abstract class BaseEndpoint {
private static readonly Log Log = Log.ForType<BaseEndpoint>();
protected IDatabaseFile Db { get; } protected IDatabaseFile Db { get; }
private readonly ServerParameters parameters; private readonly ServerParameters parameters;

View File

@@ -3,12 +3,12 @@ using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using DHT.Server.Data; using DHT.Server.Data;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Server.Json;
using DHT.Server.Service; using DHT.Server.Service;
using DHT.Utils.Http;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace DHT.Server.Endpoints { namespace DHT.Server.Endpoints {
public class TrackChannelEndpoint : BaseEndpoint { sealed class TrackChannelEndpoint : BaseEndpoint {
public TrackChannelEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {} public TrackChannelEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) { protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) {

View File

@@ -7,12 +7,12 @@ using System.Threading.Tasks;
using DHT.Server.Data; using DHT.Server.Data;
using DHT.Server.Data.Filters; using DHT.Server.Data.Filters;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Server.Json;
using DHT.Server.Service; using DHT.Server.Service;
using DHT.Utils.Http;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace DHT.Server.Endpoints { namespace DHT.Server.Endpoints {
public class TrackMessagesEndpoint : BaseEndpoint { sealed class TrackMessagesEndpoint : BaseEndpoint {
public TrackMessagesEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {} public TrackMessagesEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) { protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) {

View File

@@ -3,12 +3,12 @@ using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using DHT.Server.Data; using DHT.Server.Data;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Server.Json;
using DHT.Server.Service; using DHT.Server.Service;
using DHT.Utils.Http;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace DHT.Server.Endpoints { namespace DHT.Server.Endpoints {
public class TrackUsersEndpoint : BaseEndpoint { sealed class TrackUsersEndpoint : BaseEndpoint {
public TrackUsersEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {} public TrackUsersEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) { protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) {

View File

@@ -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());
}
}
}

View File

@@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<RootNamespace>DHT.Server</RootNamespace> <RootNamespace>DHT.Server</RootNamespace>
@@ -8,12 +7,10 @@
<Authors>chylex</Authors> <Authors>chylex</Authors>
<Company>DiscordHistoryTracker</Company> <Company>DiscordHistoryTracker</Company>
<Product>DiscordHistoryTrackerServer</Product> <Product>DiscordHistoryTrackerServer</Product>
<Version>33.0.0.0</Version> <GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
<AssemblyVersion>$(Version)</AssemblyVersion> <GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
<FileVersion>$(Version)</FileVersion> <GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
<PackageVersion>$(Version)</PackageVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' "> <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
<DebugType>none</DebugType> <DebugType>none</DebugType>
@@ -24,4 +21,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="5.0.5" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="5.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Utils\Utils.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Version.cs" Link="Version.cs" />
</ItemGroup>
</Project> </Project>

View File

@@ -3,7 +3,7 @@ using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Threading; using System.Threading;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Server.Logging; using DHT.Utils.Logging;
using Microsoft.AspNetCore; using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
@@ -11,6 +11,8 @@ using Microsoft.Extensions.DependencyInjection;
namespace DHT.Server.Service { namespace DHT.Server.Service {
public static class ServerLauncher { public static class ServerLauncher {
private static readonly Log Log = Log.ForType(typeof(ServerLauncher));
private static IWebHost? Server { get; set; } = null; private static IWebHost? Server { get; set; } = null;
public static bool IsRunning { get; private set; } public static bool IsRunning { get; private set; }
@@ -70,7 +72,7 @@ namespace DHT.Server.Service {
void SetKestrelOptions(KestrelServerOptions options) { void SetKestrelOptions(KestrelServerOptions options) {
options.Limits.MaxRequestBodySize = null; options.Limits.MaxRequestBodySize = null;
options.Limits.MinResponseDataRate = 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() Server = WebHost.CreateDefaultBuilder()

View File

@@ -1,5 +1,5 @@
namespace DHT.Server.Service { namespace DHT.Server.Service {
public struct ServerParameters { readonly struct ServerParameters {
public string Token { get; init; } public string Token { get; init; }
} }
} }

View File

@@ -8,14 +8,14 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
namespace DHT.Server.Service { namespace DHT.Server.Service {
public class Startup { sealed class Startup {
public void ConfigureServices(IServiceCollection services) { public void ConfigureServices(IServiceCollection services) {
services.Configure<JsonOptions>(options => { services.Configure<JsonOptions>(static options => {
options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict; options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict;
}); });
services.AddCors(cors => { services.AddCors(static cors => {
cors.AddDefaultPolicy(builder => { cors.AddDefaultPolicy(static builder => {
builder.WithOrigins("https://discord.com", "https://discordapp.com").AllowCredentials().AllowAnyMethod().AllowAnyHeader(); builder.WithOrigins("https://discord.com", "https://discordapp.com").AllowCredentials().AllowAnyMethod().AllowAnyHeader();
}); });
}); });

View File

@@ -10,8 +10,8 @@ namespace DHT.Server.Service {
public static int FindAvailablePort(int min, int max) { public static int FindAvailablePort(int min, int max) {
var properties = IPGlobalProperties.GetIPGlobalProperties(); var properties = IPGlobalProperties.GetIPGlobalProperties();
var occupied = new HashSet<int>(); var occupied = new HashSet<int>();
occupied.UnionWith(properties.GetActiveTcpListeners().Select(tcp => tcp.Port)); occupied.UnionWith(properties.GetActiveTcpListeners().Select(static tcp => tcp.Port));
occupied.UnionWith(properties.GetActiveTcpConnections().Select(tcp => tcp.LocalEndPoint.Port)); occupied.UnionWith(properties.GetActiveTcpConnections().Select(static tcp => tcp.LocalEndPoint.Port));
for (int port = min; port < max; port++) { for (int port = min; port < max; port++) {
if (!occupied.Contains(port)) { if (!occupied.Contains(port)) {

View File

@@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace DHT.Server.Collections { namespace DHT.Utils.Collections {
public class MultiDictionary<TKey, TValue> where TKey : notnull { public sealed class MultiDictionary<TKey, TValue> where TKey : notnull {
private readonly Dictionary<TKey, List<TValue>> dict = new(); private readonly Dictionary<TKey, List<TValue>> dict = new();
public void Add(TKey key, TValue value) { public void Add(TKey key, TValue value) {

View File

@@ -1,8 +1,8 @@
using System; using System;
using System.Net; using System.Net;
namespace DHT.Server.Service { namespace DHT.Utils.Http {
public class HttpException : Exception { public sealed class HttpException : Exception {
public HttpStatusCode StatusCode { get; } public HttpStatusCode StatusCode { get; }
public HttpException(HttpStatusCode statusCode, string message) : base(message) { public HttpException(HttpStatusCode statusCode, string message) : base(message) {

View File

@@ -1,8 +1,7 @@
using System.Net; using System.Net;
using System.Text.Json; using System.Text.Json;
using DHT.Server.Service;
namespace DHT.Server.Json { namespace DHT.Utils.Http {
public static class JsonExtensions { public static class JsonExtensions {
public static bool HasKey(this JsonElement json, string key) { public static bool HasKey(this JsonElement json, string key) {
return json.TryGetProperty(key, out _); return json.TryGetProperty(key, out _);

45
app/Utils/Logging/Log.cs Normal file
View File

@@ -0,0 +1,45 @@
using System;
using System.Diagnostics;
namespace DHT.Utils.Logging {
public sealed class Log {
public static Log ForType<T>() {
return ForType(typeof(T));
}
public static Log ForType(Type type) {
return new Log(type.Name);
}
private readonly string tag;
private Log(string tag) {
this.tag = tag;
}
private void LogLevel(ConsoleColor color, string level, string text) {
Console.ForegroundColor = color;
foreach (string line in text.Replace("\r", "").Split('\n')) {
string formatted = $"[{level}] [{tag}] {line}";
Console.WriteLine(formatted);
Trace.WriteLine(formatted);
}
}
public void Info(string message) {
LogLevel(ConsoleColor.Blue, "INFO", message);
}
public void Warn(string message) {
LogLevel(ConsoleColor.Yellow, "WARN", message);
}
public void Error(string message) {
LogLevel(ConsoleColor.Red, "ERROR", message);
}
public void Error(Exception e) {
LogLevel(ConsoleColor.Red, "ERROR", e.ToString());
}
}
}

View File

@@ -3,7 +3,7 @@ using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using JetBrains.Annotations; using JetBrains.Annotations;
namespace DHT.Desktop.Models { namespace DHT.Utils.Models {
public abstract class BaseModel : INotifyPropertyChanged { public abstract class BaseModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;

View File

@@ -4,11 +4,16 @@ using System.Reflection;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DHT.Desktop.Resources { namespace DHT.Utils.Resources {
public static class ResourceLoader { public sealed class ResourceLoader {
private static Stream GetEmbeddedStream(string filename) { private readonly Assembly assembly;
public ResourceLoader(Assembly assembly) {
this.assembly = assembly;
}
private Stream GetEmbeddedStream(string filename) {
Stream? stream = null; Stream? stream = null;
Assembly assembly = Assembly.GetExecutingAssembly();
foreach (var embeddedName in assembly.GetManifestResourceNames()) { foreach (var embeddedName in assembly.GetManifestResourceNames()) {
if (embeddedName.Replace('\\', '/') == filename) { if (embeddedName.Replace('\\', '/') == filename) {
stream = assembly.GetManifestResourceStream(embeddedName); stream = assembly.GetManifestResourceStream(embeddedName);
@@ -19,19 +24,18 @@ namespace DHT.Desktop.Resources {
return stream ?? throw new ArgumentException("Missing embedded resource: " + filename); return stream ?? throw new ArgumentException("Missing embedded resource: " + filename);
} }
private static async Task<string> ReadTextAsync(Stream stream) { private async Task<string> ReadTextAsync(Stream stream) {
using var reader = new StreamReader(stream, Encoding.UTF8); using var reader = new StreamReader(stream, Encoding.UTF8);
return await reader.ReadToEndAsync(); return await reader.ReadToEndAsync();
} }
public static async Task<string> ReadTextAsync(string filename) { public async Task<string> ReadTextAsync(string filename) {
return await ReadTextAsync(GetEmbeddedStream(filename)); return await ReadTextAsync(GetEmbeddedStream(filename));
} }
public static async Task<string> ReadJoinedAsync(string path, char separator) { public async Task<string> ReadJoinedAsync(string path, char separator) {
StringBuilder joined = new(); StringBuilder joined = new();
Assembly assembly = Assembly.GetExecutingAssembly();
foreach (var embeddedName in assembly.GetManifestResourceNames()) { foreach (var embeddedName in assembly.GetManifestResourceNames()) {
if (embeddedName.Replace('\\', '/').StartsWith(path)) { if (embeddedName.Replace('\\', '/').StartsWith(path)) {
joined.Append(await ReadTextAsync(assembly.GetManifestResourceStream(embeddedName)!)).Append(separator); joined.Append(await ReadTextAsync(assembly.GetManifestResourceStream(embeddedName)!)).Append(separator);

24
app/Utils/Utils.csproj Normal file
View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>DHT.Utils</RootNamespace>
<Nullable>enable</Nullable>
<PackageId>DiscordHistoryTrackerUtils</PackageId>
<Authors>chylex</Authors>
<Company>DiscordHistoryTracker</Company>
<Product>DiscordHistoryTrackerUtils</Product>
<GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
<GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="10.3.0" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Version.cs" Link="Version.cs" />
</ItemGroup>
</Project>

12
app/Version.cs Normal file
View File

@@ -0,0 +1,12 @@
using System.Reflection;
using DHT.Utils;
[assembly: AssemblyVersion(Version.Tag)]
[assembly: AssemblyFileVersion(Version.Tag)]
[assembly: AssemblyInformationalVersion(Version.Tag)]
namespace DHT.Utils {
static class Version {
public const string Tag = "32.2.0.0";
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
// ==UserScript== // ==UserScript==
// @name Discord History Tracker // @name Discord History Tracker
// @version v.31 // @version v.31a
// @license MIT // @license MIT
// @namespace https://chylex.com // @namespace https://chylex.com
// @homepageURL https://dht.chylex.com/ // @homepageURL https://dht.chylex.com/
@@ -21,68 +21,139 @@ var DISCORD = (function(){
return getMessageOuterElement().querySelector("[class*='scroller-']"); return getMessageOuterElement().querySelector("[class*='scroller-']");
}; };
var observerTimer = 0, waitingForCleanup = 0; var getMessageElements = function() {
return getMessageOuterElement().querySelectorAll("[class*='message-']");
};
var getReactProps = function(ele) {
var keys = Object.keys(ele || {});
var 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;
};
var getMessageElementProps = function(ele) {
const props = getReactProps(ele);
if (props.children && props.children.length >= 4) {
const childProps = props.children[3].props;
if ("message" in childProps && "channel" in childProps) {
return childProps;
}
}
return null;
};
var hasMoreMessages = function() {
return document.querySelector("#messagesNavigationDescription + [class^=container]") === null;
};
var getMessages = function() {
try {
const messages = [];
for (const ele of getMessageElements()) {
const props = getMessageElementProps(ele);
if (props != null) {
messages.push(props.message);
}
}
return messages;
} catch (e) {
console.error(e);
return [];
}
};
return { return {
/* /**
* Sets up a callback hook to trigger whenever the list of messages is updated. The callback is given a boolean value that is true if there are more messages to load. * Calls the provided function with a list of messages whenever the currently loaded messages change,
* or with `false` if there are no more messages.
*/ */
setupMessageUpdateCallback: function(callback){ setupMessageCallback: function(callback) {
var onTimerFinished = function(){ let skipsLeft = 0;
let view = getMessageOuterElement(); let waitForCleanup = false;
let hasReachedStart = false;
const previousMessages = new Set();
const intervalId = window.setInterval(() => {
if (skipsLeft > 0) {
--skipsLeft;
return;
}
const view = getMessageOuterElement();
if (!view) {
skipsLeft = 2;
return;
}
const anyMessage = DOM.queryReactClass("message", getMessageOuterElement());
const messageCount = anyMessage ? anyMessage.parentElement.children.length : 0;
if (messageCount > 300) {
if (waitForCleanup) {
return;
}
if (!view){ skipsLeft = 3;
restartTimer(500); waitForCleanup = true;
window.setTimeout(() => {
const view = getMessageScrollerElement();
view.scrollTop = view.scrollHeight / 2;
}, 1);
} }
else{ else {
let anyMessage = getMessageOuterElement().querySelector("[class*='message-']"); waitForCleanup = false;
let messages = anyMessage ? anyMessage.parentElement.children.length : 0; }
if (messages < 100){ const messages = getMessages();
waitingForCleanup = 0; let hasChanged = false;
}
for (const message of messages) {
if (waitingForCleanup > 0){ if (!previousMessages.has(message.id)) {
--waitingForCleanup; hasChanged = true;
restartTimer(750); break;
}
else{
if (messages > 300){
waitingForCleanup = 6;
DOM.setTimer(() => {
let view = getMessageScrollerElement();
view.scrollTop = view.scrollHeight/2;
}, 1);
}
callback();
restartTimer(200);
} }
} }
};
var restartTimer = function(delay){ if (!hasChanged) {
observerTimer = DOM.setTimer(onTimerFinished, delay); if (!hasReachedStart && !hasMoreMessages()) {
}; hasReachedStart = true;
callback(false);
}
return;
}
onTimerFinished(); previousMessages.clear();
window.DHT_ON_UNLOAD.push(() => window.clearInterval(observerTimer)); for (const message of messages) {
previousMessages.add(message.id);
}
hasReachedStart = false;
callback(messages);
}, 200);
window.DHT_ON_UNLOAD.push(() => window.clearInterval(intervalId));
}, },
/* /*
* Returns internal React state object of an element. * Returns internal React state object of an element.
*/ */
getReactProps: function(ele){ getReactProps: function(ele){
var keys = Object.keys(ele || {}); return getReactProps(ele);
var 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;
}, },
/* /*
@@ -90,83 +161,75 @@ var DISCORD = (function(){
* For types DM and GROUP, the server and channel names are identical. * For types DM and GROUP, the server and channel names are identical.
* For SERVER type, the channel has to be in view, otherwise Discord unloads it. * For SERVER type, the channel has to be in view, otherwise Discord unloads it.
*/ */
getSelectedChannel: function(){ getSelectedChannel: function() {
try{ try {
var obj; let obj;
var channelListEle = DOM.queryReactClass("privateChannels");
if (channelListEle){ for (const ele of getMessageElements()) {
var channel = DOM.queryReactClass("selected", channelListEle); const props = getMessageElementProps(ele);
if (!channel || !("href" in channel) || !channel.href.includes("/@me/")){ if (props != null) {
return null; obj = props.channel;
break;
} }
}
if (!obj) {
return null;
}
var dms = DOM.queryReactClass("privateChannels");
if (dms){
let name;
var linkSplit = channel.href.split("/"); for (const ele of dms.querySelectorAll("[class*='channel-'] [class*='selected-'] [class^='name-'] *, [class*='channel-'][class*='selected-'] [class^='name-'] *")) {
var link = linkSplit[linkSplit.length-1]; const node = Array.prototype.find.call(ele.childNodes, node => node.nodeType === Node.TEXT_NODE);
if (!(/^\d+$/.test(link))){
return null;
}
var name;
for(let ele of channel.querySelectorAll("[class^='name-'] *")){
let node = Array.prototype.find.call(ele.childNodes, node => node.nodeType === Node.TEXT_NODE);
if (node){ if (node) {
name = node.nodeValue; name = node.nodeValue;
break; break;
} }
} }
if (!name){ if (!name) {
return null; return null;
} }
var icon = channel.querySelector("img[class*='avatar']"); let type;
var iconParent = icon && icon.closest("foreignObject");
var iconMask = iconParent && iconParent.getAttribute("mask");
obj = { // https://discord.com/developers/docs/resources/channel#channel-object-channel-types
switch (obj.type) {
case 1: type = "DM"; break;
case 3: type = "GROUP"; break;
default: return null;
}
return {
"server": name, "server": name,
"channel": name, "channel": name,
"id": link, "id": obj.id,
"type": (iconMask && iconMask.includes("#svg-mask-avatar-default")) ? "GROUP" : "DM", "type": type,
"extra": {} "extra": {}
}; };
} }
else{ else if (obj.guild_id) {
channelListEle = document.getElementById("channels"); return {
var channel = channelListEle.querySelector("[class*='modeSelected']").parentElement;
var props = DISCORD.getReactProps(channel).children.props;
if (!props){
return null;
}
var channelObj = props.channel || props.children().props.channel;
if (!channelObj){
return null;
}
obj = {
"server": document.querySelector("nav header > h1").innerText, "server": document.querySelector("nav header > h1").innerText,
"channel": channelObj.name, "channel": obj.name,
"id": channelObj.id, "id": obj.id,
"type": "SERVER", "type": "SERVER",
"extra": { "extra": {
"position": channelObj.position, "position": obj.position,
"topic": channelObj.topic, "topic": obj.topic,
"nsfw": channelObj.nsfw "nsfw": obj.nsfw
} }
}; };
} }
else {
return obj.channel.length === 0 ? null : obj; return null;
}catch(e){ }
} catch(e) {
console.error(e); console.error(e);
return null; return null;
} }
@@ -176,32 +239,7 @@ var DISCORD = (function(){
* Returns an array containing currently loaded messages. * Returns an array containing currently loaded messages.
*/ */
getMessages: function(){ getMessages: function(){
try{ return getMessages();
var scroller = getMessageScrollerElement();
var props = DISCORD.getReactProps(scroller);
var wrappers;
try{
wrappers = props.children.props.children.props.children.props.children.find(ele => Array.isArray(ele));
}catch(e){ // old version compatibility
wrappers = props.children.find(ele => Array.isArray(ele));
}
var messages = [];
for(let obj of wrappers){
let nested = obj.props;
if (nested && nested.message){
messages.push(nested.message);
}
}
return messages;
}catch(e){
console.error(e);
return null;
}
}, },
/* /*
@@ -213,7 +251,7 @@ var DISCORD = (function(){
* Returns true if there are more messages available or if they're still loading. * Returns true if there are more messages available or if they're still loading.
*/ */
hasMoreMessages: function(){ hasMoreMessages: function(){
return document.querySelector("#messagesNavigationDescription + [class^=container]") === null; return hasMoreMessages();
}, },
/* /*
@@ -273,11 +311,15 @@ var DISCORD = (function(){
if (nextChannel === null){ if (nextChannel === null){
return false; return false;
} }
else{
nextChannel.children[0].click(); const nextChannelLink = nextChannel.querySelector("a[href^='/channels/']");
nextChannel.scrollIntoView(true); if (!nextChannelLink) {
return true; return false;
} }
nextChannelLink.click();
nextChannel.scrollIntoView(true);
return true;
} }
} }
}; };
@@ -594,7 +636,7 @@ ${radio("asm", "pause", "Pause Tracking")}
${radio("asm", "switch", "Switch to Next Channel")} ${radio("asm", "switch", "Switch to Next Channel")}
<p id='dht-cfg-note'> <p id='dht-cfg-note'>
It is recommended to disable link and image previews to avoid putting unnecessary strain on your browser.<br><br> It is recommended to disable link and image previews to avoid putting unnecessary strain on your browser.<br><br>
<sub>v.31, released 3 April 2021</sub> <sub>v.31a, released 12 Feb 2022</sub>
</p>`); </p>`);
// elements // elements
@@ -1214,7 +1256,7 @@ let stopTrackingDelayed = function(callback){
}, 200); // give the user visual feedback after clicking the button before switching off }, 200); // give the user visual feedback after clicking the button before switching off
}; };
DISCORD.setupMessageUpdateCallback(() => { DISCORD.setupMessageCallback(messages => {
if (STATE.isTracking() && ignoreMessageCallback.size === 0){ if (STATE.isTracking() && ignoreMessageCallback.size === 0){
let info = DISCORD.getSelectedChannel(); let info = DISCORD.getSelectedChannel();
@@ -1225,28 +1267,22 @@ DISCORD.setupMessageUpdateCallback(() => {
STATE.addDiscordChannel(info.server, info.type, info.id, info.channel, info.extra); STATE.addDiscordChannel(info.server, info.type, info.id, info.channel, info.extra);
let messages = DISCORD.getMessages(); if (messages !== false && !messages.length){
if (messages == null){
stopTrackingDelayed();
return;
}
else if (!messages.length){
DISCORD.loadOlderMessages(); DISCORD.loadOlderMessages();
return; return;
} }
let hasUpdatedFile = STATE.addDiscordMessages(info.id, messages); let hasUpdatedFile = messages !== false && STATE.addDiscordMessages(info.id, messages);
if (SETTINGS.autoscroll){ if (SETTINGS.autoscroll){
let action = null; let action = null;
if (!hasUpdatedFile && !STATE.isMessageFresh(messages[0].id)){ if (messages === false) {
action = SETTINGS.afterSavedMsg;
}
else if (!DISCORD.hasMoreMessages()){
action = SETTINGS.afterFirstMsg; action = SETTINGS.afterFirstMsg;
} }
else if (!hasUpdatedFile && !STATE.isMessageFresh(messages[0].id)){
action = SETTINGS.afterSavedMsg;
}
if (action === null){ if (action === null){
if (hasUpdatedFile){ if (hasUpdatedFile){

File diff suppressed because one or more lines are too long

View File

@@ -8,8 +8,8 @@ import os
import re import re
import distutils.dir_util import distutils.dir_util
VERSION_SHORT = "v.31" VERSION_SHORT = "v.31a"
VERSION_FULL = VERSION_SHORT + ", released 3 April 2021" VERSION_FULL = VERSION_SHORT + ", released 12 Feb 2022"
EXEC_UGLIFYJS_WIN = "{2}/lib/uglifyjs.cmd --parse bare_returns --compress --mangle toplevel --mangle-props keep_quoted,reserved=[{3}] --output \"{1}\" \"{0}\"" EXEC_UGLIFYJS_WIN = "{2}/lib/uglifyjs.cmd --parse bare_returns --compress --mangle toplevel --mangle-props keep_quoted,reserved=[{3}] --output \"{1}\" \"{0}\""
EXEC_UGLIFYJS_AUTO = "uglifyjs --parse bare_returns --compress --mangle toplevel --mangle-props keep_quoted,reserved=[{3}] --output \"{1}\" \"{0}\"" EXEC_UGLIFYJS_AUTO = "uglifyjs --parse bare_returns --compress --mangle toplevel --mangle-props keep_quoted,reserved=[{3}] --output \"{1}\" \"{0}\""

View File

@@ -70,3 +70,4 @@ messages
msSaveBlob msSaveBlob
messageReference messageReference
message_id message_id
guild_id

View File

@@ -7,68 +7,139 @@ var DISCORD = (function(){
return getMessageOuterElement().querySelector("[class*='scroller-']"); return getMessageOuterElement().querySelector("[class*='scroller-']");
}; };
var observerTimer = 0, waitingForCleanup = 0; var getMessageElements = function() {
return getMessageOuterElement().querySelectorAll("[class*='message-']");
};
var getReactProps = function(ele) {
var keys = Object.keys(ele || {});
var 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;
};
var getMessageElementProps = function(ele) {
const props = getReactProps(ele);
if (props.children && props.children.length >= 4) {
const childProps = props.children[3].props;
if ("message" in childProps && "channel" in childProps) {
return childProps;
}
}
return null;
};
var hasMoreMessages = function() {
return document.querySelector("#messagesNavigationDescription + [class^=container]") === null;
};
var getMessages = function() {
try {
const messages = [];
for (const ele of getMessageElements()) {
const props = getMessageElementProps(ele);
if (props != null) {
messages.push(props.message);
}
}
return messages;
} catch (e) {
console.error(e);
return [];
}
};
return { return {
/* /**
* Sets up a callback hook to trigger whenever the list of messages is updated. The callback is given a boolean value that is true if there are more messages to load. * Calls the provided function with a list of messages whenever the currently loaded messages change,
* or with `false` if there are no more messages.
*/ */
setupMessageUpdateCallback: function(callback){ setupMessageCallback: function(callback) {
var onTimerFinished = function(){ let skipsLeft = 0;
let view = getMessageOuterElement(); let waitForCleanup = false;
let hasReachedStart = false;
const previousMessages = new Set();
const intervalId = window.setInterval(() => {
if (skipsLeft > 0) {
--skipsLeft;
return;
}
const view = getMessageOuterElement();
if (!view) {
skipsLeft = 2;
return;
}
const anyMessage = DOM.queryReactClass("message", getMessageOuterElement());
const messageCount = anyMessage ? anyMessage.parentElement.children.length : 0;
if (messageCount > 300) {
if (waitForCleanup) {
return;
}
if (!view){ skipsLeft = 3;
restartTimer(500); waitForCleanup = true;
window.setTimeout(() => {
const view = getMessageScrollerElement();
view.scrollTop = view.scrollHeight / 2;
}, 1);
} }
else{ else {
let anyMessage = getMessageOuterElement().querySelector("[class*='message-']"); waitForCleanup = false;
let messages = anyMessage ? anyMessage.parentElement.children.length : 0; }
if (messages < 100){ const messages = getMessages();
waitingForCleanup = 0; let hasChanged = false;
}
for (const message of messages) {
if (waitingForCleanup > 0){ if (!previousMessages.has(message.id)) {
--waitingForCleanup; hasChanged = true;
restartTimer(750); break;
}
else{
if (messages > 300){
waitingForCleanup = 6;
DOM.setTimer(() => {
let view = getMessageScrollerElement();
view.scrollTop = view.scrollHeight/2;
}, 1);
}
callback();
restartTimer(200);
} }
} }
};
var restartTimer = function(delay){ if (!hasChanged) {
observerTimer = DOM.setTimer(onTimerFinished, delay); if (!hasReachedStart && !hasMoreMessages()) {
}; hasReachedStart = true;
callback(false);
}
return;
}
onTimerFinished(); previousMessages.clear();
window.DHT_ON_UNLOAD.push(() => window.clearInterval(observerTimer)); for (const message of messages) {
previousMessages.add(message.id);
}
hasReachedStart = false;
callback(messages);
}, 200);
window.DHT_ON_UNLOAD.push(() => window.clearInterval(intervalId));
}, },
/* /*
* Returns internal React state object of an element. * Returns internal React state object of an element.
*/ */
getReactProps: function(ele){ getReactProps: function(ele){
var keys = Object.keys(ele || {}); return getReactProps(ele);
var 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;
}, },
/* /*
@@ -76,83 +147,75 @@ var DISCORD = (function(){
* For types DM and GROUP, the server and channel names are identical. * For types DM and GROUP, the server and channel names are identical.
* For SERVER type, the channel has to be in view, otherwise Discord unloads it. * For SERVER type, the channel has to be in view, otherwise Discord unloads it.
*/ */
getSelectedChannel: function(){ getSelectedChannel: function() {
try{ try {
var obj; let obj;
var channelListEle = DOM.queryReactClass("privateChannels");
if (channelListEle){ for (const ele of getMessageElements()) {
var channel = DOM.queryReactClass("selected", channelListEle); const props = getMessageElementProps(ele);
if (!channel || !("href" in channel) || !channel.href.includes("/@me/")){ if (props != null) {
return null; obj = props.channel;
break;
} }
}
if (!obj) {
return null;
}
var dms = DOM.queryReactClass("privateChannels");
if (dms){
let name;
var linkSplit = channel.href.split("/"); for (const ele of dms.querySelectorAll("[class*='channel-'] [class*='selected-'] [class^='name-'] *, [class*='channel-'][class*='selected-'] [class^='name-'] *")) {
var link = linkSplit[linkSplit.length-1]; const node = Array.prototype.find.call(ele.childNodes, node => node.nodeType === Node.TEXT_NODE);
if (!(/^\d+$/.test(link))){
return null;
}
var name;
for(let ele of channel.querySelectorAll("[class^='name-'] *")){
let node = Array.prototype.find.call(ele.childNodes, node => node.nodeType === Node.TEXT_NODE);
if (node){ if (node) {
name = node.nodeValue; name = node.nodeValue;
break; break;
} }
} }
if (!name){ if (!name) {
return null; return null;
} }
var icon = channel.querySelector("img[class*='avatar']"); let type;
var iconParent = icon && icon.closest("foreignObject");
var iconMask = iconParent && iconParent.getAttribute("mask");
obj = { // https://discord.com/developers/docs/resources/channel#channel-object-channel-types
switch (obj.type) {
case 1: type = "DM"; break;
case 3: type = "GROUP"; break;
default: return null;
}
return {
"server": name, "server": name,
"channel": name, "channel": name,
"id": link, "id": obj.id,
"type": (iconMask && iconMask.includes("#svg-mask-avatar-default")) ? "GROUP" : "DM", "type": type,
"extra": {} "extra": {}
}; };
} }
else{ else if (obj.guild_id) {
channelListEle = document.getElementById("channels"); return {
var channel = channelListEle.querySelector("[class*='modeSelected']").parentElement;
var props = DISCORD.getReactProps(channel).children.props;
if (!props){
return null;
}
var channelObj = props.channel || props.children().props.channel;
if (!channelObj){
return null;
}
obj = {
"server": document.querySelector("nav header > h1").innerText, "server": document.querySelector("nav header > h1").innerText,
"channel": channelObj.name, "channel": obj.name,
"id": channelObj.id, "id": obj.id,
"type": "SERVER", "type": "SERVER",
"extra": { "extra": {
"position": channelObj.position, "position": obj.position,
"topic": channelObj.topic, "topic": obj.topic,
"nsfw": channelObj.nsfw "nsfw": obj.nsfw
} }
}; };
} }
else {
return obj.channel.length === 0 ? null : obj; return null;
}catch(e){ }
} catch(e) {
console.error(e); console.error(e);
return null; return null;
} }
@@ -162,32 +225,7 @@ var DISCORD = (function(){
* Returns an array containing currently loaded messages. * Returns an array containing currently loaded messages.
*/ */
getMessages: function(){ getMessages: function(){
try{ return getMessages();
var scroller = getMessageScrollerElement();
var props = DISCORD.getReactProps(scroller);
var wrappers;
try{
wrappers = props.children.props.children.props.children.props.children.find(ele => Array.isArray(ele));
}catch(e){ // old version compatibility
wrappers = props.children.find(ele => Array.isArray(ele));
}
var messages = [];
for(let obj of wrappers){
let nested = obj.props;
if (nested && nested.message){
messages.push(nested.message);
}
}
return messages;
}catch(e){
console.error(e);
return null;
}
}, },
/* /*
@@ -199,7 +237,7 @@ var DISCORD = (function(){
* Returns true if there are more messages available or if they're still loading. * Returns true if there are more messages available or if they're still loading.
*/ */
hasMoreMessages: function(){ hasMoreMessages: function(){
return document.querySelector("#messagesNavigationDescription + [class^=container]") === null; return hasMoreMessages();
}, },
/* /*
@@ -259,11 +297,15 @@ var DISCORD = (function(){
if (nextChannel === null){ if (nextChannel === null){
return false; return false;
} }
else{
nextChannel.children[0].click(); const nextChannelLink = nextChannel.querySelector("a[href^='/channels/']");
nextChannel.scrollIntoView(true); if (!nextChannelLink) {
return true; return false;
} }
nextChannelLink.click();
nextChannel.scrollIntoView(true);
return true;
} }
} }
}; };

View File

@@ -30,7 +30,7 @@ let stopTrackingDelayed = function(callback){
}, 200); // give the user visual feedback after clicking the button before switching off }, 200); // give the user visual feedback after clicking the button before switching off
}; };
DISCORD.setupMessageUpdateCallback(() => { DISCORD.setupMessageCallback(messages => {
if (STATE.isTracking() && ignoreMessageCallback.size === 0){ if (STATE.isTracking() && ignoreMessageCallback.size === 0){
let info = DISCORD.getSelectedChannel(); let info = DISCORD.getSelectedChannel();
@@ -41,28 +41,22 @@ DISCORD.setupMessageUpdateCallback(() => {
STATE.addDiscordChannel(info.server, info.type, info.id, info.channel, info.extra); STATE.addDiscordChannel(info.server, info.type, info.id, info.channel, info.extra);
let messages = DISCORD.getMessages(); if (messages !== false && !messages.length){
if (messages == null){
stopTrackingDelayed();
return;
}
else if (!messages.length){
DISCORD.loadOlderMessages(); DISCORD.loadOlderMessages();
return; return;
} }
let hasUpdatedFile = STATE.addDiscordMessages(info.id, messages); let hasUpdatedFile = messages !== false && STATE.addDiscordMessages(info.id, messages);
if (SETTINGS.autoscroll){ if (SETTINGS.autoscroll){
let action = null; let action = null;
if (!hasUpdatedFile && !STATE.isMessageFresh(messages[0].id)){ if (messages === false) {
action = SETTINGS.afterSavedMsg;
}
else if (!DISCORD.hasMoreMessages()){
action = SETTINGS.afterFirstMsg; action = SETTINGS.afterFirstMsg;
} }
else if (!hasUpdatedFile && !STATE.isMessageFresh(messages[0].id)){
action = SETTINGS.afterSavedMsg;
}
if (action === null){ if (action === null){
if (hasUpdatedFile){ if (hasUpdatedFile){

View File

@@ -15,11 +15,40 @@
<div class="inner"> <div class="inner">
<h1>Discord History Tracker <span class="version">{{{version:web}}}</span>&nbsp;<span class="bar">|</span>&nbsp;<span class="notes"><a href="https://github.com/chylex/Discord-History-Tracker/wiki/Release-Notes">Release&nbsp;Notes</a></span></h1> <h1>Discord History Tracker <span class="version">{{{version:web}}}</span>&nbsp;<span class="bar">|</span>&nbsp;<span class="notes"><a href="https://github.com/chylex/Discord-History-Tracker/wiki/Release-Notes">Release&nbsp;Notes</a></span></h1>
<p>Discord History Tracker lets you save chat history in your servers, groups, and private conversations, and view it offline.</p> <p>Discord History Tracker lets you save chat history in your servers, groups, and private conversations, and view it offline.</p>
<p>You can use Discord History Tracker either entirely in your browser, or download a desktop app for Windows / Linux / Mac. While the browser-only method is simpler and works on any device that has a modern web browser, it has significant limitations and fewer features than the app. Please read about both methods below.</p> <p>You can use Discord History Tracker either <a href="#browser-only">entirely in your browser</a>, or download a <a href="#desktop-app">desktop app</a> for Windows / Linux / Mac. While the browser-only method is simpler and works on any device that has a modern web browser, it has significant limitations and fewer features than the app. Please read about both methods below.</p>
<img src="img/tracker.png" width="851" class="dht bordered"> <img src="img/tracker.png" width="851" class="dht bordered">
<h2 id="desktop-app">Method 1: Desktop App</h2>
<p>The app can be downloaded from <a href="https://github.com/chylex/Discord-History-Tracker/releases">GitHub</a>. Every release includes 4 versions:</p>
<ul>
<li><strong>win-x64</strong> is for Windows (64-bit)</li>
<li><strong>linux-x64</strong> is for Linux (64-bit)</li>
<li><strong>osx-x64</strong> is for macOS (Intel)</li>
<li><strong>portable</strong> requires <a href="https://dotnet.microsoft.com/download/dotnet/5.0/runtime" rel="nofollow noopener">.NET 5</a> to be installed, but should run on any operating system supported by .NET</li>
</ul>
<p>For the non-portable versions: extract the <strong>DiscordHistoryTracker</strong> file into a folder, and double-click it to launch the app.<br>For the portable version: extract it into a folder, open the folder in a terminal and type: <code>dotnet DiscordHistoryTracker.dll</code></p>
<h3>How to Track Messages</h3>
<p>The app saves messages into a database file stored on your computer. When you open the app, you are given the option to create a new database file, or open an existing one.</p>
<p>In the <strong>Tracking</strong> tab, click <strong>Copy Tracking Script</strong> to generate a tracking script that works similarly to the browser-only version of Discord History Tracker, but instead of saving messages in the browser, the tracking script sends them to the app which saves them in the database file.</p>
<img src="img/app-tracker.png" class="dht bordered" alt="Screenshot of the App (Tracker tab)">
<p>See <strong>Option 2: Browser / Discord Console</strong> above for more detailed instructions on how to paste the tracking script into the browser or Discord app console.</p>
<p>When using the script for the first time, you will see a <strong>Settings</strong> dialog where you can configure the script. These settings will be remembered as long as you don't delete cookies in your browser.</p>
<p>By default, Discord History Tracker is set to automatically scroll up to load the channel history, and pause tracking if it reaches a previously saved message to avoid unnecessary history loading.</p>
<h3>How to View History</h3>
<p>In the <strong>Viewer</strong> tab, you can open a viewer in your browser, or save it as a file you can open in your browser later. You also have the option to apply filters to only view a portion of the saved messages.</p>
<img src="img/app-viewer.png" class="dht bordered" alt="Screenshot of the App (Viewer tab)">
<h3>Technical Details</h3>
<ol>
<li>The app uses SQLite, which means that you can use SQL to manually query or manipulate the database file.</li>
<li>The app communicates with the script using an integrated server. The server only listens for local connections (i.e. connections from programs running on your computer, not the internet). When you copy the tracking script, it will contain a randomly generated token that ensures only the tracking script is able to talk to the server.</li>
<li>You can use the <code>-port &lt;p&gt;</code> and <code>-token &lt;t&gt;</code> command line arguments to configure the server manually &mdash; otherwise, they will be assigned automatically in a way that allows running multiple separate instances of the app.</li>
</ol>
<h2>Method 1: Browser-Only</h2> <h2 id="browser-only">Method 2: Browser-Only</h2>
<p>A tracking script will load messages according to your settings, and save them in your browser.</p> <p>A tracking script will load messages according to your settings, and save them in your browser.</p>
<p>Because everything happens in your browser, if the browser tab is closed, or your browser or computer crashes, you will lose all progress. Your browser may also be unable to process large amounts of messages. If this is a concern, use the app method.</p> <p>Because everything happens in your browser, if the browser tab is closed, or your browser or computer crashes, you will lose all progress. Your browser may also be unable to process large amounts of messages. If this is a concern, use the app method.</p>
@@ -56,7 +85,7 @@
</ol> </ol>
<p id="tracker-copy-issue">Your browser may not support copying to clipboard, please try copying the script manually:</p> <p id="tracker-copy-issue">Your browser may not support copying to clipboard, please try copying the script manually:</p>
<textarea id="tracker-copy-contents"><?php include "./build/track.html"; ?></textarea> <textarea id="tracker-copy-contents"><?php include './build/track.html'; ?></textarea>
</div> </div>
<h4>Option 3: Bookmarklet</h4> <h4>Option 3: Bookmarklet</h4>
@@ -64,7 +93,7 @@
<p>Requires Firefox 69 or newer.</p> <p>Requires Firefox 69 or newer.</p>
<ol> <ol>
<li>Right-click <a href="<?php include "./build/track.html"; ?>" onclick="return false;" onauxclick="return false;">Discord History Tracker</a></li> <li>Right-click <a href="<?php include './build/track.html'; ?>" onclick="return false;" onauxclick="return false;">Discord History Tracker</a></li>
<li>Select &laquo;Bookmark This Link&raquo; and save the bookmark</li> <li>Select &laquo;Bookmark This Link&raquo; and save the bookmark</li>
<li>Open <a href="https://discord.com/channels/@me" rel="noreferrer">Discord</a> and click the bookmark to run the script</li> <li>Open <a href="https://discord.com/channels/@me" rel="noreferrer">Discord</a> and click the bookmark to run the script</li>
</ol> </ol>
@@ -83,35 +112,6 @@
<h3>How to View History</h3> <h3>How to View History</h3>
<p>First, save the <a href="build/viewer.html">Viewer</a> file to your computer. Then you can open the downloaded viewer in your browser, click <strong>Load File</strong>, and select the archive to view.</p> <p>First, save the <a href="build/viewer.html">Viewer</a> file to your computer. Then you can open the downloaded viewer in your browser, click <strong>Load File</strong>, and select the archive to view.</p>
<h2>Method 2: Desktop App</h2>
<p>The app can be downloaded from <a href="https://github.com/chylex/Discord-History-Tracker/releases">GitHub</a>. Every release includes 4 versions available:</p>
<ul>
<li><strong>win-x64</strong> is for Windows (64-bit)</li>
<li><strong>linux-x64</strong> is for Linux (64-bit)</li>
<li><strong>osx-x64</strong> is for macOS (Intel)</li>
<li><strong>portable</strong> requires <a href="https://dotnet.microsoft.com/download/dotnet/5.0/runtime" rel="nofollow noopener">.NET 5</a> to be installed, but should run on any operating system supported by .NET</li>
</ul>
<p>The three non-portable versions include an executable named <strong>DiscordHistoryTracker</strong> you can launch. For the portable version, extract the archive into a folder, open the folder in a terminal and type: <code>dotnet DiscordHistoryTracker.dll</code></p>
<h3>How to Track Messages</h3>
<p>The app saves messages into a database file stored on your computer. When you open the app, you are given the option to create a new database file, or open an existing one.</p>
<p>In the <strong>Tracking</strong> tab, click <strong>Copy Tracking Script</strong> to generate a tracking script that works similarly to the browser-only version of Discord History Tracker, but instead of saving messages in the browser, the tracking script sends them to the app which saves them in the database file.</p>
<img src="img/app-tracker.png" class="dht bordered" alt="Screenshot of the App (Tracker tab)">
<p>See <strong>Option 2: Browser / Discord Console</strong> above for more detailed instructions on how to paste the tracking script into the browser or Discord app console.</p>
<p>When using the script for the first time, you will see a <strong>Settings</strong> dialog where you can configure the script. These settings will be remembered as long as you don't delete cookies in your browser.</p>
<p>By default, Discord History Tracker is set to automatically scroll up to load the channel history, and pause tracking if it reaches a previously saved message to avoid unnecessary history loading.</p>
<h3>How to View History</h3>
<p>In the <strong>Viewer</strong> tab, you can open a viewer in your browser, or save it as a file you can open in your browser later. You also have the option to apply filters to only view a portion of the saved messages.</p>
<img src="img/app-viewer.png" class="dht bordered" alt="Screenshot of the App (Viewer tab)">
<h3>Technical Details</h3>
<ol>
<li>The app uses SQLite, which means that you can use SQL to manually query or manipulate the database file.</li>
<li>The app communicates with the script using an integrated server. The server only listens for local connections (i.e. connections from programs running on your computer, not the internet). When you copy the tracking script, it will contain a randomly generated token that ensures only the tracking script is able to talk to the server.</li>
<li>You can use the <code>-port &lt;p&gt;</code> and <code>-token &lt;t&gt;</code> command line arguments to configure the server manually &mdash; otherwise, they will be assigned automatically in a way that allows running multiple separate instances of the app.</li>
</ol>
<h2>External Links</h2> <h2>External Links</h2>
<p class="links"> <p class="links">
<a href="https://github.com/chylex/Discord-History-Tracker/issues">Issues&nbsp;&amp;&nbsp;Suggestions</a>&nbsp;&nbsp;&mdash;&nbsp; <a href="https://github.com/chylex/Discord-History-Tracker/issues">Issues&nbsp;&amp;&nbsp;Suggestions</a>&nbsp;&nbsp;&mdash;&nbsp;