Merge pull request #1308 from Mbucari/master
Refactors, bug fixes, and performance improvements.
This commit is contained in:
commit
64fb2ccf7c
@ -25,9 +25,8 @@ namespace AaxDecrypter
|
||||
|
||||
public override async Task CancelAsync()
|
||||
{
|
||||
IsCanceled = true;
|
||||
await base.CancelAsync();
|
||||
await (AaxConversion?.CancelAsync() ?? Task.CompletedTask);
|
||||
FinalizeDownload();
|
||||
}
|
||||
|
||||
private Mp4File Open()
|
||||
|
||||
@ -120,7 +120,12 @@ namespace AaxDecrypter
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Task CancelAsync();
|
||||
public virtual Task CancelAsync()
|
||||
{
|
||||
IsCanceled = true;
|
||||
FinalizeDownload();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
protected abstract Task<bool> Step_DownloadAndDecryptAudiobookAsync();
|
||||
|
||||
public virtual void SetCoverArt(byte[] coverArt) { }
|
||||
|
||||
@ -17,13 +17,6 @@ namespace AaxDecrypter
|
||||
AsyncSteps["Step 3: Create Cue"] = Step_CreateCueAsync;
|
||||
}
|
||||
|
||||
public override Task CancelAsync()
|
||||
{
|
||||
IsCanceled = true;
|
||||
FinalizeDownload();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||
{
|
||||
await InputFileStream.DownloadTask;
|
||||
|
||||
@ -7,10 +7,9 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="14.0.0" />
|
||||
<!-- Do not remove unused Serilog.Sinks -->
|
||||
<!-- Only ZipFile sink is currently used. By user request (June 2024) others packages are included for experimental use. -->
|
||||
<!-- Only File sink is currently used. By user request (June 2024) others packages are included for experimental use. -->
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.ZipFile" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
|
||||
@ -115,11 +115,22 @@ namespace AppScaffolding
|
||||
{
|
||||
if (config.GetObject("Serilog") is JObject serilog)
|
||||
{
|
||||
if (serilog["WriteTo"] is JArray sinks && sinks.FirstOrDefault(s => s["Name"].Value<string>() is "File") is JToken fileSink)
|
||||
bool fileChanged = false;
|
||||
if (serilog.SelectToken("$.WriteTo[?(@.Name == 'ZipFile')]", false) is JObject zipFileSink)
|
||||
{
|
||||
fileSink["Name"] = "ZipFile";
|
||||
config.SetNonString(serilog.DeepClone(), "Serilog");
|
||||
zipFileSink["Name"] = "File";
|
||||
fileChanged = true;
|
||||
}
|
||||
var hooks = $"{nameof(LibationFileManager)}.{nameof(FileSinkHook)}, {nameof(LibationFileManager)}";
|
||||
if (serilog.SelectToken("$.WriteTo[?(@.Name == 'File')].Args", false) is JObject fileSinkArgs
|
||||
&& fileSinkArgs["hooks"]?.Value<string>() != hooks)
|
||||
{
|
||||
fileSinkArgs["hooks"] = hooks;
|
||||
fileChanged = true;
|
||||
}
|
||||
|
||||
if (fileChanged)
|
||||
config.SetNonString(serilog.DeepClone(), "Serilog");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -129,17 +140,17 @@ namespace AppScaffolding
|
||||
{ "WriteTo", new JArray
|
||||
{
|
||||
// ABOUT SINKS
|
||||
// Only ZipFile sink is currently used. By user request (June 2024) others packages are included for experimental use.
|
||||
// Only File sink is currently used. By user request (June 2024) others packages are included for experimental use.
|
||||
|
||||
// new JObject { {"Name", "Console" } }, // this has caused more problems than it's solved
|
||||
new JObject
|
||||
{
|
||||
{ "Name", "ZipFile" },
|
||||
{ "Name", "File" },
|
||||
{ "Args",
|
||||
new JObject
|
||||
{
|
||||
// for this sink to work, a path must be provided. we override this below
|
||||
{ "path", Path.Combine(config.LibationFiles, "_Log.log") },
|
||||
{ "path", Path.Combine(config.LibationFiles, "Log.log") },
|
||||
{ "rollingInterval", "Month" },
|
||||
// Serilog template formatting examples
|
||||
// - default: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AaxDecrypter;
|
||||
using ApplicationServices;
|
||||
@ -18,10 +19,15 @@ namespace FileLiberator
|
||||
{
|
||||
public override string Name => "Download & Decrypt";
|
||||
private AudiobookDownloadBase abDownloader;
|
||||
private readonly CancellationTokenSource cancellationTokenSource = new();
|
||||
|
||||
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
|
||||
|
||||
public override Task CancelAsync() => abDownloader?.CancelAsync() ?? Task.CompletedTask;
|
||||
public override async Task CancelAsync()
|
||||
{
|
||||
cancellationTokenSource.Cancel();
|
||||
if (abDownloader is not null)
|
||||
await abDownloader.CancelAsync();
|
||||
}
|
||||
|
||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
@ -41,6 +47,7 @@ namespace FileLiberator
|
||||
}
|
||||
|
||||
OnBegin(libraryBook);
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
|
||||
try
|
||||
{
|
||||
@ -50,7 +57,7 @@ namespace FileLiberator
|
||||
downloadValidation(libraryBook);
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var config = Configuration.Instance;
|
||||
using var downloadOptions = await DownloadOptions.InitiateDownloadAsync(api, config, libraryBook);
|
||||
using var downloadOptions = await DownloadOptions.InitiateDownloadAsync(api, config, libraryBook, cancellationToken);
|
||||
|
||||
bool success = false;
|
||||
try
|
||||
@ -74,38 +81,32 @@ namespace FileLiberator
|
||||
.Where(f => f.FileType != FileType.AAXC)
|
||||
.Select(f => Task.Run(() => FileUtility.SaferDelete(f.Path))));
|
||||
|
||||
return
|
||||
abDownloader?.IsCanceled is true
|
||||
? new StatusHandler { "Cancelled" }
|
||||
: new StatusHandler { "Decrypt failed" };
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new StatusHandler { "Decrypt failed" };
|
||||
}
|
||||
|
||||
var finalStorageDir = getDestinationDirectory(libraryBook);
|
||||
|
||||
var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries));
|
||||
var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries, cancellationToken));
|
||||
Task[] finalTasks =
|
||||
[
|
||||
Task.Run(() => downloadCoverArt(downloadOptions)),
|
||||
Task.Run(() => downloadCoverArt(downloadOptions, cancellationToken)),
|
||||
moveFilesTask,
|
||||
Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir))
|
||||
Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir, cancellationToken))
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(finalTasks);
|
||||
}
|
||||
catch
|
||||
catch when (!moveFilesTask.IsFaulted)
|
||||
{
|
||||
//Swallow downloadCoverArt and SetCoverAsFolderIcon exceptions.
|
||||
//Only fail if the downloaded audio files failed to move to Books directory
|
||||
if (moveFilesTask.IsFaulted)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (moveFilesTask.IsCompletedSuccessfully)
|
||||
if (moveFilesTask.IsCompletedSuccessfully && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Run(() => libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion));
|
||||
|
||||
@ -115,6 +116,10 @@ namespace FileLiberator
|
||||
|
||||
return new StatusHandler();
|
||||
}
|
||||
catch when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return new StatusHandler { "Cancelled" };
|
||||
}
|
||||
finally
|
||||
{
|
||||
OnCompleted(libraryBook);
|
||||
@ -257,10 +262,11 @@ namespace FileLiberator
|
||||
|
||||
/// <summary>Move new files to 'Books' directory</summary>
|
||||
/// <returns>Return directory if audiobook file(s) were successfully created and can be located on disk. Else null.</returns>
|
||||
private static void moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries)
|
||||
private static void moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries, CancellationToken cancellationToken)
|
||||
{
|
||||
// create final directory. move each file into it
|
||||
var destinationDir = getDestinationDirectory(libraryBook);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
{
|
||||
@ -278,6 +284,7 @@ namespace FileLiberator
|
||||
|
||||
// propagate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)
|
||||
entries[i] = entry with { Path = realDest };
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
|
||||
@ -287,6 +294,7 @@ namespace FileLiberator
|
||||
SetFileTime(libraryBook, cue.Path);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
AudibleFileStorage.Audio.Refresh();
|
||||
}
|
||||
|
||||
@ -301,7 +309,7 @@ namespace FileLiberator
|
||||
private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable<FilePathCache.CacheEntry> entries)
|
||||
=> entries.FirstOrDefault(f => f.FileType == FileType.Audio);
|
||||
|
||||
private static void downloadCoverArt(DownloadOptions options)
|
||||
private static void downloadCoverArt(DownloadOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Configuration.Instance.DownloadCoverArt) return;
|
||||
|
||||
@ -316,7 +324,7 @@ namespace FileLiberator
|
||||
if (File.Exists(coverPath))
|
||||
FileUtility.SaferDelete(coverPath);
|
||||
|
||||
var picBytes = PictureStorage.GetPictureSynchronously(new(options.LibraryBook.Book.PictureLarge ?? options.LibraryBook.Book.PictureId, PictureSize.Native));
|
||||
var picBytes = PictureStorage.GetPictureSynchronously(new(options.LibraryBook.Book.PictureLarge ?? options.LibraryBook.Book.PictureId, PictureSize.Native), cancellationToken);
|
||||
if (picBytes.Length > 0)
|
||||
{
|
||||
File.WriteAllBytes(coverPath, picBytes);
|
||||
@ -327,6 +335,7 @@ namespace FileLiberator
|
||||
{
|
||||
//Failure to download cover art should not be considered a failure to download the book
|
||||
Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {options.LibraryBook.Book.AudibleProductId} to {coverPath} catalog product.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
@ -24,9 +25,10 @@ public partial class DownloadOptions
|
||||
/// <summary>
|
||||
/// Initiate an audiobook download from the audible api.
|
||||
/// </summary>
|
||||
public static async Task<DownloadOptions> InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook)
|
||||
public static async Task<DownloadOptions> InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook, CancellationToken token)
|
||||
{
|
||||
var license = await ChooseContent(api, libraryBook, config);
|
||||
var license = await ChooseContent(api, libraryBook, config, token);
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
//Some audiobooks will have incorrect chapters in the metadata returned from the license request,
|
||||
//but the metadata returned by the content metadata endpoint will be correct. Call the content
|
||||
@ -36,9 +38,8 @@ public partial class DownloadOptions
|
||||
if (metadata.ChapterInfo.RuntimeLengthMs == license.ContentMetadata.ChapterInfo.RuntimeLengthMs)
|
||||
license.ContentMetadata.ChapterInfo = metadata.ChapterInfo;
|
||||
|
||||
var options = BuildDownloadOptions(libraryBook, config, license);
|
||||
|
||||
return options;
|
||||
token.ThrowIfCancellationRequested();
|
||||
return BuildDownloadOptions(libraryBook, config, license);
|
||||
}
|
||||
|
||||
private class LicenseInfo
|
||||
@ -57,16 +58,18 @@ public partial class DownloadOptions
|
||||
=> voucher is null ? null : [new KeyData(voucher.Key, voucher.Iv)];
|
||||
}
|
||||
|
||||
private static async Task<LicenseInfo> ChooseContent(Api api, LibraryBook libraryBook, Configuration config)
|
||||
private static async Task<LicenseInfo> ChooseContent(Api api, LibraryBook libraryBook, Configuration config, CancellationToken token)
|
||||
{
|
||||
var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High;
|
||||
|
||||
if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var license = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
|
||||
return new LicenseInfo(license);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
//try to request a widevine content license using the user's spatial audio settings
|
||||
@ -85,8 +88,8 @@ public partial class DownloadOptions
|
||||
return new LicenseInfo(contentLic);
|
||||
|
||||
using var client = new HttpClient();
|
||||
using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse);
|
||||
var dash = new MpegDash(mpdResponse.Content.ReadAsStream());
|
||||
using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse, token);
|
||||
var dash = new MpegDash(mpdResponse.Content.ReadAsStream(token));
|
||||
|
||||
if (!dash.TryGetUri(new Uri(contentLic.LicenseResponse), out var contentUri))
|
||||
throw new InvalidDataException("Failed to get mpeg-dash content download url.");
|
||||
|
||||
@ -9,7 +9,7 @@ namespace LibationAvalonia.Controls
|
||||
{
|
||||
//Only SeriesEntry types have three-state checks, individual LibraryEntry books are binary.
|
||||
var ele = base.GenerateEditingElementDirect(cell, dataItem) as CheckBox;
|
||||
ele.IsThreeState = dataItem is ISeriesEntry;
|
||||
ele.IsThreeState = dataItem is SeriesEntry;
|
||||
return ele;
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,11 +34,11 @@ namespace LibationAvalonia.Controls
|
||||
private static void Cell_ContextRequested(object sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
if (sender is DataGridCell cell &&
|
||||
cell.DataContext is IGridEntry clickedEntry &&
|
||||
cell.DataContext is GridEntry clickedEntry &&
|
||||
OwningColumnProperty.GetValue(cell) is DataGridColumn column &&
|
||||
OwningGridProperty.GetValue(column) is DataGrid grid)
|
||||
{
|
||||
var allSelected = grid.SelectedItems.OfType<IGridEntry>().ToArray();
|
||||
var allSelected = grid.SelectedItems.OfType<GridEntry>().ToArray();
|
||||
var clickedIndex = Array.IndexOf(allSelected, clickedEntry);
|
||||
if (clickedIndex == -1)
|
||||
{
|
||||
@ -101,7 +101,7 @@ namespace LibationAvalonia.Controls
|
||||
private static string RemoveLineBreaks(string text)
|
||||
=> text.Replace("\r\n", "").Replace('\r', ' ').Replace('\n', ' ');
|
||||
|
||||
private string GetRowClipboardContents(IGridEntry gridEntry)
|
||||
private string GetRowClipboardContents(GridEntry gridEntry)
|
||||
{
|
||||
var contents = Grid.Columns.Where(c => c.IsVisible).OrderBy(c => c.DisplayIndex).Select(c => RemoveLineBreaks(GetCellValue(c, gridEntry))).ToArray();
|
||||
return string.Join("\t", contents);
|
||||
@ -109,7 +109,7 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
public required DataGrid Grid { get; init; }
|
||||
public required DataGridColumn Column { get; init; }
|
||||
public required IGridEntry[] GridEntries { get; init; }
|
||||
public required GridEntry[] GridEntries { get; init; }
|
||||
public required ContextMenu ContextMenu { get; init; }
|
||||
public AvaloniaList<Control> ContextMenuItems
|
||||
=> ContextMenu.ItemsSource as AvaloniaList<Control>;
|
||||
|
||||
@ -58,10 +58,5 @@ namespace LibationAvalonia.Controls.Settings
|
||||
}
|
||||
ThemeComboBox.SelectionChanged += ThemeComboBox_SelectionChanged;
|
||||
}
|
||||
|
||||
public void OpenLogFolderButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,11 +36,11 @@ public partial class ThemePreviewControl : UserControl
|
||||
PictureStorage.SetDefaultImage(PictureSize._80x80, ms1.ToArray());
|
||||
}
|
||||
|
||||
QueuedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Queued };
|
||||
WorkingBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Working };
|
||||
CompletedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Completed };
|
||||
CancelledBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Cancelled };
|
||||
FailedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Failed };
|
||||
QueuedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Queued };
|
||||
WorkingBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Working };
|
||||
CompletedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Completed };
|
||||
CancelledBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Cancelled };
|
||||
FailedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Failed };
|
||||
|
||||
//Set the current processable so that the empty queue doesn't try to advance.
|
||||
QueuedBook.AddDownloadPdf();
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using Avalonia.Threading;
|
||||
using LibationUiBase.Forms;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -17,42 +18,46 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
}
|
||||
|
||||
public async Task<string> Get2faCodeAsync(string prompt)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
var dialog = new _2faCodeDialog(prompt);
|
||||
if (await dialog.ShowDialogAsync() is DialogResult.OK)
|
||||
return dialog.Code;
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
public async Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
var dialog = new CaptchaDialog(password, captchaImage);
|
||||
if (await dialog.ShowDialogAsync() is DialogResult.OK)
|
||||
return (dialog.Password, dialog.Answer);
|
||||
return (null, null);
|
||||
}
|
||||
});
|
||||
|
||||
public async Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
var dialog = new MfaDialog(mfaConfig);
|
||||
if (await dialog.ShowDialogAsync() is DialogResult.OK)
|
||||
return (dialog.SelectedName, dialog.SelectedValue);
|
||||
return (null, null);
|
||||
}
|
||||
});
|
||||
|
||||
public async Task<(string email, string password)> GetLoginAsync()
|
||||
=> await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
var dialog = new LoginCallbackDialog(_account);
|
||||
if (await dialog.ShowDialogAsync() is DialogResult.OK)
|
||||
return (_account.AccountId, dialog.Password);
|
||||
return (null, null);
|
||||
}
|
||||
});
|
||||
|
||||
public async Task ShowApprovalNeededAsync()
|
||||
=> await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
var dialog = new ApprovalNeededDialog();
|
||||
await dialog.ShowDialogAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using Avalonia.Threading;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.Forms;
|
||||
using System;
|
||||
@ -21,6 +22,9 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
}
|
||||
|
||||
public async Task<ChoiceOut?> StartAsync(ChoiceIn choiceIn)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(() => StartAsyncInternal(choiceIn));
|
||||
|
||||
private async Task<ChoiceOut?> StartAsyncInternal(ChoiceIn choiceIn)
|
||||
{
|
||||
if (Configuration.IsWindows && Environment.OSVersion.Version.Major >= 10)
|
||||
{
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
using Avalonia.Media.Imaging;
|
||||
using DataLayer;
|
||||
using LibationUiBase.GridView;
|
||||
using System;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
|
||||
{
|
||||
private AvaloniaEntryStatus(LibraryBook libraryBook) : base(libraryBook) { }
|
||||
public static EntryStatus Create(LibraryBook libraryBook) => new AvaloniaEntryStatus(libraryBook);
|
||||
|
||||
protected override Bitmap LoadImage(byte[] picture)
|
||||
=> AvaloniaUtils.TryLoadImageOrDefault(picture, LibationFileManager.PictureSize._80x80);
|
||||
|
||||
//Button icons are handled by LiberateStatusButton
|
||||
protected override Bitmap? GetResourceImage(string rescName) => null;
|
||||
}
|
||||
}
|
||||
@ -202,7 +202,7 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
try
|
||||
{
|
||||
var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(accounts);
|
||||
var (totalProcessed, newAdded) = await Task.Run(() => LibraryCommands.ImportAccountAsync(accounts));
|
||||
|
||||
// this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop
|
||||
if (Configuration.Instance.ShowImportedStats && newAdded > 0)
|
||||
|
||||
@ -57,7 +57,7 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public void LiberateSeriesClicked(ISeriesEntry series)
|
||||
public void LiberateSeriesClicked(SeriesEntry series)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@ -5,6 +5,7 @@ using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
@ -27,7 +28,7 @@ namespace LibationAvalonia.ViewModels
|
||||
// in autoScan, new books SHALL NOT show dialog
|
||||
try
|
||||
{
|
||||
await LibraryCommands.ImportAccountAsync(accounts);
|
||||
await Task.Run(() => LibraryCommands.ImportAccountAsync(accounts));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
#nullable enable
|
||||
@ -7,7 +8,7 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
{
|
||||
private void Configure_NonUI()
|
||||
public static void Configure_NonUI()
|
||||
{
|
||||
using var ms1 = new MemoryStream();
|
||||
App.OpenAsset("img-coverart-prod-unavailable_80x80.jpg").CopyTo(ms1);
|
||||
@ -23,6 +24,20 @@ namespace LibationAvalonia.ViewModels
|
||||
PictureStorage.SetDefaultImage(PictureSize.Native, ms3.ToArray());
|
||||
|
||||
BaseUtil.SetLoadImageDelegate(AvaloniaUtils.TryLoadImageOrDefault);
|
||||
BaseUtil.SetLoadResourceImageDelegate(LoadResourceImage);
|
||||
}
|
||||
private static Avalonia.Media.Imaging.Bitmap? LoadResourceImage(string resourceName)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = App.OpenAsset(resourceName);
|
||||
return new Avalonia.Media.Imaging.Bitmap(stream);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Failed to load resource image: {ResourceName}", resourceName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
using DataLayer;
|
||||
using LibationAvalonia.Views;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels;
|
||||
|
||||
public class ProcessBookViewModel : ProcessBookViewModelBase
|
||||
{
|
||||
|
||||
public ProcessBookViewModel(LibraryBook libraryBook, LogMe logme) : base(libraryBook, logme) { }
|
||||
|
||||
protected override object? LoadImageFromBytes(byte[] bytes, PictureSize pictureSize)
|
||||
=> AvaloniaUtils.TryLoadImageOrDefault(bytes, pictureSize);
|
||||
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels;
|
||||
|
||||
public record LogEntry(DateTime LogDate, string? LogMessage)
|
||||
{
|
||||
public string LogDateString => LogDate.ToShortTimeString();
|
||||
}
|
||||
|
||||
public class ProcessQueueViewModel : ProcessQueueViewModelBase
|
||||
{
|
||||
public ProcessQueueViewModel() : base(CreateEmptyList())
|
||||
{
|
||||
Items = Queue.UnderlyingList as AvaloniaList<ProcessBookViewModelBase>
|
||||
?? throw new ArgumentNullException(nameof(Queue.UnderlyingList));
|
||||
|
||||
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
||||
}
|
||||
|
||||
private decimal _speedLimit;
|
||||
public decimal SpeedLimitIncrement { get; private set; }
|
||||
public ObservableCollection<LogEntry> LogEntries { get; } = new();
|
||||
public AvaloniaList<ProcessBookViewModelBase> Items { get; }
|
||||
|
||||
public decimal SpeedLimit
|
||||
{
|
||||
get
|
||||
{
|
||||
return _speedLimit;
|
||||
}
|
||||
set
|
||||
{
|
||||
var newValue = Math.Min(999 * 1024 * 1024, (long)(value * 1024 * 1024));
|
||||
var config = Configuration.Instance;
|
||||
config.DownloadSpeedLimit = newValue;
|
||||
|
||||
_speedLimit
|
||||
= config.DownloadSpeedLimit <= newValue ? value
|
||||
: value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
|
||||
: 0;
|
||||
|
||||
config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
|
||||
|
||||
SpeedLimitIncrement = _speedLimit > 100 ? 10
|
||||
: _speedLimit > 10 ? 1
|
||||
: _speedLimit > 1 ? 0.1m
|
||||
: 0.01m;
|
||||
|
||||
RaisePropertyChanged(nameof(SpeedLimitIncrement));
|
||||
RaisePropertyChanged(nameof(SpeedLimit));
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteLine(string text)
|
||||
=> Dispatcher.UIThread.Invoke(() => LogEntries.Add(new(DateTime.Now, text.Trim())));
|
||||
|
||||
protected override ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook)
|
||||
=> new ProcessBookViewModel(libraryBook, Logger);
|
||||
|
||||
private static AvaloniaList<ProcessBookViewModelBase> CreateEmptyList()
|
||||
{
|
||||
if (Design.IsDesignMode)
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
return new AvaloniaList<ProcessBookViewModelBase>();
|
||||
}
|
||||
}
|
||||
@ -26,9 +26,9 @@ namespace LibationAvalonia.ViewModels
|
||||
public event EventHandler<int>? RemovableCountChanged;
|
||||
|
||||
/// <summary>Backing list of all grid entries</summary>
|
||||
private readonly AvaloniaList<IGridEntry> SOURCE = new();
|
||||
private readonly AvaloniaList<GridEntry> SOURCE = new();
|
||||
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
|
||||
private HashSet<IGridEntry>? FilteredInGridEntries;
|
||||
private HashSet<GridEntry>? FilteredInGridEntries;
|
||||
public string? FilterString { get; private set; }
|
||||
|
||||
private DataGridCollectionView? _gridEntries;
|
||||
@ -43,15 +43,15 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
public List<LibraryBook> GetVisibleBookEntries()
|
||||
=> FilteredInGridEntries?
|
||||
.OfType<ILibraryBookEntry>()
|
||||
.OfType<LibraryBookEntry>()
|
||||
.Select(lbe => lbe.LibraryBook)
|
||||
.ToList()
|
||||
?? SOURCE
|
||||
.OfType<ILibraryBookEntry>()
|
||||
.OfType<LibraryBookEntry>()
|
||||
.Select(lbe => lbe.LibraryBook)
|
||||
.ToList();
|
||||
|
||||
private IEnumerable<ILibraryBookEntry> GetAllBookEntries()
|
||||
private IEnumerable<LibraryBookEntry> GetAllBookEntries()
|
||||
=> SOURCE
|
||||
.BookEntries();
|
||||
|
||||
@ -112,8 +112,8 @@ namespace LibationAvalonia.ViewModels
|
||||
var sc = await Dispatcher.UIThread.InvokeAsync(() => AvaloniaSynchronizationContext.Current);
|
||||
AvaloniaSynchronizationContext.SetSynchronizationContext(sc);
|
||||
|
||||
var geList = await LibraryBookEntry<AvaloniaEntryStatus>.GetAllProductsAsync(dbBooks);
|
||||
var seriesEntries = await SeriesEntry<AvaloniaEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
|
||||
var geList = await LibraryBookEntry.GetAllProductsAsync(dbBooks);
|
||||
var seriesEntries = await SeriesEntry.GetAllSeriesEntriesAsync(dbBooks);
|
||||
|
||||
//Add all IGridEntries to the SOURCE list. Note that SOURCE has not yet been linked to the UI via
|
||||
//the GridEntries property, so adding items to SOURCE will not trigger any refreshes or UI action.
|
||||
@ -147,8 +147,8 @@ namespace LibationAvalonia.ViewModels
|
||||
private void GridEntries_CollectionChanged(object? sender = null, EventArgs? e = null)
|
||||
{
|
||||
var count
|
||||
= FilteredInGridEntries?.OfType<ILibraryBookEntry>().Count()
|
||||
?? SOURCE.OfType<ILibraryBookEntry>().Count();
|
||||
= FilteredInGridEntries?.OfType<LibraryBookEntry>().Count()
|
||||
?? SOURCE.OfType<LibraryBookEntry>().Count();
|
||||
|
||||
VisibleCountChanged?.Invoke(this, count);
|
||||
}
|
||||
@ -223,9 +223,9 @@ namespace LibationAvalonia.ViewModels
|
||||
GridEntries_CollectionChanged();
|
||||
}
|
||||
|
||||
private void RemoveBooks(IEnumerable<ILibraryBookEntry> removedBooks, IEnumerable<ISeriesEntry> removedSeries)
|
||||
private void RemoveBooks(IEnumerable<LibraryBookEntry> removedBooks, IEnumerable<SeriesEntry> removedSeries)
|
||||
{
|
||||
foreach (var removed in removedBooks.Cast<IGridEntry>().Concat(removedSeries).Where(b => b is not null).ToList())
|
||||
foreach (var removed in removedBooks.Cast<GridEntry>().Concat(removedSeries).Where(b => b is not null).ToList())
|
||||
{
|
||||
if (GridEntries?.PassesFilter(removed) ?? false)
|
||||
GridEntries.Remove(removed);
|
||||
@ -238,21 +238,21 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void UpsertBook(LibraryBook book, ILibraryBookEntry? existingBookEntry)
|
||||
private void UpsertBook(LibraryBook book, LibraryBookEntry? existingBookEntry)
|
||||
{
|
||||
if (existingBookEntry is null)
|
||||
// Add the new product to top
|
||||
SOURCE.Insert(0, new LibraryBookEntry<AvaloniaEntryStatus>(book));
|
||||
SOURCE.Insert(0, new LibraryBookEntry(book));
|
||||
else
|
||||
// update existing
|
||||
existingBookEntry.UpdateLibraryBook(book);
|
||||
}
|
||||
|
||||
private void UpsertEpisode(LibraryBook episodeBook, ILibraryBookEntry? existingEpisodeEntry, List<ISeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
|
||||
private void UpsertEpisode(LibraryBook episodeBook, LibraryBookEntry? existingEpisodeEntry, List<SeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
|
||||
{
|
||||
if (existingEpisodeEntry is null)
|
||||
{
|
||||
ILibraryBookEntry episodeEntry;
|
||||
LibraryBookEntry episodeEntry;
|
||||
|
||||
var seriesEntry = seriesEntries.FindSeriesParent(episodeBook);
|
||||
|
||||
@ -270,7 +270,7 @@ namespace LibationAvalonia.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
seriesEntry = new SeriesEntry<AvaloniaEntryStatus>(seriesBook, episodeBook);
|
||||
seriesEntry = new SeriesEntry(seriesBook, episodeBook);
|
||||
seriesEntries.Add(seriesEntry);
|
||||
|
||||
episodeEntry = seriesEntry.Children[0];
|
||||
@ -280,7 +280,7 @@ namespace LibationAvalonia.ViewModels
|
||||
else
|
||||
{
|
||||
//Series exists. Create and add episode child then update the SeriesEntry
|
||||
episodeEntry = new LibraryBookEntry<AvaloniaEntryStatus>(episodeBook, seriesEntry);
|
||||
episodeEntry = new LibraryBookEntry(episodeBook, seriesEntry);
|
||||
seriesEntry.Children.Add(episodeEntry);
|
||||
seriesEntry.Children.Sort((c1, c2) => c1.SeriesIndex.CompareTo(c2.SeriesIndex));
|
||||
var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId);
|
||||
@ -307,7 +307,7 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ToggleSeriesExpanded(ISeriesEntry seriesEntry)
|
||||
public async Task ToggleSeriesExpanded(SeriesEntry seriesEntry)
|
||||
{
|
||||
seriesEntry.Liberate.Expanded = !seriesEntry.Liberate.Expanded;
|
||||
|
||||
@ -332,7 +332,7 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
private bool CollectionFilter(object item)
|
||||
{
|
||||
if (item is ILibraryBookEntry lbe
|
||||
if (item is LibraryBookEntry lbe
|
||||
&& lbe.Liberate.IsEpisode
|
||||
&& lbe.Parent?.Liberate?.Expanded != true)
|
||||
return false;
|
||||
@ -454,7 +454,7 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
private void GridEntry_PropertyChanged(object? sender, PropertyChangedEventArgs? e)
|
||||
{
|
||||
if (e?.PropertyName == nameof(IGridEntry.Remove) && sender is ILibraryBookEntry)
|
||||
if (e?.PropertyName == nameof(GridEntry.Remove) && sender is LibraryBookEntry)
|
||||
{
|
||||
int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true);
|
||||
RemovableCountChanged?.Invoke(this, removeCount);
|
||||
|
||||
@ -17,7 +17,7 @@ namespace LibationAvalonia.ViewModels
|
||||
public RowComparer(DataGridColumn? column)
|
||||
{
|
||||
Column = column;
|
||||
PropertyName = Column?.SortMemberPath ?? nameof(IGridEntry.DateAdded);
|
||||
PropertyName = Column?.SortMemberPath ?? nameof(GridEntry.DateAdded);
|
||||
}
|
||||
|
||||
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
|
||||
|
||||
@ -57,7 +57,13 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
config.GridFontScaleFactor = linearRangeToScaleFactor(GridFontScaleFactor);
|
||||
config.GridScaleFactor = linearRangeToScaleFactor(GridScaleFactor);
|
||||
}
|
||||
public void OpenLogFolderButton() => Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
|
||||
public void OpenLogFolderButton()
|
||||
{
|
||||
if (System.IO.File.Exists(LogFileFilter.LogFilePath))
|
||||
Go.To.File(LogFileFilter.LogFilePath);
|
||||
else
|
||||
Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
|
||||
}
|
||||
|
||||
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
|
||||
{
|
||||
|
||||
@ -53,8 +53,8 @@ namespace LibationAvalonia.Views
|
||||
private void LiberateStatusButton_DataContextChanged(object sender, EventArgs e)
|
||||
{
|
||||
//Force book status recheck when an entry is scrolled into view.
|
||||
//This will force a recheck for a paprtially downloaded file.
|
||||
var status = DataContext as ILibraryBookEntry;
|
||||
//This will force a recheck for a partially downloaded file.
|
||||
var status = DataContext as LibraryBookEntry;
|
||||
status?.Liberate.Invalidate(nameof(status.Liberate.BookStatus));
|
||||
}
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ namespace LibationAvalonia.Views
|
||||
public MainWindow()
|
||||
{
|
||||
DataContext = new MainVM(this);
|
||||
ApiExtended.LoginChoiceFactory = account => new Dialogs.Login.AvaloniaLoginChoiceEager(account);
|
||||
ApiExtended.LoginChoiceFactory = account => Dispatcher.UIThread.Invoke(() => new Dialogs.Login.AvaloniaLoginChoiceEager(account));
|
||||
|
||||
AudibleApiStorage.LoadError += AudibleApiStorage_LoadError;
|
||||
InitializeComponent();
|
||||
@ -137,7 +137,7 @@ namespace LibationAvalonia.Views
|
||||
}
|
||||
|
||||
public void ProductsDisplay_LiberateClicked(object _, LibraryBook[] libraryBook) => ViewModel.LiberateClicked(libraryBook);
|
||||
public void ProductsDisplay_LiberateSeriesClicked(object _, ISeriesEntry series) => ViewModel.LiberateSeriesClicked(series);
|
||||
public void ProductsDisplay_LiberateSeriesClicked(object _, SeriesEntry series) => ViewModel.LiberateSeriesClicked(series);
|
||||
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook[] libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook);
|
||||
|
||||
BookDetailsDialog bookDetailsForm;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels"
|
||||
xmlns:vm="clr-namespace:LibationUiBase.ProcessQueue;assembly=LibationUiBase"
|
||||
xmlns:views="clr-namespace:LibationAvalonia.Views"
|
||||
x:DataType="vm:ProcessBookViewModel"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="87" MaxHeight="87" MinHeight="87" MinWidth="300"
|
||||
|
||||
@ -2,7 +2,6 @@ using ApplicationServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using DataLayer;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationUiBase;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
|
||||
@ -31,10 +30,8 @@ namespace LibationAvalonia.Views
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
DataContext = new ProcessBookViewModel(
|
||||
context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"),
|
||||
LogMe.RegisterForm(default(ILogForm))
|
||||
);
|
||||
ViewModels.MainVM.Configure_NonUI();
|
||||
DataContext = new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -44,7 +41,7 @@ namespace LibationAvalonia.Views
|
||||
public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> CancelButtonClicked?.Invoke(DataItem);
|
||||
public void MoveFirst_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> PositionButtonClicked?.Invoke(DataItem, QueuePosition.Fisrt);
|
||||
=> PositionButtonClicked?.Invoke(DataItem, QueuePosition.First);
|
||||
public void MoveUp_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> PositionButtonClicked?.Invoke(DataItem, QueuePosition.OneUp);
|
||||
public void MoveDown_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
AllowAutoHide="False">
|
||||
<ItemsControl ItemsSource="{Binding Items}">
|
||||
<ItemsControl ItemsSource="{Binding Queue}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel />
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
using ApplicationServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data.Converters;
|
||||
using DataLayer;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationUiBase;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
using System;
|
||||
@ -17,7 +15,7 @@ namespace LibationAvalonia.Views
|
||||
{
|
||||
public partial class ProcessQueueControl : UserControl
|
||||
{
|
||||
private TrackedQueue<ProcessBookViewModelBase>? Queue => _viewModel?.Queue;
|
||||
private TrackedQueue<ProcessBookViewModel>? Queue => _viewModel?.Queue;
|
||||
private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel;
|
||||
|
||||
public ProcessQueueControl()
|
||||
@ -31,48 +29,49 @@ namespace LibationAvalonia.Views
|
||||
#if DEBUG
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
_ = LibationFileManager.Configuration.Instance.LibationFiles;
|
||||
ViewModels.MainVM.Configure_NonUI();
|
||||
var vm = new ProcessQueueViewModel();
|
||||
var Logger = LogMe.RegisterForm(vm);
|
||||
DataContext = vm;
|
||||
using var context = DbContexts.GetContext();
|
||||
List<ProcessBookViewModel> testList = new()
|
||||
{
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger)
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"))
|
||||
{
|
||||
Result = ProcessBookResult.FailedAbort,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"), Logger)
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"))
|
||||
{
|
||||
Result = ProcessBookResult.FailedSkip,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"), Logger)
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"))
|
||||
{
|
||||
Result = ProcessBookResult.FailedRetry,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"), Logger)
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"))
|
||||
{
|
||||
Result = ProcessBookResult.ValidationFail,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"), Logger)
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"))
|
||||
{
|
||||
Result = ProcessBookResult.Cancelled,
|
||||
Status = ProcessBookStatus.Cancelled,
|
||||
},
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"), Logger)
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"))
|
||||
{
|
||||
Result = ProcessBookResult.Success,
|
||||
Status = ProcessBookStatus.Completed,
|
||||
},
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"), Logger)
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"))
|
||||
{
|
||||
Result = ProcessBookResult.None,
|
||||
Status = ProcessBookStatus.Working,
|
||||
},
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger)
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"))
|
||||
{
|
||||
Result = ProcessBookResult.None,
|
||||
Status = ProcessBookStatus.Queued,
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
Width="75">
|
||||
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<CheckBox
|
||||
HorizontalAlignment="Center"
|
||||
IsThreeState="True"
|
||||
@ -70,7 +70,7 @@
|
||||
|
||||
<controls:DataGridTemplateColumnExt CanUserResize="False" CanUserSort="True" Header="Liberate" SortMemberPath="Liberate" ClipboardContentBinding="{Binding Liberate.ToolTip}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<views:LiberateStatusButton
|
||||
ToolTip.Tip="{CompiledBinding Liberate.ToolTip}"
|
||||
BookStatus="{CompiledBinding Liberate.BookStatus}"
|
||||
@ -85,7 +85,7 @@
|
||||
|
||||
<controls:DataGridTemplateColumnExt CanUserResize="False" CanUserSort="False" Header="Cover" SortMemberPath="Cover" ClipboardContentBinding="{Binding LibraryBook.Book.PictureLarge}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<Image Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Cover_Click" Source="{CompiledBinding Cover}" ToolTip.Tip="Click to see full size" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
@ -93,7 +93,7 @@
|
||||
|
||||
<controls:DataGridTemplateColumnExt Header="Title" MinWidth="10" Width="{Binding TitleWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Title" ClipboardContentBinding="{Binding Title}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||
<TextBlock Classes="h1" Text="{CompiledBinding Title}" />
|
||||
</Panel>
|
||||
@ -103,7 +103,7 @@
|
||||
|
||||
<controls:DataGridTemplateColumnExt Header="Authors" MinWidth="10" Width="{Binding AuthorsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Authors" ClipboardContentBinding="{Binding Authors}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||
<TextBlock Text="{CompiledBinding Authors}" />
|
||||
</Panel>
|
||||
@ -113,7 +113,7 @@
|
||||
|
||||
<controls:DataGridTemplateColumnExt Header="Narrators" MinWidth="10" Width="{Binding NarratorsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Narrators" ClipboardContentBinding="{Binding Narrators}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||
<TextBlock Text="{CompiledBinding Narrators}" />
|
||||
</Panel>
|
||||
@ -123,7 +123,7 @@
|
||||
|
||||
<controls:DataGridTemplateColumnExt Header="Length" MinWidth="10" Width="{Binding LengthWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Length" ClipboardContentBinding="{Binding Length}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||
<TextBlock Text="{CompiledBinding Length}" />
|
||||
</Panel>
|
||||
@ -133,7 +133,7 @@
|
||||
|
||||
<controls:DataGridTemplateColumnExt Header="Series" MinWidth="10" Width="{Binding SeriesWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Series" ClipboardContentBinding="{Binding Series}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||
<TextBlock Text="{CompiledBinding Series}" />
|
||||
</Panel>
|
||||
@ -143,7 +143,7 @@
|
||||
|
||||
<controls:DataGridTemplateColumnExt Header="Series
Order" MinWidth="10" Width="{Binding SeriesOrderWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="SeriesOrder" ClipboardContentBinding="{Binding Series}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||
<TextBlock Text="{CompiledBinding SeriesOrder}" HorizontalAlignment="Center" />
|
||||
</Panel>
|
||||
@ -153,7 +153,7 @@
|
||||
|
||||
<controls:DataGridTemplateColumnExt Header="Description" MinWidth="10" Width="{Binding DescriptionWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Description" ClipboardContentBinding="{Binding Description}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Description_Click" ToolTip.Tip="Click to see full description" >
|
||||
<TextBlock Text="{CompiledBinding Description}" VerticalAlignment="Top" />
|
||||
</Panel>
|
||||
@ -163,7 +163,7 @@
|
||||
|
||||
<controls:DataGridTemplateColumnExt Header="Category" MinWidth="10" Width="{Binding CategoryWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Category" ClipboardContentBinding="{Binding Category}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||
<TextBlock Text="{CompiledBinding Category}" />
|
||||
</Panel>
|
||||
@ -172,7 +172,7 @@
|
||||
</controls:DataGridTemplateColumnExt>
|
||||
|
||||
<controls:DataGridMyRatingColumn
|
||||
x:DataType="uibase:IGridEntry"
|
||||
x:DataType="uibase:GridEntry"
|
||||
Header="Product
Rating"
|
||||
IsReadOnly="true"
|
||||
MinWidth="10" Width="{Binding ProductRatingWidth, Mode=TwoWay}"
|
||||
@ -183,7 +183,7 @@
|
||||
|
||||
<controls:DataGridTemplateColumnExt Header="Purchase
Date" MinWidth="10" Width="{Binding PurchaseDateWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="PurchaseDate" ClipboardContentBinding="{Binding PurchaseDate}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||
<TextBlock Text="{CompiledBinding PurchaseDate}" />
|
||||
</Panel>
|
||||
@ -192,7 +192,7 @@
|
||||
</controls:DataGridTemplateColumnExt>
|
||||
|
||||
<controls:DataGridMyRatingColumn
|
||||
x:DataType="uibase:IGridEntry"
|
||||
x:DataType="uibase:GridEntry"
|
||||
Header="My Rating"
|
||||
IsReadOnly="false"
|
||||
MinWidth="10" Width="{Binding MyRatingWidth, Mode=TwoWay}"
|
||||
@ -203,7 +203,7 @@
|
||||
|
||||
<controls:DataGridTemplateColumnExt Header="Misc" MinWidth="10" Width="{Binding MiscWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Misc" ClipboardContentBinding="{Binding Misc}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||
<TextBlock Text="{CompiledBinding Misc}" TextWrapping="WrapWithOverflow" />
|
||||
</Panel>
|
||||
@ -213,7 +213,7 @@
|
||||
|
||||
<controls:DataGridTemplateColumnExt Header="Last
Download" MinWidth="10" Width="{Binding LastDownloadWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="LastDownload" ClipboardContentBinding="{Binding LastDownload}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}" ToolTip.Tip="{CompiledBinding LastDownload.ToolTipText}" DoubleTapped="Version_DoubleClick">
|
||||
<TextBlock Text="{CompiledBinding LastDownload}" TextWrapping="WrapWithOverflow" />
|
||||
</Panel>
|
||||
@ -223,7 +223,7 @@
|
||||
|
||||
<controls:DataGridTemplateColumnExt Header="Tags" MinWidth="10" Width="{Binding BookTagsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="BookTags" ClipboardContentBinding="{Binding BookTags}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<DataTemplate x:DataType="uibase:GridEntry">
|
||||
<Button
|
||||
IsVisible="{CompiledBinding !Liberate.IsSeries}"
|
||||
VerticalAlignment="Stretch"
|
||||
|
||||
@ -26,7 +26,7 @@ namespace LibationAvalonia.Views
|
||||
public partial class ProductsDisplay : UserControl
|
||||
{
|
||||
public event EventHandler<LibraryBook[]>? LiberateClicked;
|
||||
public event EventHandler<ISeriesEntry>? LiberateSeriesClicked;
|
||||
public event EventHandler<SeriesEntry>? LiberateSeriesClicked;
|
||||
public event EventHandler<LibraryBook[]>? ConvertToMp3Clicked;
|
||||
public event EventHandler<LibraryBook>? TagsButtonClicked;
|
||||
|
||||
@ -102,7 +102,7 @@ namespace LibationAvalonia.Views
|
||||
|
||||
private void ProductsDisplay_LoadingRow(object sender, DataGridRowEventArgs e)
|
||||
{
|
||||
if (e.Row.DataContext is LibraryBookEntry<AvaloniaEntryStatus> entry && entry.Liberate.IsEpisode)
|
||||
if (e.Row.DataContext is LibraryBookEntry entry && entry.Liberate.IsEpisode)
|
||||
e.Row.DynamicResource(DataGridRow.BackgroundProperty, "SeriesEntryGridBackgroundBrush");
|
||||
else
|
||||
e.Row.DynamicResource(DataGridRow.BackgroundProperty, "SystemRegionColor");
|
||||
@ -173,10 +173,10 @@ namespace LibationAvalonia.Views
|
||||
{
|
||||
switch (column.SortMemberPath)
|
||||
{
|
||||
case nameof(IGridEntry.Liberate):
|
||||
case nameof(GridEntry.Liberate):
|
||||
column.Width = new DataGridLength(BaseLiberateWidth * scaleFactor);
|
||||
break;
|
||||
case nameof(IGridEntry.Cover):
|
||||
case nameof(GridEntry.Cover):
|
||||
column.Width = new DataGridLength(BaseCoverWidth * scaleFactor);
|
||||
break;
|
||||
}
|
||||
@ -220,7 +220,7 @@ namespace LibationAvalonia.Views
|
||||
|
||||
#region Liberate all Episodes (Single series only)
|
||||
|
||||
if (entries.Length == 1 && entries[0] is ISeriesEntry seriesEntry)
|
||||
if (entries.Length == 1 && entries[0] is SeriesEntry seriesEntry)
|
||||
{
|
||||
args.ContextMenuItems.Add(new MenuItem()
|
||||
{
|
||||
@ -253,7 +253,7 @@ namespace LibationAvalonia.Views
|
||||
#endregion
|
||||
#region Locate file (Single book only)
|
||||
|
||||
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry)
|
||||
if (entries.Length == 1 && entries[0] is LibraryBookEntry entry)
|
||||
{
|
||||
args.ContextMenuItems.Add(new MenuItem
|
||||
{
|
||||
@ -301,7 +301,7 @@ namespace LibationAvalonia.Views
|
||||
|
||||
#endregion
|
||||
#region Liberate All (multiple books only)
|
||||
if (entries.OfType<ILibraryBookEntry>().Count() > 1)
|
||||
if (entries.OfType<LibraryBookEntry>().Count() > 1)
|
||||
{
|
||||
args.ContextMenuItems.Add(new MenuItem
|
||||
{
|
||||
@ -325,7 +325,7 @@ namespace LibationAvalonia.Views
|
||||
|
||||
#endregion
|
||||
#region Force Re-Download (Single book only)
|
||||
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry4)
|
||||
if (entries.Length == 1 && entries[0] is LibraryBookEntry entry4)
|
||||
{
|
||||
args.ContextMenuItems.Add(new MenuItem()
|
||||
{
|
||||
@ -361,7 +361,7 @@ namespace LibationAvalonia.Views
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry2)
|
||||
if (entries.Length == 1 && entries[0] is LibraryBookEntry entry2)
|
||||
{
|
||||
args.ContextMenuItems.Add(new MenuItem
|
||||
{
|
||||
@ -391,7 +391,7 @@ namespace LibationAvalonia.Views
|
||||
#endregion
|
||||
#region View Bookmarks/Clips (Single book only)
|
||||
|
||||
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry3 && VisualRoot is Window window)
|
||||
if (entries.Length == 1 && entries[0] is LibraryBookEntry entry3 && VisualRoot is Window window)
|
||||
{
|
||||
args.ContextMenuItems.Add(new MenuItem
|
||||
{
|
||||
@ -447,7 +447,7 @@ namespace LibationAvalonia.Views
|
||||
{
|
||||
var itemName = column.SortMemberPath;
|
||||
|
||||
if (itemName == nameof(IGridEntry.Remove))
|
||||
if (itemName == nameof(GridEntry.Remove))
|
||||
continue;
|
||||
|
||||
menuItems.Add
|
||||
@ -536,7 +536,7 @@ namespace LibationAvalonia.Views
|
||||
if (sender is not LiberateStatusButton button)
|
||||
return;
|
||||
|
||||
if (button.DataContext is ISeriesEntry sEntry && _viewModel is not null)
|
||||
if (button.DataContext is SeriesEntry sEntry && _viewModel is not null)
|
||||
{
|
||||
await _viewModel.ToggleSeriesExpanded(sEntry);
|
||||
|
||||
@ -544,7 +544,7 @@ namespace LibationAvalonia.Views
|
||||
//to the topright cell. Reset focus onto the clicked button's cell.
|
||||
button.Focus();
|
||||
}
|
||||
else if (button.DataContext is ILibraryBookEntry lbEntry)
|
||||
else if (button.DataContext is LibraryBookEntry lbEntry)
|
||||
{
|
||||
LiberateClicked?.Invoke(this, [lbEntry.LibraryBook]);
|
||||
}
|
||||
@ -558,13 +558,13 @@ namespace LibationAvalonia.Views
|
||||
|
||||
public void Version_DoubleClick(object sender, Avalonia.Input.TappedEventArgs args)
|
||||
{
|
||||
if (sender is Control panel && panel.DataContext is ILibraryBookEntry lbe && lbe.LastDownload.IsValid)
|
||||
if (sender is Control panel && panel.DataContext is LibraryBookEntry lbe && lbe.LastDownload.IsValid)
|
||||
lbe.LastDownload.OpenReleaseUrl();
|
||||
}
|
||||
|
||||
public void Cover_Click(object sender, Avalonia.Input.TappedEventArgs args)
|
||||
{
|
||||
if (sender is not Image tblock || tblock.DataContext is not IGridEntry gEntry)
|
||||
if (sender is not Image tblock || tblock.DataContext is not GridEntry gEntry)
|
||||
return;
|
||||
|
||||
if (imageDisplayDialog is null || !imageDisplayDialog.IsVisible)
|
||||
@ -605,7 +605,7 @@ namespace LibationAvalonia.Views
|
||||
|
||||
public void Description_Click(object sender, Avalonia.Input.TappedEventArgs args)
|
||||
{
|
||||
if (sender is Control tblock && tblock.DataContext is IGridEntry gEntry)
|
||||
if (sender is Control tblock && tblock.DataContext is GridEntry gEntry)
|
||||
{
|
||||
var pt = tblock.PointToScreen(tblock.Bounds.TopRight);
|
||||
var displayWindow = new DescriptionDisplayDialog
|
||||
@ -632,7 +632,7 @@ namespace LibationAvalonia.Views
|
||||
{
|
||||
var button = args.Source as Button;
|
||||
|
||||
if (button?.DataContext is ILibraryBookEntry lbEntry)
|
||||
if (button?.DataContext is LibraryBookEntry lbEntry)
|
||||
{
|
||||
TagsButtonClicked?.Invoke(this, lbEntry.LibraryBook);
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Dinah.Core.Logging;
|
||||
using FileManager;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@ -22,6 +21,7 @@ namespace LibationFileManager
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.Destructure.ByTransforming<LongPath>(lp => lp.Path)
|
||||
.Destructure.With<LogFileFilter>()
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
|
||||
113
Source/LibationFileManager/LogFileFilter.cs
Normal file
113
Source/LibationFileManager/LogFileFilter.cs
Normal file
@ -0,0 +1,113 @@
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using Serilog.Sinks.File;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager;
|
||||
|
||||
/// <summary>
|
||||
/// Hooks the file sink to set the log file path for the LogFileFilter.
|
||||
/// </summary>
|
||||
public class FileSinkHook : FileLifecycleHooks
|
||||
{
|
||||
public override Stream OnFileOpened(string path, Stream underlyingStream, Encoding encoding)
|
||||
{
|
||||
LogFileFilter.SetLogFilePath(path);
|
||||
return base.OnFileOpened(path, underlyingStream, encoding);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Identify log entries which are to be written to files, and save them to a zip file.
|
||||
///
|
||||
/// Files are detected by pattern matching. If the logged type has properties named 'filename' and 'filedata' (case insensitive)
|
||||
/// with types string and byte[] respectively, the type is destructured and written to the log zip file.
|
||||
///
|
||||
/// The zip file's name will be derived from the active log file's name, with "_AdditionalFiles.zip" appended.
|
||||
/// </summary>
|
||||
public class LogFileFilter : IDestructuringPolicy
|
||||
{
|
||||
private static readonly object lockObj = new();
|
||||
public static string? ZipFilePath { get; private set; }
|
||||
public static string? LogFilePath { get; private set; }
|
||||
public static void SetLogFilePath(string? logFilePath)
|
||||
{
|
||||
lock(lockObj)
|
||||
{
|
||||
(LogFilePath, ZipFilePath)
|
||||
= File.Exists(logFilePath) && Path.GetDirectoryName(logFilePath) is string logDir
|
||||
? (logFilePath, Path.Combine(logDir, $"{Path.GetFileNameWithoutExtension(logFilePath)}_AdditionalFiles.zip"))
|
||||
: (null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TrySaveLogFile(ref string filename, byte[] fileData, CompressionLevel compression)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (lockObj)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ZipFilePath))
|
||||
return false;
|
||||
|
||||
using var archive = new ZipArchive(File.Open(ZipFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite), ZipArchiveMode.Update, false, Encoding.UTF8);
|
||||
filename = GetUniqueEntryName(archive, filename);
|
||||
|
||||
var entry = archive.CreateEntry(filename, compression);
|
||||
using var entryStream = entry.Open();
|
||||
entryStream.Write(fileData);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetUniqueEntryName(ZipArchive archive, string filename)
|
||||
{
|
||||
var entryFileName = filename;
|
||||
for (int i = 1; archive.Entries.Any(e => e.Name == entryFileName); i++)
|
||||
{
|
||||
entryFileName = $"{Path.GetFileNameWithoutExtension(filename)}_({i++}){Path.GetExtension(filename)}";
|
||||
}
|
||||
return entryFileName;
|
||||
}
|
||||
|
||||
public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, [NotNullWhen(true)] out LogEventPropertyValue? result)
|
||||
{
|
||||
if (value?.GetType().GetProperties() is PropertyInfo[] properties && properties.Length >= 2
|
||||
&& properties.FirstOrDefault(p => p.Name.Equals("filename", StringComparison.InvariantCultureIgnoreCase)) is PropertyInfo filenameProperty && filenameProperty.PropertyType == typeof(string)
|
||||
&& properties.FirstOrDefault(p => p.Name.Equals("fileData", StringComparison.InvariantCultureIgnoreCase)) is PropertyInfo fileDataProperty && fileDataProperty.PropertyType == typeof(byte[]))
|
||||
{
|
||||
var filename = filenameProperty.GetValue(value) as string;
|
||||
var fileData = fileDataProperty.GetValue(value) as byte[];
|
||||
|
||||
if (filename != null && fileData != null && fileData.Length > 0)
|
||||
{
|
||||
var compressionProperty = properties.FirstOrDefault(f => f.PropertyType == typeof(CompressionLevel));
|
||||
var compression = compressionProperty?.GetValue(value) is CompressionLevel c ? c : CompressionLevel.Fastest;
|
||||
|
||||
result
|
||||
= TrySaveLogFile(ref filename, fileData, compression)
|
||||
? propertyValueFactory.CreatePropertyValue($"Log file '{filename}' saved in {ZipFilePath}")
|
||||
: propertyValueFactory.CreatePropertyValue($"Log file '{filename}' could not be saved in {ZipFilePath ?? "<null_path>"}. File Contents = {Convert.ToBase64String(fileData)}");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
@ -78,13 +79,13 @@ namespace LibationFileManager
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetPicturePathSynchronously(PictureDefinition def)
|
||||
public static string GetPicturePathSynchronously(PictureDefinition def, CancellationToken cancellationToken = default)
|
||||
{
|
||||
GetPictureSynchronously(def);
|
||||
GetPictureSynchronously(def, cancellationToken);
|
||||
return getPath(def);
|
||||
}
|
||||
|
||||
public static byte[] GetPictureSynchronously(PictureDefinition def)
|
||||
public static byte[] GetPictureSynchronously(PictureDefinition def, CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (cacheLocker)
|
||||
{
|
||||
@ -94,7 +95,7 @@ namespace LibationFileManager
|
||||
var bytes
|
||||
= File.Exists(path)
|
||||
? File.ReadAllBytes(path)
|
||||
: downloadBytes(def);
|
||||
: downloadBytes(def, cancellationToken);
|
||||
cache[def] = bytes;
|
||||
}
|
||||
return cache[def];
|
||||
@ -124,7 +125,7 @@ namespace LibationFileManager
|
||||
}
|
||||
|
||||
private static HttpClient imageDownloadClient { get; } = new HttpClient();
|
||||
private static byte[] downloadBytes(PictureDefinition def)
|
||||
private static byte[] downloadBytes(PictureDefinition def, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (def.PictureId is null)
|
||||
return GetDefaultImage(def.Size);
|
||||
@ -132,7 +133,7 @@ namespace LibationFileManager
|
||||
try
|
||||
{
|
||||
var sizeStr = def.Size == PictureSize.Native ? "" : $"._SL{(int)def.Size}_";
|
||||
var bytes = imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}{sizeStr}.jpg").Result;
|
||||
var bytes = imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}{sizeStr}.jpg", cancellationToken).Result;
|
||||
|
||||
// save image file. make sure to not save default image
|
||||
var path = getPath(def);
|
||||
|
||||
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationFileManager
|
||||
@ -10,7 +11,7 @@ namespace LibationFileManager
|
||||
public static class WindowsDirectory
|
||||
{
|
||||
|
||||
public static void SetCoverAsFolderIcon(string pictureId, string directory)
|
||||
public static void SetCoverAsFolderIcon(string pictureId, string directory, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -19,8 +20,7 @@ namespace LibationFileManager
|
||||
return;
|
||||
|
||||
// get path of cover art in Images dir. Download first if not exists
|
||||
var coverArtPath = PictureStorage.GetPicturePathSynchronously(new(pictureId, PictureSize._300x300));
|
||||
|
||||
var coverArtPath = PictureStorage.GetPicturePathSynchronously(new(pictureId, PictureSize._300x300), cancellationToken);
|
||||
InteropFactory.Create().SetFolderIcon(image: coverArtPath, directory: directory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@ -1,13 +1,35 @@
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationUiBase
|
||||
{
|
||||
public static class BaseUtil
|
||||
{
|
||||
/// <summary>A delegate that loads image bytes into the the UI framework's image format.</summary>
|
||||
public static Func<byte[], PictureSize, object> LoadImage { get; private set; }
|
||||
public static void SetLoadImageDelegate(Func<byte[], PictureSize, object> tryLoadImage)
|
||||
=> LoadImage = tryLoadImage;
|
||||
public static Func<byte[], PictureSize, object?> LoadImage => s_LoadImage ?? DefaultLoadImageImpl;
|
||||
|
||||
/// <summary>A delegate that loads a named resource into the the UI framework's image format.</summary>
|
||||
public static Func<string, object?> LoadResourceImage => s_LoadResourceImage ?? DefaultLoadResourceImageImpl;
|
||||
|
||||
public static void SetLoadImageDelegate(Func<byte[], PictureSize, object?> tryLoadImage)
|
||||
=> s_LoadImage = tryLoadImage;
|
||||
public static void SetLoadResourceImageDelegate(Func<string, object?> tryLoadResourceImage)
|
||||
=> s_LoadResourceImage = tryLoadResourceImage;
|
||||
|
||||
private static Func<byte[], PictureSize, object?>? s_LoadImage;
|
||||
private static Func<string, object?>? s_LoadResourceImage;
|
||||
|
||||
private static object? DefaultLoadImageImpl(byte[] imageBytes, PictureSize size)
|
||||
{
|
||||
Serilog.Log.Error("{LoadImage} called without a delegate set. Picture size: {PictureSize}", nameof(LoadImage), size);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static object? DefaultLoadResourceImageImpl(string resourceName)
|
||||
{
|
||||
Serilog.Log.Error("{LoadResourceImage} called without a delegate set. Resource name: {ResourceName}", nameof(LoadResourceImage), resourceName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,22 +1,15 @@
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Threading;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
public interface IEntryStatus
|
||||
{
|
||||
static abstract EntryStatus Create(LibraryBook libraryBook);
|
||||
}
|
||||
|
||||
//This Class holds all book entry status info to help the grid properly render entries.
|
||||
//The reason this info is in here instead of GridEntry is because all of this info is needed
|
||||
//for the "Liberate" column's display and sorting functions.
|
||||
public abstract class EntryStatus : ReactiveObject, IComparable
|
||||
public class EntryStatus : ReactiveObject, IComparable
|
||||
{
|
||||
public LiberatedStatus? PdfStatus => LibraryCommands.Pdf_Status(Book);
|
||||
public LiberatedStatus BookStatus
|
||||
@ -70,7 +63,7 @@ namespace LibationUiBase.GridView
|
||||
private readonly bool isAbsent;
|
||||
private static readonly Dictionary<string, object> iconCache = new();
|
||||
|
||||
protected EntryStatus(LibraryBook libraryBook)
|
||||
internal EntryStatus(LibraryBook libraryBook)
|
||||
{
|
||||
Book = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook)).Book;
|
||||
isAbsent = libraryBook.AbsentFromLastScan is true;
|
||||
@ -78,9 +71,6 @@ namespace LibationUiBase.GridView
|
||||
IsSeries = Book.ContentType is ContentType.Parent;
|
||||
}
|
||||
|
||||
internal protected abstract object LoadImage(byte[] picture);
|
||||
protected abstract object GetResourceImage(string rescName);
|
||||
|
||||
/// <summary>Refresh BookStatus (so partial download files are checked again in the filesystem) and raise PropertyChanged for property names.</summary>
|
||||
public void Invalidate(params string[] properties)
|
||||
{
|
||||
@ -179,7 +169,7 @@ namespace LibationUiBase.GridView
|
||||
private object GetAndCacheResource(string rescName)
|
||||
{
|
||||
if (!iconCache.ContainsKey(rescName))
|
||||
iconCache[rescName] = GetResourceImage(rescName);
|
||||
iconCache[rescName] = BaseUtil.LoadResourceImage(rescName);
|
||||
return iconCache[rescName];
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,17 +29,17 @@ public class GridContextMenu
|
||||
public string ViewBookmarksText => $"View {Accelerator}Bookmarks/Clips";
|
||||
public string ViewSeriesText => GridEntries[0].Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series";
|
||||
|
||||
public bool LiberateEpisodesEnabled => GridEntries.OfType<ISeriesEntry>().Any(sEntry => sEntry.Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload));
|
||||
public bool LiberateEpisodesEnabled => GridEntries.OfType<SeriesEntry>().Any(sEntry => sEntry.Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload));
|
||||
public bool SetDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || ge.Liberate.IsSeries);
|
||||
public bool SetNotDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || ge.Liberate.IsSeries);
|
||||
public bool ConvertToMp3Enabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
|
||||
public bool ReDownloadEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
|
||||
|
||||
private IGridEntry[] GridEntries { get; }
|
||||
public ILibraryBookEntry[] LibraryBookEntries { get; }
|
||||
private GridEntry[] GridEntries { get; }
|
||||
public LibraryBookEntry[] LibraryBookEntries { get; }
|
||||
public char Accelerator { get; }
|
||||
|
||||
public GridContextMenu(IGridEntry[] gridEntries, char accelerator)
|
||||
public GridContextMenu(GridEntry[] gridEntries, char accelerator)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gridEntries, nameof(gridEntries));
|
||||
ArgumentOutOfRangeException.ThrowIfZero(gridEntries.Length, $"{nameof(gridEntries)}.{nameof(gridEntries.Length)}");
|
||||
@ -48,9 +48,9 @@ public class GridContextMenu
|
||||
Accelerator = accelerator;
|
||||
LibraryBookEntries
|
||||
= GridEntries
|
||||
.OfType<ISeriesEntry>()
|
||||
.OfType<SeriesEntry>()
|
||||
.SelectMany(s => s.Children)
|
||||
.Concat(GridEntries.OfType<ILibraryBookEntry>())
|
||||
.Concat(GridEntries.OfType<LibraryBookEntry>())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Threading;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
@ -9,7 +8,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
@ -22,7 +21,7 @@ namespace LibationUiBase.GridView
|
||||
}
|
||||
|
||||
/// <summary>The View Model base for the DataGridView</summary>
|
||||
public abstract class GridEntry<TStatus> : ReactiveObject, IGridEntry where TStatus : IEntryStatus
|
||||
public abstract class GridEntry : ReactiveObject
|
||||
{
|
||||
[Browsable(false)] public string AudibleProductId => Book.AudibleProductId;
|
||||
[Browsable(false)] public LibraryBook LibraryBook { get; protected set; }
|
||||
@ -101,7 +100,7 @@ namespace LibationUiBase.GridView
|
||||
LibraryBook = libraryBook;
|
||||
|
||||
var expanded = Liberate?.Expanded ?? false;
|
||||
Liberate = TStatus.Create(libraryBook);
|
||||
Liberate = new EntryStatus(libraryBook);
|
||||
Liberate.Expanded = expanded;
|
||||
|
||||
Title = Book.TitleWithSubtitle;
|
||||
@ -240,7 +239,7 @@ namespace LibationUiBase.GridView
|
||||
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
||||
|
||||
// Mutable property. Set the field so PropertyChanged isn't fired.
|
||||
_lazyCover = new Lazy<object>(() => Liberate.LoadImage(picture));
|
||||
_lazyCover = new Lazy<object>(() => BaseUtil.LoadImage(picture, PictureSize._80x80));
|
||||
}
|
||||
|
||||
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
||||
@ -254,7 +253,7 @@ namespace LibationUiBase.GridView
|
||||
// logic validation
|
||||
if (e.Definition.PictureId == Book.PictureId)
|
||||
{
|
||||
_lazyCover = new Lazy<object>(() => Liberate.LoadImage(e.Picture));
|
||||
_lazyCover = new Lazy<object>(() => BaseUtil.LoadImage(e.Picture, PictureSize._80x80));
|
||||
RaisePropertyChanged(nameof(Cover));
|
||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||
}
|
||||
@ -311,6 +310,35 @@ namespace LibationUiBase.GridView
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="GridEntry"/> for all non-episode books in an enumeration of <see cref="DataLayer.LibraryBook"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Can be called from any thread, but requires the calling thread's <see cref="SynchronizationContext.Current"/> to be valid.</remarks>
|
||||
public static async Task<List<TEntry>> GetAllProductsAsync<TEntry>(IEnumerable<LibraryBook> libraryBooks, Func<LibraryBook, bool> includeIf, Func<LibraryBook, TEntry> factory)
|
||||
where TEntry : GridEntry
|
||||
{
|
||||
var products = libraryBooks.Where(includeIf).ToArray();
|
||||
if (products.Length == 0)
|
||||
return [];
|
||||
|
||||
int parallelism = int.Max(1, Environment.ProcessorCount - 1);
|
||||
|
||||
(int batchSize, int rem) = int.DivRem(products.Length, parallelism);
|
||||
if (rem != 0) batchSize++;
|
||||
|
||||
var syncContext = SynchronizationContext.Current;
|
||||
|
||||
//Asynchronously create a GridEntry for every book in the library
|
||||
var tasks = products.Chunk(batchSize).Select(batch => Task.Run(() =>
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(syncContext);
|
||||
return batch.Select(factory).OfType<TEntry>().ToArray();
|
||||
}));
|
||||
|
||||
return (await Task.WhenAll(tasks)).SelectMany(a => a).ToList();
|
||||
}
|
||||
|
||||
|
||||
~GridEntry()
|
||||
{
|
||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core.DataBinding;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
public interface IGridEntry : IMemberComparable, INotifyPropertyChanged
|
||||
{
|
||||
EntryStatus Liberate { get; }
|
||||
float SeriesIndex { get; }
|
||||
string AudibleProductId { get; }
|
||||
LibraryBook LibraryBook { get; }
|
||||
Book Book { get; }
|
||||
DateTime DateAdded { get; }
|
||||
bool? Remove { get; set; }
|
||||
string PurchaseDate { get; }
|
||||
object Cover { get; }
|
||||
string Length { get; }
|
||||
LastDownloadStatus LastDownload { get; }
|
||||
string Series { get; }
|
||||
SeriesOrder SeriesOrder { get; }
|
||||
string Title { get; }
|
||||
string Authors { get; }
|
||||
string Narrators { get; }
|
||||
string Category { get; }
|
||||
string Misc { get; }
|
||||
string Description { get; }
|
||||
Rating ProductRating { get; }
|
||||
Rating MyRating { get; set; }
|
||||
string BookTags { get; }
|
||||
void UpdateLibraryBook(LibraryBook libraryBook);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
public interface ILibraryBookEntry : IGridEntry
|
||||
{
|
||||
ISeriesEntry Parent { get; }
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
public interface ISeriesEntry : IGridEntry
|
||||
{
|
||||
List<ILibraryBookEntry> Children { get; }
|
||||
void ChildRemoveUpdate();
|
||||
void RemoveChild(ILibraryBookEntry libraryBookEntry);
|
||||
}
|
||||
}
|
||||
43
Source/LibationUiBase/GridView/LibraryBookEntry.cs
Normal file
43
Source/LibationUiBase/GridView/LibraryBookEntry.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using DataLayer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
/// <summary>The View Model for a LibraryBook that is ContentType.Product or ContentType.Episode</summary>
|
||||
public class LibraryBookEntry : GridEntry
|
||||
{
|
||||
[Browsable(false)] public override DateTime DateAdded => LibraryBook.DateAdded;
|
||||
[Browsable(false)] public SeriesEntry Parent { get; }
|
||||
|
||||
public override bool? Remove
|
||||
{
|
||||
get => remove;
|
||||
set
|
||||
{
|
||||
remove = value ?? false;
|
||||
|
||||
Parent?.ChildRemoveUpdate();
|
||||
RaisePropertyChanged(nameof(Remove));
|
||||
}
|
||||
}
|
||||
|
||||
public LibraryBookEntry(LibraryBook libraryBook, SeriesEntry parent = null)
|
||||
{
|
||||
Parent = parent;
|
||||
UpdateLibraryBook(libraryBook);
|
||||
LoadCover();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="LibraryBookEntry{TStatus}"/> for all non-episode books in an enumeration of <see cref="LibraryBook"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Can be called from any thread, but requires the calling thread's <see cref="System.Threading.SynchronizationContext.Current"/> to be valid.</remarks>
|
||||
public static async Task<List<GridEntry>> GetAllProductsAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||
=> await GetAllProductsAsync(libraryBooks, lb => lb.Book.IsProduct(), lb => new LibraryBookEntry(lb) as GridEntry);
|
||||
|
||||
protected override string GetBookTags() => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
||||
}
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
using DataLayer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
/// <summary>The View Model for a LibraryBook that is ContentType.Product or ContentType.Episode</summary>
|
||||
public class LibraryBookEntry<TStatus> : GridEntry<TStatus>, ILibraryBookEntry where TStatus : IEntryStatus
|
||||
{
|
||||
[Browsable(false)] public override DateTime DateAdded => LibraryBook.DateAdded;
|
||||
[Browsable(false)] public ISeriesEntry Parent { get; }
|
||||
|
||||
public override bool? Remove
|
||||
{
|
||||
get => remove;
|
||||
set
|
||||
{
|
||||
remove = value ?? false;
|
||||
|
||||
Parent?.ChildRemoveUpdate();
|
||||
RaisePropertyChanged(nameof(Remove));
|
||||
}
|
||||
}
|
||||
|
||||
public LibraryBookEntry(LibraryBook libraryBook, ISeriesEntry parent = null)
|
||||
{
|
||||
Parent = parent;
|
||||
UpdateLibraryBook(libraryBook);
|
||||
LoadCover();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="LibraryBookEntry{TStatus}"/> for all non-episode books in an enumeration of <see cref="LibraryBook"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Can be called from any thread, but requires the calling thread's <see cref="SynchronizationContext.Current"/> to be valid.</remarks>
|
||||
public static async Task<List<IGridEntry>> GetAllProductsAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||
{
|
||||
var products = libraryBooks.Where(lb => lb.Book.IsProduct()).ToArray();
|
||||
if (products.Length == 0)
|
||||
return [];
|
||||
|
||||
int parallelism = int.Max(1, Environment.ProcessorCount - 1);
|
||||
|
||||
(int batchSize, int rem) = int.DivRem(products.Length, parallelism);
|
||||
if (rem != 0) batchSize++;
|
||||
|
||||
var syncContext = SynchronizationContext.Current;
|
||||
|
||||
//Asynchronously create an ILibraryBookEntry for every book in the library
|
||||
var tasks = products.Chunk(batchSize).Select(batch => Task.Run(() =>
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(syncContext);
|
||||
return batch.Select(lb => new LibraryBookEntry<TStatus>(lb) as IGridEntry);
|
||||
}));
|
||||
|
||||
return (await Task.WhenAll(tasks)).SelectMany(a => a).ToList();
|
||||
}
|
||||
|
||||
protected override string GetBookTags() => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
||||
}
|
||||
}
|
||||
@ -10,19 +10,19 @@ namespace LibationUiBase.GridView
|
||||
#nullable enable
|
||||
public static class QueryExtensions
|
||||
{
|
||||
public static IEnumerable<ILibraryBookEntry> BookEntries(this IEnumerable<IGridEntry> gridEntries)
|
||||
=> gridEntries.OfType<ILibraryBookEntry>();
|
||||
public static IEnumerable<LibraryBookEntry> BookEntries(this IEnumerable<GridEntry> gridEntries)
|
||||
=> gridEntries.OfType<LibraryBookEntry>();
|
||||
|
||||
public static IEnumerable<ISeriesEntry> SeriesEntries(this IEnumerable<IGridEntry> gridEntries)
|
||||
=> gridEntries.OfType<ISeriesEntry>();
|
||||
public static IEnumerable<SeriesEntry> SeriesEntries(this IEnumerable<GridEntry> gridEntries)
|
||||
=> gridEntries.OfType<SeriesEntry>();
|
||||
|
||||
public static T? FindByAsin<T>(this IEnumerable<T> gridEntries, string audibleProductID) where T : IGridEntry
|
||||
public static T? FindByAsin<T>(this IEnumerable<T> gridEntries, string audibleProductID) where T : GridEntry
|
||||
=> gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID);
|
||||
|
||||
public static IEnumerable<ISeriesEntry> EmptySeries(this IEnumerable<IGridEntry> gridEntries)
|
||||
public static IEnumerable<SeriesEntry> EmptySeries(this IEnumerable<GridEntry> gridEntries)
|
||||
=> gridEntries.SeriesEntries().Where(i => i.Children.Count == 0);
|
||||
|
||||
public static ISeriesEntry? FindSeriesParent(this IEnumerable<IGridEntry> gridEntries, LibraryBook seriesEpisode)
|
||||
public static SeriesEntry? FindSeriesParent(this IEnumerable<GridEntry> gridEntries, LibraryBook seriesEpisode)
|
||||
{
|
||||
if (seriesEpisode.Book.SeriesLink is null) return null;
|
||||
|
||||
@ -42,14 +42,14 @@ namespace LibationUiBase.GridView
|
||||
}
|
||||
}
|
||||
|
||||
public static bool SearchSetsDiffer(this HashSet<IGridEntry>? searchSet, HashSet<IGridEntry>? otherSet)
|
||||
public static bool SearchSetsDiffer(this HashSet<GridEntry>? searchSet, HashSet<GridEntry>? otherSet)
|
||||
=> searchSet is null != otherSet is null ||
|
||||
(searchSet is not null &&
|
||||
otherSet is not null &&
|
||||
searchSet.Intersect(otherSet).Count() != searchSet.Count);
|
||||
|
||||
[return: NotNullIfNotNull(nameof(searchString))]
|
||||
public static HashSet<IGridEntry>? FilterEntries(this IEnumerable<IGridEntry> entries, string? searchString)
|
||||
public static HashSet<GridEntry>? FilterEntries(this IEnumerable<GridEntry> entries, string? searchString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchString))
|
||||
return null;
|
||||
@ -59,7 +59,7 @@ namespace LibationUiBase.GridView
|
||||
var booksFilteredIn = entries.IntersectBy(searchResultSet.Docs.Select(d => d.ProductId), l => l.AudibleProductId);
|
||||
|
||||
//Find all series containing children that match the search criteria
|
||||
var seriesFilteredIn = booksFilteredIn.OfType<ILibraryBookEntry>().Where(lbe => lbe.Parent is not null).Select(lbe => lbe.Parent).Distinct();
|
||||
var seriesFilteredIn = booksFilteredIn.OfType<LibraryBookEntry>().Where(lbe => lbe.Parent is not null).Select(lbe => lbe.Parent).Distinct();
|
||||
|
||||
return booksFilteredIn.Concat(seriesFilteredIn).ToHashSet();
|
||||
}
|
||||
|
||||
@ -10,16 +10,16 @@ namespace LibationUiBase.GridView
|
||||
/// are sorted by PropertyName while all episodes remain immediately beneath their parents and remain
|
||||
/// sorted by series index, ascending.
|
||||
/// </summary>
|
||||
public abstract class RowComparerBase : IComparer, IComparer<IGridEntry>, IComparer<object>
|
||||
public abstract class RowComparerBase : IComparer, IComparer<GridEntry>, IComparer<object>
|
||||
{
|
||||
public abstract string? PropertyName { get; set; }
|
||||
|
||||
public int Compare(object? x, object? y)
|
||||
=> Compare(x as IGridEntry, y as IGridEntry);
|
||||
=> Compare(x as GridEntry, y as GridEntry);
|
||||
|
||||
protected abstract ListSortDirection GetSortOrder();
|
||||
|
||||
private int InternalCompare(IGridEntry x, IGridEntry y)
|
||||
private int InternalCompare(GridEntry x, GridEntry y)
|
||||
{
|
||||
var val1 = x.GetMemberValue(PropertyName);
|
||||
var val2 = y.GetMemberValue(PropertyName);
|
||||
@ -32,7 +32,7 @@ namespace LibationUiBase.GridView
|
||||
: compare;
|
||||
}
|
||||
|
||||
public int Compare(IGridEntry? geA, IGridEntry? geB)
|
||||
public int Compare(GridEntry? geA, GridEntry? geB)
|
||||
{
|
||||
if (geA is null && geB is not null) return -1;
|
||||
if (geA is not null && geB is null) return 1;
|
||||
@ -40,12 +40,12 @@ namespace LibationUiBase.GridView
|
||||
|
||||
var sortDirection = GetSortOrder();
|
||||
|
||||
ISeriesEntry? parentA = null;
|
||||
ISeriesEntry? parentB = null;
|
||||
SeriesEntry? parentA = null;
|
||||
SeriesEntry? parentB = null;
|
||||
|
||||
if (geA is ILibraryBookEntry lbA && lbA.Parent is ISeriesEntry seA)
|
||||
if (geA is LibraryBookEntry lbA && lbA.Parent is SeriesEntry seA)
|
||||
parentA = seA;
|
||||
if (geB is ILibraryBookEntry lbB && lbB.Parent is ISeriesEntry seB)
|
||||
if (geB is LibraryBookEntry lbB && lbB.Parent is SeriesEntry seB)
|
||||
parentB = seB;
|
||||
|
||||
//both entries are children
|
||||
@ -59,7 +59,7 @@ namespace LibationUiBase.GridView
|
||||
//and DateAdded, compare SeriesOrder instead..
|
||||
return PropertyName switch
|
||||
{
|
||||
nameof(IGridEntry.DateAdded) or nameof(IGridEntry.PurchaseDate) => geA.SeriesOrder.CompareTo(geB.SeriesOrder),
|
||||
nameof(GridEntry.DateAdded) or nameof(GridEntry.PurchaseDate) => geA.SeriesOrder.CompareTo(geB.SeriesOrder),
|
||||
_ => InternalCompare(geA, geB),
|
||||
};
|
||||
}
|
||||
|
||||
106
Source/LibationUiBase/GridView/SeriesEntry.cs
Normal file
106
Source/LibationUiBase/GridView/SeriesEntry.cs
Normal file
@ -0,0 +1,106 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
/// <summary>The View Model for a LibraryBook that is ContentType.Parent</summary>
|
||||
public class SeriesEntry : GridEntry
|
||||
{
|
||||
public List<LibraryBookEntry> Children { get; }
|
||||
public override DateTime DateAdded => Children.Max(c => c.DateAdded);
|
||||
|
||||
private bool suspendCounting = false;
|
||||
public void ChildRemoveUpdate()
|
||||
{
|
||||
if (suspendCounting) return;
|
||||
|
||||
var removeCount = Children.Count(c => c.Remove == true);
|
||||
|
||||
remove = removeCount == 0 ? false : removeCount == Children.Count ? true : null;
|
||||
RaisePropertyChanged(nameof(Remove));
|
||||
}
|
||||
|
||||
public override bool? Remove
|
||||
{
|
||||
get => remove;
|
||||
set
|
||||
{
|
||||
remove = value ?? false;
|
||||
|
||||
suspendCounting = true;
|
||||
|
||||
foreach (var item in Children)
|
||||
item.Remove = value;
|
||||
|
||||
suspendCounting = false;
|
||||
RaisePropertyChanged(nameof(Remove));
|
||||
}
|
||||
}
|
||||
|
||||
public SeriesEntry(LibraryBook parent, LibraryBook child) : this(parent, new[] { child }) { }
|
||||
public SeriesEntry(LibraryBook parent, IEnumerable<LibraryBook> children)
|
||||
{
|
||||
LastDownload = new();
|
||||
SeriesIndex = -1;
|
||||
|
||||
Children = children
|
||||
.Select(c => new LibraryBookEntry(c, this))
|
||||
.OrderByDescending(c => c.SeriesOrder)
|
||||
.ToList<LibraryBookEntry>();
|
||||
|
||||
UpdateLibraryBook(parent);
|
||||
LoadCover();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="SeriesEntry{TStatus}"/> for all episodic series in an enumeration of <see cref="LibraryBook"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Can be called from any thread, but requires the calling thread's <see cref="SynchronizationContext.Current"/> to be valid.</remarks>
|
||||
public static async Task<List<SeriesEntry>> GetAllSeriesEntriesAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||
{
|
||||
var seriesEntries = await GetAllProductsAsync(libraryBooks, lb => lb.Book.IsEpisodeParent(), lb => new SeriesEntry(lb, []));
|
||||
var seriesDict = seriesEntries.ToDictionarySafe(s => s.AudibleProductId);
|
||||
await GetAllProductsAsync(libraryBooks, lb => lb.Book.IsEpisodeChild(), CreateAndLinkEpisodeEntry);
|
||||
|
||||
//sort episodes by series order descending and update SeriesEntry
|
||||
foreach (var series in seriesEntries)
|
||||
{
|
||||
series.Children.Sort((a, b) => -a.SeriesOrder.CompareTo(b.SeriesOrder));
|
||||
series.UpdateLibraryBook(series.LibraryBook);
|
||||
}
|
||||
|
||||
return seriesEntries.Where(s => s.Children.Count != 0).ToList();
|
||||
|
||||
//Create a LibraryBookEntry for an episode and link it to its series parent
|
||||
LibraryBookEntry CreateAndLinkEpisodeEntry(LibraryBook episode)
|
||||
{
|
||||
foreach (var s in episode.Book.SeriesLink)
|
||||
{
|
||||
if (seriesDict.TryGetValue(s.Series.AudibleSeriesId, out var seriesParent))
|
||||
{
|
||||
var entry = new LibraryBookEntry(episode, seriesParent);
|
||||
seriesParent.Children.Add(entry);
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveChild(LibraryBookEntry lbe)
|
||||
{
|
||||
Children.Remove(lbe);
|
||||
PurchaseDate = GetPurchaseDateString();
|
||||
Length = GetBookLengthString();
|
||||
}
|
||||
|
||||
protected override string GetBookTags() => null;
|
||||
protected override int GetLengthInMinutes() => Children.Count == 0 ? 0 : Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
|
||||
protected override DateTime GetPurchaseDate() => Children.Count == 0 ? default : Children.Min(c => c.LibraryBook.DateAdded);
|
||||
}
|
||||
}
|
||||
@ -1,129 +0,0 @@
|
||||
using DataLayer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
/// <summary>The View Model for a LibraryBook that is ContentType.Parent</summary>
|
||||
public class SeriesEntry<TStatus> : GridEntry<TStatus>, ISeriesEntry where TStatus : IEntryStatus
|
||||
{
|
||||
public List<ILibraryBookEntry> Children { get; }
|
||||
public override DateTime DateAdded => Children.Max(c => c.DateAdded);
|
||||
|
||||
private bool suspendCounting = false;
|
||||
public void ChildRemoveUpdate()
|
||||
{
|
||||
if (suspendCounting) return;
|
||||
|
||||
var removeCount = Children.Count(c => c.Remove == true);
|
||||
|
||||
remove = removeCount == 0 ? false : removeCount == Children.Count ? true : null;
|
||||
RaisePropertyChanged(nameof(Remove));
|
||||
}
|
||||
|
||||
public override bool? Remove
|
||||
{
|
||||
get => remove;
|
||||
set
|
||||
{
|
||||
remove = value ?? false;
|
||||
|
||||
suspendCounting = true;
|
||||
|
||||
foreach (var item in Children)
|
||||
item.Remove = value;
|
||||
|
||||
suspendCounting = false;
|
||||
RaisePropertyChanged(nameof(Remove));
|
||||
}
|
||||
}
|
||||
|
||||
public SeriesEntry(LibraryBook parent, LibraryBook child) : this(parent, new[] { child }) { }
|
||||
public SeriesEntry(LibraryBook parent, IEnumerable<LibraryBook> children)
|
||||
{
|
||||
LastDownload = new();
|
||||
SeriesIndex = -1;
|
||||
|
||||
Children = children
|
||||
.Select(c => new LibraryBookEntry<TStatus>(c, this))
|
||||
.OrderByDescending(c => c.SeriesOrder)
|
||||
.ToList<ILibraryBookEntry>();
|
||||
|
||||
UpdateLibraryBook(parent);
|
||||
LoadCover();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="SeriesEntry{TStatus}"/> for all episodic series in an enumeration of <see cref="LibraryBook"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Can be called from any thread, but requires the calling thread's <see cref="SynchronizationContext.Current"/> to be valid.</remarks>
|
||||
public static async Task<List<ISeriesEntry>> GetAllSeriesEntriesAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||
{
|
||||
var seriesBooks = libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).ToArray();
|
||||
var allEpisodes = libraryBooks.Where(lb => lb.Book.IsEpisodeChild()).ToArray();
|
||||
|
||||
var seriesEntries = new ISeriesEntry[seriesBooks.Length];
|
||||
var seriesEpisodes = new ILibraryBookEntry[seriesBooks.Length][];
|
||||
|
||||
var syncContext = SynchronizationContext.Current;
|
||||
var options = new ParallelOptions { MaxDegreeOfParallelism = int.Max(1, Environment.ProcessorCount - 1) };
|
||||
|
||||
//Asynchronously create an ILibraryBookEntry for every episode in the library
|
||||
await Parallel.ForEachAsync(getAllEpisodes(), options, createEpisodeEntry);
|
||||
|
||||
//Match all episode entries to their corresponding parents
|
||||
for (int i = seriesEntries.Length - 1; i >= 0; i--)
|
||||
{
|
||||
var series = seriesEntries[i];
|
||||
|
||||
//Sort episodes by series order descending, then add them to their parent's entry
|
||||
Array.Sort(seriesEpisodes[i], (a, b) => -a.SeriesOrder.CompareTo(b.SeriesOrder));
|
||||
series.Children.AddRange(seriesEpisodes[i]);
|
||||
series.UpdateLibraryBook(series.LibraryBook);
|
||||
}
|
||||
|
||||
return seriesEntries.Where(s => s.Children.Count != 0).Cast<ISeriesEntry>().ToList();
|
||||
|
||||
//Create a LibraryBookEntry for a single episode
|
||||
ValueTask createEpisodeEntry((int seriesIndex, int episodeIndex, LibraryBook episode) data, CancellationToken cancellationToken)
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(syncContext);
|
||||
var parent = seriesEntries[data.seriesIndex];
|
||||
seriesEpisodes[data.seriesIndex][data.episodeIndex] = new LibraryBookEntry<TStatus>(data.episode, parent);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
//Enumeration all series episodes, along with the index to its seriesEntries entry
|
||||
//and an index to its seriesEpisodes entry
|
||||
IEnumerable<(int seriesIndex, int episodeIndex, LibraryBook episode)> getAllEpisodes()
|
||||
{
|
||||
for (int i = 0; i < seriesBooks.Length; i++)
|
||||
{
|
||||
var series = seriesBooks[i];
|
||||
var childEpisodes = allEpisodes.FindChildren(series);
|
||||
|
||||
SynchronizationContext.SetSynchronizationContext(syncContext);
|
||||
seriesEntries[i] = new SeriesEntry<TStatus>(series, []);
|
||||
seriesEpisodes[i] = new ILibraryBookEntry[childEpisodes.Count];
|
||||
|
||||
for (int j = 0; j < childEpisodes.Count; j++)
|
||||
yield return (i, j, childEpisodes[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveChild(ILibraryBookEntry lbe)
|
||||
{
|
||||
Children.Remove(lbe);
|
||||
PurchaseDate = GetPurchaseDateString();
|
||||
Length = GetBookLengthString();
|
||||
}
|
||||
|
||||
protected override string GetBookTags() => null;
|
||||
protected override int GetLengthInMinutes() => Children.Count == 0 ? 0 : Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
|
||||
protected override DateTime GetPurchaseDate() => Children.Count == 0 ? default : Children.Min(c => c.LibraryBook.DateAdded);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace LibationUiBase
|
||||
{
|
||||
public interface ILogForm
|
||||
{
|
||||
void WriteLine(string text);
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase
|
||||
{
|
||||
// decouple serilog and form. include convenience factory method
|
||||
public class LogMe
|
||||
{
|
||||
public event EventHandler<string> LogInfo;
|
||||
public event EventHandler<string> LogErrorString;
|
||||
public event EventHandler<(Exception, string)> LogError;
|
||||
|
||||
private LogMe()
|
||||
{
|
||||
LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}");
|
||||
LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
|
||||
LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
|
||||
}
|
||||
private static ILogForm LogForm;
|
||||
public static LogMe RegisterForm<T>(T form) where T : ILogForm
|
||||
{
|
||||
var logMe = new LogMe();
|
||||
|
||||
if (form is null)
|
||||
return logMe;
|
||||
|
||||
LogForm = form;
|
||||
|
||||
logMe.LogInfo += LogMe_LogInfo;
|
||||
logMe.LogErrorString += LogMe_LogErrorString;
|
||||
logMe.LogError += LogMe_LogError;
|
||||
|
||||
return logMe;
|
||||
}
|
||||
|
||||
private static async void LogMe_LogError(object sender, (Exception, string) tuple)
|
||||
{
|
||||
await Task.Run(() => LogForm?.WriteLine(tuple.Item2 ?? "Automated backup: error"));
|
||||
await Task.Run(() => LogForm?.WriteLine("ERROR: " + tuple.Item1.Message));
|
||||
}
|
||||
|
||||
private static async void LogMe_LogErrorString(object sender, string text)
|
||||
{
|
||||
await Task.Run(() => LogForm?.WriteLine(text));
|
||||
}
|
||||
|
||||
private static async void LogMe_LogInfo(object sender, string text)
|
||||
{
|
||||
await Task.Run(() => LogForm?.WriteLine(text));
|
||||
}
|
||||
|
||||
public void Info(string text) => LogInfo?.Invoke(this, text);
|
||||
public void Error(string text) => LogErrorString?.Invoke(this, text);
|
||||
public void Error(Exception ex, string text = null) => LogError?.Invoke(this, (ex, text));
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
using ApplicationServices;
|
||||
using ApplicationServices;
|
||||
using AudibleApi;
|
||||
using AudibleApi.Common;
|
||||
using DataLayer;
|
||||
@ -40,9 +40,8 @@ public enum ProcessBookStatus
|
||||
/// <summary>
|
||||
/// This is the viewmodel for queued processables
|
||||
/// </summary>
|
||||
public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
public class ProcessBookViewModel : ReactiveObject
|
||||
{
|
||||
private readonly LogMe Logger;
|
||||
public LibraryBook LibraryBook { get; protected set; }
|
||||
|
||||
private ProcessBookResult _result = ProcessBookResult.None;
|
||||
@ -84,6 +83,21 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
|
||||
#endregion
|
||||
|
||||
#region Process Queue Logging
|
||||
|
||||
public event EventHandler<string>? LogWritten;
|
||||
private void OnLogWritten(string text) => LogWritten?.Invoke(this, text.Trim());
|
||||
|
||||
private void LogError(string? message, Exception? ex = null)
|
||||
{
|
||||
OnLogWritten(message ?? "Automated backup: error");
|
||||
if (ex is not null)
|
||||
OnLogWritten("ERROR: " + ex.Message);
|
||||
}
|
||||
private void LogInfo(string text) => OnLogWritten(text);
|
||||
|
||||
#endregion
|
||||
|
||||
protected Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
|
||||
protected void NextProcessable() => _currentProcessable = null;
|
||||
private Processable? _currentProcessable;
|
||||
@ -91,10 +105,9 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
/// <summary> A series of Processable actions to perform on this book </summary>
|
||||
protected Queue<Func<Processable>> Processes { get; } = new();
|
||||
|
||||
protected ProcessBookViewModelBase(LibraryBook libraryBook, LogMe logme)
|
||||
public ProcessBookViewModel(LibraryBook libraryBook)
|
||||
{
|
||||
LibraryBook = libraryBook;
|
||||
Logger = logme;
|
||||
|
||||
_title = LibraryBook.Book.TitleWithSubtitle;
|
||||
_author = LibraryBook.Book.AuthorNames();
|
||||
@ -106,15 +119,14 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
||||
|
||||
// Mutable property. Set the field so PropertyChanged isn't fired.
|
||||
_cover = LoadImageFromBytes(picture, PictureSize._80x80);
|
||||
_cover = BaseUtil.LoadImage(picture, PictureSize._80x80);
|
||||
}
|
||||
|
||||
protected abstract object? LoadImageFromBytes(byte[] bytes, PictureSize pictureSize);
|
||||
private void PictureStorage_PictureCached(object? sender, PictureCachedEventArgs e)
|
||||
{
|
||||
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
|
||||
{
|
||||
Cover = LoadImageFromBytes(e.Picture, PictureSize._80x80);
|
||||
Cover = BaseUtil.LoadImage(e.Picture, PictureSize._80x80);
|
||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||
}
|
||||
}
|
||||
@ -133,36 +145,36 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
result = ProcessBookResult.Success;
|
||||
else if (statusHandler.Errors.Contains("Cancelled"))
|
||||
{
|
||||
Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}");
|
||||
LogInfo($"{procName}: Process was cancelled - {LibraryBook.Book}");
|
||||
result = ProcessBookResult.Cancelled;
|
||||
}
|
||||
else if (statusHandler.Errors.Contains("Validation failed"))
|
||||
{
|
||||
Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}");
|
||||
LogInfo($"{procName}: Validation failed - {LibraryBook.Book}");
|
||||
result = ProcessBookResult.ValidationFail;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var errorMessage in statusHandler.Errors)
|
||||
Logger.Error($"{procName}: {errorMessage}");
|
||||
LogError($"{procName}: {errorMessage}");
|
||||
}
|
||||
}
|
||||
catch (ContentLicenseDeniedException ldex)
|
||||
{
|
||||
if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError)
|
||||
{
|
||||
Logger.Info($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}");
|
||||
LogInfo($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}");
|
||||
result = ProcessBookResult.LicenseDeniedPossibleOutage;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Info($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}");
|
||||
LogInfo($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}");
|
||||
result = ProcessBookResult.LicenseDenied;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, procName);
|
||||
LogError(procName, ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -192,15 +204,15 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, $"{CurrentProcessable.Name}: Error while cancelling");
|
||||
LogError($"{CurrentProcessable.Name}: Error while cancelling", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public ProcessBookViewModelBase AddDownloadPdf() => AddProcessable<DownloadPdf>();
|
||||
public ProcessBookViewModelBase AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
|
||||
public ProcessBookViewModelBase AddConvertToMp3() => AddProcessable<ConvertToMp3>();
|
||||
public ProcessBookViewModel AddDownloadPdf() => AddProcessable<DownloadPdf>();
|
||||
public ProcessBookViewModel AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
|
||||
public ProcessBookViewModel AddConvertToMp3() => AddProcessable<ConvertToMp3>();
|
||||
|
||||
private ProcessBookViewModelBase AddProcessable<T>() where T : Processable, new()
|
||||
private ProcessBookViewModel AddProcessable<T>() where T : Processable, new()
|
||||
{
|
||||
Processes.Enqueue(() => new T());
|
||||
return this;
|
||||
@ -252,7 +264,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
private void AudioDecodable_AuthorsDiscovered(object? sender, string authors) => Author = authors;
|
||||
private void AudioDecodable_NarratorsDiscovered(object? sender, string narrators) => Narrator = narrators;
|
||||
private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt)
|
||||
=> Cover = LoadImageFromBytes(coverArt, PictureSize._80x80);
|
||||
=> Cover = BaseUtil.LoadImage(coverArt, PictureSize._80x80);
|
||||
|
||||
private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
|
||||
{
|
||||
@ -292,7 +304,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
Status = ProcessBookStatus.Working;
|
||||
|
||||
if (sender is Processable processable)
|
||||
Logger.Info($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
|
||||
LogInfo($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
|
||||
|
||||
Title = libraryBook.Book.TitleWithSubtitle;
|
||||
Author = libraryBook.Book.AuthorNames();
|
||||
@ -303,7 +315,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
{
|
||||
if (sender is Processable processable)
|
||||
{
|
||||
Logger.Info($"{processable.Name} Step, Completed: {libraryBook.Book}");
|
||||
LogInfo($"{processable.Name} Step, Completed: {libraryBook.Book}");
|
||||
UnlinkProcessable(processable);
|
||||
}
|
||||
|
||||
@ -329,7 +341,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
if (result.HasErrors)
|
||||
{
|
||||
foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
|
||||
Logger.Error(errorMessage);
|
||||
LogError(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@ -340,7 +352,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
protected async Task<ProcessBookResult> GetFailureActionAsync(LibraryBook libraryBook)
|
||||
{
|
||||
const DialogResult SkipResult = DialogResult.Ignore;
|
||||
Logger.Error($"ERROR. All books have not been processed. Book failed: {libraryBook.Book}");
|
||||
LogError($"ERROR. All books have not been processed. Book failed: {libraryBook.Book}");
|
||||
|
||||
DialogResult? dialogResult = Configuration.Instance.BadBook switch
|
||||
{
|
||||
@ -353,7 +365,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
if (dialogResult == SkipResult)
|
||||
{
|
||||
libraryBook.UpdateBookStatus(LiberatedStatus.Error);
|
||||
Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}");
|
||||
LogInfo($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}");
|
||||
}
|
||||
|
||||
return dialogResult is SkipResult ? ProcessBookResult.FailedSkip
|
||||
@ -1,30 +1,32 @@
|
||||
using DataLayer;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using LibationUiBase.Forms;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using ApplicationServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationUiBase.ProcessQueue;
|
||||
|
||||
public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
public record LogEntry(DateTime LogDate, string LogMessage)
|
||||
{
|
||||
public abstract void WriteLine(string text);
|
||||
protected abstract ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook);
|
||||
public string LogDateString => LogDate.ToShortTimeString();
|
||||
}
|
||||
|
||||
public TrackedQueue<ProcessBookViewModelBase> Queue { get; }
|
||||
public class ProcessQueueViewModel : ReactiveObject
|
||||
{
|
||||
public ObservableCollection<LogEntry> LogEntries { get; } = new();
|
||||
public TrackedQueue<ProcessBookViewModel> Queue { get; } = new();
|
||||
public Task? QueueRunner { get; private set; }
|
||||
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
||||
protected LogMe Logger { get; }
|
||||
|
||||
public ProcessQueueViewModelBase(ICollection<ProcessBookViewModelBase>? underlyingList)
|
||||
public ProcessQueueViewModel()
|
||||
{
|
||||
Logger = LogMe.RegisterForm(this);
|
||||
Queue = new(underlyingList);
|
||||
Queue.QueuedCountChanged += Queue_QueuedCountChanged;
|
||||
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
|
||||
SpeedLimit = LibationFileManager.Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
||||
}
|
||||
|
||||
private int _completedCount;
|
||||
@ -32,6 +34,7 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
private int _queuedCount;
|
||||
private string? _runningTime;
|
||||
private bool _progressBarVisible;
|
||||
private decimal _speedLimit;
|
||||
|
||||
public int CompletedCount { get => _completedCount; private set { RaiseAndSetIfChanged(ref _completedCount, value); RaisePropertyChanged(nameof(AnyCompleted)); } }
|
||||
public int QueuedCount { get => _queuedCount; private set { this.RaiseAndSetIfChanged(ref _queuedCount, value); RaisePropertyChanged(nameof(AnyQueued)); } }
|
||||
@ -42,6 +45,32 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
public bool AnyQueued => QueuedCount > 0;
|
||||
public bool AnyErrors => ErrorCount > 0;
|
||||
public double Progress => 100d * Queue.Completed.Count / Queue.Count;
|
||||
public decimal SpeedLimitIncrement { get; private set; }
|
||||
public decimal SpeedLimit
|
||||
{
|
||||
get => _speedLimit;
|
||||
set
|
||||
{
|
||||
var newValue = Math.Min(999 * 1024 * 1024, (long)Math.Ceiling(value * 1024 * 1024));
|
||||
var config = LibationFileManager.Configuration.Instance;
|
||||
config.DownloadSpeedLimit = newValue;
|
||||
|
||||
_speedLimit
|
||||
= config.DownloadSpeedLimit <= newValue ? value
|
||||
: value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
|
||||
: 0;
|
||||
|
||||
config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
|
||||
|
||||
SpeedLimitIncrement = _speedLimit > 100 ? 10
|
||||
: _speedLimit > 10 ? 1
|
||||
: _speedLimit > 1 ? 0.1m
|
||||
: 0.01m;
|
||||
|
||||
RaisePropertyChanged(nameof(SpeedLimitIncrement));
|
||||
RaisePropertyChanged(nameof(SpeedLimit));
|
||||
}
|
||||
}
|
||||
|
||||
private void Queue_CompletedCountChanged(object? sender, int e)
|
||||
{
|
||||
@ -59,6 +88,9 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
RaisePropertyChanged(nameof(Progress));
|
||||
}
|
||||
|
||||
private void ProcessBook_LogWritten(object? sender, string logMessage)
|
||||
=> Invoke(() => LogEntries.Add(new(DateTime.Now, logMessage.Trim())));
|
||||
|
||||
#region Add Books to Queue
|
||||
|
||||
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks)
|
||||
@ -124,47 +156,50 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
}
|
||||
|
||||
private bool IsBookInQueue(LibraryBook libraryBook)
|
||||
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is not ProcessBookViewModelBase entry ? false
|
||||
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is not ProcessBookViewModel entry ? false
|
||||
: entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed ? !Queue.RemoveCompleted(entry)
|
||||
: true;
|
||||
|
||||
private bool RemoveCompleted(LibraryBook libraryBook)
|
||||
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModelBase entry
|
||||
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModel entry
|
||||
&& entry.Status is ProcessBookStatus.Completed
|
||||
&& Queue.RemoveCompleted(entry);
|
||||
|
||||
private void AddDownloadPdf(IEnumerable<LibraryBook> entries)
|
||||
private void AddDownloadPdf(IList<LibraryBook> entries)
|
||||
{
|
||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||
Serilog.Log.Logger.Information("Queueing {count} books for PDF-only download", procs.Length);
|
||||
AddToQueue(procs);
|
||||
|
||||
ProcessBookViewModelBase Create(LibraryBook entry)
|
||||
=> CreateNewProcessBook(entry).AddDownloadPdf();
|
||||
ProcessBookViewModel Create(LibraryBook entry)
|
||||
=> new ProcessBookViewModel(entry).AddDownloadPdf();
|
||||
}
|
||||
|
||||
private void AddDownloadDecrypt(IEnumerable<LibraryBook> entries)
|
||||
private void AddDownloadDecrypt(IList<LibraryBook> entries)
|
||||
{
|
||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||
Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length);
|
||||
AddToQueue(procs);
|
||||
|
||||
ProcessBookViewModelBase Create(LibraryBook entry)
|
||||
=> CreateNewProcessBook(entry).AddDownloadDecryptBook().AddDownloadPdf();
|
||||
ProcessBookViewModel Create(LibraryBook entry)
|
||||
=> new ProcessBookViewModel(entry).AddDownloadDecryptBook().AddDownloadPdf();
|
||||
}
|
||||
|
||||
private void AddConvertMp3(IEnumerable<LibraryBook> entries)
|
||||
private void AddConvertMp3(IList<LibraryBook> entries)
|
||||
{
|
||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||
Serilog.Log.Logger.Information("Queueing {count} books for mp3 conversion", procs.Length);
|
||||
AddToQueue(procs);
|
||||
|
||||
ProcessBookViewModelBase Create(LibraryBook entry)
|
||||
=> CreateNewProcessBook(entry).AddConvertToMp3();
|
||||
ProcessBookViewModel Create(LibraryBook entry)
|
||||
=> new ProcessBookViewModel(entry).AddConvertToMp3();
|
||||
}
|
||||
|
||||
private void AddToQueue(IEnumerable<ProcessBookViewModelBase> pbook)
|
||||
private void AddToQueue(IList<ProcessBookViewModel> pbook)
|
||||
{
|
||||
foreach (var book in pbook)
|
||||
book.LogWritten += ProcessBook_LogWritten;
|
||||
|
||||
Queue.Enqueue(pbook);
|
||||
if (!Running)
|
||||
QueueRunner = Task.Run(QueueLoop);
|
||||
@ -187,7 +222,7 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
|
||||
while (Queue.MoveNext())
|
||||
{
|
||||
if (Queue.Current is not ProcessBookViewModelBase nextBook)
|
||||
if (Queue.Current is not ProcessBookViewModel nextBook)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Current queue item is empty.");
|
||||
continue;
|
||||
@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
@ -7,7 +9,7 @@ namespace LibationUiBase
|
||||
{
|
||||
public enum QueuePosition
|
||||
{
|
||||
Fisrt,
|
||||
First,
|
||||
OneUp,
|
||||
OneDown,
|
||||
Last,
|
||||
@ -23,37 +25,20 @@ namespace LibationUiBase
|
||||
*
|
||||
* The index is the link position from the first link you lifted to the
|
||||
* last one in the chain.
|
||||
*
|
||||
*
|
||||
* For this to work with Avalonia's ItemsRepeater, it must be an ObservableCollection
|
||||
* (not merely a Collection with INotifyCollectionChanged, INotifyPropertyChanged).
|
||||
* So TrackedQueue maintains 2 copies of the list. The primary copy of the list is
|
||||
* split into Completed, Current and Queued and is used by ProcessQueue to keep track
|
||||
* of what's what. The secondary copy is a concatenation of primary's three sources
|
||||
* and is stored in ObservableCollection.Items. When the primary list changes, the
|
||||
* secondary list is cleared and reset to match the primary.
|
||||
*/
|
||||
public class TrackedQueue<T> where T : class
|
||||
public class TrackedQueue<T> : IReadOnlyCollection<T>, IList, INotifyCollectionChanged where T : class
|
||||
{
|
||||
public event EventHandler<int>? CompletedCountChanged;
|
||||
public event EventHandler<int>? QueuedCountChanged;
|
||||
public event NotifyCollectionChangedEventHandler? CollectionChanged;
|
||||
|
||||
public T? Current { get; private set; }
|
||||
|
||||
public IReadOnlyList<T> Queued => _queued;
|
||||
public IReadOnlyList<T> Completed => _completed;
|
||||
private List<T> Queued { get; } = new();
|
||||
|
||||
private readonly List<T> _queued = new();
|
||||
private readonly List<T> _completed = new();
|
||||
private readonly object lockObject = new();
|
||||
|
||||
private readonly ICollection<T>? _underlyingList;
|
||||
public ICollection<T>? UnderlyingList => _underlyingList;
|
||||
|
||||
public TrackedQueue(ICollection<T>? underlyingList = null)
|
||||
{
|
||||
_underlyingList = underlyingList;
|
||||
}
|
||||
private int QueueStartIndex => Completed.Count + (Current is null ? 0 : 1);
|
||||
|
||||
public T this[int index]
|
||||
{
|
||||
@ -61,17 +46,10 @@ namespace LibationUiBase
|
||||
{
|
||||
lock (lockObject)
|
||||
{
|
||||
if (index < _completed.Count)
|
||||
return _completed[index];
|
||||
index -= _completed.Count;
|
||||
|
||||
if (index == 0 && Current != null) return Current;
|
||||
|
||||
if (Current != null) index--;
|
||||
|
||||
if (index < _queued.Count) return _queued.ElementAt(index);
|
||||
|
||||
throw new IndexOutOfRangeException();
|
||||
return index < Completed.Count ? Completed[index]
|
||||
: index == Completed.Count && Current is not null ? Current
|
||||
: index < Count ? Queued[index - QueueStartIndex]
|
||||
: throw new IndexOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,7 +60,7 @@ namespace LibationUiBase
|
||||
{
|
||||
lock (lockObject)
|
||||
{
|
||||
return _queued.Count + _completed.Count + (Current == null ? 0 : 1);
|
||||
return QueueStartIndex + Queued.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -91,131 +69,117 @@ namespace LibationUiBase
|
||||
{
|
||||
lock (lockObject)
|
||||
{
|
||||
if (_completed.Contains(item))
|
||||
return _completed.IndexOf(item);
|
||||
|
||||
if (Current == item) return _completed.Count;
|
||||
|
||||
if (_queued.Contains(item))
|
||||
return _queued.IndexOf(item) + (Current is null ? 0 : 1);
|
||||
return -1;
|
||||
int index = _completed.IndexOf(item);
|
||||
if (index < 0 && item == Current)
|
||||
index = Completed.Count;
|
||||
if (index < 0)
|
||||
{
|
||||
index = Queued.IndexOf(item);
|
||||
if (index >= 0)
|
||||
index += QueueStartIndex;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
public bool RemoveQueued(T item)
|
||||
{
|
||||
bool itemsRemoved;
|
||||
int queuedCount;
|
||||
int queuedCount, queueIndex;
|
||||
|
||||
lock (lockObject)
|
||||
{
|
||||
itemsRemoved = _queued.Remove(item);
|
||||
queuedCount = _queued.Count;
|
||||
queueIndex = Queued.IndexOf(item);
|
||||
if (queueIndex >= 0)
|
||||
Queued.RemoveAt(queueIndex);
|
||||
queuedCount = Queued.Count;
|
||||
}
|
||||
|
||||
if (itemsRemoved)
|
||||
if (queueIndex >= 0)
|
||||
{
|
||||
QueuedCountChanged?.Invoke(this, queuedCount);
|
||||
RebuildSecondary();
|
||||
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, QueueStartIndex + queueIndex));
|
||||
return true;
|
||||
}
|
||||
return itemsRemoved;
|
||||
}
|
||||
|
||||
public void ClearCurrent()
|
||||
{
|
||||
lock (lockObject)
|
||||
Current = null;
|
||||
RebuildSecondary();
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool RemoveCompleted(T item)
|
||||
{
|
||||
bool itemsRemoved;
|
||||
int completedCount;
|
||||
int completedCount, completedIndex;
|
||||
|
||||
lock (lockObject)
|
||||
{
|
||||
itemsRemoved = _completed.Remove(item);
|
||||
completedIndex = _completed.IndexOf(item);
|
||||
if (completedIndex >= 0)
|
||||
_completed.RemoveAt(completedIndex);
|
||||
completedCount = _completed.Count;
|
||||
}
|
||||
|
||||
if (itemsRemoved)
|
||||
if (completedIndex >= 0)
|
||||
{
|
||||
CompletedCountChanged?.Invoke(this, completedCount);
|
||||
RebuildSecondary();
|
||||
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, completedIndex));
|
||||
return true;
|
||||
}
|
||||
return itemsRemoved;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ClearCurrent()
|
||||
{
|
||||
T? current;
|
||||
lock (lockObject)
|
||||
{
|
||||
current = Current;
|
||||
Current = null;
|
||||
}
|
||||
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, current, _completed.Count));
|
||||
}
|
||||
|
||||
public void ClearQueue()
|
||||
{
|
||||
List<T> queuedItems;
|
||||
lock (lockObject)
|
||||
_queued.Clear();
|
||||
{
|
||||
queuedItems = Queued.ToList();
|
||||
Queued.Clear();
|
||||
}
|
||||
QueuedCountChanged?.Invoke(this, 0);
|
||||
RebuildSecondary();
|
||||
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, queuedItems, QueueStartIndex));
|
||||
}
|
||||
|
||||
public void ClearCompleted()
|
||||
{
|
||||
List<T> completedItems;
|
||||
lock (lockObject)
|
||||
{
|
||||
completedItems = _completed.ToList();
|
||||
_completed.Clear();
|
||||
}
|
||||
CompletedCountChanged?.Invoke(this, 0);
|
||||
RebuildSecondary();
|
||||
}
|
||||
|
||||
public bool Any(Func<T, bool> predicate)
|
||||
{
|
||||
lock (lockObject)
|
||||
{
|
||||
return (Current != null && predicate(Current)) || _completed.Any(predicate) || _queued.Any(predicate);
|
||||
}
|
||||
}
|
||||
|
||||
public T? FirstOrDefault(Func<T, bool> predicate)
|
||||
{
|
||||
lock (lockObject)
|
||||
{
|
||||
return Current != null && predicate(Current) ? Current
|
||||
: _completed.FirstOrDefault(predicate) is T completed ? completed
|
||||
: _queued.FirstOrDefault(predicate);
|
||||
}
|
||||
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, completedItems, 0));
|
||||
}
|
||||
|
||||
public void MoveQueuePosition(T item, QueuePosition requestedPosition)
|
||||
{
|
||||
int oldIndex, newIndex;
|
||||
lock (lockObject)
|
||||
{
|
||||
if (_queued.Count == 0 || !_queued.Contains(item)) return;
|
||||
oldIndex = Queued.IndexOf(item);
|
||||
newIndex = requestedPosition switch
|
||||
{
|
||||
QueuePosition.First => 0,
|
||||
QueuePosition.OneUp => oldIndex - 1,
|
||||
QueuePosition.OneDown => oldIndex + 1,
|
||||
QueuePosition.Last or _ => Queued.Count - 1
|
||||
};
|
||||
|
||||
if ((requestedPosition == QueuePosition.Fisrt || requestedPosition == QueuePosition.OneUp) && _queued[0] == item)
|
||||
return;
|
||||
if ((requestedPosition == QueuePosition.Last || requestedPosition == QueuePosition.OneDown) && _queued[^1] == item)
|
||||
if (oldIndex < 0 || newIndex < 0 || newIndex >= Queued.Count || newIndex == oldIndex)
|
||||
return;
|
||||
|
||||
int queueIndex = _queued.IndexOf(item);
|
||||
|
||||
if (requestedPosition == QueuePosition.OneUp)
|
||||
{
|
||||
_queued.RemoveAt(queueIndex);
|
||||
_queued.Insert(queueIndex - 1, item);
|
||||
Queued.RemoveAt(oldIndex);
|
||||
Queued.Insert(newIndex, item);
|
||||
}
|
||||
else if (requestedPosition == QueuePosition.OneDown)
|
||||
{
|
||||
_queued.RemoveAt(queueIndex);
|
||||
_queued.Insert(queueIndex + 1, item);
|
||||
}
|
||||
else if (requestedPosition == QueuePosition.Fisrt)
|
||||
{
|
||||
_queued.RemoveAt(queueIndex);
|
||||
_queued.Insert(0, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
_queued.RemoveAt(queueIndex);
|
||||
_queued.Insert(_queued.Count, item);
|
||||
}
|
||||
}
|
||||
RebuildSecondary();
|
||||
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, QueueStartIndex + newIndex, QueueStartIndex + oldIndex));
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
@ -232,15 +196,15 @@ namespace LibationUiBase
|
||||
completedCount = _completed.Count;
|
||||
completedChanged = true;
|
||||
}
|
||||
if (_queued.Count == 0)
|
||||
if (Queued.Count == 0)
|
||||
{
|
||||
Current = null;
|
||||
return false;
|
||||
}
|
||||
Current = _queued[0];
|
||||
_queued.RemoveAt(0);
|
||||
Current = Queued[0];
|
||||
Queued.RemoveAt(0);
|
||||
|
||||
queuedCount = _queued.Count;
|
||||
queuedCount = Queued.Count;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -249,34 +213,48 @@ namespace LibationUiBase
|
||||
if (completedChanged)
|
||||
CompletedCountChanged?.Invoke(this, completedCount);
|
||||
QueuedCountChanged?.Invoke(this, queuedCount);
|
||||
RebuildSecondary();
|
||||
}
|
||||
}
|
||||
|
||||
public void Enqueue(IEnumerable<T> item)
|
||||
public void Enqueue(IList<T> item)
|
||||
{
|
||||
int queueCount;
|
||||
lock (lockObject)
|
||||
{
|
||||
_queued.AddRange(item);
|
||||
queueCount = _queued.Count;
|
||||
Queued.AddRange(item);
|
||||
queueCount = Queued.Count;
|
||||
}
|
||||
foreach (var i in item)
|
||||
_underlyingList?.Add(i);
|
||||
QueuedCountChanged?.Invoke(this, queueCount);
|
||||
}
|
||||
|
||||
private void RebuildSecondary()
|
||||
{
|
||||
_underlyingList?.Clear();
|
||||
foreach (var item in GetAllItems())
|
||||
_underlyingList?.Add(item);
|
||||
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, QueueStartIndex + Queued.Count));
|
||||
}
|
||||
|
||||
public IEnumerable<T> GetAllItems()
|
||||
{
|
||||
lock (lockObject)
|
||||
{
|
||||
if (Current is null) return Completed.Concat(Queued);
|
||||
return Completed.Concat(new List<T> { Current }).Concat(Queued);
|
||||
return Completed.Concat([Current]).Concat(Queued);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator() => GetAllItems().GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
#region IList interface implementation
|
||||
object? IList.this[int index] { get => this[index]; set => throw new NotSupportedException(); }
|
||||
public bool IsReadOnly => true;
|
||||
public bool IsFixedSize => false;
|
||||
public bool IsSynchronized => false;
|
||||
public object SyncRoot => this;
|
||||
public int IndexOf(object? value) => value is T t ? IndexOf(t) : -1;
|
||||
public bool Contains(object? value) => IndexOf(value) >= 0;
|
||||
//These aren't used by anything, but they are IList interface members and this class needs to be an IList for Avalonia
|
||||
public int Add(object? value) => throw new NotSupportedException();
|
||||
public void Clear() => throw new NotSupportedException();
|
||||
public void Insert(int index, object? value) => throw new NotSupportedException();
|
||||
public void Remove(object? value) => throw new NotSupportedException();
|
||||
public void RemoveAt(int index) => throw new NotSupportedException();
|
||||
public void CopyTo(Array array, int index) => throw new NotSupportedException();
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,18 +5,19 @@ namespace LibationWinForms.Dialogs.Login
|
||||
{
|
||||
public abstract class WinformLoginBase
|
||||
{
|
||||
private readonly IWin32Window _owner;
|
||||
protected WinformLoginBase(IWin32Window owner)
|
||||
protected Control Owner { get; }
|
||||
protected WinformLoginBase(Control owner)
|
||||
{
|
||||
_owner = owner;
|
||||
Owner = owner;
|
||||
}
|
||||
|
||||
/// <returns>True if ShowDialog's DialogResult == OK</returns>
|
||||
protected bool ShowDialog(Form dialog)
|
||||
=> Owner.Invoke(() =>
|
||||
{
|
||||
var result = dialog.ShowDialog(_owner);
|
||||
var result = dialog.ShowDialog(Owner);
|
||||
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
|
||||
return result == DialogResult.OK;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,48 +13,53 @@ namespace LibationWinForms.Login
|
||||
|
||||
public string DeviceName { get; } = "Libation";
|
||||
|
||||
public WinformLoginCallback(Account account, IWin32Window owner) : base(owner)
|
||||
public WinformLoginCallback(Account account, Control owner) : base(owner)
|
||||
{
|
||||
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
}
|
||||
|
||||
public Task<string> Get2faCodeAsync(string prompt)
|
||||
=> Owner.Invoke(() =>
|
||||
{
|
||||
using var dialog = new _2faCodeDialog(prompt);
|
||||
if (ShowDialog(dialog))
|
||||
return Task.FromResult(dialog.Code);
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
});
|
||||
|
||||
public Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
|
||||
=> Owner.Invoke(() =>
|
||||
{
|
||||
using var dialog = new CaptchaDialog(password, captchaImage);
|
||||
if (ShowDialog(dialog))
|
||||
return Task.FromResult((dialog.Password, dialog.Answer));
|
||||
return Task.FromResult<(string, string)>((null, null));
|
||||
}
|
||||
});
|
||||
|
||||
public Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
|
||||
=> Owner.Invoke(() =>
|
||||
{
|
||||
using var dialog = new MfaDialog(mfaConfig);
|
||||
if (ShowDialog(dialog))
|
||||
return Task.FromResult((dialog.SelectedName, dialog.SelectedValue));
|
||||
return Task.FromResult<(string, string)>((null, null));
|
||||
}
|
||||
});
|
||||
|
||||
public Task<(string email, string password)> GetLoginAsync()
|
||||
=> Owner.Invoke(() =>
|
||||
{
|
||||
using var dialog = new LoginCallbackDialog(_account);
|
||||
if (ShowDialog(dialog))
|
||||
return Task.FromResult((dialog.Email, dialog.Password));
|
||||
return Task.FromResult<(string, string)>((null, null));
|
||||
}
|
||||
});
|
||||
|
||||
public Task ShowApprovalNeededAsync()
|
||||
=> Owner.Invoke(() =>
|
||||
{
|
||||
using var dialog = new ApprovalNeededDialog();
|
||||
ShowDialog(dialog);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -13,13 +13,16 @@ namespace LibationWinForms.Login
|
||||
|
||||
private Account _account { get; }
|
||||
|
||||
public WinformLoginChoiceEager(Account account, IWin32Window owner) : base(owner)
|
||||
public WinformLoginChoiceEager(Account account, Control owner) : base(owner)
|
||||
{
|
||||
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
LoginCallback = new WinformLoginCallback(_account, owner);
|
||||
}
|
||||
|
||||
public Task<ChoiceOut> StartAsync(ChoiceIn choiceIn)
|
||||
=> Owner.Invoke(() => StartAsyncInternal(choiceIn));
|
||||
|
||||
private Task<ChoiceOut> StartAsyncInternal(ChoiceIn choiceIn)
|
||||
{
|
||||
if (Environment.OSVersion.Version.Major >= 10)
|
||||
{
|
||||
|
||||
@ -11,7 +11,13 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class SettingsDialog
|
||||
{
|
||||
private void logsBtn_Click(object sender, EventArgs e) => Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
|
||||
private void logsBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (File.Exists(LogFileFilter.LogFilePath))
|
||||
Go.To.File(LogFileFilter.LogFilePath);
|
||||
else
|
||||
Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
|
||||
}
|
||||
|
||||
private void Load_Important(Configuration config)
|
||||
{
|
||||
|
||||
2
Source/LibationWinForms/Form1.Designer.cs
generated
2
Source/LibationWinForms/Form1.Designer.cs
generated
@ -538,7 +538,7 @@
|
||||
this.productsDisplay.VisibleCountChanged += new System.EventHandler<int>(this.productsDisplay_VisibleCountChanged);
|
||||
this.productsDisplay.RemovableCountChanged += new System.EventHandler<int>(this.productsDisplay_RemovableCountChanged);
|
||||
this.productsDisplay.LiberateClicked += ProductsDisplay_LiberateClicked;
|
||||
this.productsDisplay.LiberateSeriesClicked += new System.EventHandler<LibationUiBase.GridView.ISeriesEntry>(this.ProductsDisplay_LiberateSeriesClicked);
|
||||
this.productsDisplay.LiberateSeriesClicked += new System.EventHandler<LibationUiBase.GridView.SeriesEntry>(this.ProductsDisplay_LiberateSeriesClicked);
|
||||
this.productsDisplay.ConvertToMp3Clicked += ProductsDisplay_ConvertToMp3Clicked;
|
||||
this.productsDisplay.InitialLoaded += new System.EventHandler(this.productsDisplay_InitialLoaded);
|
||||
//
|
||||
|
||||
@ -48,7 +48,7 @@ namespace LibationWinForms
|
||||
}
|
||||
}
|
||||
|
||||
private void ProductsDisplay_LiberateSeriesClicked(object sender, ISeriesEntry series)
|
||||
private void ProductsDisplay_LiberateSeriesClicked(object sender, SeriesEntry series)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@ -32,11 +32,7 @@ namespace LibationWinForms
|
||||
// in autoScan, new books SHALL NOT show dialog
|
||||
try
|
||||
{
|
||||
Task importAsync() => LibraryCommands.ImportAccountAsync(accounts);
|
||||
if (InvokeRequired)
|
||||
await Invoke(importAsync);
|
||||
else
|
||||
await importAsync();
|
||||
await Task.Run(() => LibraryCommands.ImportAccountAsync(accounts));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
@ -74,7 +74,7 @@ namespace LibationWinForms
|
||||
{
|
||||
try
|
||||
{
|
||||
var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(accounts);
|
||||
var (totalProcessed, newAdded) = await Task.Run(() => LibraryCommands.ImportAccountAsync(accounts));
|
||||
|
||||
// this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop
|
||||
if (Configuration.Instance.ShowImportedStats && newAdded > 0)
|
||||
|
||||
@ -22,6 +22,7 @@ namespace LibationWinForms
|
||||
PictureStorage.SetDefaultImage(PictureSize.Native, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||
|
||||
BaseUtil.SetLoadImageDelegate(WinFormsUtil.TryLoadImageOrDefault);
|
||||
BaseUtil.SetLoadResourceImageDelegate(Properties.Resources.ResourceManager.GetObject);
|
||||
|
||||
// wire-up event to automatically download after scan.
|
||||
// winforms only. this should NOT be allowed in cli
|
||||
|
||||
@ -28,7 +28,7 @@ namespace LibationWinForms.GridView
|
||||
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
|
||||
{
|
||||
// series
|
||||
if (rowIndex >= 0 && DataGridView.GetBoundItem<IGridEntry>(rowIndex) is ISeriesEntry)
|
||||
if (rowIndex >= 0 && DataGridView.GetBoundItem<GridEntry>(rowIndex) is SeriesEntry)
|
||||
{
|
||||
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, DataGridViewPaintParts.Background | DataGridViewPaintParts.Border);
|
||||
}
|
||||
|
||||
@ -24,24 +24,24 @@ namespace LibationWinForms.GridView
|
||||
* event. Adding or removing from the underlying list will not change the
|
||||
* BindingList's subscription to that item.
|
||||
*/
|
||||
internal class GridEntryBindingList : BindingList<IGridEntry>, IBindingListView
|
||||
internal class GridEntryBindingList : BindingList<GridEntry>, IBindingListView
|
||||
{
|
||||
public GridEntryBindingList(IEnumerable<IGridEntry> enumeration) : base(new List<IGridEntry>(enumeration))
|
||||
public GridEntryBindingList(IEnumerable<GridEntry> enumeration) : base(new List<GridEntry>(enumeration))
|
||||
{
|
||||
SearchEngineCommands.SearchEngineUpdated += SearchEngineCommands_SearchEngineUpdated;
|
||||
ListChanged += GridEntryBindingList_ListChanged;
|
||||
}
|
||||
|
||||
/// <returns>All items in the list, including those filtered out.</returns>
|
||||
public List<IGridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
|
||||
public List<GridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
|
||||
|
||||
/// <summary>All items that pass the current filter</summary>
|
||||
public IEnumerable<ILibraryBookEntry> GetFilteredInItems()
|
||||
public IEnumerable<LibraryBookEntry> GetFilteredInItems()
|
||||
=> FilteredInGridEntries?
|
||||
.OfType<ILibraryBookEntry>()
|
||||
.OfType<LibraryBookEntry>()
|
||||
?? FilterRemoved
|
||||
.OfType<ILibraryBookEntry>()
|
||||
.Union(Items.OfType<ILibraryBookEntry>());
|
||||
.OfType<LibraryBookEntry>()
|
||||
.Union(Items.OfType<LibraryBookEntry>());
|
||||
|
||||
public bool SupportsFiltering => true;
|
||||
public string Filter
|
||||
@ -67,12 +67,12 @@ namespace LibationWinForms.GridView
|
||||
protected override ListSortDirection SortDirectionCore => Comparer.SortOrder;
|
||||
|
||||
/// <summary> Items that were removed from the base list due to filtering </summary>
|
||||
private readonly List<IGridEntry> FilterRemoved = new();
|
||||
private readonly List<GridEntry> FilterRemoved = new();
|
||||
private string FilterString;
|
||||
private bool isSorted;
|
||||
private PropertyDescriptor propertyDescriptor;
|
||||
/// <summary> All GridEntries present in the current filter set. If null, no filter is applied and all entries are filtered in.(This was a performance choice)</summary>
|
||||
private HashSet<IGridEntry> FilteredInGridEntries;
|
||||
private HashSet<GridEntry> FilteredInGridEntries;
|
||||
|
||||
#region Unused - Advanced Filtering
|
||||
public bool SupportsAdvancedSorting => false;
|
||||
@ -84,7 +84,7 @@ namespace LibationWinForms.GridView
|
||||
public ListSortDescriptionCollection SortDescriptions => throw new NotImplementedException();
|
||||
#endregion
|
||||
|
||||
public new void Remove(IGridEntry entry)
|
||||
public new void Remove(GridEntry entry)
|
||||
{
|
||||
FilterRemoved.Remove(entry);
|
||||
base.Remove(entry);
|
||||
@ -122,13 +122,13 @@ namespace LibationWinForms.GridView
|
||||
ResetList();
|
||||
RaiseListChangedEvents = priorState;
|
||||
|
||||
void addRemovedItemsBack(List<IGridEntry> addBackEntries)
|
||||
void addRemovedItemsBack(List<GridEntry> addBackEntries)
|
||||
{
|
||||
//Add removed entries back into Items so they are displayed
|
||||
//(except for episodes that are collapsed)
|
||||
foreach (var addBack in addBackEntries)
|
||||
{
|
||||
if (addBack is ILibraryBookEntry lbe && lbe.Parent is ISeriesEntry se && !se.Liberate.Expanded)
|
||||
if (addBack is LibraryBookEntry lbe && lbe.Parent is SeriesEntry se && !se.Liberate.Expanded)
|
||||
continue;
|
||||
|
||||
FilterRemoved.Remove(addBack);
|
||||
@ -160,7 +160,7 @@ namespace LibationWinForms.GridView
|
||||
ExpandItem(series);
|
||||
}
|
||||
|
||||
public void CollapseItem(ISeriesEntry sEntry)
|
||||
public void CollapseItem(SeriesEntry sEntry)
|
||||
{
|
||||
foreach (var episode in sEntry.Children.Intersect(Items.BookEntries()).ToList())
|
||||
{
|
||||
@ -171,7 +171,7 @@ namespace LibationWinForms.GridView
|
||||
sEntry.Liberate.Expanded = false;
|
||||
}
|
||||
|
||||
public void ExpandItem(ISeriesEntry sEntry)
|
||||
public void ExpandItem(SeriesEntry sEntry)
|
||||
{
|
||||
var sindex = Items.IndexOf(sEntry);
|
||||
|
||||
@ -208,7 +208,7 @@ namespace LibationWinForms.GridView
|
||||
|
||||
private void SortInternal()
|
||||
{
|
||||
var itemsList = (List<IGridEntry>)Items;
|
||||
var itemsList = (List<GridEntry>)Items;
|
||||
//User Order/OrderDescending and replace items in list instead of using List.Sort() to achieve stable sorting.
|
||||
var sortedItems = Comparer.OrderEntries(itemsList).ToList();
|
||||
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
using DataLayer;
|
||||
using LibationUiBase.GridView;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public class LiberateDataGridViewImageButtonColumn : DataGridViewButtonColumn, IDataGridScaleColumn
|
||||
@ -20,16 +22,18 @@ namespace LibationWinForms.GridView
|
||||
|
||||
private static readonly Brush DISABLED_GRAY = new SolidBrush(Color.FromArgb(0x60, Color.LightGray));
|
||||
private static readonly Color HiddenForeColor = Color.LightGray;
|
||||
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
|
||||
private static readonly Color SERIES_BG_COLOR = Color.FromArgb(230, 255, 230);
|
||||
|
||||
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object? value, object? formattedValue, string? errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
|
||||
{
|
||||
if (value is WinFormsEntryStatus status)
|
||||
if (OwningRow is DataGridViewRow row && row.DataGridView is DataGridView grid && value is EntryStatus status)
|
||||
{
|
||||
if (status.BookStatus is LiberatedStatus.Error || status.IsUnavailable)
|
||||
//Don't paint the button graphic
|
||||
paintParts ^= DataGridViewPaintParts.ContentBackground | DataGridViewPaintParts.ContentForeground | DataGridViewPaintParts.SelectionBackground;
|
||||
|
||||
DataGridView.Rows[rowIndex].DefaultCellStyle.BackColor = (Color)status.BackgroundBrush;
|
||||
DataGridView.Rows[rowIndex].DefaultCellStyle.ForeColor = status.Opacity == 1 ? DataGridView.DefaultCellStyle.ForeColor : HiddenForeColor;
|
||||
row.DefaultCellStyle.BackColor = status.IsEpisode ? SERIES_BG_COLOR : grid.DefaultCellStyle.BackColor;
|
||||
row.DefaultCellStyle.ForeColor = status.Opacity == 1 ? grid.DefaultCellStyle.ForeColor : HiddenForeColor;
|
||||
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
|
||||
|
||||
DrawButtonImage(graphics, (Image)status.ButtonImage, cellBounds);
|
||||
|
||||
@ -22,7 +22,7 @@ namespace LibationWinForms.GridView
|
||||
public event EventHandler<int> VisibleCountChanged;
|
||||
public event EventHandler<int> RemovableCountChanged;
|
||||
public event EventHandler<LibraryBook[]> LiberateClicked;
|
||||
public event EventHandler<ISeriesEntry> LiberateSeriesClicked;
|
||||
public event EventHandler<SeriesEntry> LiberateSeriesClicked;
|
||||
public event EventHandler<LibraryBook[]> ConvertToMp3Clicked;
|
||||
public event EventHandler InitialLoaded;
|
||||
|
||||
@ -36,7 +36,7 @@ namespace LibationWinForms.GridView
|
||||
#region Button controls
|
||||
|
||||
private ImageDisplay imageDisplay;
|
||||
private void productsGrid_CoverClicked(IGridEntry liveGridEntry)
|
||||
private void productsGrid_CoverClicked(GridEntry liveGridEntry)
|
||||
{
|
||||
var picDef = new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureLarge ?? liveGridEntry.LibraryBook.Book.PictureId, PictureSize.Native);
|
||||
|
||||
@ -71,7 +71,7 @@ namespace LibationWinForms.GridView
|
||||
imageDisplay.Show(null);
|
||||
}
|
||||
|
||||
private void productsGrid_DescriptionClicked(IGridEntry liveGridEntry, Rectangle cellRectangle)
|
||||
private void productsGrid_DescriptionClicked(GridEntry liveGridEntry, Rectangle cellRectangle)
|
||||
{
|
||||
var displayWindow = new DescriptionDisplay
|
||||
{
|
||||
@ -90,7 +90,7 @@ namespace LibationWinForms.GridView
|
||||
displayWindow.Show(this);
|
||||
}
|
||||
|
||||
private async void productsGrid_DetailsClicked(ILibraryBookEntry liveGridEntry)
|
||||
private async void productsGrid_DetailsClicked(LibraryBookEntry liveGridEntry)
|
||||
{
|
||||
// HACK: workaround for a Winforms bug.
|
||||
// This event is fired by the DataGridCell.OnMouseUpInternal
|
||||
@ -124,12 +124,12 @@ namespace LibationWinForms.GridView
|
||||
|
||||
#region Cell Context Menu
|
||||
|
||||
private void productsGrid_CellContextMenuStripNeeded(IGridEntry[] entries, ContextMenuStrip ctxMenu)
|
||||
private void productsGrid_CellContextMenuStripNeeded(GridEntry[] entries, ContextMenuStrip ctxMenu)
|
||||
{
|
||||
var ctx = new GridContextMenu(entries, '&');
|
||||
#region Liberate all Episodes (Single series only)
|
||||
|
||||
if (entries.Length == 1 && entries[0] is ISeriesEntry seriesEntry)
|
||||
if (entries.Length == 1 && entries[0] is SeriesEntry seriesEntry)
|
||||
{
|
||||
var liberateEpisodesMenuItem = new ToolStripMenuItem()
|
||||
{
|
||||
@ -166,7 +166,7 @@ namespace LibationWinForms.GridView
|
||||
#endregion
|
||||
#region Locate file (Single book only)
|
||||
|
||||
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry)
|
||||
if (entries.Length == 1 && entries[0] is LibraryBookEntry entry)
|
||||
{
|
||||
var locateFileMenuItem = new ToolStripMenuItem() { Text = ctx.LocateFileText };
|
||||
ctxMenu.Items.Add(locateFileMenuItem);
|
||||
@ -199,7 +199,7 @@ namespace LibationWinForms.GridView
|
||||
|
||||
#endregion
|
||||
#region Liberate All (multiple books only)
|
||||
if (entries.OfType<ILibraryBookEntry>().Count() > 1)
|
||||
if (entries.OfType<LibraryBookEntry>().Count() > 1)
|
||||
{
|
||||
var downloadSelectedMenuItem = new ToolStripMenuItem()
|
||||
{
|
||||
@ -230,7 +230,7 @@ namespace LibationWinForms.GridView
|
||||
#endregion
|
||||
#region Force Re-Download (Single book only)
|
||||
|
||||
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry4)
|
||||
if (entries.Length == 1 && entries[0] is LibraryBookEntry entry4)
|
||||
{
|
||||
var reDownloadMenuItem = new ToolStripMenuItem()
|
||||
{
|
||||
@ -269,7 +269,7 @@ namespace LibationWinForms.GridView
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry2)
|
||||
if (entries.Length == 1 && entries[0] is LibraryBookEntry entry2)
|
||||
{
|
||||
var folderTemplateMenuItem = new ToolStripMenuItem { Text = ctx.FolderTemplateText };
|
||||
var fileTemplateMenuItem = new ToolStripMenuItem { Text = ctx.FileTemplateText };
|
||||
@ -288,7 +288,7 @@ namespace LibationWinForms.GridView
|
||||
#endregion
|
||||
#region View Bookmarks/Clips (Single book only)
|
||||
|
||||
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry3)
|
||||
if (entries.Length == 1 && entries[0] is LibraryBookEntry entry3)
|
||||
{
|
||||
var bookRecordMenuItem = new ToolStripMenuItem { Text = ctx.ViewBookmarksText };
|
||||
bookRecordMenuItem.Click += (_, _) => new BookRecordsDialog(entry3.LibraryBook).ShowDialog(this);
|
||||
@ -417,7 +417,7 @@ namespace LibationWinForms.GridView
|
||||
VisibleCountChanged?.Invoke(this, count);
|
||||
}
|
||||
|
||||
private void productsGrid_LiberateClicked(ILibraryBookEntry liveGridEntry)
|
||||
private void productsGrid_LiberateClicked(LibraryBookEntry liveGridEntry)
|
||||
{
|
||||
if (liveGridEntry.LibraryBook.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error
|
||||
&& !liveGridEntry.Liberate.IsUnavailable)
|
||||
|
||||
@ -270,7 +270,7 @@ namespace LibationWinForms.GridView
|
||||
//
|
||||
// syncBindingSource
|
||||
//
|
||||
syncBindingSource.DataSource = typeof(IGridEntry);
|
||||
syncBindingSource.DataSource = typeof(GridEntry);
|
||||
//
|
||||
// ProductsGrid
|
||||
//
|
||||
|
||||
@ -15,10 +15,10 @@ using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public delegate void GridEntryClickedEventHandler(IGridEntry liveGridEntry);
|
||||
public delegate void LibraryBookEntryClickedEventHandler(ILibraryBookEntry liveGridEntry);
|
||||
public delegate void GridEntryRectangleClickedEventHandler(IGridEntry liveGridEntry, Rectangle cellRectangle);
|
||||
public delegate void ProductsGridCellContextMenuStripNeededEventHandler(IGridEntry[] liveGridEntry, ContextMenuStrip ctxMenu);
|
||||
public delegate void GridEntryClickedEventHandler(GridEntry liveGridEntry);
|
||||
public delegate void LibraryBookEntryClickedEventHandler(LibraryBookEntry liveGridEntry);
|
||||
public delegate void GridEntryRectangleClickedEventHandler(GridEntry liveGridEntry, Rectangle cellRectangle);
|
||||
public delegate void ProductsGridCellContextMenuStripNeededEventHandler(GridEntry[] liveGridEntry, ContextMenuStrip ctxMenu);
|
||||
|
||||
public partial class ProductsGrid : UserControl
|
||||
{
|
||||
@ -37,8 +37,8 @@ namespace LibationWinForms.GridView
|
||||
=> bindingList
|
||||
?.GetFilteredInItems()
|
||||
.Select(lbe => lbe.LibraryBook) ?? Enumerable.Empty<LibraryBook>();
|
||||
internal IEnumerable<ILibraryBookEntry> GetAllBookEntries()
|
||||
=> bindingList?.AllItems().BookEntries() ?? Enumerable.Empty<ILibraryBookEntry>();
|
||||
internal IEnumerable<LibraryBookEntry> GetAllBookEntries()
|
||||
=> bindingList?.AllItems().BookEntries() ?? Enumerable.Empty<LibraryBookEntry>();
|
||||
|
||||
public ProductsGrid()
|
||||
{
|
||||
@ -189,7 +189,7 @@ namespace LibationWinForms.GridView
|
||||
.Distinct()
|
||||
.OrderBy(r => r.Index)
|
||||
.Select(r => r.DataBoundItem)
|
||||
.OfType<IGridEntry>()
|
||||
.OfType<GridEntry>()
|
||||
.ToArray();
|
||||
|
||||
var clickedIndex = Array.IndexOf(allSelected, clickedEntry);
|
||||
@ -225,7 +225,7 @@ namespace LibationWinForms.GridView
|
||||
return;
|
||||
|
||||
var entry = getGridEntry(e.RowIndex);
|
||||
if (entry is ILibraryBookEntry lbEntry)
|
||||
if (entry is LibraryBookEntry lbEntry)
|
||||
{
|
||||
if (e.ColumnIndex == liberateGVColumn.Index)
|
||||
LiberateClicked?.Invoke(lbEntry);
|
||||
@ -236,7 +236,7 @@ namespace LibationWinForms.GridView
|
||||
else if (e.ColumnIndex == coverGVColumn.Index)
|
||||
CoverClicked?.Invoke(lbEntry);
|
||||
}
|
||||
else if (entry is ISeriesEntry sEntry)
|
||||
else if (entry is SeriesEntry sEntry)
|
||||
{
|
||||
if (e.ColumnIndex == liberateGVColumn.Index)
|
||||
{
|
||||
@ -265,7 +265,7 @@ namespace LibationWinForms.GridView
|
||||
}
|
||||
}
|
||||
|
||||
private IGridEntry getGridEntry(int rowIndex) => gridEntryDataGridView.GetBoundItem<IGridEntry>(rowIndex);
|
||||
private GridEntry getGridEntry(int rowIndex) => gridEntryDataGridView.GetBoundItem<GridEntry>(rowIndex);
|
||||
|
||||
#endregion
|
||||
|
||||
@ -295,8 +295,8 @@ namespace LibationWinForms.GridView
|
||||
var sc = Invoke(() => System.Threading.SynchronizationContext.Current);
|
||||
System.Threading.SynchronizationContext.SetSynchronizationContext(sc);
|
||||
|
||||
var geList = await LibraryBookEntry<WinFormsEntryStatus>.GetAllProductsAsync(dbBooks);
|
||||
var seriesEntries = await SeriesEntry<WinFormsEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
|
||||
var geList = await LibraryBookEntry.GetAllProductsAsync(dbBooks);
|
||||
var seriesEntries = await SeriesEntry.GetAllSeriesEntriesAsync(dbBooks);
|
||||
|
||||
geList.AddRange(seriesEntries);
|
||||
//Sort descending by date (default sort property)
|
||||
@ -383,7 +383,7 @@ namespace LibationWinForms.GridView
|
||||
gridEntryDataGridView.FirstDisplayedScrollingRowIndex = topRow;
|
||||
}
|
||||
|
||||
public void RemoveBooks(IEnumerable<ILibraryBookEntry> removedBooks)
|
||||
public void RemoveBooks(IEnumerable<LibraryBookEntry> removedBooks)
|
||||
{
|
||||
if (bindingList == null)
|
||||
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(RemoveBooks)}");
|
||||
@ -398,34 +398,34 @@ namespace LibationWinForms.GridView
|
||||
.AllItems()
|
||||
.EmptySeries();
|
||||
|
||||
foreach (var removed in removedBooks.Cast<IGridEntry>().Concat(removedSeries))
|
||||
foreach (var removed in removedBooks.Cast<GridEntry>().Concat(removedSeries))
|
||||
//no need to re-filter for removed books
|
||||
bindingList.Remove(removed);
|
||||
|
||||
VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
|
||||
}
|
||||
|
||||
private void AddOrUpdateBook(LibraryBook book, ILibraryBookEntry? existingBookEntry)
|
||||
private void AddOrUpdateBook(LibraryBook book, LibraryBookEntry? existingBookEntry)
|
||||
{
|
||||
if (bindingList == null)
|
||||
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(AddOrUpdateBook)}");
|
||||
|
||||
if (existingBookEntry is null)
|
||||
// Add the new product to top
|
||||
bindingList.Insert(0, new LibraryBookEntry<WinFormsEntryStatus>(book));
|
||||
bindingList.Insert(0, new LibraryBookEntry(book));
|
||||
else
|
||||
// update existing
|
||||
existingBookEntry.UpdateLibraryBook(book);
|
||||
}
|
||||
|
||||
private void AddOrUpdateEpisode(LibraryBook episodeBook, ILibraryBookEntry? existingEpisodeEntry, List<ISeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
|
||||
private void AddOrUpdateEpisode(LibraryBook episodeBook, LibraryBookEntry? existingEpisodeEntry, List<SeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
|
||||
{
|
||||
if (bindingList == null)
|
||||
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(AddOrUpdateEpisode)}");
|
||||
|
||||
if (existingEpisodeEntry is null)
|
||||
{
|
||||
ILibraryBookEntry episodeEntry;
|
||||
LibraryBookEntry episodeEntry;
|
||||
|
||||
var seriesEntry = seriesEntries.FindSeriesParent(episodeBook);
|
||||
|
||||
@ -443,7 +443,7 @@ namespace LibationWinForms.GridView
|
||||
return;
|
||||
}
|
||||
|
||||
seriesEntry = new SeriesEntry<WinFormsEntryStatus>(seriesBook, episodeBook);
|
||||
seriesEntry = new SeriesEntry(seriesBook, episodeBook);
|
||||
seriesEntries.Add(seriesEntry);
|
||||
|
||||
episodeEntry = seriesEntry.Children[0];
|
||||
@ -453,7 +453,7 @@ namespace LibationWinForms.GridView
|
||||
else
|
||||
{
|
||||
//Series exists. Create and add episode child then update the SeriesEntry
|
||||
episodeEntry = new LibraryBookEntry<WinFormsEntryStatus>(episodeBook, seriesEntry);
|
||||
episodeEntry = new LibraryBookEntry(episodeBook, seriesEntry);
|
||||
seriesEntry.Children.Add(episodeEntry);
|
||||
seriesEntry.Children.Sort((c1, c2) => c1.SeriesIndex.CompareTo(c2.SeriesIndex));
|
||||
var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId);
|
||||
|
||||
@ -8,13 +8,13 @@ namespace LibationWinForms.GridView
|
||||
internal class RowComparer : RowComparerBase
|
||||
{
|
||||
public ListSortDirection SortOrder { get; set; } = ListSortDirection.Descending;
|
||||
public override string PropertyName { get; set; } = nameof(IGridEntry.DateAdded);
|
||||
public override string PropertyName { get; set; } = nameof(GridEntry.DateAdded);
|
||||
protected override ListSortDirection GetSortOrder() => SortOrder;
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for ordering grid entries
|
||||
/// </summary>
|
||||
public IOrderedEnumerable<IGridEntry> OrderEntries(IEnumerable<IGridEntry> entries)
|
||||
public IOrderedEnumerable<GridEntry> OrderEntries(IEnumerable<GridEntry> entries)
|
||||
=> SortOrder is ListSortDirection.Descending ? entries.OrderDescending(this) : entries.Order(this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
using DataLayer;
|
||||
using LibationUiBase.GridView;
|
||||
using System.Drawing;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public class WinFormsEntryStatus : EntryStatus, IEntryStatus
|
||||
{
|
||||
private static readonly Color SERIES_BG_COLOR = Color.FromArgb(230, 255, 230);
|
||||
public Color BackgroundBrush => IsEpisode ? SERIES_BG_COLOR : SystemColors.ControlLightLight;
|
||||
|
||||
private WinFormsEntryStatus(LibraryBook libraryBook) : base(libraryBook) { }
|
||||
public static EntryStatus Create(LibraryBook libraryBook) => new WinFormsEntryStatus(libraryBook);
|
||||
|
||||
protected override Image LoadImage(byte[] picture)
|
||||
=> WinFormsUtil.TryLoadImageOrDefault(picture, LibationFileManager.PictureSize._80x80);
|
||||
|
||||
protected override Image GetResourceImage(string rescName)
|
||||
{
|
||||
var image = Properties.Resources.ResourceManager.GetObject(rescName);
|
||||
return image as Bitmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,33 +3,20 @@ using System;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationWinForms.ProcessQueue
|
||||
{
|
||||
internal partial class ProcessBookControl : UserControl
|
||||
{
|
||||
private readonly int CancelBtnDistanceFromEdge;
|
||||
private readonly int ProgressBarDistanceFromEdge;
|
||||
private object? m_OldContext;
|
||||
|
||||
private static Color FailedColor { get; } = Color.LightCoral;
|
||||
private static Color CancelledColor { get; } = Color.Khaki;
|
||||
private static Color QueuedColor { get; } = SystemColors.Control;
|
||||
private static Color SuccessColor { get; } = Color.PaleGreen;
|
||||
|
||||
private ProcessBookViewModelBase m_Context;
|
||||
public ProcessBookViewModelBase Context
|
||||
{
|
||||
get => m_Context;
|
||||
set
|
||||
{
|
||||
if (m_Context != value)
|
||||
{
|
||||
OnContextChanging();
|
||||
m_Context = value;
|
||||
OnContextChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ProcessBookControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
@ -41,35 +28,41 @@ namespace LibationWinForms.ProcessQueue
|
||||
ProgressBarDistanceFromEdge = Width - progressBar1.Location.X - progressBar1.Width;
|
||||
}
|
||||
|
||||
private void OnContextChanging()
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
if (Context is not null)
|
||||
Context.PropertyChanged -= Context_PropertyChanged;
|
||||
if (m_OldContext is ProcessBookViewModel oldContext)
|
||||
oldContext.PropertyChanged -= DataContext_PropertyChanged;
|
||||
|
||||
if (DataContext is ProcessBookViewModel newContext)
|
||||
{
|
||||
m_OldContext = newContext;
|
||||
newContext.PropertyChanged += DataContext_PropertyChanged;
|
||||
DataContext_PropertyChanged(DataContext, new System.ComponentModel.PropertyChangedEventArgs(null));
|
||||
}
|
||||
|
||||
private void OnContextChanged()
|
||||
{
|
||||
Context.PropertyChanged += Context_PropertyChanged;
|
||||
Context_PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(null));
|
||||
base.OnDataContextChanged(e);
|
||||
}
|
||||
|
||||
private void Context_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
private void DataContext_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (sender is not ProcessBookViewModel vm)
|
||||
return;
|
||||
|
||||
SuspendLayout();
|
||||
if (e.PropertyName is null or nameof(Context.Cover))
|
||||
SetCover(Context.Cover as Image);
|
||||
if (e.PropertyName is null or nameof(Context.Title) or nameof(Context.Author) or nameof(Context.Narrator))
|
||||
SetBookInfo($"{Context.Title}\r\nBy {Context.Author}\r\nNarrated by {Context.Narrator}");
|
||||
if (e.PropertyName is null or nameof(Context.Status) or nameof(Context.StatusText))
|
||||
SetStatus(Context.Status, Context.StatusText);
|
||||
if (e.PropertyName is null or nameof(Context.Progress))
|
||||
SetProgress(Context.Progress);
|
||||
if (e.PropertyName is null or nameof(Context.TimeRemaining))
|
||||
SetRemainingTime(Context.TimeRemaining);
|
||||
if (e.PropertyName is null or nameof(vm.Cover))
|
||||
SetCover(vm.Cover as Image);
|
||||
if (e.PropertyName is null or nameof(vm.Title) or nameof(vm.Author) or nameof(vm.Narrator))
|
||||
SetBookInfo($"{vm.Title}\r\nBy {vm.Author}\r\nNarrated by {vm.Narrator}");
|
||||
if (e.PropertyName is null or nameof(vm.Status) or nameof(vm.StatusText))
|
||||
SetStatus(vm.Status, vm.StatusText);
|
||||
if (e.PropertyName is null or nameof(vm.Progress))
|
||||
SetProgress(vm.Progress);
|
||||
if (e.PropertyName is null or nameof(vm.TimeRemaining))
|
||||
SetRemainingTime(vm.TimeRemaining);
|
||||
ResumeLayout();
|
||||
}
|
||||
|
||||
private void SetCover(Image cover) => pictureBox1.Image = cover;
|
||||
private void SetCover(Image? cover) => pictureBox1.Image = cover;
|
||||
private void SetBookInfo(string title) => bookInfoLbl.Text = title;
|
||||
private void SetRemainingTime(TimeSpan remaining)
|
||||
=> remainingTimeLbl.Text = $"{remaining:mm\\:ss}";
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
|
||||
namespace LibationWinForms.ProcessQueue;
|
||||
|
||||
public class ProcessBookViewModel : ProcessBookViewModelBase
|
||||
{
|
||||
public ProcessBookViewModel(LibraryBook libraryBook, LogMe logme) : base(libraryBook, logme) { }
|
||||
|
||||
protected override object LoadImageFromBytes(byte[] bytes, PictureSize pictureSize)
|
||||
=> WinFormsUtil.TryLoadImageOrDefault(bytes, PictureSize._80x80);
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@ -27,49 +28,47 @@ internal partial class ProcessQueueControl : UserControl
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var speedLimitMBps = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
||||
numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps;
|
||||
statusStrip1.Items.Add(PopoutButton);
|
||||
|
||||
virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked;
|
||||
virtualFlowControl2.DataContext = ViewModel.Queue;
|
||||
|
||||
ViewModel.LogWritten += (_, text) => WriteLine(text);
|
||||
ViewModel.PropertyChanged += ProcessQueue_PropertyChanged;
|
||||
virtualFlowControl2.Items = ViewModel.Items;
|
||||
Load += ProcessQueueControl_Load;
|
||||
ViewModel.LogEntries.CollectionChanged += LogEntries_CollectionChanged;
|
||||
}
|
||||
|
||||
private void ProcessQueueControl_Load(object? sender, EventArgs e)
|
||||
private void LogEntries_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (!IsDisposed && e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
|
||||
{
|
||||
foreach(var entry in e.NewItems?.OfType<LogEntry>() ?? [])
|
||||
logDGV.Rows.Add(entry.LogDate, entry.LogMessage);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnLoad(EventArgs e)
|
||||
{
|
||||
if (DesignMode) return;
|
||||
ProcessQueue_PropertyChanged(this, new PropertyChangedEventArgs(null));
|
||||
}
|
||||
|
||||
public void WriteLine(string text)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
logDGV.Rows.Add(DateTime.Now, text.Trim());
|
||||
}
|
||||
|
||||
private async void cancelAllBtn_Click(object? sender, EventArgs e)
|
||||
{
|
||||
ViewModel.Queue.ClearQueue();
|
||||
if (ViewModel.Queue.Current is not null)
|
||||
await ViewModel.Queue.Current.CancelAsync();
|
||||
virtualFlowControl2.RefreshDisplay();
|
||||
}
|
||||
|
||||
private void btnClearFinished_Click(object? sender, EventArgs e)
|
||||
{
|
||||
ViewModel.Queue.ClearCompleted();
|
||||
virtualFlowControl2.RefreshDisplay();
|
||||
|
||||
if (!ViewModel.Running)
|
||||
runningTimeLbl.Text = string.Empty;
|
||||
}
|
||||
|
||||
private void clearLogBtn_Click(object? sender, EventArgs e)
|
||||
{
|
||||
ViewModel.LogEntries.Clear();
|
||||
logDGV.Rows.Clear();
|
||||
}
|
||||
|
||||
@ -92,7 +91,6 @@ internal partial class ProcessQueueControl : UserControl
|
||||
{
|
||||
queueNumberLbl.Text = ViewModel.QueuedCount.ToString();
|
||||
queueNumberLbl.Visible = ViewModel.QueuedCount > 0;
|
||||
virtualFlowControl2.RefreshDisplay();
|
||||
}
|
||||
if (e.PropertyName is null or nameof(ViewModel.ErrorCount))
|
||||
{
|
||||
@ -117,16 +115,22 @@ internal partial class ProcessQueueControl : UserControl
|
||||
{
|
||||
runningTimeLbl.Text = ViewModel.RunningTime;
|
||||
}
|
||||
if (e.PropertyName is null or nameof(ViewModel.SpeedLimit))
|
||||
{
|
||||
numericUpDown1.Value = ViewModel.SpeedLimit;
|
||||
numericUpDown1.Increment = ViewModel.SpeedLimitIncrement;
|
||||
numericUpDown1.DecimalPlaces = ViewModel.SpeedLimit >= 10 ? 0 : ViewModel.SpeedLimit >= 1 ? 1 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View notified the model that a botton was clicked
|
||||
/// View notified the model that a button was clicked
|
||||
/// </summary>
|
||||
/// <param name="sender">the <see cref="ProcessBookControl"/> whose button was clicked</param>
|
||||
/// <param name="buttonName">The name of the button clicked</param>
|
||||
private async void VirtualFlowControl2_ButtonClicked(object? sender, string buttonName)
|
||||
{
|
||||
if (sender is not ProcessBookControl control || control.Context is not ProcessBookViewModel item)
|
||||
if (sender is not ProcessBookControl control || control.DataContext is not ProcessBookViewModel item)
|
||||
return;
|
||||
|
||||
try
|
||||
@ -135,13 +139,12 @@ internal partial class ProcessQueueControl : UserControl
|
||||
{
|
||||
await item.CancelAsync();
|
||||
ViewModel.Queue.RemoveQueued(item);
|
||||
virtualFlowControl2.RefreshDisplay();
|
||||
}
|
||||
else
|
||||
{
|
||||
QueuePosition? position = buttonName switch
|
||||
{
|
||||
nameof(ProcessBookControl.moveFirstBtn) => QueuePosition.Fisrt,
|
||||
nameof(ProcessBookControl.moveFirstBtn) => QueuePosition.First,
|
||||
nameof(ProcessBookControl.moveUpBtn) => QueuePosition.OneUp,
|
||||
nameof(ProcessBookControl.moveDownBtn) => QueuePosition.OneDown,
|
||||
nameof(ProcessBookControl.moveLastBtn) => QueuePosition.Last,
|
||||
@ -149,10 +152,7 @@ internal partial class ProcessQueueControl : UserControl
|
||||
};
|
||||
|
||||
if (position is not null)
|
||||
{
|
||||
ViewModel.Queue.MoveQueuePosition(item, position.Value);
|
||||
virtualFlowControl2.RefreshDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception ex)
|
||||
@ -164,27 +164,7 @@ internal partial class ProcessQueueControl : UserControl
|
||||
#endregion
|
||||
|
||||
private void numericUpDown1_ValueChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var newValue = (long)(numericUpDown1.Value * 1024 * 1024);
|
||||
|
||||
var config = Configuration.Instance;
|
||||
config.DownloadSpeedLimit = newValue;
|
||||
if (config.DownloadSpeedLimit > newValue)
|
||||
numericUpDown1.Value =
|
||||
numericUpDown1.Value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
|
||||
: 0;
|
||||
|
||||
numericUpDown1.Increment =
|
||||
numericUpDown1.Value > 100 ? 10
|
||||
: numericUpDown1.Value > 10 ? 1
|
||||
: numericUpDown1.Value > 1 ? 0.1m
|
||||
: 0.01m;
|
||||
|
||||
numericUpDown1.DecimalPlaces =
|
||||
numericUpDown1.Value >= 10 ? 0
|
||||
: numericUpDown1.Value >= 1 ? 1
|
||||
: 2;
|
||||
}
|
||||
=> ViewModel.SpeedLimit = numericUpDown1.Value;
|
||||
}
|
||||
|
||||
public class NumericUpDownSuffix : NumericUpDown
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
using DataLayer;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationWinForms.ProcessQueue;
|
||||
|
||||
internal class ProcessQueueViewModel : ProcessQueueViewModelBase
|
||||
{
|
||||
public event EventHandler<string>? LogWritten;
|
||||
public List<ProcessBookViewModelBase> Items { get; }
|
||||
|
||||
public ProcessQueueViewModel() : base(new List<ProcessBookViewModelBase>())
|
||||
{
|
||||
Items = Queue.UnderlyingList as List<ProcessBookViewModelBase>
|
||||
?? throw new ArgumentNullException(nameof(Queue.UnderlyingList));
|
||||
}
|
||||
|
||||
public override void WriteLine(string text) => Invoke(() => LogWritten?.Invoke(this, text.Trim()));
|
||||
|
||||
protected override ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook)
|
||||
=> new ProcessBookViewModel(libraryBook, Logger);
|
||||
}
|
||||
@ -1,9 +1,11 @@
|
||||
using LibationUiBase.ProcessQueue;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationWinForms.ProcessQueue
|
||||
{
|
||||
internal partial class VirtualFlowControl : UserControl
|
||||
@ -11,18 +13,28 @@ namespace LibationWinForms.ProcessQueue
|
||||
/// <summary>
|
||||
/// Triggered when one of the <see cref="ProcessBookControl"/>'s buttons has been clicked
|
||||
/// </summary>
|
||||
public event EventHandler<string> ButtonClicked;
|
||||
public event EventHandler<string>? ButtonClicked;
|
||||
public IList? Items { get; private set; }
|
||||
|
||||
private List<ProcessBookViewModelBase> m_Items;
|
||||
public List<ProcessBookViewModelBase> Items
|
||||
private object? m_OldContext;
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
get => m_Items;
|
||||
set
|
||||
if (m_OldContext is INotifyCollectionChanged oldNotify)
|
||||
oldNotify.CollectionChanged -= Items_CollectionChanged;
|
||||
|
||||
if (DataContext is INotifyCollectionChanged newNotify)
|
||||
{
|
||||
m_Items = value;
|
||||
if (m_Items is not null)
|
||||
RefreshDisplay();
|
||||
m_OldContext = newNotify;
|
||||
newNotify.CollectionChanged += Items_CollectionChanged;
|
||||
}
|
||||
|
||||
Items = DataContext as IList;
|
||||
base.OnDataContextChanged(e);
|
||||
}
|
||||
|
||||
private void Items_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
RefreshDisplay();
|
||||
}
|
||||
|
||||
public void RefreshDisplay()
|
||||
@ -65,7 +77,7 @@ namespace LibationWinForms.ProcessQueue
|
||||
#region Instance variables
|
||||
|
||||
/// <summary>
|
||||
/// The total height, inclusing margins, of the repeated <see cref="ProcessBookControl"/>
|
||||
/// The total height, including margins, of the repeated <see cref="ProcessBookControl"/>
|
||||
/// </summary>
|
||||
private readonly int VirtualControlHeight;
|
||||
/// <summary>
|
||||
@ -137,14 +149,15 @@ namespace LibationWinForms.ProcessQueue
|
||||
/// <summary>
|
||||
/// Handles all button clicks from all <see cref="ProcessBookControl"/>, detects which one sent the click, and fires <see cref="ButtonClicked"/> to notify the model of the click
|
||||
/// </summary>
|
||||
private void ControlButton_Click(object sender, EventArgs e)
|
||||
private void ControlButton_Click(object? sender, EventArgs e)
|
||||
{
|
||||
Control button = sender as Control;
|
||||
Control form = button.Parent;
|
||||
while (form is not ProcessBookControl)
|
||||
form = form.Parent;
|
||||
Control? button = sender as Control;
|
||||
Control? form = button?.Parent;
|
||||
while (form is not null and not ProcessBookControl)
|
||||
form = form?.Parent;
|
||||
|
||||
ButtonClicked?.Invoke(form, button.Name);
|
||||
if (form is not null && button?.Name is string buttonText)
|
||||
ButtonClicked?.Invoke(form, buttonText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -176,7 +189,7 @@ namespace LibationWinForms.ProcessQueue
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculated the virtual controls that are in view at the currrent scroll position and windows size,
|
||||
/// Calculated the virtual controls that are in view at the current scroll position and windows size,
|
||||
/// positions <see cref="panel1"/> to simulate scroll activity, then fires updates the controls with
|
||||
/// the context corresponding to the virtual scroll position
|
||||
/// </summary>
|
||||
@ -195,9 +208,10 @@ namespace LibationWinForms.ProcessQueue
|
||||
numVisible = Math.Min(numVisible, VirtualControlCount);
|
||||
numVisible = Math.Min(numVisible, VirtualControlCount - firstVisible);
|
||||
|
||||
for (int i = 0; i < numVisible; i++)
|
||||
if (Items is IList items)
|
||||
{
|
||||
BookControls[i].Context = Items[firstVisible + i];
|
||||
for (int i = 0; i < numVisible; i++)
|
||||
BookControls[i].DataContext = items[firstVisible + i];
|
||||
}
|
||||
|
||||
for (int i = 0; i < BookControls.Count; i++)
|
||||
|
||||
@ -6,4 +6,3 @@ For reasons I cannot figure out, upgrading the major .net version breaks Libatio
|
||||
|
||||
* https://github.com/Mbucari/AAXClean.Codecs
|
||||
* https://github.com/Mbucari/AAXClean
|
||||
* https://github.com/Mbucari/serilog-sinks-zipfile
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user