diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml
index 37063783..44efcb1e 100644
--- a/.github/workflows/build-windows.yml
+++ b/.github/workflows/build-windows.yml
@@ -15,6 +15,10 @@ on:
description: "Skip running unit tests"
required: false
default: true
+ architecture:
+ type: string
+ description: "CPU architecture targeted by the build."
+ required: true
env:
DOTNET_CONFIGURATION: "Release"
@@ -22,8 +26,11 @@ env:
jobs:
build:
- name: "${{ matrix.os }}-${{ matrix.release_name }}"
+ name: "${{ matrix.os }}-${{ matrix.release_name }}-${{ inputs.architecture }}"
runs-on: windows-latest
+ env:
+ OUTPUT_NAME: "${{ matrix.os }}-${{ matrix.release_name }}-${{ inputs.architecture }}"
+ RUNTIME_ID: "win-${{ inputs.architecture }}"
strategy:
matrix:
os: [Windows]
@@ -63,38 +70,42 @@ jobs:
run: |
dotnet publish `
Libation${{ matrix.ui }}/Libation${{ matrix.ui }}.csproj `
+ --runtime ${{ env.RUNTIME_ID }} `
--configuration ${{ env.DOTNET_CONFIGURATION }} `
- --output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
+ --output bin/Publish/${{ env.OUTPUT_NAME }} `
-p:PublishProfile=Libation${{ matrix.ui }}/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
dotnet publish `
LoadByOS/${{ matrix.os }}ConfigApp/${{ matrix.os }}ConfigApp.csproj `
+ --runtime ${{ env.RUNTIME_ID }} `
--configuration ${{ env.DOTNET_CONFIGURATION }} `
- --output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
+ --output bin/Publish/${{ env.OUTPUT_NAME }} `
-p:PublishProfile=LoadByOS/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
dotnet publish `
LibationCli/LibationCli.csproj `
+ --runtime ${{ env.RUNTIME_ID }} `
--configuration ${{ env.DOTNET_CONFIGURATION }} `
- --output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
+ --output bin/Publish/${{ env.OUTPUT_NAME }} `
-p:DefineConstants="${{ matrix.release_name }}" `
-p:PublishProfile=LibationCli/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
dotnet publish `
Hangover${{ matrix.ui }}/Hangover${{ matrix.ui }}.csproj `
+ --runtime ${{ env.RUNTIME_ID }} `
--configuration ${{ env.DOTNET_CONFIGURATION }} `
- --output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
+ --output bin/Publish/${{ env.OUTPUT_NAME }} `
-p:PublishProfile=Hangover${{ matrix.ui }}/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
- name: Zip artifact
id: zip
working-directory: ./Source/bin/Publish
run: |
- $bin_dir = "${{ matrix.os }}-${{ matrix.release_name }}\"
+ $bin_dir = "${{ env.OUTPUT_NAME }}\"
$delfiles = @(
"WindowsConfigApp.exe",
"WindowsConfigApp.runtimeconfig.json",
"WindowsConfigApp.deps.json"
)
foreach ($file in $delfiles){ if (test-path $bin_dir$file){ Remove-Item $bin_dir$file } }
- $artifact="${{ matrix.prefix }}Libation.${{ steps.get_version.outputs.version }}-" + "${{ matrix.os }}".ToLower() + "-${{ matrix.release_name }}"
+ $artifact="${{ matrix.prefix }}Libation.${{ steps.get_version.outputs.version }}-" + "${{ matrix.os }}".ToLower() + "-${{ matrix.release_name }}-${{ inputs.architecture }}"
"artifact=$artifact" >> $env:GITHUB_OUTPUT
Compress-Archive -Path "${bin_dir}*" -DestinationPath "$artifact.zip"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a67098ea..341f8033 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -18,10 +18,14 @@ on:
jobs:
windows:
+ strategy:
+ matrix:
+ architecture: [x64]
uses: ./.github/workflows/build-windows.yml
with:
version_override: ${{ inputs.version_override }}
run_unit_tests: ${{ inputs.run_unit_tests }}
+ architecture: ${{ matrix.architecture }}
linux:
strategy:
diff --git a/.releaseindex.json b/.releaseindex.json
index 9d9dea23..7ae58e51 100644
--- a/.releaseindex.json
+++ b/.releaseindex.json
@@ -1,10 +1,10 @@
{
- "WindowsClassic": "Classic-Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-classic\\.zip",
- "WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-chardonnay\\.zip",
- "LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-amd64\\.deb",
- "LinuxAvalonia_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-amd64\\.rpm",
- "MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-macOS-chardonnay-x64\\.tgz",
- "LinuxAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-arm64\\.deb",
- "LinuxAvalonia_Arm64_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-arm64\\.rpm",
- "MacOSAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+-macOS-chardonnay-arm64\\.tgz"
+ "WindowsClassic": "Classic-Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-classic-x64\\.zip",
+ "WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-chardonnay-x64\\.zip",
+ "LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-amd64\\.deb",
+ "LinuxAvalonia_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-amd64\\.rpm",
+ "MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-macOS-chardonnay-x64\\.tgz",
+ "LinuxAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-arm64\\.deb",
+ "LinuxAvalonia_Arm64_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-arm64\\.rpm",
+ "MacOSAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-macOS-chardonnay-arm64\\.tgz"
}
diff --git a/Source/AaxDecrypter/AaxDecrypter.csproj b/Source/AaxDecrypter/AaxDecrypter.csproj
index 8255f5b9..d585820a 100644
--- a/Source/AaxDecrypter/AaxDecrypter.csproj
+++ b/Source/AaxDecrypter/AaxDecrypter.csproj
@@ -13,7 +13,7 @@
-
+
diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs
index 19c154b0..41d25595 100644
--- a/Source/AppScaffolding/LibationScaffolding.cs
+++ b/Source/AppScaffolding/LibationScaffolding.cs
@@ -290,33 +290,24 @@ namespace AppScaffolding
public static UpgradeProperties GetLatestRelease()
{
// timed out
- (var latest, var zip) = getLatestRelease(TimeSpan.FromSeconds(10));
+ (var version, var latest, var zip) = getLatestRelease(TimeSpan.FromSeconds(10));
- if (latest is null || zip is null)
- return null;
-
- var latestVersionString = latest.TagName.Trim('v');
- if (!Version.TryParse(latestVersionString, out var latestRelease))
- return null;
-
- // we're up to date
- if (latestRelease <= BuildVersion)
+ if (version is null || latest is null || zip is null)
return null;
// we have an update
-
var zipUrl = zip?.BrowserDownloadUrl;
Log.Logger.Information("Update available: {@DebugInfo}", new
{
- latestRelease = latestRelease.ToString(),
+ latestRelease = version.ToString(),
latest.HtmlUrl,
zipUrl
});
- return new(zipUrl, latest.HtmlUrl, zip.Name, latestRelease, latest.Body);
+ return new(zipUrl, latest.HtmlUrl, zip.Name, version, latest.Body);
}
- private static (Octokit.Release, Octokit.ReleaseAsset) getLatestRelease(TimeSpan timeout)
+ private static (Version releaseVersion, Octokit.Release, Octokit.ReleaseAsset) getLatestRelease(TimeSpan timeout)
{
try
{
@@ -330,15 +321,23 @@ namespace AppScaffolding
{
Log.Logger.Error(aggEx, "Checking for new version too often");
}
- return (null, null);
+ return (null, null, null);
}
- private static async System.Threading.Tasks.Task<(Octokit.Release, Octokit.ReleaseAsset)> getLatestRelease()
+ private static async System.Threading.Tasks.Task<(Version releaseVersion, Octokit.Release, Octokit.ReleaseAsset)> getLatestRelease()
{
const string ownerAccount = "rmcrackan";
const string repoName = "Libation";
var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue(repoName));
+ //https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-the-latest-release
+ var latestRelease = await gitHubClient.Repository.Release.GetLatest(ownerAccount, repoName);
+
+ //Ensure that latest release is greater than the current version
+ var latestVersionString = latestRelease.TagName.Trim('v');
+ if (!Version.TryParse(latestVersionString, out var releaseVersion) || releaseVersion <= BuildVersion)
+ return (null, null, null);
+
//Download the release index
var bts = await gitHubClient.Repository.Content.GetRawContent(ownerAccount, repoName, ".releaseindex.json");
var releaseIndex = JObject.Parse(System.Text.Encoding.ASCII.GetString(bts));
@@ -356,10 +355,7 @@ namespace AppScaffolding
var regex = new System.Text.RegularExpressions.Regex(regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
- //https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-the-latest-release
- var latestRelease = await gitHubClient.Repository.Release.GetLatest(ownerAccount, repoName);
-
- return (latestRelease, latestRelease?.Assets?.FirstOrDefault(a => regex.IsMatch(a.Name)));
+ return (releaseVersion, latestRelease, latestRelease?.Assets?.FirstOrDefault(a => regex.IsMatch(a.Name)));
}
}
diff --git a/Source/ApplicationServices/ApplicationServices.csproj b/Source/ApplicationServices/ApplicationServices.csproj
index 0fcb98cc..f59184aa 100644
--- a/Source/ApplicationServices/ApplicationServices.csproj
+++ b/Source/ApplicationServices/ApplicationServices.csproj
@@ -5,8 +5,8 @@
-
-
+
+
diff --git a/Source/AudibleUtilities/AudibleUtilities.csproj b/Source/AudibleUtilities/AudibleUtilities.csproj
index c550207d..8007d316 100644
--- a/Source/AudibleUtilities/AudibleUtilities.csproj
+++ b/Source/AudibleUtilities/AudibleUtilities.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/Source/DataLayer/DataLayer.csproj b/Source/DataLayer/DataLayer.csproj
index ef9dd47c..aa0eff88 100644
--- a/Source/DataLayer/DataLayer.csproj
+++ b/Source/DataLayer/DataLayer.csproj
@@ -12,12 +12,12 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs
index 91f89995..deca475b 100644
--- a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs
+++ b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs
@@ -103,13 +103,11 @@ namespace DataLayer
) == true
).ToList();
- public static IEnumerable UnLiberated(this IEnumerable bookList)
- => bookList
- .Where(
- lb =>
- !lb.AbsentFromLastScan &&
- (lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload
- || lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
- );
+ public static bool NeedsPdfDownload(this LibraryBook libraryBook)
+ => !libraryBook.AbsentFromLastScan && libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated;
+ public static bool NeedsBookDownload(this LibraryBook libraryBook)
+ => !libraryBook.AbsentFromLastScan && libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload;
+ public static IEnumerable UnLiberated(this IEnumerable bookList)
+ => bookList.Where(lb => lb.NeedsPdfDownload() || lb.NeedsBookDownload());
}
}
diff --git a/Source/FileManager/FileManager.csproj b/Source/FileManager/FileManager.csproj
index c66f3236..df55768f 100644
--- a/Source/FileManager/FileManager.csproj
+++ b/Source/FileManager/FileManager.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/Source/HangoverAvalonia/HangoverAvalonia.csproj b/Source/HangoverAvalonia/HangoverAvalonia.csproj
index fd506340..9b140e6a 100644
--- a/Source/HangoverAvalonia/HangoverAvalonia.csproj
+++ b/Source/HangoverAvalonia/HangoverAvalonia.csproj
@@ -71,12 +71,12 @@
-
-
+
+
-
-
-
+
+
+
diff --git a/Source/LibationAvalonia/App.axaml.cs b/Source/LibationAvalonia/App.axaml.cs
index 3201b953..9c89e841 100644
--- a/Source/LibationAvalonia/App.axaml.cs
+++ b/Source/LibationAvalonia/App.axaml.cs
@@ -16,6 +16,8 @@ using Dinah.Core;
using LibationAvalonia.Themes;
using Avalonia.Data.Core.Plugins;
using System.Linq;
+using LibationUiBase.Forms;
+using Avalonia.Controls;
#nullable enable
namespace LibationAvalonia
@@ -42,6 +44,9 @@ namespace LibationAvalonia
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
+ MessageBoxBase.ShowAsyncImpl = (owner, message, caption, buttons, icon, defaultButton, saveAndRestorePosition) =>
+ MessageBox.Show(owner as Window, message, caption, buttons, icon, defaultButton, saveAndRestorePosition);
+
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
diff --git a/Source/LibationAvalonia/AvaloniaUtils.cs b/Source/LibationAvalonia/AvaloniaUtils.cs
index 11508b70..4bfc1aa7 100644
--- a/Source/LibationAvalonia/AvaloniaUtils.cs
+++ b/Source/LibationAvalonia/AvaloniaUtils.cs
@@ -4,6 +4,7 @@ using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Media.Imaging;
using Avalonia.VisualTree;
using LibationFileManager;
+using LibationUiBase.Forms;
using System.Threading.Tasks;
#nullable enable
diff --git a/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs b/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs
index b5ac7fcc..fe51170e 100644
--- a/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs
+++ b/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs
@@ -1,5 +1,6 @@
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Platform.Storage;
using Dinah.Core;
using LibationFileManager;
using ReactiveUI;
@@ -90,7 +91,7 @@ namespace LibationAvalonia.Controls
var selectedFolders = await (VisualRoot as Window).StorageProvider.OpenFolderPickerAsync(options);
- directoryState.CustomDir = selectedFolders.SingleOrDefault()?.Path?.LocalPath ?? directoryState.CustomDir;
+ directoryState.CustomDir = selectedFolders.SingleOrDefault()?.TryGetLocalPath() ?? directoryState.CustomDir;
}
private void DirectoryOrCustomSelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
diff --git a/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs b/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs
index 8cb6af48..a7f1d601 100644
--- a/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs
+++ b/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs
@@ -4,6 +4,7 @@ using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels.Settings;
using LibationFileManager;
using LibationFileManager.Templates;
+using LibationUiBase.Forms;
using System.Linq;
using System.Threading.Tasks;
diff --git a/Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml.cs b/Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml.cs
index 0a55b0e4..2be2bb5b 100644
--- a/Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml.cs
+++ b/Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml.cs
@@ -3,6 +3,7 @@ using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels.Settings;
using LibationFileManager;
using LibationFileManager.Templates;
+using LibationUiBase.Forms;
using System.Threading.Tasks;
namespace LibationAvalonia.Controls.Settings
diff --git a/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs b/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs
index deeaf945..214ea33b 100644
--- a/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs
+++ b/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs
@@ -1,12 +1,10 @@
using Avalonia.Controls;
-using Avalonia.Media.Imaging;
using DataLayer;
using Dinah.Core.ErrorHandling;
using LibationAvalonia.ViewModels;
using LibationFileManager;
-using NPOI.Util.Collections;
+using LibationUiBase.ProcessQueue;
using System.Collections.Generic;
-using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
diff --git a/Source/LibationAvalonia/Dialogs/AboutDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/AboutDialog.axaml.cs
index cbb6de11..80f87d1b 100644
--- a/Source/LibationAvalonia/Dialogs/AboutDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/AboutDialog.axaml.cs
@@ -3,6 +3,7 @@ using LibationAvalonia.Controls;
using LibationAvalonia.ViewModels;
using LibationFileManager;
using LibationUiBase;
+using LibationUiBase.Forms;
using ReactiveUI;
using System;
using System.Collections.Generic;
diff --git a/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml.cs
index fe74cea9..bb8b3168 100644
--- a/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml.cs
@@ -3,6 +3,7 @@ using AudibleUtilities;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
+using LibationUiBase.Forms;
using ReactiveUI;
using System;
using System.Collections.Generic;
diff --git a/Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml.cs
index bb04161e..d3860579 100644
--- a/Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml.cs
@@ -7,6 +7,7 @@ using Avalonia.Platform.Storage;
using Avalonia.Threading;
using DataLayer;
using FileLiberator;
+using LibationUiBase.Forms;
using ReactiveUI;
using System;
using System.Collections.Generic;
diff --git a/Source/LibationAvalonia/Dialogs/DialogWindow.cs b/Source/LibationAvalonia/Dialogs/DialogWindow.cs
index bda5e804..016bbd3a 100644
--- a/Source/LibationAvalonia/Dialogs/DialogWindow.cs
+++ b/Source/LibationAvalonia/Dialogs/DialogWindow.cs
@@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Styling;
using LibationFileManager;
+using LibationUiBase.Forms;
using System;
using System.Threading.Tasks;
diff --git a/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs
index b7224ae3..2e2c447f 100644
--- a/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs
@@ -6,6 +6,7 @@ using Avalonia.Styling;
using Dinah.Core;
using LibationFileManager;
using LibationFileManager.Templates;
+using LibationUiBase.Forms;
using ReactiveUI;
using System;
using System.IO;
diff --git a/Source/LibationAvalonia/Dialogs/ImageDisplayDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/ImageDisplayDialog.axaml.cs
index 051d78e1..86b9933a 100644
--- a/Source/LibationAvalonia/Dialogs/ImageDisplayDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/ImageDisplayDialog.axaml.cs
@@ -1,5 +1,6 @@
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
+using LibationUiBase.Forms;
using ReactiveUI;
using System;
using System.ComponentModel;
diff --git a/Source/LibationAvalonia/Dialogs/LibationFilesDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/LibationFilesDialog.axaml.cs
index a20a588a..73c8011a 100644
--- a/Source/LibationAvalonia/Dialogs/LibationFilesDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/LibationFilesDialog.axaml.cs
@@ -1,4 +1,5 @@
using LibationFileManager;
+using LibationUiBase.Forms;
using System.Collections.Generic;
namespace LibationAvalonia.Dialogs
diff --git a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginCallback.cs b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginCallback.cs
index e5234d7c..bf4f8b56 100644
--- a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginCallback.cs
+++ b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginCallback.cs
@@ -1,5 +1,6 @@
using AudibleApi;
using AudibleUtilities;
+using LibationUiBase.Forms;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
diff --git a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs
index b8122f9d..d2872e3f 100644
--- a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs
+++ b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs
@@ -1,6 +1,7 @@
using AudibleApi;
using AudibleUtilities;
using LibationFileManager;
+using LibationUiBase.Forms;
using System;
using System.Threading.Tasks;
diff --git a/Source/LibationAvalonia/Dialogs/Login/MfaDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/Login/MfaDialog.axaml.cs
index 9cbf1363..90e348dc 100644
--- a/Source/LibationAvalonia/Dialogs/Login/MfaDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/Login/MfaDialog.axaml.cs
@@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
+using LibationUiBase.Forms;
using ReactiveUI;
using System.Collections.Generic;
using System.Linq;
diff --git a/Source/LibationAvalonia/Dialogs/Login/WebLoginDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/Login/WebLoginDialog.axaml.cs
index d6c1c644..b2df7078 100644
--- a/Source/LibationAvalonia/Dialogs/Login/WebLoginDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/Login/WebLoginDialog.axaml.cs
@@ -1,5 +1,6 @@
using Avalonia.Controls;
using Dinah.Core;
+using LibationUiBase.Forms;
using System;
namespace LibationAvalonia.Dialogs.Login
diff --git a/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs
index 85d04f5c..ddcfecf6 100644
--- a/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs
@@ -1,6 +1,7 @@
using Avalonia.Controls;
using Dinah.Core;
using FileManager;
+using LibationUiBase.Forms;
using System;
namespace LibationAvalonia.Dialogs
diff --git a/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml b/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml
index 204a23a4..2b2ef05b 100644
--- a/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml
+++ b/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml
@@ -1,12 +1,13 @@
+ Title="{CompiledBinding Caption}" ShowInTaskbar="True">
@@ -14,14 +15,22 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -35,13 +44,13 @@
-
diff --git a/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml.cs b/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml.cs
index c1cbb009..1633112f 100644
--- a/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml.cs
@@ -1,4 +1,5 @@
using LibationAvalonia.ViewModels.Dialogs;
+using LibationUiBase.Forms;
namespace LibationAvalonia.Dialogs
{
diff --git a/Source/LibationAvalonia/Dialogs/ScanAccountsDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/ScanAccountsDialog.axaml.cs
index 912f2c0d..cdd22318 100644
--- a/Source/LibationAvalonia/Dialogs/ScanAccountsDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/ScanAccountsDialog.axaml.cs
@@ -1,6 +1,6 @@
using AudibleUtilities;
using Avalonia.Controls;
-using Avalonia.Interactivity;
+using LibationUiBase.Forms;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
diff --git a/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml.cs
index ba1839b9..5155973e 100644
--- a/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml.cs
@@ -1,6 +1,7 @@
using Avalonia.Controls;
using LibationAvalonia.ViewModels.Settings;
using LibationFileManager;
+using LibationUiBase.Forms;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs
diff --git a/Source/LibationAvalonia/Dialogs/SetupDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/SetupDialog.axaml.cs
index bc73eaed..9745b9b0 100644
--- a/Source/LibationAvalonia/Dialogs/SetupDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/SetupDialog.axaml.cs
@@ -1,5 +1,6 @@
using Avalonia.Controls;
using LibationFileManager;
+using LibationUiBase.Forms;
namespace LibationAvalonia.Dialogs
{
diff --git a/Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml.cs
index 618f5a44..e3a2d9e3 100644
--- a/Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml.cs
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform.Storage;
+using LibationUiBase.Forms;
#nullable enable
namespace LibationAvalonia.Dialogs;
diff --git a/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml.cs
index cb298132..712b4ff5 100644
--- a/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml.cs
@@ -1,6 +1,7 @@
using AppScaffolding;
using Avalonia.Controls;
using Dinah.Core;
+using LibationUiBase.Forms;
namespace LibationAvalonia.Dialogs
{
diff --git a/Source/LibationAvalonia/LibationAvalonia.csproj b/Source/LibationAvalonia/LibationAvalonia.csproj
index d7860a57..10d24d0c 100644
--- a/Source/LibationAvalonia/LibationAvalonia.csproj
+++ b/Source/LibationAvalonia/LibationAvalonia.csproj
@@ -73,13 +73,13 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/Source/LibationAvalonia/MessageBox.cs b/Source/LibationAvalonia/MessageBox.cs
index 0142bc41..290b254f 100644
--- a/Source/LibationAvalonia/MessageBox.cs
+++ b/Source/LibationAvalonia/MessageBox.cs
@@ -6,6 +6,7 @@ using DataLayer;
using Dinah.Core.Logging;
using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels.Dialogs;
+using LibationUiBase.Forms;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -13,92 +14,45 @@ using System.Threading.Tasks;
namespace LibationAvalonia
{
- public enum DialogResult
- {
- None = 0,
- OK = 1,
- Cancel = 2,
- Abort = 3,
- Retry = 4,
- Ignore = 5,
- Yes = 6,
- No = 7,
- TryAgain = 10,
- Continue = 11
- }
-
- public enum MessageBoxIcon
- {
- None = 0,
- Error = 16,
- Hand = 16,
- Stop = 16,
- Question = 32,
- Exclamation = 48,
- Warning = 48,
- Asterisk = 64,
- Information = 64
- }
-
- public enum MessageBoxButtons
- {
- OK,
- OKCancel,
- AbortRetryIgnore,
- YesNoCancel,
- YesNo,
- RetryCancel,
- CancelTryContinue
- }
-
- public enum MessageBoxDefaultButton
- {
- Button1,
- Button2 = 256,
- Button3 = 512,
- }
public class MessageBox
{
-
public static Task Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
=> ShowCoreAsync(null, text, caption, buttons, icon, defaultButton);
public static Task Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, bool saveAndRestorePosition = true)
- => ShowCoreAsync(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1, saveAndRestorePosition);
+ => ShowCoreAsync(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1, saveAndRestorePosition);
public static Task Show(string text, string caption, MessageBoxButtons buttons)
- => ShowCoreAsync(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
+ => ShowCoreAsync(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task Show(string text, string caption)
- => ShowCoreAsync(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
+ => ShowCoreAsync(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task Show(string text)
- => ShowCoreAsync(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
- public static Task Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
- => ShowCoreAsync(owner, text, caption, buttons, icon, defaultButton);
-
+ => ShowCoreAsync(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
+ public static Task Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
+ => ShowCoreAsync(owner, text, caption, buttons, icon, defaultButton, saveAndRestorePosition);
public static Task Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
- => ShowCoreAsync(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
+ => ShowCoreAsync(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
public static Task Show(Window owner, string text, string caption, MessageBoxButtons buttons)
- => ShowCoreAsync(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
+ => ShowCoreAsync(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task Show(Window owner, string text, string caption)
- => ShowCoreAsync(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
+ => ShowCoreAsync(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task Show(Window owner, string text)
=> ShowCoreAsync(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
-
public static async Task VerboseLoggingWarning_ShowIfTrue()
{
// when turning on debug (and especially Verbose) to share logs, some privacy settings may not be obscured
if (Serilog.Log.Logger.IsVerboseEnabled())
- await Show(@"
-Warning: verbose logging is enabled.
+ await Show("""
+ Warning: verbose logging is enabled.
-This should be used for debugging only. It creates many
-more logs and debug files, neither of which are as
-strictly anonymous.
+ This should be used for debugging only. It creates many
+ more logs and debug files, neither of which are as
+ strictly anonymous.
-When you are finished debugging, it's highly recommended
-to set your debug MinimumLevel to Information and restart
-Libation.
-".Trim(), "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ When you are finished debugging, it's highly recommended
+ to set your debug MinimumLevel to Information and restart
+ Libation.
+ """, "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
///
@@ -138,7 +92,8 @@ Libation.
{
// for development and debugging, show me what broke!
if (System.Diagnostics.Debugger.IsAttached)
- throw exception;
+ //Wrap the exception to preserve its stack trace.
+ throw new Exception("An unhandled exception was encountered", exception);
try
{
@@ -152,12 +107,12 @@ Libation.
}
private static async Task ShowCoreAsync(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
+ => await Dispatcher.UIThread.InvokeAsync(async () =>
{
owner = owner?.IsLoaded is true ? owner : null;
- var dialog = await Dispatcher.UIThread.InvokeAsync(() => CreateMessageBox(owner, message, caption, buttons, icon, defaultButton, saveAndRestorePosition));
-
+ var dialog = CreateMessageBox(owner, message, caption, buttons, icon, defaultButton, saveAndRestorePosition);
return await DisplayWindow(dialog, owner);
- }
+ });
private static MessageBoxWindow CreateMessageBox(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
{
@@ -175,7 +130,6 @@ Libation.
tbx.MinWidth = vm.TextBlockMinWidth;
tbx.Text = message;
-
var thisScreen = owner.Screens?.ScreenFromVisual(owner);
var maxSize
@@ -229,6 +183,5 @@ Libation.
return await toDisplay.ShowDialog(owner);
}
}
-
}
}
diff --git a/Source/LibationAvalonia/ViewModels/Dialogs/MessageBoxViewModel.cs b/Source/LibationAvalonia/ViewModels/Dialogs/MessageBoxViewModel.cs
index 4bef5275..ecf4bc90 100644
--- a/Source/LibationAvalonia/ViewModels/Dialogs/MessageBoxViewModel.cs
+++ b/Source/LibationAvalonia/ViewModels/Dialogs/MessageBoxViewModel.cs
@@ -1,4 +1,5 @@
-using System;
+using LibationUiBase.Forms;
+using System;
namespace LibationAvalonia.ViewModels.Dialogs
{
diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Filters.cs b/Source/LibationAvalonia/ViewModels/MainVM.Filters.cs
index b355788d..4f8ac7d2 100644
--- a/Source/LibationAvalonia/ViewModels/MainVM.Filters.cs
+++ b/Source/LibationAvalonia/ViewModels/MainVM.Filters.cs
@@ -4,6 +4,7 @@ using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Input;
using LibationFileManager;
+using LibationUiBase.Forms;
using ReactiveUI;
using System;
using System.Linq;
diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Import.cs b/Source/LibationAvalonia/ViewModels/MainVM.Import.cs
index 213a7cf4..952fb045 100644
--- a/Source/LibationAvalonia/ViewModels/MainVM.Import.cs
+++ b/Source/LibationAvalonia/ViewModels/MainVM.Import.cs
@@ -7,6 +7,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Input;
+using LibationUiBase.Forms;
#nullable enable
namespace LibationAvalonia.ViewModels
diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs b/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs
index b69a8ef2..267dc00b 100644
--- a/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs
+++ b/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs
@@ -4,6 +4,9 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using DataLayer;
+using LibationUiBase.Forms;
+using LibationUiBase;
+using System.Collections.Generic;
#nullable enable
namespace LibationAvalonia.ViewModels
@@ -12,19 +15,14 @@ namespace LibationAvalonia.ViewModels
{
public void Configure_Liberate() { }
- public void BackupAllBooks()
+ public async Task BackupAllBooks()
{
try
{
- setQueueCollapseState(false);
+ var unliberated = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking().UnLiberated().ToArray());
- Serilog.Log.Logger.Information("Begin backing up all library books");
-
- ProcessQueue.AddDownloadDecrypt(
- DbContexts
- .GetLibrary_Flat_NoTracking()
- .UnLiberated()
- );
+ if (ProcessQueue.QueueDownloadDecrypt(unliberated))
+ setQueueCollapseState(false);
}
catch (Exception ex)
{
@@ -32,10 +30,10 @@ namespace LibationAvalonia.ViewModels
}
}
- public void BackupAllPdfs()
+ public async Task BackupAllPdfs()
{
- setQueueCollapseState(false);
- ProcessQueue.AddDownloadPdf(DbContexts.GetLibrary_Flat_NoTracking().Where(lb => lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated));
+ if (ProcessQueue.QueueDownloadPdf(await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking())))
+ setQueueCollapseState(false);
}
public async Task ConvertAllToMp3Async()
@@ -48,12 +46,8 @@ namespace LibationAvalonia.ViewModels
"Convert all M4b => Mp3?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
- if (result == DialogResult.Yes)
- {
+ if (result == DialogResult.Yes && ProcessQueue.QueueConvertToMp3(await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking())))
setQueueCollapseState(false);
- ProcessQueue.AddConvertMp3(DbContexts.GetLibrary_Flat_NoTracking().Where(lb => lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is ContentType.Product));
- }
- //Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
}
private void setQueueCollapseState(bool collapsed)
diff --git a/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs b/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs
index b964e6b9..a35f1036 100644
--- a/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs
+++ b/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs
@@ -1,10 +1,11 @@
-using LibationFileManager;
-using System;
-using System.Linq;
-using DataLayer;
+using DataLayer;
using Dinah.Core;
+using LibationFileManager;
+using LibationUiBase;
using LibationUiBase.GridView;
using ReactiveUI;
+using System;
+using System.Linq;
#nullable enable
namespace LibationAvalonia.ViewModels
@@ -37,50 +38,16 @@ namespace LibationAvalonia.ViewModels
{
try
{
- if (libraryBooks.Length == 1)
+ if (ProcessQueue.QueueDownloadDecrypt(libraryBooks))
+ setQueueCollapseState(false);
+ else if (libraryBooks.Length == 1 && libraryBooks[0].Book.Audio_Exists())
{
- var item = libraryBooks[0];
-
- //Remove this item from the queue if it's already present and completed.
- //Only do this when adding a single book at a time to prevent accidental
- //extra downloads when queueing in batches.
- ProcessQueue.RemoveCompleted(item);
-
- if (item.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
+ // liberated: open explorer to file
+ var filePath = AudibleFileStorage.Audio.GetPath(libraryBooks[0].Book.AudibleProductId);
+ if (!Go.To.File(filePath?.ShortPathName))
{
- Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", item);
- setQueueCollapseState(false);
- ProcessQueue.AddDownloadDecrypt(item);
- }
- else if (item.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
- {
- Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
- setQueueCollapseState(false);
- ProcessQueue.AddDownloadPdf(item);
- }
- else if (item.Book.Audio_Exists())
- {
- // liberated: open explorer to file
- var filePath = AudibleFileStorage.Audio.GetPath(item.Book.AudibleProductId);
-
- if (!Go.To.File(filePath?.ShortPathName))
- {
- var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
- await MessageBox.Show($"File not found" + suffix);
- }
- }
- }
- else
- {
- var toLiberate
- = libraryBooks
- .Where(x => x.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload || x.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
- .ToArray();
-
- if (toLiberate.Length > 0)
- {
- setQueueCollapseState(false);
- ProcessQueue.AddDownloadDecrypt(toLiberate);
+ var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
+ await MessageBox.Show($"File not found" + suffix);
}
}
}
@@ -94,11 +61,10 @@ namespace LibationAvalonia.ViewModels
{
try
{
- setQueueCollapseState(false);
-
Serilog.Log.Logger.Information("Begin backing up all {series} episodes", series.LibraryBook);
- ProcessQueue.AddDownloadDecrypt(series.Children.Select(c => c.LibraryBook).UnLiberated());
+ if (ProcessQueue.QueueDownloadDecrypt(series.Children.Select(c => c.LibraryBook).UnLiberated().ToArray()))
+ setQueueCollapseState(false);
}
catch (Exception ex)
{
@@ -110,13 +76,8 @@ namespace LibationAvalonia.ViewModels
{
try
{
- var preLiberated = libraryBooks.Where(lb => lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated).ToArray();
- if (preLiberated.Length > 0)
- {
- Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
+ if (ProcessQueue.QueueConvertToMp3(libraryBooks))
setQueueCollapseState(false);
- ProcessQueue.AddConvertMp3(preLiberated);
- }
}
catch (Exception ex)
{
diff --git a/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs b/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs
index f9a2aeb7..728a0b1f 100644
--- a/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs
+++ b/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs
@@ -5,6 +5,9 @@ using DataLayer;
using Avalonia.Threading;
using LibationAvalonia.Dialogs;
using ReactiveUI;
+using LibationUiBase.Forms;
+using System.Linq;
+using LibationUiBase;
#nullable enable
namespace LibationAvalonia.ViewModels
@@ -71,15 +74,8 @@ namespace LibationAvalonia.ViewModels
{
try
{
- setQueueCollapseState(false);
-
- Serilog.Log.Logger.Information("Begin backing up visible library books");
-
- ProcessQueue.AddDownloadDecrypt(
- ProductsDisplay
- .GetVisibleBookEntries()
- .UnLiberated()
- );
+ if (ProcessQueue.QueueDownloadDecrypt(ProductsDisplay.GetVisibleBookEntries().UnLiberated().ToArray()))
+ setQueueCollapseState(false);
}
catch (Exception ex)
{
diff --git a/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs b/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs
index 908ee707..3bc4b30f 100644
--- a/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs
+++ b/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs
@@ -1,420 +1,17 @@
-using ApplicationServices;
-using AudibleApi;
-using AudibleApi.Common;
-using Avalonia.Media;
-using Avalonia.Media.Imaging;
-using Avalonia.Threading;
-using DataLayer;
-using Dinah.Core;
-using Dinah.Core.ErrorHandling;
-using FileLiberator;
+using DataLayer;
using LibationFileManager;
using LibationUiBase;
-using ReactiveUI;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
+using LibationUiBase.ProcessQueue;
#nullable enable
-namespace LibationAvalonia.ViewModels
+namespace LibationAvalonia.ViewModels;
+
+public class ProcessBookViewModel : ProcessBookViewModelBase
{
- public enum ProcessBookResult
- {
- None,
- Success,
- Cancelled,
- ValidationFail,
- FailedRetry,
- FailedSkip,
- FailedAbort,
- LicenseDenied,
- LicenseDeniedPossibleOutage
- }
- public enum ProcessBookStatus
- {
- Queued,
- Cancelled,
- Working,
- Completed,
- Failed
- }
+ public ProcessBookViewModel(LibraryBook libraryBook, LogMe logme) : base(libraryBook, logme) { }
- ///
- /// This is the viewmodel for queued processables
- ///
- public class ProcessBookViewModel : ViewModelBase
- {
- public event EventHandler? Completed;
+ protected override object? LoadImageFromBytes(byte[] bytes, PictureSize pictureSize)
+ => AvaloniaUtils.TryLoadImageOrDefault(bytes, pictureSize);
- public LibraryBook LibraryBook { get; private set; }
-
- private ProcessBookResult _result = ProcessBookResult.None;
- private ProcessBookStatus _status = ProcessBookStatus.Queued;
- private string? _narrator;
- private string? _author;
- private string? _title;
- private int _progress;
- private string? _eta;
- private Bitmap? _cover;
-
- #region Properties exposed to the view
- public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } }
- public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
- public string? Narrator { get => _narrator; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
- public string? Author { get => _author; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _author, value)); }
- public string? Title { get => _title; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _title, value)); }
- public int Progress { get => _progress; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _progress, value)); }
- public string? ETA { get => _eta; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _eta, value)); }
- public Bitmap? Cover { get => _cover; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
- public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
- public bool IsDownloading => Status is ProcessBookStatus.Working;
- public bool Queued => Status is ProcessBookStatus.Queued;
-
- public string StatusText => Result switch
- {
- ProcessBookResult.Success => "Finished",
- ProcessBookResult.Cancelled => "Cancelled",
- ProcessBookResult.ValidationFail => "Validation fail",
- ProcessBookResult.FailedRetry => "Error, will retry later",
- ProcessBookResult.FailedSkip => "Error, Skipping",
- ProcessBookResult.FailedAbort => "Error, Abort",
- ProcessBookResult.LicenseDenied => "License Denied",
- ProcessBookResult.LicenseDeniedPossibleOutage => "Possible Service Interruption",
- _ => Status.ToString(),
- };
-
- #endregion
-
- private TimeSpan TimeRemaining { set { ETA = $"ETA: {value:mm\\:ss}"; } }
- private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
- private Processable? NextProcessable() => _currentProcessable = null;
- private Processable? _currentProcessable;
- private readonly Queue> Processes = new();
- private readonly LogMe Logger;
-
- public ProcessBookViewModel(LibraryBook libraryBook, LogMe logme)
- {
- LibraryBook = libraryBook;
- Logger = logme;
-
- _title = LibraryBook.Book.TitleWithSubtitle;
- _author = LibraryBook.Book.AuthorNames();
- _narrator = LibraryBook.Book.NarratorNames();
-
- (bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._80x80));
-
- if (isDefault)
- PictureStorage.PictureCached += PictureStorage_PictureCached;
-
- // Mutable property. Set the field so PropertyChanged isn't fired.
- _cover = AvaloniaUtils.TryLoadImageOrDefault(picture, PictureSize._80x80);
- }
-
- private void PictureStorage_PictureCached(object? sender, PictureCachedEventArgs e)
- {
- if (e.Definition.PictureId == LibraryBook.Book.PictureId)
- {
- Cover = AvaloniaUtils.TryLoadImageOrDefault(e.Picture, PictureSize._80x80);
- PictureStorage.PictureCached -= PictureStorage_PictureCached;
- }
- }
-
- public async Task ProcessOneAsync()
- {
- string procName = CurrentProcessable.Name;
- ProcessBookResult result = ProcessBookResult.None;
- try
- {
- LinkProcessable(CurrentProcessable);
-
- var statusHandler = await CurrentProcessable.ProcessSingleAsync(LibraryBook, validate: true);
-
- if (statusHandler.IsSuccess)
- result = ProcessBookResult.Success;
- else if (statusHandler.Errors.Contains("Cancelled"))
- {
- Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}");
- result = ProcessBookResult.Cancelled;
- }
- else if (statusHandler.Errors.Contains("Validation failed"))
- {
- Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}");
- result = ProcessBookResult.ValidationFail;
- }
- else
- {
- foreach (var errorMessage in statusHandler.Errors)
- Logger.Error($"{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}");
- 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}");
- result = ProcessBookResult.LicenseDenied;
- }
- }
- catch (Exception ex)
- {
- Logger.Error(ex, procName);
- }
- finally
- {
- if (result == ProcessBookResult.None)
- result = await showRetry(LibraryBook);
-
- var status = result switch
- {
- ProcessBookResult.Success => ProcessBookStatus.Completed,
- ProcessBookResult.Cancelled => ProcessBookStatus.Cancelled,
- _ => ProcessBookStatus.Failed,
- };
-
- await Dispatcher.UIThread.InvokeAsync(() => Status = status);
- }
-
- await Dispatcher.UIThread.InvokeAsync(() => Result = result);
- return result;
- }
-
- public async Task CancelAsync()
- {
- try
- {
- if (CurrentProcessable is AudioDecodable audioDecodable)
- await audioDecodable.CancelAsync();
- }
- catch (Exception ex)
- {
- Logger.Error(ex, $"{CurrentProcessable.Name}: Error while cancelling");
- }
- }
-
- public void AddDownloadPdf() => AddProcessable();
- public void AddDownloadDecryptBook() => AddProcessable();
- public void AddConvertToMp3() => AddProcessable();
-
- private void AddProcessable() where T : Processable, new()
- {
- Processes.Enqueue(() => new T());
- }
-
- public override string ToString() => LibraryBook.ToString();
-
- #region Subscribers and Unsubscribers
-
- private void LinkProcessable(Processable processable)
- {
- processable.Begin += Processable_Begin;
- processable.Completed += Processable_Completed;
- processable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
- processable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
-
- if (processable is AudioDecodable audioDecodable)
- {
- audioDecodable.RequestCoverArt += AudioDecodable_RequestCoverArt;
- audioDecodable.TitleDiscovered += AudioDecodable_TitleDiscovered;
- audioDecodable.AuthorsDiscovered += AudioDecodable_AuthorsDiscovered;
- audioDecodable.NarratorsDiscovered += AudioDecodable_NarratorsDiscovered;
- audioDecodable.CoverImageDiscovered += AudioDecodable_CoverImageDiscovered;
- }
- }
-
- private void UnlinkProcessable(Processable processable)
- {
- processable.Begin -= Processable_Begin;
- processable.Completed -= Processable_Completed;
- processable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
- processable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
-
- if (processable is AudioDecodable audioDecodable)
- {
- audioDecodable.RequestCoverArt -= AudioDecodable_RequestCoverArt;
- audioDecodable.TitleDiscovered -= AudioDecodable_TitleDiscovered;
- audioDecodable.AuthorsDiscovered -= AudioDecodable_AuthorsDiscovered;
- audioDecodable.NarratorsDiscovered -= AudioDecodable_NarratorsDiscovered;
- audioDecodable.CoverImageDiscovered -= AudioDecodable_CoverImageDiscovered;
- }
- }
-
- #endregion
-
- #region AudioDecodable event handlers
-
- private void AudioDecodable_TitleDiscovered(object? sender, string title) => Title = title;
-
- private void AudioDecodable_AuthorsDiscovered(object? sender, string authors) => Author = authors;
-
- private void AudioDecodable_NarratorsDiscovered(object? sender, string narrators) => Narrator = narrators;
-
-
- private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
- {
- var quality
- = Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
- ? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
- : new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
-
- byte[] coverData = PictureStorage.GetPictureSynchronously(quality);
-
- AudioDecodable_CoverImageDiscovered(this, coverData);
- return coverData;
- }
-
- private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt)
- {
- using var ms = new System.IO.MemoryStream(coverArt);
- Cover = new Avalonia.Media.Imaging.Bitmap(ms);
- }
-
- #endregion
-
- #region Streamable event handlers
- private void Streamable_StreamingTimeRemaining(object? sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining;
-
-
- private void Streamable_StreamingProgressChanged(object? sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
- {
- if (!downloadProgress.ProgressPercentage.HasValue)
- return;
-
- if (downloadProgress.ProgressPercentage == 0)
- TimeRemaining = TimeSpan.Zero;
- else
- Progress = (int)downloadProgress.ProgressPercentage;
- }
-
- #endregion
-
- #region Processable event handlers
-
- private async void Processable_Begin(object? sender, LibraryBook libraryBook)
- {
- await Dispatcher.UIThread.InvokeAsync(() => Status = ProcessBookStatus.Working);
-
- if (sender is Processable processable)
- Logger.Info($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
-
- Title = libraryBook.Book.TitleWithSubtitle;
- Author = libraryBook.Book.AuthorNames();
- Narrator = libraryBook.Book.NarratorNames();
- }
-
- private async void Processable_Completed(object? sender, LibraryBook libraryBook)
- {
- if (sender is Processable processable)
- {
- Logger.Info($"{processable.Name} Step, Completed: {libraryBook.Book}");
- UnlinkProcessable(processable);
- }
-
- if (Processes.Count == 0)
- {
- Completed?.Invoke(this, EventArgs.Empty);
- return;
- }
-
- NextProcessable();
- LinkProcessable(CurrentProcessable);
-
- StatusHandler result;
- try
- {
- result = await CurrentProcessable.ProcessSingleAsync(libraryBook, validate: true);
- }
- catch (Exception ex)
- {
- Serilog.Log.Logger.Error(ex, $"{nameof(Processable_Completed)} error");
-
- result = new StatusHandler();
- result.AddError($"{nameof(Processable_Completed)} error. See log for details. Error summary: {ex.Message}");
- }
-
- if (result.HasErrors)
- {
- foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
- Logger.Error(errorMessage);
-
- Completed?.Invoke(this, EventArgs.Empty);
- }
- }
-
- #endregion
-
- #region Failure Handler
-
- private async Task showRetry(LibraryBook libraryBook)
- {
- Logger.Error("ERROR. All books have not been processed. Most recent book: processing failed");
-
- DialogResult? dialogResult = Configuration.Instance.BadBook switch
- {
- Configuration.BadBookAction.Abort => DialogResult.Abort,
- Configuration.BadBookAction.Retry => DialogResult.Retry,
- Configuration.BadBookAction.Ignore => DialogResult.Ignore,
- Configuration.BadBookAction.Ask => null,
- _ => null
- };
-
- string details;
- try
- {
- static string trunc(string str)
- => string.IsNullOrWhiteSpace(str) ? "[empty]"
- : (str.Length > 50) ? $"{str.Truncate(47)}..."
- : str;
-
- details =
- $@" Title: {libraryBook.Book.TitleWithSubtitle}
- ID: {libraryBook.Book.AudibleProductId}
- Author: {trunc(libraryBook.Book.AuthorNames())}
- Narr: {trunc(libraryBook.Book.NarratorNames())}";
- }
- catch
- {
- details = "[Error retrieving details]";
- }
-
- // if null then ask user
- dialogResult ??= await MessageBox.Show(string.Format(SkipDialogText + "\r\n\r\nSee Settings to avoid this box in the future.", details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
-
- if (dialogResult == DialogResult.Abort)
- return ProcessBookResult.FailedAbort;
-
- if (dialogResult == SkipResult)
- {
- libraryBook.UpdateBookStatus(LiberatedStatus.Error);
-
- Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}");
-
- return ProcessBookResult.FailedSkip;
- }
-
- return ProcessBookResult.FailedRetry;
- }
-
- private static string SkipDialogText => @"
-An error occurred while trying to process this book.
-{0}
-
-- ABORT: Stop processing books.
-
-- RETRY: retry this book later. Just skip it for now. Continue processing books. (Will try this book again later.)
-
-- IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.)
-".Trim();
- private static MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
- private static MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
- private static DialogResult SkipResult => DialogResult.Ignore;
- }
-
- #endregion
-}
+}
\ No newline at end of file
diff --git a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs b/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs
index 29620384..de329357 100644
--- a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs
+++ b/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs
@@ -1,289 +1,74 @@
-using ApplicationServices;
-using Avalonia.Collections;
+using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Threading;
using DataLayer;
using LibationFileManager;
-using LibationUiBase;
-using ReactiveUI;
+using LibationUiBase.ProcessQueue;
using System;
-using System.Collections.Generic;
using System.Collections.ObjectModel;
-using System.Linq;
-using System.Threading.Tasks;
#nullable enable
-namespace LibationAvalonia.ViewModels
+namespace LibationAvalonia.ViewModels;
+
+public record LogEntry(DateTime LogDate, string? LogMessage)
{
+ public string LogDateString => LogDate.ToShortTimeString();
+}
- public class ProcessQueueViewModel : ViewModelBase, ILogForm
+public class ProcessQueueViewModel : ProcessQueueViewModelBase
+{
+ public ProcessQueueViewModel() : base(CreateEmptyList())
{
- public ObservableCollection LogEntries { get; } = new();
- public AvaloniaList Items { get; } = new();
- public TrackedQueue Queue { get; }
- public ProcessBookViewModel? SelectedItem { get; set; }
- public Task? QueueRunner { get; private set; }
- public bool Running => !QueueRunner?.IsCompleted ?? false;
+ Items = Queue.UnderlyingList as AvaloniaList
+ ?? throw new ArgumentNullException(nameof(Queue.UnderlyingList));
- private readonly LogMe Logger;
+ SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
+ }
- public ProcessQueueViewModel()
+ private decimal _speedLimit;
+ public decimal SpeedLimitIncrement { get; private set; }
+ public ObservableCollection LogEntries { get; } = new();
+ public AvaloniaList Items { get; }
+
+ public decimal SpeedLimit
+ {
+ get
{
- Logger = LogMe.RegisterForm(this);
- Queue = new(Items);
- Queue.QueuededCountChanged += Queue_QueuededCountChanged;
- Queue.CompletedCountChanged += Queue_CompletedCountChanged;
-
- if (Design.IsDesignMode)
- _ = Configuration.Instance.LibationFiles;
-
- SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
+ return _speedLimit;
}
-
- private int _completedCount;
- private int _errorCount;
- private int _queuedCount;
- private string? _runningTime;
- private bool _progressBarVisible;
- private decimal _speedLimit;
-
- public int CompletedCount { get => _completedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); }); }
- public int QueuedCount { get => _queuedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); }); }
- public int ErrorCount { get => _errorCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); }); }
- public string? RunningTime { get => _runningTime; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
- public bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
- public bool AnyCompleted => CompletedCount > 0;
- public bool AnyQueued => QueuedCount > 0;
- public bool AnyErrors => ErrorCount > 0;
- public double Progress => 100d * Queue.Completed.Count / Queue.Count;
-
- public decimal SpeedLimit
+ set
{
- get
- {
- return _speedLimit;
- }
- set
- {
- var newValue = Math.Min(999 * 1024 * 1024, (long)(value * 1024 * 1024));
- var config = Configuration.Instance;
- config.DownloadSpeedLimit = newValue;
+ 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;
+ _speedLimit
+ = config.DownloadSpeedLimit <= newValue ? value
+ : value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
+ : 0;
- config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
+ config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
- SpeedLimitIncrement = _speedLimit > 100 ? 10
- : _speedLimit > 10 ? 1
- : _speedLimit > 1 ? 0.1m
- : 0.01m;
+ SpeedLimitIncrement = _speedLimit > 100 ? 10
+ : _speedLimit > 10 ? 1
+ : _speedLimit > 1 ? 0.1m
+ : 0.01m;
- Dispatcher.UIThread.Invoke(() =>
- {
- this.RaisePropertyChanged(nameof(SpeedLimitIncrement));
- this.RaisePropertyChanged();
- });
- }
- }
-
- public decimal SpeedLimitIncrement { get; private set; }
-
- private void Queue_CompletedCountChanged(object? sender, int e)
- {
- int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
- int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
-
- ErrorCount = errCount;
- CompletedCount = completeCount;
- Dispatcher.UIThread.Invoke(() => this.RaisePropertyChanged(nameof(Progress)));
- }
- private void Queue_QueuededCountChanged(object? sender, int cueCount)
- {
- QueuedCount = cueCount;
- Dispatcher.UIThread.Invoke(() => this.RaisePropertyChanged(nameof(Progress)));
- }
-
- public void WriteLine(string text)
- {
- Dispatcher.UIThread.Invoke(() =>
- LogEntries.Add(new()
- {
- LogDate = DateTime.Now,
- LogMessage = text.Trim()
- }));
- }
-
-
- #region Add Books to Queue
-
- private bool isBookInQueue(LibraryBook libraryBook)
- {
- var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
- if (entry == null)
- return false;
- else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
- return !Queue.RemoveCompleted(entry);
- else
- return true;
- }
-
- public bool RemoveCompleted(LibraryBook libraryBook)
- => Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModel entry
- && entry.Status is ProcessBookStatus.Completed
- && Queue.RemoveCompleted(entry);
-
- public void AddDownloadPdf(LibraryBook libraryBook)
- => AddDownloadPdf(new List() { libraryBook });
-
- public void AddDownloadDecrypt(LibraryBook libraryBook)
- => AddDownloadDecrypt(new List() { libraryBook });
-
- public void AddConvertMp3(LibraryBook libraryBook)
- => AddConvertMp3(new List() { libraryBook });
-
- public void AddDownloadPdf(IEnumerable entries)
- {
- List procs = new();
- foreach (var entry in entries)
- {
- if (isBookInQueue(entry))
- continue;
-
- ProcessBookViewModel pbook = new(entry, Logger);
- pbook.AddDownloadPdf();
- procs.Add(pbook);
- }
-
- Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
- AddToQueue(procs);
- }
-
- public void AddDownloadDecrypt(IEnumerable entries)
- {
- List procs = new();
- foreach (var entry in entries)
- {
- if (isBookInQueue(entry))
- continue;
-
- ProcessBookViewModel pbook = new(entry, Logger);
- pbook.AddDownloadDecryptBook();
- pbook.AddDownloadPdf();
- procs.Add(pbook);
- }
-
- Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
- AddToQueue(procs);
- }
-
- public void AddConvertMp3(IEnumerable entries)
- {
- List procs = new();
- foreach (var entry in entries)
- {
- if (isBookInQueue(entry))
- continue;
-
- ProcessBookViewModel pbook = new(entry, Logger);
- pbook.AddConvertToMp3();
- procs.Add(pbook);
- }
-
- Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
- AddToQueue(procs);
- }
-
- public void AddToQueue(IEnumerable pbook)
- {
- Dispatcher.UIThread.Invoke(() =>
- {
- Queue.Enqueue(pbook);
- if (!Running)
- QueueRunner = QueueLoop();
- });
- }
-
- #endregion
-
- DateTime StartingTime;
- private async Task QueueLoop()
- {
- try
- {
- Serilog.Log.Logger.Information("Begin processing queue");
-
- RunningTime = string.Empty;
- ProgressBarVisible = true;
- StartingTime = DateTime.Now;
-
- using var counterTimer = new System.Threading.Timer(CounterTimer_Tick, null, 0, 500);
-
- bool shownServiceOutageMessage = false;
-
- while (Queue.MoveNext())
- {
- if (Queue.Current is not ProcessBookViewModel nextBook)
- {
- Serilog.Log.Logger.Information("Current queue item is empty.");
- continue;
- }
-
- Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook.LibraryBook);
-
- var result = await nextBook.ProcessOneAsync();
-
- Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook.LibraryBook, result);
-
- if (result == ProcessBookResult.ValidationFail)
- Queue.ClearCurrent();
- else if (result == ProcessBookResult.FailedAbort)
- Queue.ClearQueue();
- else if (result == ProcessBookResult.FailedSkip)
- nextBook.LibraryBook.UpdateBookStatus(LiberatedStatus.Error);
- else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage)
- {
- await MessageBox.Show(@$"
-You were denied a content license for {nextBook.LibraryBook.Book.TitleWithSubtitle}
-
-This error appears to be caused by a temporary interruption of service that sometimes affects Libation's users. This type of error usually resolves itself in 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app.
-",
- "Possible Interruption of Service",
- MessageBoxButtons.OK,
- MessageBoxIcon.Asterisk);
- shownServiceOutageMessage = true;
- }
- }
- Serilog.Log.Logger.Information("Completed processing queue");
-
- Queue_CompletedCountChanged(this, 0);
- ProgressBarVisible = false;
- }
- catch (Exception ex)
- {
- Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items");
- }
- }
-
- private void CounterTimer_Tick(object? state)
- {
- string timeToStr(TimeSpan time)
- {
- string minsSecs = $"{time:mm\\:ss}";
- if (time.TotalHours >= 1)
- return $"{time.TotalHours:F0}:{minsSecs}";
- return minsSecs;
- }
- RunningTime = timeToStr(DateTime.Now - StartingTime);
+ RaisePropertyChanged(nameof(SpeedLimitIncrement));
+ RaisePropertyChanged(nameof(SpeedLimit));
}
}
- public class LogEntry
+ 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 CreateEmptyList()
{
- public DateTime LogDate { get; init; }
- public string LogDateString => LogDate.ToShortTimeString();
- public string? LogMessage { get; init; }
+ if (Design.IsDesignMode)
+ _ = Configuration.Instance.LibationFiles;
+ return new AvaloniaList();
}
}
diff --git a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs
index 13e282f5..e7416e14 100644
--- a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs
+++ b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs
@@ -5,8 +5,8 @@ using Avalonia.Controls;
using Avalonia.Threading;
using DataLayer;
using Dinah.Core.Collections.Generic;
-using LibationAvalonia.Dialogs.Login;
using LibationFileManager;
+using LibationUiBase.Forms;
using LibationUiBase.GridView;
using ReactiveUI;
using System;
diff --git a/Source/LibationAvalonia/Views/MainWindow.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.axaml.cs
index 22262c5a..7132e6f5 100644
--- a/Source/LibationAvalonia/Views/MainWindow.axaml.cs
+++ b/Source/LibationAvalonia/Views/MainWindow.axaml.cs
@@ -7,6 +7,7 @@ using FileManager;
using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels;
using LibationFileManager;
+using LibationUiBase.Forms;
using LibationUiBase.GridView;
using ReactiveUI;
using System;
diff --git a/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs
index b79ea913..9e66987d 100644
--- a/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs
+++ b/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs
@@ -4,6 +4,7 @@ using Avalonia.Controls;
using DataLayer;
using LibationAvalonia.ViewModels;
using LibationUiBase;
+using LibationUiBase.ProcessQueue;
namespace LibationAvalonia.Views
{
diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml
index 8d3a8ebd..f65a400e 100644
--- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml
+++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml
@@ -35,6 +35,11 @@
VerticalScrollBarVisibility="Auto"
AllowAutoHide="False">
+
+
+
+
+
diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
index de001556..97ae56f9 100644
--- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
+++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
@@ -5,6 +5,7 @@ using Avalonia.Data.Converters;
using DataLayer;
using LibationAvalonia.ViewModels;
using LibationUiBase;
+using LibationUiBase.ProcessQueue;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -16,7 +17,7 @@ namespace LibationAvalonia.Views
{
public partial class ProcessQueueControl : UserControl
{
- private TrackedQueue? Queue => _viewModel?.Queue;
+ private TrackedQueue? Queue => _viewModel?.Queue;
private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel;
public ProcessQueueControl()
diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
index d94adb16..7cc9b681 100644
--- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
+++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
@@ -2,7 +2,6 @@ using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input.Platform;
-using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using DataLayer;
@@ -13,6 +12,7 @@ using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels;
using LibationFileManager;
using LibationFileManager.Templates;
+using LibationUiBase.Forms;
using LibationUiBase.GridView;
using ReactiveUI;
using System;
diff --git a/Source/LibationAvalonia/Walkthrough.cs b/Source/LibationAvalonia/Walkthrough.cs
index 2c50a505..a170d139 100644
--- a/Source/LibationAvalonia/Walkthrough.cs
+++ b/Source/LibationAvalonia/Walkthrough.cs
@@ -9,6 +9,7 @@ using Dinah.Core.StepRunner;
using LibationAvalonia.Dialogs;
using LibationAvalonia.Views;
using LibationFileManager;
+using LibationUiBase.Forms;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
diff --git a/Source/LibationFileManager/LibationFileManager.csproj b/Source/LibationFileManager/LibationFileManager.csproj
index 8712a88b..cdd49260 100644
--- a/Source/LibationFileManager/LibationFileManager.csproj
+++ b/Source/LibationFileManager/LibationFileManager.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/Source/LibationUiBase/GridView/EntryStatus.cs b/Source/LibationUiBase/GridView/EntryStatus.cs
index 1e8bfc0d..e2354372 100644
--- a/Source/LibationUiBase/GridView/EntryStatus.cs
+++ b/Source/LibationUiBase/GridView/EntryStatus.cs
@@ -16,9 +16,8 @@ namespace LibationUiBase.GridView
//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 : SynchronizeInvoker, IComparable, INotifyPropertyChanged
+ public abstract class EntryStatus : ReactiveObject, IComparable
{
- public event PropertyChangedEventHandler PropertyChanged;
public LiberatedStatus? PdfStatus => LibraryCommands.Pdf_Status(Book);
public LiberatedStatus BookStatus
{
@@ -81,8 +80,6 @@ namespace LibationUiBase.GridView
internal protected abstract object LoadImage(byte[] picture);
protected abstract object GetResourceImage(string rescName);
- public void RaisePropertyChanged(PropertyChangedEventArgs args) => this.UIThreadSync(() => PropertyChanged?.Invoke(this, args));
- public void RaisePropertyChanged(string propertyName) => RaisePropertyChanged(new PropertyChangedEventArgs(propertyName));
/// Refresh BookStatus (so partial download files are checked again in the filesystem) and raise PropertyChanged for property names.
public void Invalidate(params string[] properties)
@@ -104,7 +101,13 @@ namespace LibationUiBase.GridView
else if (!IsUnavailable && second.IsUnavailable) return -1;
else if (BookStatus == LiberatedStatus.Liberated && second.BookStatus != LiberatedStatus.Liberated) return -1;
else if (BookStatus != LiberatedStatus.Liberated && second.BookStatus == LiberatedStatus.Liberated) return 1;
- else return BookStatus.CompareTo(second.BookStatus);
+
+ var statusCompare = BookStatus.CompareTo(second.BookStatus);
+ if (statusCompare != 0) return statusCompare;
+ else if (PdfStatus is null && second.PdfStatus is null) return 0;
+ else if (PdfStatus is null && second.PdfStatus is not null) return 1;
+ else if (PdfStatus is not null && second.PdfStatus is null) return -1;
+ else return PdfStatus.Value.CompareTo(second.PdfStatus.Value);
}
private object GetLiberateIcon()
diff --git a/Source/LibationUiBase/GridView/GridEntry[TStatus].cs b/Source/LibationUiBase/GridView/GridEntry[TStatus].cs
index f8ef11d4..a57b52db 100644
--- a/Source/LibationUiBase/GridView/GridEntry[TStatus].cs
+++ b/Source/LibationUiBase/GridView/GridEntry[TStatus].cs
@@ -22,7 +22,7 @@ namespace LibationUiBase.GridView
}
/// The View Model base for the DataGridView
- public abstract class GridEntry : SynchronizeInvoker, IGridEntry where TStatus : IEntryStatus
+ public abstract class GridEntry : ReactiveObject, IGridEntry where TStatus : IEntryStatus
{
[Browsable(false)] public string AudibleProductId => Book.AudibleProductId;
[Browsable(false)] public LibraryBook LibraryBook { get; protected set; }
@@ -183,19 +183,6 @@ namespace LibationUiBase.GridView
}
}
- private TRet RaiseAndSetIfChanged(ref TRet backingField, TRet newValue, [CallerMemberName] string propertyName = null)
- {
- if (EqualityComparer.Default.Equals(backingField, newValue)) return newValue;
-
- backingField = newValue;
- RaisePropertyChanged(new PropertyChangedEventArgs(propertyName));
- return newValue;
- }
-
- public event PropertyChangedEventHandler PropertyChanged;
- public void RaisePropertyChanged(PropertyChangedEventArgs args) => this.UIThreadSync(() => PropertyChanged?.Invoke(this, args));
- public void RaisePropertyChanged(string propertyName) => RaisePropertyChanged(new PropertyChangedEventArgs(propertyName));
-
#endregion
#region Sorting
@@ -228,16 +215,16 @@ namespace LibationUiBase.GridView
// Instantiate comparers for every exposed member object type.
private static readonly Dictionary memberTypeComparers = new()
{
- { typeof(RemoveStatus), new ObjectComparer() },
- { typeof(string), new ObjectComparer() },
- { typeof(int), new ObjectComparer() },
- { typeof(float), new ObjectComparer() },
- { typeof(bool), new ObjectComparer() },
- { typeof(Rating), new ObjectComparer() },
- { typeof(DateTime), new ObjectComparer() },
- { typeof(EntryStatus), new ObjectComparer() },
- { typeof(SeriesOrder), new ObjectComparer() },
- { typeof(LastDownloadStatus), new ObjectComparer() },
+ { typeof(RemoveStatus), Comparer.Default },
+ { typeof(string), Comparer.Default },
+ { typeof(int), Comparer .Default },
+ { typeof(float), Comparer.Default },
+ { typeof(bool), Comparer.Default },
+ { typeof(Rating), Comparer.Default },
+ { typeof(DateTime), Comparer.Default },
+ { typeof(EntryStatus), Comparer.Default },
+ { typeof(SeriesOrder), Comparer.Default },
+ { typeof(LastDownloadStatus), Comparer.Default },
};
#endregion
diff --git a/Source/LibationUiBase/GridView/ObjectComparer[T].cs b/Source/LibationUiBase/GridView/ObjectComparer[T].cs
deleted file mode 100644
index e66beca0..00000000
--- a/Source/LibationUiBase/GridView/ObjectComparer[T].cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-using System.Collections;
-
-namespace LibationUiBase.GridView
-{
- public class ObjectComparer : IComparer where T : IComparable
- {
- public int Compare(object x, object y) => ((T)x).CompareTo(y);
- }
-}
diff --git a/Source/LibationUiBase/LibationUiBase.csproj b/Source/LibationUiBase/LibationUiBase.csproj
index 9493bebe..e863eea2 100644
--- a/Source/LibationUiBase/LibationUiBase.csproj
+++ b/Source/LibationUiBase/LibationUiBase.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/Source/LibationUiBase/MessageBoxBase.cs b/Source/LibationUiBase/MessageBoxBase.cs
new file mode 100644
index 00000000..4ff8c24d
--- /dev/null
+++ b/Source/LibationUiBase/MessageBoxBase.cs
@@ -0,0 +1,133 @@
+using MathNet.Numerics;
+using System.IO;
+using System.Threading.Tasks;
+
+#nullable enable
+namespace LibationUiBase.Forms;
+
+public enum DialogResult
+{
+ /// Nothing is returned from the dialog box. This means that the modal dialog continues running.
+ None = 0,
+ /// The dialog box return value is OK (usually sent from a button labeled OK).
+ OK = 1, //IDOK
+ /// The dialog box return value is Cancel (usually sent from a button labeled Cancel).
+ Cancel = 2, //IDCANCEL
+ /// The dialog box return value is Abort (usually sent from a button labeled Abort).
+ Abort = 3, //IDABORT
+ /// The dialog box return value is Retry (usually sent from a button labeled Retry).
+ Retry = 4, //IDRETRY
+ /// The dialog box return value is Ignore (usually sent from a button labeled Ignore).
+ Ignore = 5, //IDIGNORE
+ /// The dialog box return value is Yes (usually sent from a button labeled Yes).
+ Yes = 6, //IDYES
+ /// The dialog box return value is No (usually sent from a button labeled No).
+ No = 7, //IDNO
+ /// The dialog box return value is Try Again (usually sent from a button labeled Try Again).
+ TryAgain = 10, //IDTRYAGAIN
+ /// The dialog box return value is Continue (usually sent from a button labeled Continue).
+ Continue = 11 //IDCONTINUE
+}
+
+public enum MessageBoxIcon
+{
+ /// Specifies that the message box contain no symbols.
+ None = 0x00000000,
+ /// Specifies that the message box contains a hand symbol.
+ Hand = 0x00000010, //MB_ICONHAND
+ /// Specifies that the message box contains a question mark symbol.
+ Question = 0x00000020, //MB_ICONQUESTION
+ /// Specifies that the message box contains an exclamation symbol.
+ Exclamation = 0x00000030, //MB_ICONEXCLAMATION
+ /// Specifies that the message box contains an asterisk symbol.
+ Asterisk = 0x00000040, //MB_ICONASTERISK
+ /// Specifies that the message box contains a hand icon. This field is constant.
+ Stop = Hand,
+ /// Specifies that the message box contains a hand icon.
+ Error = Hand,
+ /// Specifies that the message box contains an exclamation icon.
+ Warning = Exclamation,
+ /// Specifies that the message box contains an asterisk icon.
+ Information = Asterisk
+}
+
+public enum MessageBoxButtons
+{
+ /// Specifies that the message box contains an OK button.
+ OK = 0x00000000, //MB_OK
+ /// Specifies that the message box contains OK and Cancel buttons.
+ OKCancel = 0x00000001, //MB_OKCANCEL
+ /// Specifies that the message box contains Abort, Retry, and Ignore buttons.
+ AbortRetryIgnore = 0x00000002, //MB_ABORTRETRYIGNORE
+ /// Specifies that the message box contains Yes, No, and Cancel buttons.
+ YesNoCancel = 0x00000003, //MB_YESNOCANCEL
+ /// Specifies that the message box contains Yes and No buttons.
+ YesNo = 0x00000004, //MB_YESNO
+ /// Specifies that the message box contains Retry and Cancel buttons.
+ RetryCancel = 0x00000005, //MB_RETRYCANCEL
+ /// Specifies that the message box contains Cancel, Try Again, and Continue buttons.
+ CancelTryContinue = 0x00000006 //MB_CANCELTRYCONTINUE
+}
+
+public enum MessageBoxDefaultButton
+{
+ /// Specifies that the first button on the message box should be the default button.
+ Button1 = 0x00000000, //MB_DEFBUTTON1
+ /// Specifies that the second button on the message box should be the default button.
+ Button2 = 0x00000100, //MB_DEFBUTTON2
+ /// Specifies that the third button on the message box should be the default button.
+ Button3 = 0x00000200, //MB_DEFBUTTON3
+ /// Specifies that the Help button on the message box should be the default button.
+ Button4 = 0x00000300, //MB_DEFBUTTON4
+}
+
+///
+/// Displays a message box in front of the specified object and with the specified text, caption, buttons, icon, and default button.
+///
+/// An implementation of a GUI window that will own the modal dialog box
+/// The text to display in the message box
+/// The text to display in the title bar of the message box
+/// One of the values that specifies which buttons to disply in the message box
+/// One of the values that specifies which icon to disply in the message box
+/// One of the values that specifies the default button of the message box
+/// A value indicating whether the message box's position should be saved and restored the next time it is shown
+/// One of the values
+public delegate Task ShowAsyncDelegate(object? owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true);
+
+public static class MessageBoxBase
+{
+ private static ShowAsyncDelegate? s_ShowAsyncImpl;
+ public static ShowAsyncDelegate ShowAsyncImpl
+ {
+ get => s_ShowAsyncImpl ?? DefaultShowAsyncImpl;
+ set => s_ShowAsyncImpl = value;
+ }
+
+ private static Task DefaultShowAsyncImpl(object? owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
+ {
+ // default to a no-op impl
+ Serilog.Log.Logger.Error("MessageBoxBase implementation not set. {@DebugInfo}", new { owner, message, caption, buttons, icon, defaultButton });
+ return Task.FromResult(DialogResult.None);
+ }
+
+ public static Task Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
+ => ShowAsyncImpl(null, text, caption, buttons, icon, defaultButton);
+ public static Task Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, bool saveAndRestorePosition = true)
+ => ShowAsyncImpl(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1, saveAndRestorePosition);
+ public static Task Show(string text, string caption, MessageBoxButtons buttons)
+ => ShowAsyncImpl(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
+ public static Task Show(string text, string caption)
+ => ShowAsyncImpl(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
+ public static Task Show(string text)
+ => ShowAsyncImpl(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
+ public static Task Show(object? owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
+ => ShowAsyncImpl(owner, text, caption, buttons, icon, defaultButton);
+ public static Task Show(object? owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
+ => ShowAsyncImpl(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
+ public static Task Show(object? owner, string text, string caption, MessageBoxButtons buttons)
+ => ShowAsyncImpl(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
+ public static Task Show(object? owner, string text, string caption)
+ => ShowAsyncImpl(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
+ public static Task Show(object? owner, string text)
+ => ShowAsyncImpl(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
+}
\ No newline at end of file
diff --git a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs
new file mode 100644
index 00000000..62b00e7d
--- /dev/null
+++ b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs
@@ -0,0 +1,414 @@
+using ApplicationServices;
+using AudibleApi;
+using AudibleApi.Common;
+using DataLayer;
+using Dinah.Core;
+using Dinah.Core.ErrorHandling;
+using FileLiberator;
+using LibationFileManager;
+using LibationUiBase.Forms;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+#nullable enable
+namespace LibationUiBase.ProcessQueue;
+
+public enum ProcessBookResult
+{
+ None,
+ Success,
+ Cancelled,
+ ValidationFail,
+ FailedRetry,
+ FailedSkip,
+ FailedAbort,
+ LicenseDenied,
+ LicenseDeniedPossibleOutage
+}
+
+public enum ProcessBookStatus
+{
+ Queued,
+ Cancelled,
+ Working,
+ Completed,
+ Failed
+}
+
+///
+/// This is the viewmodel for queued processables
+///
+public abstract class ProcessBookViewModelBase : ReactiveObject
+{
+ private readonly LogMe Logger;
+ public LibraryBook LibraryBook { get; protected set; }
+
+ private ProcessBookResult _result = ProcessBookResult.None;
+ private ProcessBookStatus _status = ProcessBookStatus.Queued;
+ private string? _narrator;
+ private string? _author;
+ private string? _title;
+ private int _progress;
+ private string? _eta;
+ private object? _cover;
+ private TimeSpan _timeRemaining;
+
+ #region Properties exposed to the view
+ public ProcessBookResult Result { get => _result; set { RaiseAndSetIfChanged(ref _result, value); RaisePropertyChanged(nameof(StatusText)); } }
+ public ProcessBookStatus Status { get => _status; set { RaiseAndSetIfChanged(ref _status, value); RaisePropertyChanged(nameof(IsFinished)); RaisePropertyChanged(nameof(IsDownloading)); RaisePropertyChanged(nameof(Queued)); } }
+ public string? Narrator { get => _narrator; set => RaiseAndSetIfChanged(ref _narrator, value); }
+ public string? Author { get => _author; set => RaiseAndSetIfChanged(ref _author, value); }
+ public string? Title { get => _title; set => RaiseAndSetIfChanged(ref _title, value); }
+ public int Progress { get => _progress; protected set => RaiseAndSetIfChanged(ref _progress, value); }
+ public TimeSpan TimeRemaining { get => _timeRemaining; set { RaiseAndSetIfChanged(ref _timeRemaining, value); ETA = $"ETA: {value:mm\\:ss}"; } }
+ public string? ETA { get => _eta; private set => RaiseAndSetIfChanged(ref _eta, value); }
+ public object? Cover { get => _cover; protected set => RaiseAndSetIfChanged(ref _cover, value); }
+ public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
+ public bool IsDownloading => Status is ProcessBookStatus.Working;
+ public bool Queued => Status is ProcessBookStatus.Queued;
+
+ public string StatusText => Result switch
+ {
+ ProcessBookResult.Success => "Finished",
+ ProcessBookResult.Cancelled => "Cancelled",
+ ProcessBookResult.ValidationFail => "Validation fail",
+ ProcessBookResult.FailedRetry => "Error, will retry later",
+ ProcessBookResult.FailedSkip => "Error, Skipping",
+ ProcessBookResult.FailedAbort => "Error, Abort",
+ ProcessBookResult.LicenseDenied => "License Denied",
+ ProcessBookResult.LicenseDeniedPossibleOutage => "Possible Service Interruption",
+ _ => Status.ToString(),
+ };
+
+ #endregion
+
+ protected Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
+ protected void NextProcessable() => _currentProcessable = null;
+ private Processable? _currentProcessable;
+
+ /// A series of Processable actions to perform on this book
+ protected Queue> Processes { get; } = new();
+
+ protected ProcessBookViewModelBase(LibraryBook libraryBook, LogMe logme)
+ {
+ LibraryBook = libraryBook;
+ Logger = logme;
+
+ _title = LibraryBook.Book.TitleWithSubtitle;
+ _author = LibraryBook.Book.AuthorNames();
+ _narrator = LibraryBook.Book.NarratorNames();
+
+ (bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._80x80));
+
+ if (isDefault)
+ PictureStorage.PictureCached += PictureStorage_PictureCached;
+
+ // Mutable property. Set the field so PropertyChanged isn't fired.
+ _cover = LoadImageFromBytes(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);
+ PictureStorage.PictureCached -= PictureStorage_PictureCached;
+ }
+ }
+
+ public async Task ProcessOneAsync()
+ {
+ string procName = CurrentProcessable.Name;
+ ProcessBookResult result = ProcessBookResult.None;
+ try
+ {
+ LinkProcessable(CurrentProcessable);
+
+ var statusHandler = await CurrentProcessable.ProcessSingleAsync(LibraryBook, validate: true);
+
+ if (statusHandler.IsSuccess)
+ result = ProcessBookResult.Success;
+ else if (statusHandler.Errors.Contains("Cancelled"))
+ {
+ Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}");
+ result = ProcessBookResult.Cancelled;
+ }
+ else if (statusHandler.Errors.Contains("Validation failed"))
+ {
+ Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}");
+ result = ProcessBookResult.ValidationFail;
+ }
+ else
+ {
+ foreach (var errorMessage in statusHandler.Errors)
+ Logger.Error($"{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}");
+ 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}");
+ result = ProcessBookResult.LicenseDenied;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, procName);
+ }
+ finally
+ {
+ if (result == ProcessBookResult.None)
+ result = await GetFailureActionAsync(LibraryBook);
+
+ var status = result switch
+ {
+ ProcessBookResult.Success => ProcessBookStatus.Completed,
+ ProcessBookResult.Cancelled => ProcessBookStatus.Cancelled,
+ _ => ProcessBookStatus.Failed,
+ };
+
+ Status = status;
+ }
+
+ Result = result;
+ return result;
+ }
+
+ public async Task CancelAsync()
+ {
+ try
+ {
+ if (CurrentProcessable is AudioDecodable audioDecodable)
+ await audioDecodable.CancelAsync();
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"{CurrentProcessable.Name}: Error while cancelling");
+ }
+ }
+
+ public ProcessBookViewModelBase AddDownloadPdf() => AddProcessable();
+ public ProcessBookViewModelBase AddDownloadDecryptBook() => AddProcessable();
+ public ProcessBookViewModelBase AddConvertToMp3() => AddProcessable();
+
+ private ProcessBookViewModelBase AddProcessable() where T : Processable, new()
+ {
+ Processes.Enqueue(() => new T());
+ return this;
+ }
+
+ public override string ToString() => LibraryBook.ToString();
+
+ #region Subscribers and Unsubscribers
+
+ private void LinkProcessable(Processable processable)
+ {
+ processable.Begin += Processable_Begin;
+ processable.Completed += Processable_Completed;
+ processable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
+ processable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
+
+ if (processable is AudioDecodable audioDecodable)
+ {
+ audioDecodable.RequestCoverArt += AudioDecodable_RequestCoverArt;
+ audioDecodable.TitleDiscovered += AudioDecodable_TitleDiscovered;
+ audioDecodable.AuthorsDiscovered += AudioDecodable_AuthorsDiscovered;
+ audioDecodable.NarratorsDiscovered += AudioDecodable_NarratorsDiscovered;
+ audioDecodable.CoverImageDiscovered += AudioDecodable_CoverImageDiscovered;
+ }
+ }
+
+ private void UnlinkProcessable(Processable processable)
+ {
+ processable.Begin -= Processable_Begin;
+ processable.Completed -= Processable_Completed;
+ processable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
+ processable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
+
+ if (processable is AudioDecodable audioDecodable)
+ {
+ audioDecodable.RequestCoverArt -= AudioDecodable_RequestCoverArt;
+ audioDecodable.TitleDiscovered -= AudioDecodable_TitleDiscovered;
+ audioDecodable.AuthorsDiscovered -= AudioDecodable_AuthorsDiscovered;
+ audioDecodable.NarratorsDiscovered -= AudioDecodable_NarratorsDiscovered;
+ audioDecodable.CoverImageDiscovered -= AudioDecodable_CoverImageDiscovered;
+ }
+ }
+
+ #endregion
+
+ #region AudioDecodable event handlers
+
+ private void AudioDecodable_TitleDiscovered(object? sender, string title) => Title = title;
+ 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);
+
+ private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
+ {
+ var quality
+ = Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
+ ? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
+ : new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
+
+ byte[] coverData = PictureStorage.GetPictureSynchronously(quality);
+
+ AudioDecodable_CoverImageDiscovered(this, coverData);
+ return coverData;
+ }
+
+ #endregion
+
+ #region Streamable event handlers
+
+ private void Streamable_StreamingTimeRemaining(object? sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining;
+ private void Streamable_StreamingProgressChanged(object? sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
+ {
+ if (!downloadProgress.ProgressPercentage.HasValue)
+ return;
+
+ if (downloadProgress.ProgressPercentage == 0)
+ TimeRemaining = TimeSpan.Zero;
+ else
+ Progress = (int)downloadProgress.ProgressPercentage;
+ }
+
+ #endregion
+
+ #region Processable event handlers
+
+ private void Processable_Begin(object? sender, LibraryBook libraryBook)
+ {
+ Status = ProcessBookStatus.Working;
+
+ if (sender is Processable processable)
+ Logger.Info($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
+
+ Title = libraryBook.Book.TitleWithSubtitle;
+ Author = libraryBook.Book.AuthorNames();
+ Narrator = libraryBook.Book.NarratorNames();
+ }
+
+ private async void Processable_Completed(object? sender, LibraryBook libraryBook)
+ {
+ if (sender is Processable processable)
+ {
+ Logger.Info($"{processable.Name} Step, Completed: {libraryBook.Book}");
+ UnlinkProcessable(processable);
+ }
+
+ if (Processes.Count == 0)
+ return;
+
+ NextProcessable();
+ LinkProcessable(CurrentProcessable);
+
+ StatusHandler result;
+ try
+ {
+ result = await CurrentProcessable.ProcessSingleAsync(libraryBook, validate: true);
+ }
+ catch (Exception ex)
+ {
+ Serilog.Log.Logger.Error(ex, $"{nameof(Processable_Completed)} error");
+
+ result = new StatusHandler();
+ result.AddError($"{nameof(Processable_Completed)} error. See log for details. Error summary: {ex.Message}");
+ }
+
+ if (result.HasErrors)
+ {
+ foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
+ Logger.Error(errorMessage);
+ }
+ }
+
+ #endregion
+
+ #region Failure Handler
+
+ protected async Task GetFailureActionAsync(LibraryBook libraryBook)
+ {
+ const DialogResult SkipResult = DialogResult.Ignore;
+ Logger.Error($"ERROR. All books have not been processed. Book failed: {libraryBook.Book}");
+
+ DialogResult? dialogResult = Configuration.Instance.BadBook switch
+ {
+ Configuration.BadBookAction.Abort => DialogResult.Abort,
+ Configuration.BadBookAction.Retry => DialogResult.Retry,
+ Configuration.BadBookAction.Ignore => DialogResult.Ignore,
+ Configuration.BadBookAction.Ask or _ => await ShowRetryDialogAsync(libraryBook)
+ };
+
+ if (dialogResult == SkipResult)
+ {
+ libraryBook.UpdateBookStatus(LiberatedStatus.Error);
+ Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}");
+ }
+
+ return dialogResult is SkipResult ? ProcessBookResult.FailedSkip
+ : dialogResult is DialogResult.Abort ? ProcessBookResult.FailedAbort
+ : ProcessBookResult.FailedRetry;
+ }
+
+ protected async Task ShowRetryDialogAsync(LibraryBook libraryBook)
+ {
+ string details;
+ try
+ {
+ static string trunc(string str)
+ => string.IsNullOrWhiteSpace(str) ? "[empty]"
+ : (str.Length > 50) ? $"{str.Truncate(47)}..."
+ : str;
+
+ details = $"""
+ Title: {libraryBook.Book.TitleWithSubtitle}
+ ID: {libraryBook.Book.AudibleProductId}
+ Author: {trunc(libraryBook.Book.AuthorNames())}
+ Narr: {trunc(libraryBook.Book.NarratorNames())}
+ """;
+ }
+ catch
+ {
+ details = "[Error retrieving details]";
+ }
+
+ var skipDialogText = $"""
+ An error occurred while trying to process this book.
+ {details}
+
+ - ABORT: Stop processing books.
+
+ - RETRY: Skip this book for now, but retry if it is requeued. Continue processing the queued books.
+
+ - IGNORE: Permanently ignore this book. Continue processing the queued books. (Will not try this book again later.)
+
+ See Settings in the Download/Decrypt tab to avoid this box in the future.
+ """;
+
+ const MessageBoxButtons SkipDialogButtons = MessageBoxButtons.AbortRetryIgnore;
+ const MessageBoxDefaultButton SkipDialogDefaultButton = MessageBoxDefaultButton.Button1;
+
+ try
+ {
+ return await MessageBoxBase.Show(skipDialogText, "Skip this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
+ }
+ catch (Exception ex)
+ {
+ Serilog.Log.Logger.Error(ex, "Error showing retry dialog. Defaulting to 'Retry'; action.");
+ return DialogResult.Retry;
+ }
+ }
+
+ #endregion
+}
diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs
new file mode 100644
index 00000000..a6452ceb
--- /dev/null
+++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs
@@ -0,0 +1,235 @@
+using DataLayer;
+using LibationUiBase.Forms;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using ApplicationServices;
+using System.Threading.Tasks;
+
+#nullable enable
+namespace LibationUiBase.ProcessQueue;
+
+public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
+{
+ public abstract void WriteLine(string text);
+ protected abstract ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook);
+
+ public TrackedQueue Queue { get; }
+ public Task? QueueRunner { get; private set; }
+ public bool Running => !QueueRunner?.IsCompleted ?? false;
+ protected LogMe Logger { get; }
+
+ public ProcessQueueViewModelBase(ICollection? underlyingList)
+ {
+ Logger = LogMe.RegisterForm(this);
+ Queue = new(underlyingList);
+ Queue.QueuedCountChanged += Queue_QueuedCountChanged;
+ Queue.CompletedCountChanged += Queue_CompletedCountChanged;
+ }
+
+ private int _completedCount;
+ private int _errorCount;
+ private int _queuedCount;
+ private string? _runningTime;
+ private bool _progressBarVisible;
+
+ 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)); } }
+ public int ErrorCount { get => _errorCount; private set { RaiseAndSetIfChanged(ref _errorCount, value); RaisePropertyChanged(nameof(AnyErrors)); } }
+ public string? RunningTime { get => _runningTime; set => RaiseAndSetIfChanged(ref _runningTime, value); }
+ public bool ProgressBarVisible { get => _progressBarVisible; set => RaiseAndSetIfChanged(ref _progressBarVisible, value); }
+ public bool AnyCompleted => CompletedCount > 0;
+ public bool AnyQueued => QueuedCount > 0;
+ public bool AnyErrors => ErrorCount > 0;
+ public double Progress => 100d * Queue.Completed.Count / Queue.Count;
+
+ private void Queue_CompletedCountChanged(object? sender, int e)
+ {
+ int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
+ int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
+
+ ErrorCount = errCount;
+ CompletedCount = completeCount;
+ RaisePropertyChanged(nameof(Progress));
+ }
+
+ private void Queue_QueuedCountChanged(object? sender, int cueCount)
+ {
+ QueuedCount = cueCount;
+ RaisePropertyChanged(nameof(Progress));
+ }
+
+ #region Add Books to Queue
+
+ public bool QueueDownloadPdf(IList libraryBooks)
+ {
+ var needsPdf = libraryBooks.Where(lb => lb.NeedsPdfDownload()).ToArray();
+ if (needsPdf.Length > 0)
+ {
+ Serilog.Log.Logger.Information("Begin download {count} pdfs", needsPdf.Length);
+ AddDownloadPdf(needsPdf);
+ return true;
+ }
+ return false;
+ }
+
+ public bool QueueConvertToMp3(IList libraryBooks)
+ {
+ //Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
+ var preLiberated = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product).ToArray();
+ if (preLiberated.Length > 0)
+ {
+ Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
+ AddConvertMp3(preLiberated);
+ return true;
+ }
+ return false;
+ }
+
+ public bool QueueDownloadDecrypt(IList libraryBooks)
+ {
+ if (libraryBooks.Count == 1)
+ {
+ var item = libraryBooks[0];
+
+ if (item.AbsentFromLastScan)
+ return false;
+ else if (item.NeedsBookDownload())
+ {
+ RemoveCompleted(item);
+ Serilog.Log.Logger.Information("Begin single library book backup of {libraryBook}", item);
+ AddDownloadDecrypt([item]);
+ return true;
+ }
+ else if (item.NeedsPdfDownload())
+ {
+ RemoveCompleted(item);
+ Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
+ AddDownloadPdf([item]);
+ return true;
+ }
+ }
+ else
+ {
+ var toLiberate = libraryBooks.UnLiberated().ToArray();
+
+ if (toLiberate.Length > 0)
+ {
+ Serilog.Log.Logger.Information("Begin backup of {count} library books", toLiberate.Length);
+ AddDownloadDecrypt(toLiberate);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private bool IsBookInQueue(LibraryBook libraryBook)
+ => Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is not ProcessBookViewModelBase 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
+ && entry.Status is ProcessBookStatus.Completed
+ && Queue.RemoveCompleted(entry);
+
+ private void AddDownloadPdf(IEnumerable 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();
+ }
+
+ private void AddDownloadDecrypt(IEnumerable 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();
+ }
+
+ private void AddConvertMp3(IEnumerable 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();
+ }
+
+ private void AddToQueue(IEnumerable pbook)
+ {
+ Queue.Enqueue(pbook);
+ if (!Running)
+ QueueRunner = Task.Run(QueueLoop);
+ }
+
+ #endregion
+
+ private async Task QueueLoop()
+ {
+ try
+ {
+ Serilog.Log.Logger.Information("Begin processing queue");
+
+ RunningTime = string.Empty;
+ ProgressBarVisible = true;
+ var startingTime = DateTime.Now;
+ bool shownServiceOutageMessage = false;
+
+ using var counterTimer = new System.Threading.Timer(_ => RunningTime = timeToStr(DateTime.Now - startingTime), null, 0, 500);
+
+ while (Queue.MoveNext())
+ {
+ if (Queue.Current is not ProcessBookViewModelBase nextBook)
+ {
+ Serilog.Log.Logger.Information("Current queue item is empty.");
+ continue;
+ }
+
+ Serilog.Log.Logger.Information("Begin processing queued item: '{item_LibraryBook}'", nextBook.LibraryBook);
+
+ var result = await nextBook.ProcessOneAsync();
+
+ Serilog.Log.Logger.Information("Completed processing queued item: '{item_LibraryBook}' with result: {result}", nextBook.LibraryBook, result);
+
+ if (result == ProcessBookResult.ValidationFail)
+ Queue.ClearCurrent();
+ else if (result == ProcessBookResult.FailedAbort)
+ Queue.ClearQueue();
+ else if (result == ProcessBookResult.FailedSkip)
+ nextBook.LibraryBook.UpdateBookStatus(LiberatedStatus.Error);
+ else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage)
+ {
+ await MessageBoxBase.Show($"""
+ You were denied a content license for {nextBook.LibraryBook.Book.TitleWithSubtitle}
+
+ This error appears to be caused by a temporary interruption of service that sometimes affects Libation's users. This type of error usually resolves itself in 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app.
+ """,
+ "Possible Interruption of Service",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Asterisk);
+ shownServiceOutageMessage = true;
+ }
+ }
+ Serilog.Log.Logger.Information("Completed processing queue");
+
+ Queue_CompletedCountChanged(this, 0);
+ ProgressBarVisible = false;
+ }
+ catch (Exception ex)
+ {
+ Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items");
+ }
+
+ string timeToStr(TimeSpan time)
+ => time.TotalHours < 1 ? $"{time:mm\\:ss}"
+ : $"{time.TotalHours:F0}:{time:mm\\:ss}";
+ }
+}
diff --git a/Source/LibationUiBase/ReactiveObject.cs b/Source/LibationUiBase/ReactiveObject.cs
new file mode 100644
index 00000000..e86ed991
--- /dev/null
+++ b/Source/LibationUiBase/ReactiveObject.cs
@@ -0,0 +1,33 @@
+using Dinah.Core.Threading;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+#nullable enable
+namespace LibationUiBase;
+
+public class ReactiveObject : SynchronizeInvoker, INotifyPropertyChanged, INotifyPropertyChanging
+{
+ public event PropertyChangedEventHandler? PropertyChanged;
+ public event PropertyChangingEventHandler? PropertyChanging;
+
+ public void RaisePropertyChanging(PropertyChangingEventArgs args) => Invoke(() => PropertyChanging?.Invoke(this, args));
+ public void RaisePropertyChanging(string propertyName) => RaisePropertyChanging(new PropertyChangingEventArgs(propertyName));
+ public void RaisePropertyChanged(PropertyChangedEventArgs args) => Invoke(() => PropertyChanged?.Invoke(this, args));
+ public void RaisePropertyChanged(string propertyName) => RaisePropertyChanged(new PropertyChangedEventArgs(propertyName));
+
+ public TRet RaiseAndSetIfChanged(ref TRet backingField, TRet newValue, [CallerMemberName] string? propertyName = null)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(propertyName, nameof(propertyName));
+
+ if (!EqualityComparer.Default.Equals(backingField, newValue))
+ {
+ RaisePropertyChanging(propertyName);
+ backingField = newValue;
+ RaisePropertyChanged(propertyName!);
+ }
+
+ return newValue;
+ }
+}
diff --git a/Source/LibationUiBase/TrackedQueue[T].cs b/Source/LibationUiBase/TrackedQueue[T].cs
index 51f36f0b..746f8c6b 100644
--- a/Source/LibationUiBase/TrackedQueue[T].cs
+++ b/Source/LibationUiBase/TrackedQueue[T].cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
+#nullable enable
namespace LibationUiBase
{
public enum QueuePosition
@@ -34,10 +35,10 @@ namespace LibationUiBase
*/
public class TrackedQueue where T : class
{
- public event EventHandler CompletedCountChanged;
- public event EventHandler QueuededCountChanged;
+ public event EventHandler? CompletedCountChanged;
+ public event EventHandler? QueuedCountChanged;
- public T Current { get; private set; }
+ public T? Current { get; private set; }
public IReadOnlyList Queued => _queued;
public IReadOnlyList Completed => _completed;
@@ -46,9 +47,10 @@ namespace LibationUiBase
private readonly List _completed = new();
private readonly object lockObject = new();
- private readonly ICollection _underlyingList;
+ private readonly ICollection? _underlyingList;
+ public ICollection? UnderlyingList => _underlyingList;
- public TrackedQueue(ICollection underlyingList = null)
+ public TrackedQueue(ICollection? underlyingList = null)
{
_underlyingList = underlyingList;
}
@@ -113,7 +115,7 @@ namespace LibationUiBase
if (itemsRemoved)
{
- QueuededCountChanged?.Invoke(this, queuedCount);
+ QueuedCountChanged?.Invoke(this, queuedCount);
RebuildSecondary();
}
return itemsRemoved;
@@ -149,7 +151,7 @@ namespace LibationUiBase
{
lock (lockObject)
_queued.Clear();
- QueuededCountChanged?.Invoke(this, 0);
+ QueuedCountChanged?.Invoke(this, 0);
RebuildSecondary();
}
@@ -169,7 +171,7 @@ namespace LibationUiBase
}
}
- public T FirstOrDefault(Func predicate)
+ public T? FirstOrDefault(Func predicate)
{
lock (lockObject)
{
@@ -246,7 +248,7 @@ namespace LibationUiBase
{
if (completedChanged)
CompletedCountChanged?.Invoke(this, completedCount);
- QueuededCountChanged?.Invoke(this, queuedCount);
+ QueuedCountChanged?.Invoke(this, queuedCount);
RebuildSecondary();
}
}
@@ -261,7 +263,7 @@ namespace LibationUiBase
}
foreach (var i in item)
_underlyingList?.Add(i);
- QueuededCountChanged?.Invoke(this, queueCount);
+ QueuedCountChanged?.Invoke(this, queueCount);
}
private void RebuildSecondary()
diff --git a/Source/LibationWinForms/Form1.Liberate.cs b/Source/LibationWinForms/Form1.Liberate.cs
index 256b977e..9b905be4 100644
--- a/Source/LibationWinForms/Form1.Liberate.cs
+++ b/Source/LibationWinForms/Form1.Liberate.cs
@@ -1,4 +1,5 @@
using DataLayer;
+using LibationUiBase;
using System;
using System.Linq;
using System.Threading.Tasks;
@@ -11,19 +12,13 @@ namespace LibationWinForms
private void Configure_Liberate() { }
//GetLibrary_Flat_NoTracking() may take a long time on a hugh library. so run in new thread
- private void beginBookBackupsToolStripMenuItem_Click(object _ = null, EventArgs __ = null)
+ private async void beginBookBackupsToolStripMenuItem_Click(object _ = null, EventArgs __ = null)
{
try
{
- SetQueueCollapseState(false);
-
- Serilog.Log.Logger.Information("Begin backing up all library books");
-
- processBookQueue1.AddDownloadDecrypt(
- ApplicationServices.DbContexts
- .GetLibrary_Flat_NoTracking()
- .UnLiberated()
- );
+ var unliberated = await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking().UnLiberated().ToArray());
+ if (processBookQueue1.ViewModel.QueueDownloadDecrypt(unliberated))
+ SetQueueCollapseState(false);
}
catch (Exception ex)
{
@@ -33,9 +28,8 @@ namespace LibationWinForms
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
{
- SetQueueCollapseState(false);
- await Task.Run(() => processBookQueue1.AddDownloadPdf(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
- .Where(lb => lb.Book.UserDefinedItem.PdfStatus is DataLayer.LiberatedStatus.NotLiberated)));
+ if (processBookQueue1.ViewModel.QueueDownloadPdf(await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking())))
+ SetQueueCollapseState(false);
}
private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e)
@@ -48,13 +42,8 @@ namespace LibationWinForms
"Convert all M4b => Mp3?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
- if (result == DialogResult.Yes)
- {
+ if (result == DialogResult.Yes && processBookQueue1.ViewModel.QueueConvertToMp3(await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking())))
SetQueueCollapseState(false);
- await Task.Run(() => processBookQueue1.AddConvertMp3(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
- .Where(lb => lb.Book.UserDefinedItem.BookStatus is DataLayer.LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product)));
- }
- //Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
}
}
}
diff --git a/Source/LibationWinForms/Form1.ProcessQueue.cs b/Source/LibationWinForms/Form1.ProcessQueue.cs
index f9c7ccc4..f6260e5e 100644
--- a/Source/LibationWinForms/Form1.ProcessQueue.cs
+++ b/Source/LibationWinForms/Form1.ProcessQueue.cs
@@ -1,6 +1,8 @@
using DataLayer;
using Dinah.Core;
using LibationFileManager;
+using LibationUiBase;
+using LibationUiBase.Forms;
using LibationUiBase.GridView;
using LibationWinForms.ProcessQueue;
using System;
@@ -14,7 +16,7 @@ namespace LibationWinForms
int WidthChange = 0;
private void Configure_ProcessQueue()
{
- processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
+ processBookQueue1.PopoutButton.Click += ProcessBookQueue1_PopOut;
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
int width = this.Width;
@@ -27,49 +29,16 @@ namespace LibationWinForms
{
try
{
- if (libraryBooks.Length == 1)
+ if (processBookQueue1.ViewModel.QueueDownloadDecrypt(libraryBooks))
+ SetQueueCollapseState(false);
+ else if (libraryBooks.Length == 1 && libraryBooks[0].Book.Audio_Exists())
{
- var item = libraryBooks[0];
-
- //Remove this item from the queue if it's already present and completed.
- //Only do this when adding a single book at a time to prevent accidental
- //extra downloads when queueing in batches.
- processBookQueue1.RemoveCompleted(item);
-
- if (item.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
+ // liberated: open explorer to file
+ var filePath = AudibleFileStorage.Audio.GetPath(libraryBooks[0].Book.AudibleProductId);
+ if (!Go.To.File(filePath?.ShortPathName))
{
- Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", item);
- SetQueueCollapseState(false);
- processBookQueue1.AddDownloadDecrypt(item);
- }
- else if (item.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
- {
- Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
- SetQueueCollapseState(false);
- processBookQueue1.AddDownloadPdf(item);
- }
- else if (item.Book.Audio_Exists())
- {
- // liberated: open explorer to file
- var filePath = AudibleFileStorage.Audio.GetPath(item.Book.AudibleProductId);
- if (!Go.To.File(filePath?.ShortPathName))
- {
- var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
- MessageBox.Show($"File not found" + suffix);
- }
- }
- }
- else
- {
- var toLiberate
- = libraryBooks
- .Where(x => x.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload || x.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
- .ToArray();
-
- if (toLiberate.Length > 0)
- {
- SetQueueCollapseState(false);
- processBookQueue1.AddDownloadDecrypt(toLiberate);
+ var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
+ MessageBox.Show($"File not found" + suffix);
}
}
}
@@ -83,11 +52,10 @@ namespace LibationWinForms
{
try
{
- SetQueueCollapseState(false);
-
Serilog.Log.Logger.Information("Begin backing up all {series} episodes", series.LibraryBook);
- processBookQueue1.AddDownloadDecrypt(series.Children.Select(c => c.LibraryBook).UnLiberated());
+ if (processBookQueue1.ViewModel.QueueDownloadDecrypt(series.Children.Select(c => c.LibraryBook).UnLiberated().ToArray()))
+ SetQueueCollapseState(false);
}
catch (Exception ex)
{
@@ -99,13 +67,8 @@ namespace LibationWinForms
{
try
{
- var preLiberated = libraryBooks.Where(lb => lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated).ToArray();
- if (preLiberated.Length > 0)
- {
- Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
+ if (processBookQueue1.ViewModel.QueueConvertToMp3(libraryBooks))
SetQueueCollapseState(false);
- processBookQueue1.AddConvertMp3(preLiberated);
- }
}
catch (Exception ex)
{
@@ -124,10 +87,14 @@ namespace LibationWinForms
}
else if (!collapsed && splitContainer1.Panel2Collapsed)
{
+ if (!processBookQueue1.PopoutButton.Visible)
+ //Queue is in popout mode. Do nothing.
+ return;
+
Width += WidthChange;
splitContainer1.Panel2.Controls.Add(processBookQueue1);
splitContainer1.Panel2Collapsed = false;
- processBookQueue1.popoutBtn.Visible = true;
+ processBookQueue1.PopoutButton.Visible = true;
}
Configuration.Instance.SetNonString(splitContainer1.Panel2Collapsed, nameof(splitContainer1.Panel2Collapsed));
@@ -147,7 +114,7 @@ namespace LibationWinForms
dockForm.FormClosing += DockForm_FormClosing;
splitContainer1.Panel2.Controls.Remove(processBookQueue1);
splitContainer1.Panel2Collapsed = true;
- processBookQueue1.popoutBtn.Visible = false;
+ processBookQueue1.PopoutButton.Visible = false;
dockForm.PassControl(processBookQueue1);
dockForm.Show();
this.Width -= dockForm.WidthChange;
@@ -164,7 +131,7 @@ namespace LibationWinForms
this.Width += dockForm.WidthChange;
splitContainer1.Panel2.Controls.Add(dockForm.RegainControl());
splitContainer1.Panel2Collapsed = false;
- processBookQueue1.popoutBtn.Visible = true;
+ processBookQueue1.PopoutButton.Visible = true;
dockForm.SaveSizeAndLocation(Configuration.Instance);
this.Focus();
toggleQueueHideBtn.Visible = true;
diff --git a/Source/LibationWinForms/Form1.VisibleBooks.cs b/Source/LibationWinForms/Form1.VisibleBooks.cs
index ac24d058..fd248c3b 100644
--- a/Source/LibationWinForms/Form1.VisibleBooks.cs
+++ b/Source/LibationWinForms/Form1.VisibleBooks.cs
@@ -5,6 +5,7 @@ using System.Windows.Forms;
using ApplicationServices;
using DataLayer;
using Dinah.Core.Threading;
+using LibationUiBase;
using LibationWinForms.Dialogs;
namespace LibationWinForms
@@ -25,9 +26,17 @@ namespace LibationWinForms
}
private async void setLiberatedVisibleMenuItemAsync(object _, object __)
=> await Task.Run(setLiberatedVisibleMenuItem);
+
+ private static DateTime lastVisibleCountUpdated;
void setLiberatedVisibleMenuItem()
{
+ //Assume that all calls to update arrive in order,
+ //Only display results of the latest book count.
+ var updaterTime = lastVisibleCountUpdated = DateTime.UtcNow;
var libraryStats = LibraryCommands.GetCounts(productsDisplay.GetVisible());
+ if (updaterTime < lastVisibleCountUpdated)
+ return;
+
this.UIThreadSync(() =>
{
if (libraryStats.HasPendingBooks)
@@ -50,15 +59,8 @@ namespace LibationWinForms
{
try
{
- SetQueueCollapseState(false);
-
- Serilog.Log.Logger.Information("Begin backing up visible library books");
-
- processBookQueue1.AddDownloadDecrypt(
- productsDisplay
- .GetVisible()
- .UnLiberated()
- );
+ if (processBookQueue1.ViewModel.QueueDownloadDecrypt(productsDisplay.GetVisible().UnLiberated().ToArray()))
+ SetQueueCollapseState(false);
}
catch (Exception ex)
{
diff --git a/Source/LibationWinForms/MessageBoxLib.cs b/Source/LibationWinForms/MessageBoxLib.cs
index 62a7f9f8..e189ef1f 100644
--- a/Source/LibationWinForms/MessageBoxLib.cs
+++ b/Source/LibationWinForms/MessageBoxLib.cs
@@ -23,7 +23,8 @@ namespace LibationWinForms
{
// for development and debugging, show me what broke!
if (System.Diagnostics.Debugger.IsAttached)
- throw exception;
+ //Wrap the exception to preserve its stack trace.
+ throw new Exception("An unhandled exception was encountered", exception);
try
{
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBook.cs b/Source/LibationWinForms/ProcessQueue/ProcessBook.cs
deleted file mode 100644
index ce09658a..00000000
--- a/Source/LibationWinForms/ProcessQueue/ProcessBook.cs
+++ /dev/null
@@ -1,410 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Drawing;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Threading.Tasks;
-using System.Windows.Forms;
-using ApplicationServices;
-using AudibleApi.Common;
-using AudibleApi;
-using DataLayer;
-using Dinah.Core;
-using Dinah.Core.ErrorHandling;
-using FileLiberator;
-using LibationFileManager;
-using LibationUiBase;
-
-namespace LibationWinForms.ProcessQueue
-{
- public enum ProcessBookResult
- {
- None,
- Success,
- Cancelled,
- ValidationFail,
- FailedRetry,
- FailedSkip,
- FailedAbort,
- LicenseDenied,
- LicenseDeniedPossibleOutage
- }
-
- public enum ProcessBookStatus
- {
- Queued,
- Cancelled,
- Working,
- Completed,
- Failed
- }
-
- ///
- /// This is the viewmodel for queued processables
- ///
- public class ProcessBook : INotifyPropertyChanged
- {
- public event EventHandler Completed;
- public event PropertyChangedEventHandler PropertyChanged;
-
- private ProcessBookResult _result = ProcessBookResult.None;
- private ProcessBookStatus _status = ProcessBookStatus.Queued;
- private string _bookText;
- private int _progress;
- private TimeSpan _timeRemaining;
- private Image _cover;
-
- public ProcessBookResult Result { get => _result; private set { _result = value; NotifyPropertyChanged(); } }
- public ProcessBookStatus Status { get => _status; private set { _status = value; NotifyPropertyChanged(); } }
- public string BookText { get => _bookText; private set { _bookText = value; NotifyPropertyChanged(); } }
- public int Progress { get => _progress; private set { _progress = value; NotifyPropertyChanged(); } }
- public TimeSpan TimeRemaining { get => _timeRemaining; private set { _timeRemaining = value; NotifyPropertyChanged(); } }
- public Image Cover { get => _cover; private set { _cover = value; NotifyPropertyChanged(); } }
-
- public LibraryBook LibraryBook { get; private set; }
- private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
- private Processable NextProcessable() => _currentProcessable = null;
- private Processable _currentProcessable;
- private readonly Queue> Processes = new();
- private readonly LogMe Logger;
-
- public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
- => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
-
- public ProcessBook(LibraryBook libraryBook, LogMe logme)
- {
- LibraryBook = libraryBook;
- Logger = logme;
-
- title = LibraryBook.Book.TitleWithSubtitle;
- authorNames = LibraryBook.Book.AuthorNames();
- narratorNames = LibraryBook.Book.NarratorNames();
- _bookText = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}";
-
- (bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._80x80));
-
- if (isDefault)
- PictureStorage.PictureCached += PictureStorage_PictureCached;
- _cover = WinFormsUtil.TryLoadImageOrDefault(picture, PictureSize._80x80); ;
-
- }
-
- private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
- {
- if (e.Definition.PictureId == LibraryBook.Book.PictureId)
- {
- Cover = WinFormsUtil.TryLoadImageOrDefault(e.Picture, PictureSize._80x80);
- PictureStorage.PictureCached -= PictureStorage_PictureCached;
- }
- }
-
- public async Task ProcessOneAsync()
- {
- string procName = CurrentProcessable.Name;
- try
- {
- LinkProcessable(CurrentProcessable);
-
- var statusHandler = await CurrentProcessable.ProcessSingleAsync(LibraryBook, validate: true);
-
- if (statusHandler.IsSuccess)
- return Result = ProcessBookResult.Success;
- else if (statusHandler.Errors.Contains("Cancelled"))
- {
- Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}");
- return Result = ProcessBookResult.Cancelled;
- }
- else if (statusHandler.Errors.Contains("Validation failed"))
- {
- Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}");
- return Result = ProcessBookResult.ValidationFail;
- }
-
- foreach (var errorMessage in statusHandler.Errors)
- Logger.Error($"{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}");
- return 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}");
- return Result = ProcessBookResult.LicenseDenied;
- }
- }
- catch (Exception ex)
- {
- Logger.Error(ex, procName);
- }
- finally
- {
- if (Result == ProcessBookResult.None)
- Result = showRetry(LibraryBook);
-
- Status = Result switch
- {
- ProcessBookResult.Success => ProcessBookStatus.Completed,
- ProcessBookResult.Cancelled => ProcessBookStatus.Cancelled,
- _ => ProcessBookStatus.Failed,
- };
- }
-
- return Result;
- }
-
- public async Task CancelAsync()
- {
- try
- {
- if (CurrentProcessable is AudioDecodable audioDecodable)
- await audioDecodable.CancelAsync();
- }
- catch (Exception ex)
- {
- Logger.Error(ex, $"{CurrentProcessable.Name}: Error while cancelling");
- }
- }
-
- public void AddDownloadPdf() => AddProcessable();
- public void AddDownloadDecryptBook() => AddProcessable();
- public void AddConvertToMp3() => AddProcessable();
-
- private void AddProcessable() where T : Processable, new()
- {
- Processes.Enqueue(() => new T());
- }
-
- public override string ToString() => LibraryBook.ToString();
-
- #region Subscribers and Unsubscribers
-
- private void LinkProcessable(Processable processable)
- {
- processable.Begin += Processable_Begin;
- processable.Completed += Processable_Completed;
- processable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
- processable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
-
- if (processable is AudioDecodable audioDecodable)
- {
- audioDecodable.RequestCoverArt += AudioDecodable_RequestCoverArt;
- audioDecodable.TitleDiscovered += AudioDecodable_TitleDiscovered;
- audioDecodable.AuthorsDiscovered += AudioDecodable_AuthorsDiscovered;
- audioDecodable.NarratorsDiscovered += AudioDecodable_NarratorsDiscovered;
- audioDecodable.CoverImageDiscovered += AudioDecodable_CoverImageDiscovered;
- }
- }
-
- private void UnlinkProcessable(Processable processable)
- {
- processable.Begin -= Processable_Begin;
- processable.Completed -= Processable_Completed;
- processable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
- processable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
-
- if (processable is AudioDecodable audioDecodable)
- {
- audioDecodable.RequestCoverArt -= AudioDecodable_RequestCoverArt;
- audioDecodable.TitleDiscovered -= AudioDecodable_TitleDiscovered;
- audioDecodable.AuthorsDiscovered -= AudioDecodable_AuthorsDiscovered;
- audioDecodable.NarratorsDiscovered -= AudioDecodable_NarratorsDiscovered;
- audioDecodable.CoverImageDiscovered -= AudioDecodable_CoverImageDiscovered;
- }
- }
-
- #endregion
-
- #region AudioDecodable event handlers
-
- private string title;
- private string authorNames;
- private string narratorNames;
- private void AudioDecodable_TitleDiscovered(object sender, string title)
- {
- this.title = title;
- updateBookInfo();
- }
-
- private void AudioDecodable_AuthorsDiscovered(object sender, string authors)
- {
- authorNames = authors;
- updateBookInfo();
- }
-
- private void AudioDecodable_NarratorsDiscovered(object sender, string narrators)
- {
- narratorNames = narrators;
- updateBookInfo();
- }
-
- private void updateBookInfo()
- {
- BookText = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}";
- }
-
- private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
- {
- var quality
- = Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
- ? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
- : new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
-
- byte[] coverData = PictureStorage.GetPictureSynchronously(quality);
-
- AudioDecodable_CoverImageDiscovered(this, coverData);
- return coverData;
- }
-
- private void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
- {
- Cover = WinFormsUtil.TryLoadImageOrDefault(coverArt, PictureSize._80x80);
- }
-
- #endregion
-
- #region Streamable event handlers
- private void Streamable_StreamingTimeRemaining(object sender, TimeSpan timeRemaining)
- {
- TimeRemaining = timeRemaining;
- }
-
- private void Streamable_StreamingProgressChanged(object sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
- {
- if (!downloadProgress.ProgressPercentage.HasValue)
- return;
-
- if (downloadProgress.ProgressPercentage == 0)
- TimeRemaining = TimeSpan.Zero;
- else
- Progress = (int)downloadProgress.ProgressPercentage;
- }
-
- #endregion
-
- #region Processable event handlers
-
- private void Processable_Begin(object sender, LibraryBook libraryBook)
- {
- Status = ProcessBookStatus.Working;
-
- Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}");
-
- title = libraryBook.Book.TitleWithSubtitle;
- authorNames = libraryBook.Book.AuthorNames();
- narratorNames = libraryBook.Book.NarratorNames();
- updateBookInfo();
- }
-
- private async void Processable_Completed(object sender, LibraryBook libraryBook)
- {
- Logger.Info($"{((Processable)sender).Name} Step, Completed: {libraryBook.Book}");
- UnlinkProcessable((Processable)sender);
-
- if (Processes.Count == 0)
- {
- Completed?.Invoke(this, EventArgs.Empty);
- return;
- }
-
- NextProcessable();
- LinkProcessable(CurrentProcessable);
-
- StatusHandler result;
- try
- {
- result = await CurrentProcessable.ProcessSingleAsync(libraryBook, validate: true);
- }
- catch (Exception ex)
- {
- Serilog.Log.Logger.Error(ex, $"{nameof(Processable_Completed)} error");
-
- result = new StatusHandler();
- result.AddError($"{nameof(Processable_Completed)} error. See log for details. Error summary: {ex.Message}");
- }
-
- if (result.HasErrors)
- {
- foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
- Logger.Error(errorMessage);
-
- Completed?.Invoke(this, EventArgs.Empty);
- }
- }
-
- #endregion
-
- #region Failure Handler
-
- private ProcessBookResult showRetry(LibraryBook libraryBook)
- {
- Logger.Error("ERROR. All books have not been processed. Most recent book: processing failed");
-
- DialogResult? dialogResult = Configuration.Instance.BadBook switch
- {
- Configuration.BadBookAction.Abort => DialogResult.Abort,
- Configuration.BadBookAction.Retry => DialogResult.Retry,
- Configuration.BadBookAction.Ignore => DialogResult.Ignore,
- Configuration.BadBookAction.Ask => null,
- _ => null
- };
-
- string details;
- try
- {
- static string trunc(string str)
- => string.IsNullOrWhiteSpace(str) ? "[empty]"
- : (str.Length > 50) ? $"{str.Truncate(47)}..."
- : str;
-
- details =
-$@" Title: {libraryBook.Book.TitleWithSubtitle}
- ID: {libraryBook.Book.AudibleProductId}
- Author: {trunc(libraryBook.Book.AuthorNames())}
- Narr: {trunc(libraryBook.Book.NarratorNames())}";
- }
- catch
- {
- details = "[Error retrieving details]";
- }
-
- // if null then ask user
- dialogResult ??= MessageBox.Show(string.Format(SkipDialogText + "\r\n\r\nSee Settings to avoid this box in the future.", details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
-
- if (dialogResult == DialogResult.Abort)
- return ProcessBookResult.FailedAbort;
-
- if (dialogResult == SkipResult)
- {
- libraryBook.UpdateBookStatus(LiberatedStatus.Error);
-
- Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}");
-
- return ProcessBookResult.FailedSkip;
- }
-
- return ProcessBookResult.FailedRetry;
- }
-
-
- private string SkipDialogText => @"
-An error occurred while trying to process this book.
-{0}
-
-- ABORT: Stop processing books.
-
-- RETRY: retry this book later. Just skip it for now. Continue processing books. (Will try this book again later.)
-
-- IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.)
-".Trim();
- private MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
- private MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
- private DialogResult SkipResult => DialogResult.Ignore;
- }
-
- #endregion
-}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs
index 9f840338..8bcfd6a2 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs
@@ -1,4 +1,5 @@
-using System;
+using LibationUiBase.ProcessQueue;
+using System;
using System.Drawing;
using System.Windows.Forms;
@@ -6,45 +7,74 @@ namespace LibationWinForms.ProcessQueue
{
internal partial class ProcessBookControl : UserControl
{
- private static int ControlNumberCounter = 0;
-
- ///
- /// The contol's position within
- ///
- public int ControlNumber { get; }
- private ProcessBookStatus Status { get; set; } = ProcessBookStatus.Queued;
private readonly int CancelBtnDistanceFromEdge;
private readonly int ProgressBarDistanceFromEdge;
- public static Color FailedColor = Color.LightCoral;
- public static Color CancelledColor = Color.Khaki;
- public static Color QueuedColor = SystemColors.Control;
- public static Color SuccessColor = Color.PaleGreen;
+ 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();
- statusLbl.Text = "Queued";
remainingTimeLbl.Visible = false;
progressBar1.Visible = false;
etaLbl.Visible = false;
CancelBtnDistanceFromEdge = Width - cancelBtn.Location.X;
ProgressBarDistanceFromEdge = Width - progressBar1.Location.X - progressBar1.Width;
- ControlNumber = ControlNumberCounter++;
}
- public void SetCover(Image cover)
+ private void OnContextChanging()
{
- pictureBox1.Image = cover;
+ if (Context is not null)
+ Context.PropertyChanged -= Context_PropertyChanged;
}
- public void SetBookInfo(string title)
+ private void OnContextChanged()
{
- bookInfoLbl.Text = title;
+ Context.PropertyChanged += Context_PropertyChanged;
+ Context_PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(null));
}
- public void SetProgrss(int progress)
+ private void Context_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ 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);
+ ResumeLayout();
+ }
+
+ 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}";
+
+ private void SetProgress(int progress)
{
//Disable slow fill
//https://stackoverflow.com/a/5332770/3335599
@@ -53,34 +83,9 @@ namespace LibationWinForms.ProcessQueue
progressBar1.Value = progress;
}
- public void SetRemainingTime(TimeSpan remaining)
+ private void SetStatus(ProcessBookStatus status, string statusText)
{
- remainingTimeLbl.Text = $"{remaining:mm\\:ss}";
- }
-
- public void SetResult(ProcessBookResult result)
- {
- (string statusText, ProcessBookStatus status) = result switch
- {
- ProcessBookResult.Success => ("Finished", ProcessBookStatus.Completed),
- ProcessBookResult.Cancelled => ("Cancelled", ProcessBookStatus.Cancelled),
- ProcessBookResult.FailedRetry => ("Error, will retry later", ProcessBookStatus.Failed),
- ProcessBookResult.FailedSkip => ("Error, Skipping", ProcessBookStatus.Failed),
- ProcessBookResult.FailedAbort => ("Error, Abort", ProcessBookStatus.Failed),
- ProcessBookResult.ValidationFail => ("Validation fail", ProcessBookStatus.Failed),
- ProcessBookResult.LicenseDenied => ("License Denied", ProcessBookStatus.Failed),
- ProcessBookResult.LicenseDeniedPossibleOutage => ("Possible Service Interruption", ProcessBookStatus.Failed),
- _ => ("UNKNOWN", ProcessBookStatus.Failed),
- };
-
- SetStatus(status, statusText);
- }
-
- public void SetStatus(ProcessBookStatus status, string statusText = null)
- {
- Status = status;
-
- Color backColor = Status switch
+ Color backColor = status switch
{
ProcessBookStatus.Completed => SuccessColor,
ProcessBookStatus.Cancelled => CancelledColor,
@@ -89,27 +94,25 @@ namespace LibationWinForms.ProcessQueue
_ => FailedColor
};
- SuspendLayout();
-
- cancelBtn.Visible = Status is ProcessBookStatus.Queued or ProcessBookStatus.Working;
- moveLastBtn.Visible = Status == ProcessBookStatus.Queued;
- moveDownBtn.Visible = Status == ProcessBookStatus.Queued;
- moveUpBtn.Visible = Status == ProcessBookStatus.Queued;
- moveFirstBtn.Visible = Status == ProcessBookStatus.Queued;
- remainingTimeLbl.Visible = Status == ProcessBookStatus.Working;
- progressBar1.Visible = Status == ProcessBookStatus.Working;
- etaLbl.Visible = Status == ProcessBookStatus.Working;
- statusLbl.Visible = Status != ProcessBookStatus.Working;
- statusLbl.Text = statusText ?? Status.ToString();
+ cancelBtn.Visible = status is ProcessBookStatus.Queued or ProcessBookStatus.Working;
+ moveLastBtn.Visible = status == ProcessBookStatus.Queued;
+ moveDownBtn.Visible = status == ProcessBookStatus.Queued;
+ moveUpBtn.Visible = status == ProcessBookStatus.Queued;
+ moveFirstBtn.Visible = status == ProcessBookStatus.Queued;
+ remainingTimeLbl.Visible = status == ProcessBookStatus.Working;
+ progressBar1.Visible = status == ProcessBookStatus.Working;
+ etaLbl.Visible = status == ProcessBookStatus.Working;
+ statusLbl.Visible = status != ProcessBookStatus.Working;
+ statusLbl.Text = statusText;
BackColor = backColor;
int deltaX = Width - cancelBtn.Location.X - CancelBtnDistanceFromEdge;
- if (Status is ProcessBookStatus.Queued or ProcessBookStatus.Working && deltaX != 0)
+ if (status is ProcessBookStatus.Queued or ProcessBookStatus.Working && deltaX != 0)
{
//If the last book to occupy this control before resizing was not
//queued, the buttons were not Visible so the Anchor property was
- //ignored. Manually resize and reposition everyhting
+ //ignored. Manually resize and reposition everything
cancelBtn.Location = new Point(cancelBtn.Location.X + deltaX, cancelBtn.Location.Y);
moveFirstBtn.Location = new Point(moveFirstBtn.Location.X + deltaX, moveFirstBtn.Location.Y);
@@ -129,13 +132,8 @@ namespace LibationWinForms.ProcessQueue
{
bookInfoLbl.Width = moveLastBtn.Location.X - bookInfoLbl.Location.X - bookInfoLbl.Padding.Left + moveLastBtn.Padding.Right;
}
-
- ResumeLayout();
}
- public override string ToString()
- {
- return bookInfoLbl.Text ?? "[NO TITLE]";
- }
+ public override string ToString() => bookInfoLbl.Text ?? "[NO TITLE]";
}
}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs
new file mode 100644
index 00000000..f302b4b8
--- /dev/null
+++ b/Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs
@@ -0,0 +1,14 @@
+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);
+}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs
index d9a97565..85fdb856 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs
@@ -55,7 +55,6 @@
this.panel2 = new System.Windows.Forms.Panel();
this.logCopyBtn = new System.Windows.Forms.Button();
this.clearLogBtn = new System.Windows.Forms.Button();
- this.counterTimer = new System.Windows.Forms.Timer(this.components);
this.statusStrip1.SuspendLayout();
this.tabControl1.SuspendLayout();
this.tabPage1.SuspendLayout();
@@ -163,7 +162,6 @@
this.virtualFlowControl2.Name = "virtualFlowControl2";
this.virtualFlowControl2.Size = new System.Drawing.Size(390, 424);
this.virtualFlowControl2.TabIndex = 3;
- this.virtualFlowControl2.VirtualControlCount = 0;
//
// panel1
//
@@ -329,11 +327,6 @@
this.clearLogBtn.UseVisualStyleBackColor = true;
this.clearLogBtn.Click += new System.EventHandler(this.clearLogBtn_Click);
//
- // counterTimer
- //
- this.counterTimer.Interval = 950;
- this.counterTimer.Tick += new System.EventHandler(this.CounterTimer_Tick);
- //
// ProcessQueueControl
//
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
@@ -377,7 +370,6 @@
private System.Windows.Forms.Panel panel3;
private System.Windows.Forms.Panel panel4;
private System.Windows.Forms.ToolStripStatusLabel runningTimeLbl;
- private System.Windows.Forms.Timer counterTimer;
private System.Windows.Forms.DataGridView logDGV;
private System.Windows.Forms.DataGridViewTextBoxColumn timestampColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn logEntryColumn;
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
index 5f10bc04..7a67d31f 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
@@ -1,499 +1,220 @@
-using System;
-using System.Collections.Generic;
+using LibationFileManager;
+using LibationUiBase;
+using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
+using System.Drawing;
using System.Linq;
-using System.Threading.Tasks;
using System.Windows.Forms;
-using ApplicationServices;
-using LibationFileManager;
-using LibationUiBase;
-namespace LibationWinForms.ProcessQueue
+#nullable enable
+namespace LibationWinForms.ProcessQueue;
+
+internal partial class ProcessQueueControl : UserControl
{
- internal partial class ProcessQueueControl : UserControl, ILogForm
+ public ProcessQueueViewModel ViewModel { get; } = new();
+ public ToolStripButton PopoutButton { get; } = new()
{
- private TrackedQueue Queue = new();
- private readonly LogMe Logger;
- private int QueuedCount
- {
- set
- {
- queueNumberLbl.Text = value.ToString();
- queueNumberLbl.Visible = value > 0;
- }
- }
- private int ErrorCount
- {
- set
- {
- errorNumberLbl.Text = value.ToString();
- errorNumberLbl.Visible = value > 0;
- }
- }
+ DisplayStyle = ToolStripItemDisplayStyle.Text,
+ Name = nameof(PopoutButton),
+ Text = "Pop Out",
+ TextAlign = ContentAlignment.MiddleCenter,
+ Alignment = ToolStripItemAlignment.Right,
+ Anchor = AnchorStyles.Bottom | AnchorStyles.Right,
+ };
- private int CompletedCount
- {
- set
- {
- completedNumberLbl.Text = value.ToString();
- completedNumberLbl.Visible = value > 0;
- }
- }
+ public ProcessQueueControl()
+ {
+ InitializeComponent();
- public Task QueueRunner { get; private set; }
- public bool Running => !QueueRunner?.IsCompleted ?? false;
- public ToolStripButton popoutBtn = new();
+ var speedLimitMBps = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
+ numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps;
+ statusStrip1.Items.Add(PopoutButton);
- public ProcessQueueControl()
- {
- InitializeComponent();
+ virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked;
- var speedLimitMBps = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
- numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps;
+ ViewModel.LogWritten += (_, text) => WriteLine(text);
+ ViewModel.PropertyChanged += ProcessQueue_PropertyChanged;
+ virtualFlowControl2.Items = ViewModel.Items;
+ Load += ProcessQueueControl_Load;
+ }
- popoutBtn.DisplayStyle = ToolStripItemDisplayStyle.Text;
- popoutBtn.Name = "popoutBtn";
- popoutBtn.Text = "Pop Out";
- popoutBtn.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
- popoutBtn.Alignment = ToolStripItemAlignment.Right;
- popoutBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
- statusStrip1.Items.Add(popoutBtn);
+ private void ProcessQueueControl_Load(object? sender, EventArgs e)
+ {
+ if (DesignMode) return;
+ ProcessQueue_PropertyChanged(this, new PropertyChangedEventArgs(null));
+ }
- Logger = LogMe.RegisterForm(this);
+ public void WriteLine(string text)
+ {
+ if (!IsDisposed)
+ logDGV.Rows.Add(DateTime.Now, text.Trim());
+ }
- virtualFlowControl2.RequestData += VirtualFlowControl1_RequestData;
- virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked;
+ 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();
+ }
- Queue.QueuededCountChanged += Queue_QueuededCountChanged;
- Queue.CompletedCountChanged += Queue_CompletedCountChanged;
-
- Load += ProcessQueueControl_Load;
- }
-
- private void ProcessQueueControl_Load(object sender, EventArgs e)
- {
- if (DesignMode) return;
+ private void btnClearFinished_Click(object? sender, EventArgs e)
+ {
+ ViewModel.Queue.ClearCompleted();
+ virtualFlowControl2.RefreshDisplay();
+ if (!ViewModel.Running)
runningTimeLbl.Text = string.Empty;
- QueuedCount = 0;
- ErrorCount = 0;
- CompletedCount = 0;
- }
+ }
- private bool isBookInQueue(DataLayer.LibraryBook libraryBook)
+ private void clearLogBtn_Click(object? sender, EventArgs e)
+ {
+ logDGV.Rows.Clear();
+ }
+
+ private void LogCopyBtn_Click(object? sender, EventArgs e)
+ {
+ string logText = string.Join("\r\n", logDGV.Rows.Cast().Select(r => $"{r.Cells[0].Value}\t{r.Cells[1].Value}"));
+ Clipboard.SetDataObject(logText, false, 5, 150);
+ }
+
+ private void LogDGV_Resize(object? sender, EventArgs e)
+ {
+ logDGV.Columns[1].Width = logDGV.Width - logDGV.Columns[0].Width;
+ }
+
+ #region View-Model update event handling
+
+ private void ProcessQueue_PropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName is null or nameof(ViewModel.QueuedCount))
{
- var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
- if (entry == null)
- return false;
- else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
- return !Queue.RemoveCompleted(entry);
- else
- return true;
+ queueNumberLbl.Text = ViewModel.QueuedCount.ToString();
+ queueNumberLbl.Visible = ViewModel.QueuedCount > 0;
+ virtualFlowControl2.RefreshDisplay();
}
-
- public bool RemoveCompleted(DataLayer.LibraryBook libraryBook)
- => Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBook entry
- && entry.Status is ProcessBookStatus.Completed
- && Queue.RemoveCompleted(entry);
-
- public void AddDownloadPdf(DataLayer.LibraryBook libraryBook)
- => AddDownloadPdf(new List() { libraryBook });
-
- public void AddDownloadDecrypt(DataLayer.LibraryBook libraryBook)
- => AddDownloadDecrypt(new List() { libraryBook });
-
- public void AddConvertMp3(DataLayer.LibraryBook libraryBook)
- => AddConvertMp3(new List() { libraryBook });
-
- public void AddDownloadPdf(IEnumerable entries)
+ if (e.PropertyName is null or nameof(ViewModel.ErrorCount))
{
- List procs = new();
- foreach (var entry in entries)
- {
- if (isBookInQueue(entry))
- continue;
-
- ProcessBook pbook = new(entry, Logger);
- pbook.PropertyChanged += Pbook_DataAvailable;
- pbook.AddDownloadPdf();
- procs.Add(pbook);
- }
-
- Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
- AddToQueue(procs);
+ errorNumberLbl.Text = ViewModel.ErrorCount.ToString();
+ errorNumberLbl.Visible = ViewModel.ErrorCount > 0;
}
-
- public void AddDownloadDecrypt(IEnumerable entries)
+ if (e.PropertyName is null or nameof(ViewModel.CompletedCount))
{
- List procs = new();
- foreach (var entry in entries)
- {
- if (isBookInQueue(entry))
- continue;
-
- ProcessBook pbook = new(entry, Logger);
- pbook.PropertyChanged += Pbook_DataAvailable;
- pbook.AddDownloadDecryptBook();
- pbook.AddDownloadPdf();
- procs.Add(pbook);
- }
-
- Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
- AddToQueue(procs);
+ completedNumberLbl.Text = ViewModel.CompletedCount.ToString();
+ completedNumberLbl.Visible = ViewModel.CompletedCount > 0;
}
-
- public void AddConvertMp3(IEnumerable entries)
+ if (e.PropertyName is null or nameof(ViewModel.Progress))
{
- List procs = new();
- foreach (var entry in entries)
- {
- if (isBookInQueue(entry))
- continue;
-
- ProcessBook pbook = new(entry, Logger);
- pbook.PropertyChanged += Pbook_DataAvailable;
- pbook.AddConvertToMp3();
- procs.Add(pbook);
- }
-
- Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
- AddToQueue(procs);
+ toolStripProgressBar1.Maximum = ViewModel.Queue.Count;
+ toolStripProgressBar1.Value = ViewModel.Queue.Completed.Count;
}
- private void AddToQueue(IEnumerable pbook)
+ if (e.PropertyName is null or nameof(ViewModel.ProgressBarVisible))
{
- BeginInvoke(() =>
- {
- Queue.Enqueue(pbook);
- if (!Running)
- QueueRunner = QueueLoop();
- });
+ toolStripProgressBar1.Visible = ViewModel.ProgressBarVisible;
}
-
- DateTime StartingTime;
- private async Task QueueLoop()
+ if (e.PropertyName is null or nameof(ViewModel.RunningTime))
{
- try
- {
- Serilog.Log.Logger.Information("Begin processing queue");
-
- StartingTime = DateTime.Now;
- counterTimer.Start();
-
- bool shownServiceOutageMessage = false;
-
- while (Queue.MoveNext())
- {
- var nextBook = Queue.Current;
-
- Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook?.LibraryBook);
-
- var result = await nextBook.ProcessOneAsync();
-
- Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook?.LibraryBook, result);
-
- if (result == ProcessBookResult.ValidationFail)
- Queue.ClearCurrent();
- else if (result == ProcessBookResult.FailedAbort)
- Queue.ClearQueue();
- else if (result == ProcessBookResult.FailedSkip)
- nextBook.LibraryBook.UpdateBookStatus(DataLayer.LiberatedStatus.Error);
- else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage)
- {
- MessageBox.Show(@$"
-You were denied a content license for {nextBook.LibraryBook.Book.TitleWithSubtitle}
-
-This error appears to be caused by a temporary interruption of service that sometimes affects Libation's users. This type of error usually resolves itself in 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app.
-",
- "Possible Interruption of Service",
- MessageBoxButtons.OK,
- MessageBoxIcon.Asterisk);
- shownServiceOutageMessage = true;
- }
- }
- Serilog.Log.Logger.Information("Completed processing queue");
-
- Queue_CompletedCountChanged(this, 0);
- counterTimer.Stop();
- virtualFlowControl2.VirtualControlCount = Queue.Count;
- UpdateAllControls();
- }
- catch (Exception ex)
- {
- Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items");
- }
- }
-
- public void WriteLine(string text)
- {
- if (IsDisposed) return;
-
- var timeStamp = DateTime.Now;
- Invoke(() => logDGV.Rows.Add(timeStamp, text.Trim()));
- }
-
- #region Control event handlers
-
- private void Queue_CompletedCountChanged(object sender, int e)
- {
- int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
- int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
-
- ErrorCount = errCount;
- CompletedCount = completeCount;
- UpdateProgressBar();
- }
- private void Queue_QueuededCountChanged(object sender, int cueCount)
- {
- QueuedCount = cueCount;
- virtualFlowControl2.VirtualControlCount = Queue.Count;
- UpdateProgressBar();
- }
- private void UpdateProgressBar()
- {
- toolStripProgressBar1.Maximum = Queue.Count;
- toolStripProgressBar1.Value = Queue.Completed.Count;
- }
-
- private async void cancelAllBtn_Click(object sender, EventArgs e)
- {
- Queue.ClearQueue();
- if (Queue.Current is not null)
- await Queue.Current.CancelAsync();
- virtualFlowControl2.VirtualControlCount = Queue.Count;
- UpdateAllControls();
- }
-
- private void btnClearFinished_Click(object sender, EventArgs e)
- {
- Queue.ClearCompleted();
- virtualFlowControl2.VirtualControlCount = Queue.Count;
- UpdateAllControls();
-
- if (!Running)
- runningTimeLbl.Text = string.Empty;
- }
-
- private void CounterTimer_Tick(object sender, EventArgs e)
- {
- string timeToStr(TimeSpan time)
- {
- string minsSecs = $"{time:mm\\:ss}";
- if (time.TotalHours >= 1)
- return $"{time.TotalHours:F0}:{minsSecs}";
- return minsSecs;
- }
-
- if (Running)
- runningTimeLbl.Text = timeToStr(DateTime.Now - StartingTime);
- }
-
- private void clearLogBtn_Click(object sender, EventArgs e)
- {
- logDGV.Rows.Clear();
- }
-
- private void LogCopyBtn_Click(object sender, EventArgs e)
- {
- string logText = string.Join("\r\n", logDGV.Rows.Cast().Select(r => $"{r.Cells[0].Value}\t{r.Cells[1].Value}"));
- Clipboard.SetDataObject(logText, false, 5, 150);
- }
-
- private void LogDGV_Resize(object sender, EventArgs e)
- {
- logDGV.Columns[1].Width = logDGV.Width - logDGV.Columns[0].Width;
- }
-
- #endregion
-
- #region View-Model update event handling
-
- ///
- /// Index of the first visible in the
- ///
- private int FirstVisible = 0;
- ///
- /// Number of visible in the
- ///
- private int NumVisible = 0;
- ///
- /// Controls displaying the state, starting with
- ///
- private IReadOnlyList Panels;
-
- ///
- /// Updates the display of a single at within
- ///
- /// index of the within the
- /// The nme of the property that needs updating. If null, all properties are updated.
- private void UpdateControl(int queueIndex, string propertyName = null)
- {
- try
- {
- int i = queueIndex - FirstVisible;
-
- if (i > NumVisible || i < 0) return;
-
- var proc = Queue[queueIndex];
-
- Invoke(() =>
- {
- Panels[i].SuspendLayout();
- if (propertyName is null or nameof(proc.Cover))
- Panels[i].SetCover(proc.Cover);
- if (propertyName is null or nameof(proc.BookText))
- Panels[i].SetBookInfo(proc.BookText);
-
- if (proc.Result != ProcessBookResult.None)
- {
- Panels[i].SetResult(proc.Result);
- return;
- }
-
- if (propertyName is null or nameof(proc.Status))
- Panels[i].SetStatus(proc.Status);
- if (propertyName is null or nameof(proc.Progress))
- Panels[i].SetProgrss(proc.Progress);
- if (propertyName is null or nameof(proc.TimeRemaining))
- Panels[i].SetRemainingTime(proc.TimeRemaining);
- Panels[i].ResumeLayout();
- });
- }
- catch (Exception ex)
- {
- Serilog.Log.Logger.Error(ex, "Error updating the queued item's display.");
- }
- }
-
- private void UpdateAllControls()
- {
- int numToShow = Math.Min(NumVisible, Queue.Count - FirstVisible);
-
- for (int i = 0; i < numToShow; i++)
- UpdateControl(FirstVisible + i);
- }
-
-
- ///
- /// View notified the model that a botton was clicked
- ///
- /// index of the within
- /// The clicked control to update
- private async void VirtualFlowControl2_ButtonClicked(int queueIndex, string buttonName, ProcessBookControl panelClicked)
- {
- try
- {
- ProcessBook item = Queue[queueIndex];
- if (buttonName == nameof(panelClicked.cancelBtn))
- {
- if (item is not null)
- await item.CancelAsync();
- Queue.RemoveQueued(item);
- virtualFlowControl2.VirtualControlCount = Queue.Count;
- }
- else if (buttonName == nameof(panelClicked.moveFirstBtn))
- {
- Queue.MoveQueuePosition(item, QueuePosition.Fisrt);
- UpdateAllControls();
- }
- else if (buttonName == nameof(panelClicked.moveUpBtn))
- {
- Queue.MoveQueuePosition(item, QueuePosition.OneUp);
- UpdateControl(queueIndex);
- if (queueIndex > 0)
- UpdateControl(queueIndex - 1);
- }
- else if (buttonName == nameof(panelClicked.moveDownBtn))
- {
- Queue.MoveQueuePosition(item, QueuePosition.OneDown);
- UpdateControl(queueIndex);
- if (queueIndex + 1 < Queue.Count)
- UpdateControl(queueIndex + 1);
- }
- else if (buttonName == nameof(panelClicked.moveLastBtn))
- {
- Queue.MoveQueuePosition(item, QueuePosition.Last);
- UpdateAllControls();
- }
- }
- catch(Exception ex)
- {
- Serilog.Log.Logger.Error(ex, "Error handling button click from queued item");
- }
- }
-
- ///
- /// View needs updating
- ///
- private void VirtualFlowControl1_RequestData(int firstIndex, int numVisible, IReadOnlyList panelsToFill)
- {
- FirstVisible = firstIndex;
- NumVisible = numVisible;
- Panels = panelsToFill;
- UpdateAllControls();
- }
-
- ///
- /// Model updates the view
- ///
- private void Pbook_DataAvailable(object sender, PropertyChangedEventArgs e)
- {
- int index = Queue.IndexOf((ProcessBook)sender);
- UpdateControl(index, e.PropertyName);
- }
-
- #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;
+ runningTimeLbl.Text = ViewModel.RunningTime;
}
}
- public class NumericUpDownSuffix : NumericUpDown
+
+ ///
+ /// View notified the model that a botton was clicked
+ ///
+ /// the whose button was clicked
+ /// The name of the button clicked
+ private async void VirtualFlowControl2_ButtonClicked(object? sender, string buttonName)
{
- [Description("Suffix displayed after numeric value."), Category("Data")]
- [Browsable(true)]
- [EditorBrowsable(EditorBrowsableState.Always)]
- [DisallowNull]
- public string Suffix
+ if (sender is not ProcessBookControl control || control.Context is not ProcessBookViewModel item)
+ return;
+
+ try
{
- get => _suffix;
- set
+ if (buttonName is nameof(ProcessBookControl.cancelBtn))
{
- base.Text = string.IsNullOrEmpty(_suffix) ? base.Text : base.Text.Replace(_suffix, value);
- _suffix = value;
- ChangingText = true;
+ await item.CancelAsync();
+ ViewModel.Queue.RemoveQueued(item);
+ virtualFlowControl2.RefreshDisplay();
+ }
+ else
+ {
+ QueuePosition? position = buttonName switch
+ {
+ nameof(ProcessBookControl.moveFirstBtn) => QueuePosition.Fisrt,
+ nameof(ProcessBookControl.moveUpBtn) => QueuePosition.OneUp,
+ nameof(ProcessBookControl.moveDownBtn) => QueuePosition.OneDown,
+ nameof(ProcessBookControl.moveLastBtn) => QueuePosition.Last,
+ _ => null
+ };
+
+ if (position is not null)
+ {
+ ViewModel.Queue.MoveQueuePosition(item, position.Value);
+ virtualFlowControl2.RefreshDisplay();
+ }
}
}
- private string _suffix = string.Empty;
- public override string Text
+ catch(Exception ex)
{
- get => string.IsNullOrEmpty(Suffix) ? base.Text : base.Text.Replace(Suffix, string.Empty);
- set
- {
- if (Value == Minimum)
- base.Text = "∞";
- else
- base.Text = value + Suffix;
- }
+ Serilog.Log.Logger.Error(ex, "Error handling button click from queued item");
+ }
+ }
+
+ #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;
+ }
+}
+
+public class NumericUpDownSuffix : NumericUpDown
+{
+ [Description("Suffix displayed after numeric value."), Category("Data")]
+ [Browsable(true)]
+ [EditorBrowsable(EditorBrowsableState.Always)]
+ [DisallowNull]
+ public string Suffix
+ {
+ get => _suffix;
+ set
+ {
+ base.Text = string.IsNullOrEmpty(_suffix) ? base.Text : base.Text.Replace(_suffix, value);
+ _suffix = value;
+ ChangingText = true;
+ }
+ }
+ private string _suffix = string.Empty;
+
+ [AllowNull]
+ public override string Text
+ {
+ get => string.IsNullOrEmpty(Suffix) ? base.Text : base.Text.Replace(Suffix, string.Empty);
+ set
+ {
+ if (Value == Minimum)
+ base.Text = "∞";
+ else
+ base.Text = value + Suffix;
}
}
}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueViewModel.cs
new file mode 100644
index 00000000..e8fb4d1e
--- /dev/null
+++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueViewModel.cs
@@ -0,0 +1,24 @@
+using DataLayer;
+using LibationUiBase.ProcessQueue;
+using System;
+using System.Collections.Generic;
+
+#nullable enable
+namespace LibationWinForms.ProcessQueue;
+
+internal class ProcessQueueViewModel : ProcessQueueViewModelBase
+{
+ public event EventHandler? LogWritten;
+ public List Items { get; }
+
+ public ProcessQueueViewModel() : base(new List())
+ {
+ Items = Queue.UnderlyingList as List
+ ?? 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);
+}
diff --git a/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs b/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs
index e0cadbc7..5733ba48 100644
--- a/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs
+++ b/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs
@@ -1,44 +1,42 @@
-using System;
+using LibationUiBase.ProcessQueue;
+using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace LibationWinForms.ProcessQueue
{
-
- internal delegate void RequestDataDelegate(int queueIndex, int numVisible, IReadOnlyList panelsToFill);
- internal delegate void ControlButtonClickedDelegate(int queueIndex, string buttonName, ProcessBookControl panelClicked);
internal partial class VirtualFlowControl : UserControl
{
- ///
- /// Triggered when the needs to update the displayed s
- ///
- public event RequestDataDelegate RequestData;
///
/// Triggered when one of the 's buttons has been clicked
///
- public event ControlButtonClickedDelegate ButtonClicked;
+ public event EventHandler ButtonClicked;
+
+ private List m_Items;
+ public List Items
+ {
+ get => m_Items;
+ set
+ {
+ m_Items = value;
+ if (m_Items is not null)
+ RefreshDisplay();
+ }
+ }
+
+ public void RefreshDisplay()
+ {
+ AdjustScrollBar();
+ DoVirtualScroll();
+ }
#region Dynamic Properties
///
/// The number of virtual s in the
///
- public int VirtualControlCount
- {
- get => _virtualControlCount;
- set
- {
- if (_virtualControlCount == 0)
- vScrollBar1.Value = 0;
-
- _virtualControlCount = value;
- AdjustScrollBar();
- DoVirtualScroll();
- }
- }
-
- private int _virtualControlCount;
+ public int VirtualControlCount => Items?.Count ?? 0;
int ScrollValue => Math.Max(vScrollBar1.Value, 0);
///
@@ -100,12 +98,7 @@ namespace LibationWinForms.ProcessQueue
{
InitializeComponent();
- panel1.Resize += (_, _) =>
- {
- AdjustScrollBar();
- DoVirtualScroll();
- };
-
+ panel1.Resize += (_, _) => RefreshDisplay();
var control = InitControl(0);
VirtualControlHeight = this.DpiUnscale(control.Height + control.Margin.Top + control.Margin.Bottom);
@@ -151,9 +144,7 @@ namespace LibationWinForms.ProcessQueue
while (form is not ProcessBookControl)
form = form.Parent;
- int clickedIndex = BookControls.IndexOf((ProcessBookControl)form);
-
- ButtonClicked?.Invoke(FirstVisibleVirtualIndex + clickedIndex, button.Name, BookControls[clickedIndex]);
+ ButtonClicked?.Invoke(form, button.Name);
}
///
@@ -174,19 +165,20 @@ namespace LibationWinForms.ProcessQueue
else
{
vScrollBar1.Enabled = true;
- vScrollBar1.LargeChange = LargeScrollChange;
//https://stackoverflow.com/a/2882878/3335599
- int newMaximum = VirtualHeight + vScrollBar1.LargeChange - 1;
+ int newMaximum = VirtualHeight + LargeScrollChange - 1;
if (newMaximum < vScrollBar1.Maximum)
vScrollBar1.Value = Math.Max(vScrollBar1.Value - (vScrollBar1.Maximum - newMaximum), 0);
vScrollBar1.Maximum = newMaximum;
+ vScrollBar1.LargeChange = LargeScrollChange;
}
}
///
/// Calculated the virtual controls that are in view at the currrent scroll position and windows size,
- /// positions to simulate scroll activity, then fires to notify the model to update all visible controls
+ /// positions to simulate scroll activity, then fires updates the controls with
+ /// the context corresponding to the virtual scroll position
///
private void DoVirtualScroll()
{
@@ -203,10 +195,15 @@ namespace LibationWinForms.ProcessQueue
numVisible = Math.Min(numVisible, VirtualControlCount);
numVisible = Math.Min(numVisible, VirtualControlCount - firstVisible);
- RequestData?.Invoke(firstVisible, numVisible, BookControls);
+ for (int i = 0; i < numVisible; i++)
+ {
+ BookControls[i].Context = Items[firstVisible + i];
+ }
for (int i = 0; i < BookControls.Count; i++)
+ {
BookControls[i].Visible = i < numVisible;
+ }
}
///
diff --git a/Source/LibationWinForms/Program.cs b/Source/LibationWinForms/Program.cs
index 35aa0224..ba1416f0 100644
--- a/Source/LibationWinForms/Program.cs
+++ b/Source/LibationWinForms/Program.cs
@@ -22,6 +22,7 @@ namespace LibationWinForms
static void Main()
{
Task> libraryLoadTask;
+
try
{
//// Uncomment to see Console. Must be called before anything writes to Console.
@@ -86,7 +87,31 @@ namespace LibationWinForms
var form1 = new Form1();
form1.Load += async (_, _) => await form1.InitLibraryAsync(await libraryLoadTask);
+ LibationUiBase.Forms.MessageBoxBase.ShowAsyncImpl = ShowMessageBox;
Application.Run(form1);
+
+ #region Message Box Handler for LibationUiBase
+ Task ShowMessageBox(
+ object owner,
+ string message,
+ string caption,
+ LibationUiBase.Forms.MessageBoxButtons buttons,
+ LibationUiBase.Forms.MessageBoxIcon icon,
+ LibationUiBase.Forms.MessageBoxDefaultButton defaultButton,
+ bool _)
+ {
+ var result = form1.Invoke(() =>
+ MessageBox.Show(
+ owner as IWin32Window ?? form1,
+ message,
+ caption,
+ (MessageBoxButtons)buttons,
+ (MessageBoxIcon)icon,
+ (MessageBoxDefaultButton)defaultButton));
+
+ return Task.FromResult((LibationUiBase.Forms.DialogResult)result);
+ }
+ #endregion;
}
private static void RunInstaller(Configuration config)
diff --git a/Source/LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj b/Source/LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj
index f3171f1f..a5f48815 100644
--- a/Source/LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj
+++ b/Source/LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/Source/_Tests/AudibleUtilities.Tests/AudibleUtilities.Tests.csproj b/Source/_Tests/AudibleUtilities.Tests/AudibleUtilities.Tests.csproj
index b6127939..8054b41c 100644
--- a/Source/_Tests/AudibleUtilities.Tests/AudibleUtilities.Tests.csproj
+++ b/Source/_Tests/AudibleUtilities.Tests/AudibleUtilities.Tests.csproj
@@ -6,10 +6,10 @@
-
-
-
-
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Source/_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj b/Source/_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj
index 5d6132c2..7e51effb 100644
--- a/Source/_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj
+++ b/Source/_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj
@@ -6,10 +6,10 @@
-
-
-
-
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Source/_Tests/FileManager.Tests/FileManager.Tests.csproj b/Source/_Tests/FileManager.Tests/FileManager.Tests.csproj
index c5ceeb4d..287477bc 100644
--- a/Source/_Tests/FileManager.Tests/FileManager.Tests.csproj
+++ b/Source/_Tests/FileManager.Tests/FileManager.Tests.csproj
@@ -7,10 +7,10 @@
-
-
-
-
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Source/_Tests/LibationFileManager.Tests/LibationFileManager.Tests.csproj b/Source/_Tests/LibationFileManager.Tests/LibationFileManager.Tests.csproj
index 2a68084e..c51dd03a 100644
--- a/Source/_Tests/LibationFileManager.Tests/LibationFileManager.Tests.csproj
+++ b/Source/_Tests/LibationFileManager.Tests/LibationFileManager.Tests.csproj
@@ -7,10 +7,10 @@
-
-
-
-
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs
index 282a8354..4f92c27d 100644
--- a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs
+++ b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs
@@ -117,7 +117,6 @@ namespace TemplatesTests
[DataRow(" 42 ", "", "", "1 8 1 8")]
[DataRow(" - ", "", "", "1 8 - 1 8")]
[DataRow("4 - 4", "", "", "1 8 - 1 8")]
- [DataRow("4 - 4", "", "", "1 8 - 1 8")]
[DataRow("", "", "", "100")]
[DataRow(" ", "", "", "100")]
[DataRow(" - - ", "", "", "- 100 -")]
diff --git a/Source/_Tests/LibationSearchEngine.Tests/LibationSearchEngine.Tests.csproj b/Source/_Tests/LibationSearchEngine.Tests/LibationSearchEngine.Tests.csproj
index d6e52ffc..88e9775e 100644
--- a/Source/_Tests/LibationSearchEngine.Tests/LibationSearchEngine.Tests.csproj
+++ b/Source/_Tests/LibationSearchEngine.Tests/LibationSearchEngine.Tests.csproj
@@ -7,10 +7,10 @@
-
-
-
-
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Source/_Tests/LibationSearchEngine.Tests/SearchEngineTests.cs b/Source/_Tests/LibationSearchEngine.Tests/SearchEngineTests.cs
index bc3702a7..492627f6 100644
--- a/Source/_Tests/LibationSearchEngine.Tests/SearchEngineTests.cs
+++ b/Source/_Tests/LibationSearchEngine.Tests/SearchEngineTests.cs
@@ -61,9 +61,6 @@ namespace SearchEngineTests
[DataRow("AudibleProductId:B000000123", "audibleproductid:b000000123")]
[DataRow("ProductId:B000000123", "productid:b000000123")]
- // bool keyword. Append :True
- [DataRow("israted", "israted:True")]
-
// bool keyword with [:bool]. Do not add :True
[DataRow("israted:True", "israted:True")]
[DataRow("isRated:false", "israted:false")]