1
0
mirror of https://github.com/chylex/Discord-History-Tracker.git synced 2025-09-17 16:24:47 +02:00

6 Commits

Author SHA1 Message Date
b2276600c7 Release v46.0 2025-03-18 21:03:45 +01:00
40269f591b Fix the Export All button text 2025-03-18 21:03:44 +01:00
dea3b272c0 Enable full trimming in .NET 2025-03-18 20:23:20 +01:00
e66206b4a8 Update About page 2025-03-18 17:35:39 +01:00
780d5ae421 Optimize viewer message export 2025-03-18 17:35:38 +01:00
38f79dee7d Replace CommunityToolkit.Mvvm with PropertyChanged.SourceGenerator 2025-03-18 13:55:32 +01:00
25 changed files with 211 additions and 189 deletions

2
app/.editorconfig Normal file
View File

@@ -0,0 +1,2 @@
[*.cs]
propertychanged.auto_notify = false

View File

@@ -23,7 +23,10 @@
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.3" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.3" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="999.0.0-build.0.g0d941a6a62" />
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -1,11 +1,11 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using PropertyChanged.SourceGenerator;
namespace DHT.Desktop.Dialogs.CheckBox;
class CheckBoxDialogModel : ObservableObject {
partial class CheckBoxDialogModel {
public string Title { get; init; } = "";
private IReadOnlyList<CheckBoxItem> items = [];
@@ -28,7 +28,10 @@ class CheckBoxDialogModel : ObservableObject {
private bool pauseCheckEvents = false;
[DependsOn(nameof(Items))]
public bool AreAllSelected => Items.All(static item => item.IsChecked);
[DependsOn(nameof(Items))]
public bool AreNoneSelected => Items.All(static item => !item.IsChecked);
public void SelectAll() => SetAllChecked(true);
@@ -46,8 +49,7 @@ class CheckBoxDialogModel : ObservableObject {
}
private void UpdateBulkButtons() {
OnPropertyChanged(nameof(AreAllSelected));
OnPropertyChanged(nameof(AreNoneSelected));
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Items)));
}
private void OnItemPropertyChanged(object? sender, PropertyChangedEventArgs e) {

View File

@@ -1,12 +1,12 @@
using CommunityToolkit.Mvvm.ComponentModel;
using PropertyChanged.SourceGenerator;
namespace DHT.Desktop.Dialogs.CheckBox;
partial class CheckBoxItem : ObservableObject {
partial class CheckBoxItem {
public string Title { get; init; } = "";
public object? Item { get; init; } = null;
[ObservableProperty]
[Notify]
private bool isChecked = false;
}

View File

@@ -1,30 +1,23 @@
using CommunityToolkit.Mvvm.ComponentModel;
using PropertyChanged.SourceGenerator;
namespace DHT.Desktop.Dialogs.Progress;
sealed partial class ProgressItem : ObservableObject {
[ObservableProperty(Setter = Access.Private)]
[NotifyPropertyChangedFor(nameof(Opacity))]
private bool isVisible = false;
public double Opacity => IsVisible ? 1.0 : 0.0;
sealed partial class ProgressItem {
[Notify]
private string message = "";
public string Message {
get => message;
set {
SetProperty(ref message, value);
IsVisible = !string.IsNullOrEmpty(value);
}
}
[ObservableProperty]
[Notify]
private string items = "";
[ObservableProperty]
[Notify]
private long progress = 0L;
[ObservableProperty]
[Notify]
private bool isIndeterminate;
[DependsOn(nameof(Message))]
public bool IsVisible => !string.IsNullOrEmpty(Message);
[DependsOn(nameof(IsVisible))]
public double Opacity => IsVisible ? 1.0 : 0.0;
}

View File

@@ -1,11 +1,11 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using PropertyChanged.SourceGenerator;
namespace DHT.Desktop.Dialogs.TextBox;
class TextBoxDialogModel : ObservableObject {
partial class TextBoxDialogModel {
public string Title { get; init; } = "";
public string Description { get; init; } = "";
@@ -27,10 +27,11 @@ class TextBoxDialogModel : ObservableObject {
}
}
[DependsOn(nameof(Items))]
public bool HasErrors => Items.Any(static item => !item.IsValid);
private void OnItemErrorsChanged(object? sender, DataErrorsChangedEventArgs e) {
OnPropertyChanged(nameof(HasErrors));
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Items)));
}
}

View File

