mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2025-04-19 13:15:44 +02:00
Compare commits
3 Commits
0fff3e8eaf
...
d0955b6853
Author | SHA1 | Date | |
---|---|---|---|
d0955b6853 | |||
712f17b684 | |||
ae64747ce4 |
@ -2,15 +2,11 @@ using System;
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using DHT.Desktop.Dialogs.Message;
|
|
||||||
using DHT.Utils.Logging;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Dialogs.Progress;
|
namespace DHT.Desktop.Dialogs.Progress;
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
public sealed partial class ProgressDialog : Window {
|
public sealed partial class ProgressDialog : Window {
|
||||||
private static readonly Log Log = Log.ForType<ProgressDialog>();
|
|
||||||
|
|
||||||
internal static async Task Show(Window owner, string title, Func<ProgressDialog, IProgressCallback, Task> action) {
|
internal static async Task Show(Window owner, string title, Func<ProgressDialog, IProgressCallback, Task> action) {
|
||||||
var taskCompletionSource = new TaskCompletionSource();
|
var taskCompletionSource = new TaskCompletionSource();
|
||||||
var dialog = new ProgressDialog();
|
var dialog = new ProgressDialog();
|
||||||
@ -88,11 +84,6 @@ public sealed partial class ProgressDialog : Window {
|
|||||||
|
|
||||||
public async Task ShowProgressDialog(Window owner) {
|
public async Task ShowProgressDialog(Window owner) {
|
||||||
await ShowDialog(owner);
|
await ShowDialog(owner);
|
||||||
try {
|
|
||||||
await progressTask;
|
await progressTask;
|
||||||
} catch (Exception e) {
|
|
||||||
Log.Error(e);
|
|
||||||
await Dialog.ShowOk(owner, "Unexpected Error", e.Message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
|
|
||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<Style Selector="WrapPanel > Button">
|
<Style Selector="Expander">
|
||||||
<Setter Property="Margin" Value="0 0 10 10" />
|
<Setter Property="Margin" Value="0 5 0 0" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="DataGridColumnHeader">
|
<Style Selector="DataGridColumnHeader">
|
||||||
<Setter Property="FontWeight" Value="Medium" />
|
<Setter Property="FontWeight" Value="Medium" />
|
||||||
@ -30,17 +30,16 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
|
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical" Spacing="20">
|
||||||
<WrapPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||||
<Button Command="{Binding OnClickToggleDownload}" Content="{Binding ToggleDownloadButtonText}" IsEnabled="{Binding IsToggleDownloadButtonEnabled}" />
|
<Button Command="{Binding OnClickToggleDownload}" Content="{Binding ToggleDownloadButtonText}" IsEnabled="{Binding IsToggleDownloadButtonEnabled}" />
|
||||||
<Button Command="{Binding OnClickRetryFailedDownloads}" IsEnabled="{Binding IsRetryFailedOnDownloadsButtonEnabled}">Retry Failed Downloads</Button>
|
<Button Command="{Binding OnClickRetryFailedDownloads}" IsEnabled="{Binding IsRetryFailedOnDownloadsButtonEnabled}">Retry Failed Downloads</Button>
|
||||||
<Button Command="{Binding OnClickDeleteOrphanedDownloads}">Delete Orphaned Downloads</Button>
|
</StackPanel>
|
||||||
</WrapPanel>
|
|
||||||
<StackPanel Orientation="Vertical" Spacing="20" Margin="0 10 0 0">
|
|
||||||
<controls:DownloadItemFilterPanel DataContext="{Binding FilterModel}" IsEnabled="{Binding !$parent[UserControl].((pages:DownloadsPageModel)DataContext).IsDownloading}" />
|
<controls:DownloadItemFilterPanel DataContext="{Binding FilterModel}" IsEnabled="{Binding !$parent[UserControl].((pages:DownloadsPageModel)DataContext).IsDownloading}" />
|
||||||
<TextBlock TextWrapping="Wrap">
|
<TextBlock TextWrapping="Wrap">
|
||||||
Downloading state and filter settings are remembered per-database.
|
Downloading state and filter settings are remembered per-database.
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="12">
|
||||||
<Expander Header="Download Status" IsExpanded="True">
|
<Expander Header="Download Status" IsExpanded="True">
|
||||||
<DataGrid ItemsSource="{Binding StatisticsRows}" AutoGenerateColumns="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserSortColumns="False" IsReadOnly="True">
|
<DataGrid ItemsSource="{Binding StatisticsRows}" AutoGenerateColumns="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserSortColumns="False" IsReadOnly="True">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using DHT.Desktop.Common;
|
using DHT.Desktop.Common;
|
||||||
using DHT.Desktop.Dialogs.Message;
|
|
||||||
using DHT.Desktop.Dialogs.Progress;
|
|
||||||
using DHT.Desktop.Main.Controls;
|
using DHT.Desktop.Main.Controls;
|
||||||
using DHT.Server;
|
using DHT.Server;
|
||||||
using DHT.Server.Data;
|
|
||||||
using DHT.Server.Data.Aggregations;
|
using DHT.Server.Data.Aggregations;
|
||||||
using DHT.Server.Data.Filters;
|
using DHT.Server.Data.Filters;
|
||||||
using DHT.Server.Data.Settings;
|
using DHT.Server.Data.Settings;
|
||||||
@ -53,7 +48,6 @@ sealed partial class DownloadsPageModel : ObservableObject, IAsyncDisposable {
|
|||||||
|
|
||||||
public bool IsDownloading => state.Downloader.IsDownloading;
|
public bool IsDownloading => state.Downloader.IsDownloading;
|
||||||
|
|
||||||
private readonly Window window;
|
|
||||||
private readonly State state;
|
private readonly State state;
|
||||||
private readonly ThrottledTask<DownloadStatusStatistics> downloadStatisticsTask;
|
private readonly ThrottledTask<DownloadStatusStatistics> downloadStatisticsTask;
|
||||||
private readonly IDisposable downloadItemCountSubscription;
|
private readonly IDisposable downloadItemCountSubscription;
|
||||||
@ -61,10 +55,9 @@ sealed partial class DownloadsPageModel : ObservableObject, IAsyncDisposable {
|
|||||||
private IDisposable? finishedItemsSubscription;
|
private IDisposable? finishedItemsSubscription;
|
||||||
private DownloadItemFilter? currentDownloadFilter;
|
private DownloadItemFilter? currentDownloadFilter;
|
||||||
|
|
||||||
public DownloadsPageModel() : this(null!, State.Dummy) {}
|
public DownloadsPageModel() : this(State.Dummy) {}
|
||||||
|
|
||||||
public DownloadsPageModel(Window window, State state) {
|
public DownloadsPageModel(State state) {
|
||||||
this.window = window;
|
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
|
||||||
FilterModel = new DownloadItemFilterPanelModel(state);
|
FilterModel = new DownloadItemFilterPanelModel(state);
|
||||||
@ -165,50 +158,6 @@ sealed partial class DownloadsPageModel : ObservableObject, IAsyncDisposable {
|
|||||||
downloadStatisticsTask.Post(cancellationToken => state.Db.Downloads.GetStatistics(currentDownloadFilter ?? new DownloadItemFilter(), cancellationToken));
|
downloadStatisticsTask.Post(cancellationToken => state.Db.Downloads.GetStatistics(currentDownloadFilter ?? new DownloadItemFilter(), cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string DeleteOrphanedDownloadsTitle = "Delete Orphaned Downloads";
|
|
||||||
|
|
||||||
public async Task OnClickDeleteOrphanedDownloads() {
|
|
||||||
await ProgressDialog.Show(window, DeleteOrphanedDownloadsTitle, DeleteOrphanedDownloads);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeleteOrphanedDownloads(ProgressDialog dialog, IProgressCallback callback) {
|
|
||||||
await callback.UpdateIndeterminate("Searching for orphaned downloads...");
|
|
||||||
|
|
||||||
HashSet<string> reachableNormalizedUrls = [];
|
|
||||||
HashSet<string> orphanedNormalizedUrls = [];
|
|
||||||
|
|
||||||
await foreach (Download download in state.Db.Downloads.FindAllDownloadableUrls()) {
|
|
||||||
reachableNormalizedUrls.Add(download.NormalizedUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
await foreach (Download download in state.Db.Downloads.Get()) {
|
|
||||||
string normalizedUrl = download.NormalizedUrl;
|
|
||||||
if (!reachableNormalizedUrls.Contains(normalizedUrl)) {
|
|
||||||
orphanedNormalizedUrls.Add(normalizedUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orphanedNormalizedUrls.Count == 0) {
|
|
||||||
await Dialog.ShowOk(window, DeleteOrphanedDownloadsTitle, "No orphaned downloads found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await Dialog.ShowYesNo(window, DeleteOrphanedDownloadsTitle, orphanedNormalizedUrls.Count + " orphaned download(s) will be removed from this database. This action cannot be undone. Proceed?") != DialogResult.YesNo.Yes) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await callback.UpdateIndeterminate("Deleting orphaned downloads...");
|
|
||||||
await state.Db.Downloads.Remove(orphanedNormalizedUrls);
|
|
||||||
RecomputeDownloadStatistics();
|
|
||||||
|
|
||||||
if (await Dialog.ShowYesNo(window, DeleteOrphanedDownloadsTitle, "Orphaned downloads deleted. Vacuum database now to reclaim space?") != DialogResult.YesNo.Yes) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await callback.UpdateIndeterminate("Vacuuming database...");
|
|
||||||
await state.Db.Vacuum();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateStatistics(DownloadStatusStatistics statusStatistics) {
|
private void UpdateStatistics(DownloadStatusStatistics statusStatistics) {
|
||||||
statisticsPending.Items = statusStatistics.PendingCount;
|
statisticsPending.Items = statusStatistics.PendingCount;
|
||||||
statisticsPending.Size = statusStatistics.PendingTotalSize;
|
statisticsPending.Size = statusStatistics.PendingTotalSize;
|
||||||
|
@ -49,7 +49,7 @@ sealed class MainContentScreenModel : IAsyncDisposable {
|
|||||||
TrackingPageModel = new TrackingPageModel(window);
|
TrackingPageModel = new TrackingPageModel(window);
|
||||||
TrackingPage = new TrackingPage { DataContext = TrackingPageModel };
|
TrackingPage = new TrackingPage { DataContext = TrackingPageModel };
|
||||||
|
|
||||||
DownloadsPageModel = new DownloadsPageModel(window, state);
|
DownloadsPageModel = new DownloadsPageModel(state);
|
||||||
DownloadsPage = new DownloadsPage { DataContext = DownloadsPageModel };
|
DownloadsPage = new DownloadsPage { DataContext = DownloadsPageModel };
|
||||||
|
|
||||||
ViewerPageModel = new ViewerPageModel(window, state);
|
ViewerPageModel = new ViewerPageModel(window, state);
|
||||||
|
@ -32,10 +32,6 @@ public interface IDownloadRepository {
|
|||||||
|
|
||||||
Task<int> RetryFailed(CancellationToken cancellationToken = default);
|
Task<int> RetryFailed(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task Remove(ICollection<string> normalizedUrls);
|
|
||||||
|
|
||||||
IAsyncEnumerable<Data.Download> FindAllDownloadableUrls(CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
internal sealed class Dummy : IDownloadRepository {
|
internal sealed class Dummy : IDownloadRepository {
|
||||||
public IObservable<long> TotalCount { get; } = Observable.Return(0L);
|
public IObservable<long> TotalCount { get; } = Observable.Return(0L);
|
||||||
|
|
||||||
@ -74,13 +70,5 @@ public interface IDownloadRepository {
|
|||||||
public Task<int> RetryFailed(CancellationToken cancellationToken) {
|
public Task<int> RetryFailed(CancellationToken cancellationToken) {
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Remove(ICollection<string> normalizedUrls) {
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAsyncEnumerable<Data.Download> FindAllDownloadableUrls(CancellationToken cancellationToken) {
|
|
||||||
return AsyncEnumerable.Empty<Data.Download>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -321,62 +321,4 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep
|
|||||||
cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success);
|
cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success);
|
||||||
return await cmd.ExecuteNonQueryAsync(cancellationToken);
|
return await cmd.ExecuteNonQueryAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Remove(ICollection<string> normalizedUrls) {
|
|
||||||
await using (var conn = await pool.Take()) {
|
|
||||||
await conn.BeginTransactionAsync();
|
|
||||||
|
|
||||||
await using (var cmd = conn.Command("DELETE FROM download_metadata WHERE normalized_url = :normalized_url")) {
|
|
||||||
cmd.Add(":normalized_url", SqliteType.Text);
|
|
||||||
|
|
||||||
foreach (string normalizedUrl in normalizedUrls) {
|
|
||||||
cmd.Set(":normalized_url", normalizedUrl);
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await conn.CommitTransactionAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateTotalCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async IAsyncEnumerable<Data.Download> FindAllDownloadableUrls([EnumeratorCancellation] CancellationToken cancellationToken = default) {
|
|
||||||
await using var conn = await pool.Take();
|
|
||||||
|
|
||||||
await using (var cmd = conn.Command("SELECT normalized_url, download_url, type, size FROM attachments")) {
|
|
||||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
|
||||||
|
|
||||||
while (await reader.ReadAsync(cancellationToken)) {
|
|
||||||
yield return DownloadLinkExtractor.FromAttachment(reader.GetString(0), reader.GetString(1), reader.IsDBNull(2) ? null : reader.GetString(2), reader.GetUint64(3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await using (var cmd = conn.Command("SELECT json FROM message_embeds")) {
|
|
||||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
|
||||||
|
|
||||||
while (await reader.ReadAsync(cancellationToken)) {
|
|
||||||
var result = await DownloadLinkExtractor.TryFromEmbedJson(reader.GetStream(0));
|
|
||||||
if (result is not null) {
|
|
||||||
yield return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await using (var cmd = conn.Command("SELECT id, avatar_url FROM users WHERE avatar_url IS NOT NULL")) {
|
|
||||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
|
||||||
|
|
||||||
while (await reader.ReadAsync(cancellationToken)) {
|
|
||||||
yield return DownloadLinkExtractor.FromUserAvatar(reader.GetUint64(0), reader.GetString(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await using (var cmd = conn.Command("SELECT DISTINCT emoji_id, emoji_flags FROM message_reactions WHERE emoji_id IS NOT NULL")) {
|
|
||||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
|
||||||
|
|
||||||
while (await reader.ReadAsync(cancellationToken)) {
|
|
||||||
yield return DownloadLinkExtractor.FromEmoji(reader.GetUint64(0), (EmojiFlags) reader.GetInt16(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -28,11 +28,7 @@ static class DownloadLinkExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Data.Download FromAttachment(Attachment attachment) {
|
public static Data.Download FromAttachment(Attachment attachment) {
|
||||||
return FromAttachment(attachment.NormalizedUrl, attachment.DownloadUrl, attachment.Type, attachment.Size);
|
return new Data.Download(attachment.NormalizedUrl, attachment.DownloadUrl, DownloadStatus.Pending, attachment.Type, attachment.Size);
|
||||||
}
|
|
||||||
|
|
||||||
public static Data.Download FromAttachment(string normalizedUrl, string downloadUrl, string? type, ulong size) {
|
|
||||||
return new Data.Download(normalizedUrl, downloadUrl, DownloadStatus.Pending, type, size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<Data.Download?> TryFromEmbedJson(Stream jsonStream) {
|
public static async Task<Data.Download?> TryFromEmbedJson(Stream jsonStream) {
|
||||||
|
BIN
app/empty.dht
BIN
app/empty.dht
Binary file not shown.
Loading…
Reference in New Issue
Block a user