@@ -1,25 +1,22 @@
using System;
using System.Collections;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using PropertyChanged.SourceGenerator;
namespace DHT.Desktop.Dialogs.TextBox;
class TextBoxItem : ObservableObject, INotifyDataErrorInfo {
partial class TextBoxItem : INotifyDataErrorInfo {
public string Title { get; init; } = "";
public object? Item { get; init; } = null;
public Func<string, bool> ValidityCheck { get; init; } = static _ => true;
public bool IsValid => ValidityCheck(Value);
[Notify]
private string value = string.Empty;
public string Value {
get => value;
set {
SetProperty(ref this.value, value);
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
}
private void OnValueChanged() {
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
}
public IEnumerable GetErrors(string? propertyName) {

View File

@@ -3,12 +3,12 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:main="clr-namespace:DHT.Desktop.Main"
mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="360"
mc:Ignorable="d" d:DesignWidth="510" d:DesignHeight="375"
x:Class="DHT.Desktop.Main.AboutWindow"
x:DataType="main:AboutWindowModel"
Title="About Discord History Tracker"
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
Width="480" Height="360" CanResize="False"
Width="510" Height="375" CanResize="False"
WindowStartupLocation="CenterOwner">
<Design.DataContext>
@@ -16,10 +16,6 @@
</Design.DataContext>
<Window.Styles>
<Style Selector="StackPanel">
<Setter Property="Orientation" Value="Horizontal" />
<Setter Property="Spacing" Value="5" />
</Style>
<Style Selector="TextBlock">
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="VerticalAlignment" Value="Center" />
@@ -33,44 +29,45 @@
<StackPanel Orientation="Vertical" Margin="20" Spacing="12">
<TextBlock VerticalAlignment="Center">
Discord History Tracker was created by chylex and released under the MIT license.
</TextBlock>
<StackPanel Orientation="Vertical" Spacing="3">
<TextBlock TextWrapping="Wrap">Discord History Tracker was created by chylex.</TextBlock>
<TextBlock>It is available under the MIT license.</TextBlock>
</StackPanel>
<StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Command="{Binding ShowOfficialWebsite}">Official Website</Button>
<Button Command="{Binding ShowIssueTracker}">Issue Tracker</Button>
<Button Command="{Binding ShowSourceCode}">Source Code</Button>
</StackPanel>
<Grid RowDefinitions="Auto,5,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="175,125,*" Margin="0 10 0 0">
<Grid RowDefinitions="Auto,5,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="*,115,95" Margin="0 10 0 0">
<TextBlock Grid.Row="0" Grid.Column="0" FontWeight="Bold">Third-Party Software</TextBlock>
<TextBlock Grid.Row="0" Grid.Column="1" FontWeight="Bold">License</TextBlock>
<TextBlock Grid.Row="0" Grid.Column="2" FontWeight="Bold">Link</TextBlock>
<TextBlock Grid.Row="2" Grid.Column="0">.NET 8</TextBlock>
<TextBlock Grid.Row="2" Grid.Column="0">.NET</TextBlock>
<TextBlock Grid.Row="2" Grid.Column="1">MIT</TextBlock>
<Button Grid.Row="2" Grid.Column="2" Command="{Binding ShowLibraryNetCore}">GitHub</Button>
<TextBlock Grid.Row="3" Grid.Column="0">Avalonia</TextBlock>
<TextBlock Grid.Row="3" Grid.Column="1">MIT</TextBlock>
<Button Grid.Row="3" Grid.Column="2" Command="{Binding ShowLibraryAvalonia}">NuGet</Button>
<Button Grid.Row="3" Grid.Column="2" Command="{Binding ShowLibraryAvalonia}">GitHub</Button>
<TextBlock Grid.Row="4" Grid.Column="0">MVVM Toolkit</TextBlock>
<TextBlock Grid.Row="4" Grid.Column="0">Rx.NET</TextBlock>
<TextBlock Grid.Row="4" Grid.Column="1">MIT</TextBlock>
<Button Grid.Row="4" Grid.Column="2" Command="{Binding ShowLibraryCommunityToolkit}">GitHub</Button>
<Button Grid.Row="4" Grid.Column="2" Command="{Binding ShowLibraryRxNet}">GitHub</Button>
<TextBlock Grid.Row="5" Grid.Column="0">SQLite</TextBlock>
<TextBlock Grid.Row="5" Grid.Column="1">Public Domain</TextBlock>
<Button Grid.Row="5" Grid.Column="2" Command="{Binding ShowLibrarySqlite}">Official Website</Button>
<Button Grid.Row="5" Grid.Column="2" Command="{Binding ShowLibrarySqlite}">Website</Button>
<TextBlock Grid.Row="6" Grid.Column="0">Microsoft.Data.Sqlite</TextBlock>
<TextBlock Grid.Row="6" Grid.Column="1">Apache-2.0</TextBlock>
<Button Grid.Row="6" Grid.Column="2" Command="{Binding ShowLibrarySqliteAdoNet}">NuGet</Button>
<TextBlock Grid.Row="7" Grid.Column="0">Rx.NET</TextBlock>
<TextBlock Grid.Row="7" Grid.Column="0">PropertyChanged.SourceGenerator</TextBlock>
<TextBlock Grid.Row="7" Grid.Column="1">MIT</TextBlock>
<Button Grid.Row="7" Grid.Column="2" Command="{Binding ShowLibraryRxNet}">GitHub</Button>
<Button Grid.Row="7" Grid.Column="2" Command="{Binding ShowLibraryPropertyChangedSourceGenerator}">GitHub</Button>
</Grid>
</StackPanel>

View File

@@ -20,11 +20,11 @@ sealed class AboutWindowModel {
}
public void ShowLibraryAvalonia() {
SystemUtils.OpenUrl("https://www.nuget.org/packages/Avalonia");
SystemUtils.OpenUrl("https://github.com/AvaloniaUI/Avalonia");
}
public void ShowLibraryCommunityToolkit() {
SystemUtils.OpenUrl("https://github.com/CommunityToolkit/dotnet");
public void ShowLibraryPropertyChangedSourceGenerator() {
SystemUtils.OpenUrl("https://github.com/canton7/PropertyChanged.SourceGenerator");
}
public void ShowLibrarySqlite() {

View File

@@ -5,17 +5,17 @@ using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.ReactiveUI;
using CommunityToolkit.Mvvm.ComponentModel;
using DHT.Desktop.Common;
using DHT.Server;
using DHT.Server.Data.Filters;
using DHT.Server.Data.Settings;
using DHT.Utils.Logging;
using DHT.Utils.Tasks;
using PropertyChanged.SourceGenerator;
namespace DHT.Desktop.Main.Controls;
sealed partial class DownloadItemFilterPanelModel : ObservableObject, IAsyncDisposable {
sealed partial class DownloadItemFilterPanelModel : IAsyncDisposable {
private static readonly Log Log = Log.ForType<DownloadItemFilterPanelModel>();
public sealed record Unit(string Name, uint Scale);
@@ -32,15 +32,16 @@ sealed partial class DownloadItemFilterPanelModel : ObservableObject, IAsyncDisp
nameof(MaximumSizeUnit),
];
public string FilterStatisticsText { get; private set; } = "";
[Notify(Setter.Private)]
private string filterStatisticsText = "";
[ObservableProperty]
[Notify]
private bool limitSize = false;
[ObservableProperty]
[Notify]
private ulong maximumSize = 0UL;
[ObservableProperty]
[Notify]
private Unit maximumSizeUnit = AllUnits[0];
public IEnumerable<Unit> Units => AllUnits;
@@ -151,9 +152,7 @@ sealed partial class DownloadItemFilterPanelModel : ObservableObject, IAsyncDisp
private void UpdateFilterStatisticsText() {
string matchingItemCountStr = matchingItemCount?.Format() ?? "(...)";
string totalItemCountStr = totalItemCount?.Format() ?? "(...)";
FilterStatisticsText = verb + " " + matchingItemCountStr + " out of " + totalItemCountStr + " file" + (totalItemCount is null or 1 ? "." : "s.");
OnPropertyChanged(nameof(FilterStatisticsText));
}
public DownloadItemFilter CreateFilter() {

View File

@@ -7,7 +7,6 @@ using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.ReactiveUI;
using CommunityToolkit.Mvvm.ComponentModel;
using DHT.Desktop.Common;
using DHT.Desktop.Dialogs.CheckBox;
using DHT.Desktop.Dialogs.Message;
@@ -16,10 +15,11 @@ using DHT.Server;
using DHT.Server.Data;
using DHT.Server.Data.Filters;
using DHT.Utils.Tasks;
using PropertyChanged.SourceGenerator;
namespace DHT.Desktop.Main.Controls;
sealed partial class MessageFilterPanelModel : ObservableObject, IDisposable {
sealed partial class MessageFilterPanelModel : IDisposable {
private static readonly HashSet<string> FilterProperties = [
nameof(FilterByDate),
nameof(StartDate),
@@ -30,37 +30,38 @@ sealed partial class MessageFilterPanelModel : ObservableObject, IDisposable {
nameof(IncludedUsers),
];
public string FilterStatisticsText { get; private set; } = "";
public event PropertyChangedEventHandler? FilterPropertyChanged;
public bool HasAnyFilters => FilterByDate || FilterByChannel || FilterByUser;
[ObservableProperty]
[Notify]
private string filterStatisticsText = "";
[Notify]
private bool filterByDate = false;
[ObservableProperty]
[Notify]
private DateTime? startDate = null;
[ObservableProperty]
[Notify]
private DateTime? endDate = null;
[ObservableProperty]
[Notify]
private bool filterByChannel = false;
[ObservableProperty]
[Notify]
private HashSet<ulong>? includedChannels = null;
[ObservableProperty]
[Notify]
private bool filterByUser = false;
[ObservableProperty]
[Notify]
private HashSet<ulong>? includedUsers = null;
[ObservableProperty]
[Notify]
private string channelFilterLabel = "";
[ObservableProperty]
[Notify]
private string userFilterLabel = "";
private readonly Window window;
@@ -181,9 +182,7 @@ sealed partial class MessageFilterPanelModel : ObservableObject, IDisposable {
private void UpdateFilterStatisticsText() {
string exportedMessageCountStr = exportedMessageCount?.Format() ?? "(...)";
string totalMessageCountStr = totalMessageCount?.Format() ?? "(...)";
FilterStatisticsText = verb + " " + exportedMessageCountStr + " out of " + totalMessageCountStr + " message" + (totalMessageCount is null or 1 ? "." : "s.");
OnPropertyChanged(nameof(FilterStatisticsText));
}
public async Task OpenChannelFilterDialog() {

View File

@@ -1,30 +1,30 @@
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Server;
using DHT.Server;
using DHT.Server.Service;
using DHT.Utils.Logging;
using PropertyChanged.SourceGenerator;
namespace DHT.Desktop.Main.Controls;
sealed partial class ServerConfigurationPanelModel : ObservableObject, IDisposable {
sealed partial class ServerConfigurationPanelModel : IDisposable {
private static readonly Log Log = Log.ForType<ServerConfigurationPanelModel>();
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasMadeChanges))]
[Notify]
private string inputPort;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasMadeChanges))]
[Notify]
private string inputToken;
[DependsOn(nameof(InputPort), nameof(InputToken))]
public bool HasMadeChanges => ServerConfiguration.Port.ToString() != InputPort || ServerConfiguration.Token != InputToken;
[ObservableProperty(Setter = Access.Private)]
[Notify(Setter.Private)]
private bool isToggleServerButtonEnabled = true;
public string ToggleServerButtonText => server.IsRunning ? "Stop Server" : "Start Server";
@@ -53,7 +53,7 @@ sealed partial class ServerConfigurationPanelModel : ObservableObject, IDisposab
}
private void UpdateServerStatus() {
OnPropertyChanged(nameof(ToggleServerButtonText));
OnPropertyChanged(new PropertyChangedEventArgs(nameof(ToggleServerButtonText)));
}
private async Task StartServer() {
@@ -106,7 +106,7 @@ sealed partial class ServerConfigurationPanelModel : ObservableObject, IDisposab
ServerConfiguration.Port = port;
ServerConfiguration.Token = inputToken;
OnPropertyChanged(nameof(HasMadeChanges));
OnPropertyChanged(new PropertyChangedEventArgs(nameof(HasMadeChanges)));
await StartServer();
}

View File

@@ -2,27 +2,27 @@ using System;
using System.Reactive.Linq;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using DHT.Server;
using DHT.Server.Service;
using PropertyChanged.SourceGenerator;
namespace DHT.Desktop.Main.Controls;
sealed partial class StatusBarModel : ObservableObject, IDisposable {
[ObservableProperty(Setter = Access.Private)]
sealed partial class StatusBarModel : IDisposable {
[Notify(Setter.Private)]
private long? serverCount;
[ObservableProperty(Setter = Access.Private)]
[Notify(Setter.Private)]
private long? channelCount;
[ObservableProperty(Setter = Access.Private)]
[Notify(Setter.Private)]
private long? messageCount;
[ObservableProperty(Setter = Access.Private)]
[NotifyPropertyChangedFor(nameof(ServerStatusText))]
[Notify(Setter.Private)]
private ServerManager.Status serverStatus;
public string ServerStatusText => serverStatus switch {
[DependsOn(nameof(ServerStatus))]
public string ServerStatusText => ServerStatus switch {
ServerManager.Status.Starting => "STARTING",
ServerManager.Status.Started => "READY",
ServerManager.Status.Stopping => "STOPPING",
@@ -45,7 +45,7 @@ sealed partial class StatusBarModel : ObservableObject, IDisposable {
channelCountSubscription = state.Db.Channels.TotalCount.ObserveOn(AvaloniaScheduler.Instance).Subscribe(newChannelCount => ChannelCount = newChannelCount);
messageCountSubscription = state.Db.Messages.TotalCount.ObserveOn(AvaloniaScheduler.Instance).Subscribe(newMessageCount => MessageCount = newMessageCount);
state.Server.StatusChanged += OnServerStatusChanged;
state.Server.StatusChanged += OnStateServerStatusChanged;
serverStatus = state.Server.IsRunning ? ServerManager.Status.Started : ServerManager.Status.Stopped;
}
@@ -54,10 +54,10 @@ sealed partial class StatusBarModel : ObservableObject, IDisposable {
channelCountSubscription.Dispose();
messageCountSubscription.Dispose();
state.Server.StatusChanged -= OnServerStatusChanged;
state.Server.StatusChanged -= OnStateServerStatusChanged;
}
private void OnServerStatusChanged(object? sender, ServerManager.Status e) {
private void OnStateServerStatusChanged(object? sender, ServerManager.Status e) {
Dispatcher.UIThread.InvokeAsync(() => ServerStatus = e);
}
}

View File

@@ -1,11 +1,11 @@
using CommunityToolkit.Mvvm.ComponentModel;
using PropertyChanged.SourceGenerator;
namespace DHT.Desktop.Main.Dialogs;
sealed partial class NewDatabaseSettingsDialogModel : ObservableObject {
[ObservableProperty]
sealed partial class NewDatabaseSettingsDialogModel {
[Notify]
private bool separateFileForDownloads = true;
[ObservableProperty]
[Notify]
private bool downloadsAutoStart = true;
}

View File

@@ -3,25 +3,25 @@ using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Main.Screens;
using DHT.Desktop.Server;
using DHT.Server;
using DHT.Server.Database;
using DHT.Utils.Logging;
using PropertyChanged.SourceGenerator;
namespace DHT.Desktop.Main;
sealed partial class MainWindowModel : ObservableObject, IAsyncDisposable {
sealed partial class MainWindowModel : IAsyncDisposable {
private const string DefaultTitle = "Discord History Tracker";
private static readonly Log Log = Log.ForType<MainWindowModel>();
[ObservableProperty(Setter = Access.Private)]
[Notify(Setter.Private)]
private string title = DefaultTitle;
[ObservableProperty(Setter = Access.Private)]
[Notify(Setter.Private)]
private UserControl currentScreen;
private readonly WelcomeScreen welcomeScreen;

View File

@@ -35,7 +35,7 @@
<Button Command="{Binding OnClickToggleDownload}" Content="{Binding ToggleDownloadButtonText}" IsEnabled="{Binding IsToggleDownloadButtonEnabled}" />
<Button Command="{Binding OnClickRetryFailed}" IsEnabled="{Binding IsRetryFailedOnDownloadsButtonEnabled}">Retry Failed</Button>
<Button Command="{Binding OnClickDeleteOrphaned}">Delete Orphaned</Button>
<Button Command="{Binding OnClickExportAll}" IsEnabled="{Binding HasSuccessfulDownloads}">Export All</Button>
<Button Command="{Binding OnClickExportAll}" IsEnabled="{Binding HasSuccessfulDownloads}">Export All...</Button>
</WrapPanel>
<StackPanel Orientation="Vertical" Spacing="20" Margin="0 10 0 0">
<controls:DownloadItemFilterPanel DataContext="{Binding FilterModel}" IsEnabled="{Binding !$parent[UserControl].((pages:DownloadsPageModel)DataContext).IsDownloading}" />

View File

@@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.ReactiveUI;
using CommunityToolkit.Mvvm.ComponentModel;
using DHT.Desktop.Common;
using DHT.Desktop.Dialogs.File;
using DHT.Desktop.Dialogs.Message;
@@ -20,31 +20,32 @@ using DHT.Server.Data.Settings;
using DHT.Server.Download;
using DHT.Utils.Logging;
using DHT.Utils.Tasks;
using PropertyChanged.SourceGenerator;
namespace DHT.Desktop.Main.Pages;
sealed partial class DownloadsPageModel : ObservableObject, IAsyncDisposable {
sealed partial class DownloadsPageModel : IAsyncDisposable {
private static readonly Log Log = Log.ForType<DownloadsPageModel>();
[ObservableProperty(Setter = Access.Private)]
[Notify(Setter.Private)]
private bool isToggleDownloadButtonEnabled = true;
[DependsOn(nameof(IsDownloading))]
public string ToggleDownloadButtonText => IsDownloading ? "Stop Downloading" : "Start Downloading";
[ObservableProperty(Setter = Access.Private)]
[NotifyPropertyChangedFor(nameof(IsRetryFailedOnDownloadsButtonEnabled))]
[Notify(Setter.Private)]
private bool isRetryingFailedDownloads = false;
[ObservableProperty(Setter = Access.Private)]
[Notify(Setter.Private)]
private bool hasSuccessfulDownloads;
[ObservableProperty(Setter = Access.Private)]
[NotifyPropertyChangedFor(nameof(IsRetryFailedOnDownloadsButtonEnabled))]
[Notify(Setter.Private)]
private bool hasFailedDownloads;
[DependsOn(nameof(IsRetryingFailedDownloads), nameof(HasFailedDownloads))]
public bool IsRetryFailedOnDownloadsButtonEnabled => !IsRetryingFailedDownloads && HasFailedDownloads;
[ObservableProperty(Setter = Access.Private)]
[Notify(Setter.Private)]
private string downloadMessage = "";
public DownloadItemFilterPanelModel FilterModel { get; }
@@ -145,8 +146,7 @@ sealed partial class DownloadsPageModel : ObservableObject, IAsyncDisposable {
private void OnDownloadStateChanged() {
RecomputeDownloadStatistics();
OnPropertyChanged(nameof(ToggleDownloadButtonText));
OnPropertyChanged(nameof(IsDownloading));
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsDownloading)));
}
private void OnItemFinished(DownloadItem item) {
@@ -281,21 +281,19 @@ sealed partial class DownloadsPageModel : ObservableObject, IAsyncDisposable {
HasFailedDownloads = statusStatistics.FailedCount > 0;
}
[ObservableObject]
public sealed partial class StatisticsRow(string state) {
public string State { get; } = state;
[ObservableProperty]
[Notify]
private int items;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SizeText))]
[Notify]
private ulong? size;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SizeText))]
[Notify]
private bool hasFilesWithUnknownSize;
[DependsOn(nameof(Size), nameof(HasFilesWithUnknownSize))]
public string SizeText {
get {
if (size == null) {

View File

@@ -4,28 +4,27 @@ using System.Threading.Tasks;
using System.Web;
using Avalonia.Controls;
using Avalonia.Input.Platform;
using CommunityToolkit.Mvvm.ComponentModel;
using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Discord;
using DHT.Desktop.Server;
using PropertyChanged.SourceGenerator;
using static DHT.Desktop.Program;
namespace DHT.Desktop.Main.Pages;
sealed partial class TrackingPageModel : ObservableObject {
[ObservableProperty(Setter = Access.Private)]
sealed partial class TrackingPageModel {
[Notify(Setter.Private)]
private bool isCopyTrackingScriptButtonEnabled = true;
[ObservableProperty(Setter = Access.Private)]
[NotifyPropertyChangedFor(nameof(ToggleAppDevToolsButtonText))]
[Notify(Setter.Private)]
private bool? areDevToolsEnabled = null;
[ObservableProperty(Setter = Access.Private)]
[NotifyPropertyChangedFor(nameof(ToggleAppDevToolsButtonText))]
[Notify(Setter.Private)]
private bool isToggleAppDevToolsButtonEnabled = false;
public string OpenDevToolsShortcutText { get; } = OperatingSystem.IsMacOS() ? "Cmd+Shift+I" : "Ctrl+Shift+I";
[DependsOn(nameof(AreDevToolsEnabled), nameof(IsToggleAppDevToolsButtonEnabled))]
public string ToggleAppDevToolsButtonText {
get {
if (!AreDevToolsEnabled.HasValue) {

View File

@@ -3,7 +3,6 @@ using System.ComponentModel;
using System.Threading.Tasks;
using System.Web;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using DHT.Desktop.Common;
using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Dialogs.Progress;
@@ -13,16 +12,17 @@ using DHT.Server;
using DHT.Server.Data.Filters;
using DHT.Server.Service.Viewer;
using DHT.Utils.Logging;
using PropertyChanged.SourceGenerator;
namespace DHT.Desktop.Main.Pages;
sealed partial class ViewerPageModel : ObservableObject, IDisposable {
sealed partial class ViewerPageModel : IDisposable {
private static readonly Log Log = Log.ForType<ViewerPageModel>();
public bool DatabaseToolFilterModeKeep { get; set; } = true;
public bool DatabaseToolFilterModeRemove { get; set; } = false;
[ObservableProperty]
[Notify]
private bool hasFilters = false;
public MessageFilterPanelModel FilterModel { get; }

View File

@@ -5,7 +5,6 @@ using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using DHT.Desktop.Common;
using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Dialogs.Progress;
@@ -14,15 +13,16 @@ using DHT.Server.Database;
using DHT.Server.Database.Sqlite.Schema;
using DHT.Server.Database.Sqlite.Utils;
using DHT.Utils.Logging;
using PropertyChanged.SourceGenerator;
namespace DHT.Desktop.Main.Screens;
sealed partial class WelcomeScreenModel : ObservableObject {
sealed partial class WelcomeScreenModel {
private static readonly Log Log = Log.ForType<WelcomeScreenModel>();
public string Version => Program.Version;
[ObservableProperty(Setter = Access.Private)]
[Notify(Setter.Private)]
private bool isOpenOrCreateDatabaseButtonEnabled = true;
public event EventHandler<IDatabaseFile>? DatabaseSelected;

View File

@@ -21,12 +21,13 @@
<PropertyGroup>
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
<TrimMode>full</TrimMode>
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
<EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>
<EventSourceSupport>false</EventSourceSupport>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
<XmlResolverIsNetworkingEnabledByDefault>false</XmlResolverIsNetworkingEnabledByDefault>
</PropertyGroup>
<PropertyGroup>

View File

@@ -1,16 +1,20 @@
using System;
using System.Buffers.Text;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace DHT.Server.Database.Export;
sealed class SnowflakeJsonSerializer : JsonConverter<Snowflake> {
private const int MaxUlongStringLength = 20;
public override Snowflake Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
return new Snowflake(ulong.Parse(reader.GetString()!));
}
public override void Write(Utf8JsonWriter writer, Snowflake value, JsonSerializerOptions options) {
writer.WriteStringValue(value.Id.ToString());
writer.WriteStringValue(Format(value, stackalloc byte[MaxUlongStringLength]));
}
public override Snowflake ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
@@ -18,6 +22,14 @@ sealed class SnowflakeJsonSerializer : JsonConverter<Snowflake> {
}
public override void WriteAsPropertyName(Utf8JsonWriter writer, Snowflake value, JsonSerializerOptions options) {
writer.WritePropertyName(value.Id.ToString());
writer.WritePropertyName(Format(value, stackalloc byte[MaxUlongStringLength]));
}
private static ReadOnlySpan<byte> Format(Snowflake value, Span<byte> destination) {
if (!Utf8Formatter.TryFormat(value.Id, destination, out int bytesWritten)) {
Debug.Fail("Failed to format Snowflake value.");
}
return destination[..bytesWritten];
}
}

View File

@@ -30,7 +30,7 @@ static class ViewerJson {
public required string Name { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Parent { get; init; }
public Snowflake? Parent { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? Position { get; init; }
@@ -55,7 +55,7 @@ static class ViewerJson {
public long? Te { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? R { get; init; }
public Snowflake? R { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public JsonMessageAttachment[]? A { get; init; }
@@ -80,7 +80,7 @@ static class ViewerJson {
public sealed class JsonMessageReaction {
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Id { get; init; }
public Snowflake? Id { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? N { get; init; }

View File

@@ -2,13 +2,15 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using DHT.Server.Data;
using DHT.Server.Data.Filters;
using DHT.Utils.Logging;
using Channel = System.Threading.Channels.Channel;
using DiscordChannel = DHT.Server.Data.Channel;
namespace DHT.Server.Database.Export;
@@ -18,12 +20,12 @@ static class ViewerJsonExport {
public static async Task GetMetadata(Stream stream, IDatabaseFile db, MessageFilter? filter = null, CancellationToken cancellationToken = default) {
Perf perf = Log.Start();
var includedChannels = new List<Channel>();
var includedChannels = new List<DiscordChannel>();
var includedServerIds = new HashSet<ulong>();
HashSet<ulong>? channelIdFilter = filter?.ChannelIds;
await foreach (Channel channel in db.Channels.Get(cancellationToken)) {
await foreach (DiscordChannel channel in db.Channels.Get(cancellationToken)) {
if (channelIdFilter == null || channelIdFilter.Contains(channel.Id)) {
includedChannels.Add(channel);
includedServerIds.Add(channel.Server);
@@ -53,11 +55,30 @@ static class ViewerJsonExport {
ReadOnlyMemory<byte> newLine = "\n"u8.ToArray();
await foreach (ViewerJson.JsonMessage message in GenerateMessageList(db, filter, cancellationToken)) {
await JsonSerializer.SerializeAsync(stream, message, ViewerJsonMessageContext.Default.JsonMessage, cancellationToken);
Channel<Message> channel = Channel.CreateBounded<Message>(new BoundedChannelOptions(32) {
SingleWriter = true,
SingleReader = true,
AllowSynchronousContinuations = true,
FullMode = BoundedChannelFullMode.Wait,
});
Task writerTask = Task.Run(async () => {
try {
await foreach (Message message in db.Messages.Get(filter, cancellationToken)) {
await channel.Writer.WriteAsync(message, cancellationToken);
}
} finally {
channel.Writer.Complete();
}
}, cancellationToken);
await foreach (Message message in channel.Reader.ReadAllAsync(cancellationToken)) {
await JsonSerializer.SerializeAsync(stream, ToJsonMessage(message), ViewerJsonMessageContext.Default.JsonMessage, cancellationToken);
await stream.WriteAsync(newLine, cancellationToken);
}
await writerTask;
perf.Step("Generate and serialize messages to JSON");
perf.End();
}
@@ -93,14 +114,14 @@ static class ViewerJsonExport {
return servers;
}
private static Dictionary<Snowflake, ViewerJson.JsonChannel> GenerateChannelList(List<Channel> includedChannels) {
private static Dictionary<Snowflake, ViewerJson.JsonChannel> GenerateChannelList(List<DiscordChannel> includedChannels) {
var channels = new Dictionary<Snowflake, ViewerJson.JsonChannel>();
foreach (Channel channel in includedChannels) {
foreach (DiscordChannel channel in includedChannels) {
channels[channel.Id] = new ViewerJson.JsonChannel {
Server = channel.Server,
Name = channel.Name,
Parent = channel.ParentId?.ToString(),
Parent = channel.ParentId,
Position = channel.Position,
Topic = channel.Topic,
Nsfw = channel.Nsfw,
@@ -110,40 +131,38 @@ static class ViewerJsonExport {
return channels;
}
private static async IAsyncEnumerable<ViewerJson.JsonMessage> GenerateMessageList(IDatabaseFile db, MessageFilter? filter, [EnumeratorCancellation] CancellationToken cancellationToken) {
await foreach (Message message in db.Messages.Get(filter, cancellationToken)) {
yield return new ViewerJson.JsonMessage {
Id = message.Id,
C = message.Channel,
U = message.Sender,
T = message.Timestamp,
M = string.IsNullOrEmpty(message.Text) ? null : message.Text,
Te = message.EditTimestamp,
R = message.RepliedToId?.ToString(),
private static ViewerJson.JsonMessage ToJsonMessage(Message message) {
return new ViewerJson.JsonMessage {
Id = message.Id,
C = message.Channel,
U = message.Sender,
T = message.Timestamp,
M = string.IsNullOrEmpty(message.Text) ? null : message.Text,
Te = message.EditTimestamp,
R = message.RepliedToId,
A = message.Attachments.IsEmpty ? null : message.Attachments.Select(static attachment => {
var a = new ViewerJson.JsonMessageAttachment {
Url = attachment.DownloadUrl,
Name = Uri.TryCreate(attachment.NormalizedUrl, UriKind.Absolute, out Uri? uri) ? Path.GetFileName(uri.LocalPath) : attachment.NormalizedUrl,
};
A = message.Attachments.IsEmpty ? null : message.Attachments.Select(static attachment => {
var a = new ViewerJson.JsonMessageAttachment {
Url = attachment.DownloadUrl,
Name = Uri.TryCreate(attachment.NormalizedUrl, UriKind.Absolute, out Uri? uri) ? Path.GetFileName(uri.LocalPath) : attachment.NormalizedUrl,
};
if (attachment is { Width: not null, Height: not null }) {
a.Width = attachment.Width;
a.Height = attachment.Height;
}
if (attachment is { Width: not null, Height: not null }) {
a.Width = attachment.Width;
a.Height = attachment.Height;
}
return a;
}).ToArray(),
return a;
}).ToArray(),
E = message.Embeds.IsEmpty ? null : message.Embeds.Select(static embed => embed.Json).ToArray(),
E = message.Embeds.IsEmpty ? null : message.Embeds.Select(static embed => embed.Json).ToArray(),
Re = message.Reactions.IsEmpty ? null : message.Reactions.Select(static reaction => new ViewerJson.JsonMessageReaction {
Id = reaction.EmojiId?.ToString(),
N = reaction.EmojiName,
A = reaction.EmojiFlags.HasFlag(EmojiFlags.Animated),
C = reaction.Count,
}).ToArray(),
};
}
Re = message.Reactions.IsEmpty ? null : message.Reactions.Select(static reaction => new ViewerJson.JsonMessageReaction {
Id = reaction.EmojiId,
N = reaction.EmojiName,
A = reaction.EmojiFlags.HasFlag(EmojiFlags.Animated),
C = reaction.Count,
}).ToArray(),
};
}
}

View File

@@ -8,5 +8,5 @@ using DHT.Utils;
namespace DHT.Utils;
static class Version {
public const string Tag = "45.0.0.0";
public const string Tag = "46.0.0.0";
}