diff --git a/Scripts/Bundle_Debian.sh b/Scripts/Bundle_Debian.sh
index 3ca7ef13..b69eb76f 100644
--- a/Scripts/Bundle_Debian.sh
+++ b/Scripts/Bundle_Debian.sh
@@ -113,6 +113,7 @@ Essential: no
Priority: optional
Maintainer: github.com/rmcrackan
Description: liberate your audiobooks
+Recommends: libgtk-3-0, libwebkit2gtk-4.1-0
" >> $FOLDER_DEBIAN/control
echo "Changing permissions for pre- and post-install files..."
diff --git a/Scripts/Bundle_Redhat.sh b/Scripts/Bundle_Redhat.sh
index 1b18f1d1..05d27ac6 100644
--- a/Scripts/Bundle_Redhat.sh
+++ b/Scripts/Bundle_Redhat.sh
@@ -62,7 +62,7 @@ License: GPLv3+
URL: https://github.com/rmcrackan/Libation
Source0: https://github.com/rmcrackan/Libation
-Requires: bash
+Requires: bash gtk3 webkit2gtk4.1
%define __os_install_post %{nil}
diff --git a/Source/AppScaffolding/AppScaffolding.csproj b/Source/AppScaffolding/AppScaffolding.csproj
index 046a0e54..25cff2d7 100644
--- a/Source/AppScaffolding/AppScaffolding.csproj
+++ b/Source/AppScaffolding/AppScaffolding.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/Source/ApplicationServices/ApplicationServices.csproj b/Source/ApplicationServices/ApplicationServices.csproj
index 8bd2a9c7..2105a0b0 100644
--- a/Source/ApplicationServices/ApplicationServices.csproj
+++ b/Source/ApplicationServices/ApplicationServices.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/Source/ApplicationServices/LibraryExporter.cs b/Source/ApplicationServices/LibraryExporter.cs
index 1f16052a..7ec92f0b 100644
--- a/Source/ApplicationServices/LibraryExporter.cs
+++ b/Source/ApplicationServices/LibraryExporter.cs
@@ -1,11 +1,12 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
+using ClosedXML.Excel;
using CsvHelper;
using CsvHelper.Configuration.Attributes;
using DataLayer;
using Newtonsoft.Json;
-using NPOI.XSSF.UserModel;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
namespace ApplicationServices
{
@@ -208,19 +209,11 @@ namespace ApplicationServices
{
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
- var workbook = new XSSFWorkbook();
- var sheet = workbook.CreateSheet("Library");
+ using var workbook = new XLWorkbook();
+ var sheet = workbook.AddWorksheet("Library");
- var detailSubtotalFont = workbook.CreateFont();
- detailSubtotalFont.IsBold = true;
-
- var detailSubtotalCellStyle = workbook.CreateCellStyle();
- detailSubtotalCellStyle.SetFont(detailSubtotalFont);
// headers
- var rowIndex = 0;
- var row = sheet.CreateRow(rowIndex);
-
var columns = new[] {
nameof(ExportDto.Account),
nameof(ExportDto.DateAdded),
@@ -261,81 +254,71 @@ namespace ApplicationServices
nameof(ExportDto.ChannelCount),
nameof(ExportDto.BitRate)
};
- var col = 0;
+
+ int rowIndex = 1, col = 1;
+ var headerRow = sheet.Row(rowIndex++);
foreach (var c in columns)
{
- var cell = row.CreateCell(col++);
- var name = ExportDto.GetName(c);
- cell.SetCellValue(name);
- cell.CellStyle = detailSubtotalCellStyle;
+ var headerCell = headerRow.Cell(col++);
+ headerCell.Value = ExportDto.GetName(c);
+ headerCell.Style.Font.Bold = true;
}
- var dateFormat = workbook.CreateDataFormat();
- var dateStyle = workbook.CreateCellStyle();
- dateStyle.DataFormat = dateFormat.GetFormat("MM/dd/yyyy HH:mm:ss");
-
- rowIndex++;
+ var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm:ss";
// Add data rows
foreach (var dto in dtos)
{
- col = 0;
- row = sheet.CreateRow(rowIndex++);
+ col = 1;
+ var row = sheet.Row(rowIndex++);
- row.CreateCell(col++).SetCellValue(dto.Account);
- row.CreateCell(col++).SetCellValue(dto.DateAdded).CellStyle = dateStyle;
- row.CreateCell(col++).SetCellValue(dto.AudibleProductId);
- row.CreateCell(col++).SetCellValue(dto.Locale);
- row.CreateCell(col++).SetCellValue(dto.Title);
- row.CreateCell(col++).SetCellValue(dto.Subtitle);
- row.CreateCell(col++).SetCellValue(dto.AuthorNames);
- row.CreateCell(col++).SetCellValue(dto.NarratorNames);
- row.CreateCell(col++).SetCellValue(dto.LengthInMinutes);
- row.CreateCell(col++).SetCellValue(dto.Description);
- row.CreateCell(col++).SetCellValue(dto.Publisher);
- row.CreateCell(col++).SetCellValue(dto.HasPdf);
- row.CreateCell(col++).SetCellValue(dto.SeriesNames);
- row.CreateCell(col++).SetCellValue(dto.SeriesOrder);
- row.CreateCell(col++).SetCellValue(dto.CommunityRatingOverall);
- row.CreateCell(col++).SetCellValue(dto.CommunityRatingPerformance);
- row.CreateCell(col++).SetCellValue(dto.CommunityRatingStory);
- row.CreateCell(col++).SetCellValue(dto.PictureId);
- row.CreateCell(col++).SetCellValue(dto.IsAbridged);
- row.CreateCell(col++).SetCellValue(dto.DatePublished).CellStyle = dateStyle;
- row.CreateCell(col++).SetCellValue(dto.CategoriesNames);
- row.CreateCell(col++).SetCellValue(dto.MyRatingOverall);
- row.CreateCell(col++).SetCellValue(dto.MyRatingPerformance);
- row.CreateCell(col++).SetCellValue(dto.MyRatingStory);
- row.CreateCell(col++).SetCellValue(dto.MyLibationTags);
- row.CreateCell(col++).SetCellValue(dto.BookStatus);
- row.CreateCell(col++).SetCellValue(dto.PdfStatus);
- row.CreateCell(col++).SetCellValue(dto.ContentType);
- row.CreateCell(col++).SetCellValue(dto.Language);
- row.CreateCell(col++).SetCellValue(dto.LastDownloaded).CellStyle = dateStyle;
- row.CreateCell(col++).SetCellValue(dto.LastDownloadedVersion);
- row.CreateCell(col++).SetCellValue(dto.IsFinished);
- row.CreateCell(col++).SetCellValue(dto.IsSpatial);
- row.CreateCell(col++).SetCellValue(dto.LastDownloadedFileVersion);
- row.CreateCell(col++).SetCellValue(dto.CodecString);
- row.CreateCell(col++).SetCellValue(dto.SampleRate);
- row.CreateCell(col++).SetCellValue(dto.ChannelCount);
- row.CreateCell(col++).SetCellValue(dto.BitRate);
+ row.Cell(col++).Value = dto.Account;
+ row.Cell(col++).SetDate(dto.DateAdded, dateFormat);
+ row.Cell(col++).Value = dto.AudibleProductId;
+ row.Cell(col++).Value = dto.Locale;
+ row.Cell(col++).Value = dto.Title;
+ row.Cell(col++).Value = dto.Subtitle;
+ row.Cell(col++).Value = dto.AuthorNames;
+ row.Cell(col++).Value = dto.NarratorNames;
+ row.Cell(col++).Value = dto.LengthInMinutes;
+ row.Cell(col++).Value = dto.Description;
+ row.Cell(col++).Value = dto.Publisher;
+ row.Cell(col++).Value = dto.HasPdf;
+ row.Cell(col++).Value = dto.SeriesNames;
+ row.Cell(col++).Value = dto.SeriesOrder;
+ row.Cell(col++).Value = dto.CommunityRatingOverall;
+ row.Cell(col++).Value = dto.CommunityRatingPerformance;
+ row.Cell(col++).Value = dto.CommunityRatingStory;
+ row.Cell(col++).Value = dto.PictureId;
+ row.Cell(col++).Value = dto.IsAbridged;
+ row.Cell(col++).SetDate(dto.DatePublished, dateFormat);
+ row.Cell(col++).Value = dto.CategoriesNames;
+ row.Cell(col++).Value = dto.MyRatingOverall;
+ row.Cell(col++).Value = dto.MyRatingPerformance;
+ row.Cell(col++).Value = dto.MyRatingStory;
+ row.Cell(col++).Value = dto.MyLibationTags;
+ row.Cell(col++).Value = dto.BookStatus;
+ row.Cell(col++).Value = dto.PdfStatus;
+ row.Cell(col++).Value = dto.ContentType;
+ row.Cell(col++).Value = dto.Language;
+ row.Cell(col++).SetDate(dto.LastDownloaded, dateFormat);
+ row.Cell(col++).Value = dto.LastDownloadedVersion;
+ row.Cell(col++).Value = dto.IsFinished;
+ row.Cell(col++).Value = dto.IsSpatial;
+ row.Cell(col++).Value = dto.LastDownloadedFileVersion;
+ row.Cell(col++).Value = dto.CodecString;
+ row.Cell(col++).Value = dto.SampleRate;
+ row.Cell(col++).Value = dto.ChannelCount;
+ row.Cell(col++).Value = dto.BitRate;
}
- using var fileData = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create);
- workbook.Write(fileData);
+ workbook.SaveAs(saveFilePath);
}
- private static NPOI.SS.UserModel.ICell SetCellValue(this NPOI.SS.UserModel.ICell cell, DateTime? nullableDate)
- => nullableDate.HasValue ? cell.SetCellValue(nullableDate.Value)
- : cell.SetCellType(NPOI.SS.UserModel.CellType.Numeric);
-
- private static NPOI.SS.UserModel.ICell SetCellValue(this NPOI.SS.UserModel.ICell cell, int? nullableInt)
- => nullableInt.HasValue ? cell.SetCellValue(nullableInt.Value)
- : cell.SetCellType(NPOI.SS.UserModel.CellType.Numeric);
-
- private static NPOI.SS.UserModel.ICell SetCellValue(this NPOI.SS.UserModel.ICell cell, float? nullableFloat)
- => nullableFloat.HasValue ? cell.SetCellValue(nullableFloat.Value)
- : cell.SetCellType(NPOI.SS.UserModel.CellType.Numeric);
+ private static void SetDate(this IXLCell cell, DateTime? value, string dateFormat)
+ {
+ cell.Value = value;
+ cell.Style.DateFormat.Format = dateFormat;
+ }
}
}
diff --git a/Source/ApplicationServices/RecordExporter.cs b/Source/ApplicationServices/RecordExporter.cs
index 52506383..b888222f 100644
--- a/Source/ApplicationServices/RecordExporter.cs
+++ b/Source/ApplicationServices/RecordExporter.cs
@@ -1,10 +1,11 @@
using AudibleApi.Common;
+using ClosedXML.Excel;
using CsvHelper;
using DataLayer;
using Newtonsoft.Json.Linq;
-using NPOI.XSSF.UserModel;
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
namespace ApplicationServices
@@ -16,19 +17,10 @@ namespace ApplicationServices
if (!records.Any())
return;
- using var workbook = new XSSFWorkbook();
- var sheet = workbook.CreateSheet("Records");
-
- var detailSubtotalFont = workbook.CreateFont();
- detailSubtotalFont.IsBold = true;
-
- var detailSubtotalCellStyle = workbook.CreateCellStyle();
- detailSubtotalCellStyle.SetFont(detailSubtotalFont);
+ using var workbook = new XLWorkbook();
+ var worksheet = workbook.AddWorksheet("Records");
// headers
- var rowIndex = 0;
- var row = sheet.CreateRow(rowIndex);
-
var columns = new List
{
nameof(Type.Name),
@@ -49,56 +41,52 @@ namespace ApplicationServices
if (records.OfType().Any())
columns.Add(nameof(Clip.Title));
- var col = 0;
+ int rowIndex = 1, col = 1;
+ var headerRow = worksheet.Row(rowIndex++);
foreach (var c in columns)
{
- var cell = row.CreateCell(col++);
- cell.SetCellValue(c);
- cell.CellStyle = detailSubtotalCellStyle;
+ var headerCell = headerRow.Cell(col++);
+ headerCell.Value = c;
+ headerCell.Style.Font.Bold = true;
}
- var dateFormat = workbook.CreateDataFormat();
- var dateStyle = workbook.CreateCellStyle();
- dateStyle.DataFormat = dateFormat.GetFormat("MM/dd/yyyy HH:mm:ss");
+ var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm:ss";
// Add data rows
foreach (var record in records)
{
- col = 0;
+ col = 1;
+ var row = worksheet.Row(rowIndex++);
- row = sheet.CreateRow(++rowIndex);
-
- row.CreateCell(col++).SetCellValue(record.GetType().Name);
-
- var dateCreatedCell = row.CreateCell(col++);
- dateCreatedCell.CellStyle = dateStyle;
- dateCreatedCell.SetCellValue(record.Created.DateTime);
-
- row.CreateCell(col++).SetCellValue(record.Start.TotalMilliseconds);
+ row.Cell(col++).Value = record.GetType().Name;
+ row.Cell(col++).SetDate(record.Created.DateTime, dateFormat);
+ row.Cell(col++).Value = record.Start.TotalMilliseconds;
if (record is IAnnotation annotation)
{
- row.CreateCell(col++).SetCellValue(annotation.AnnotationId);
- var lastModifiedCell = row.CreateCell(col++);
- lastModifiedCell.CellStyle = dateStyle;
- lastModifiedCell.SetCellValue(annotation.LastModified.DateTime);
+ row.Cell(col++).Value = annotation.AnnotationId;
+ row.Cell(col++).SetDate(annotation.LastModified.DateTime, dateFormat);
if (annotation is IRangeAnnotation rangeAnnotation)
{
- row.CreateCell(col++).SetCellValue(rangeAnnotation.End.TotalMilliseconds);
- row.CreateCell(col++).SetCellValue(rangeAnnotation.Text);
+ row.Cell(col++).Value = rangeAnnotation.End.TotalMilliseconds;
+ row.Cell(col++).Value = rangeAnnotation.Text;
if (rangeAnnotation is Clip clip)
- row.CreateCell(col++).SetCellValue(clip.Title);
+ row.Cell(col++).Value = clip.Title;
}
}
}
- using var fileData = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create);
- workbook.Write(fileData);
+ workbook.SaveAs(saveFilePath);
}
+ private static void SetDate(this IXLCell cell, DateTime? value, string dateFormat)
+ {
+ cell.Value = value;
+ cell.Style.DateFormat.Format = dateFormat;
+ }
public static void ToJson(string saveFilePath, LibraryBook libraryBook, IEnumerable records)
{
if (!records.Any())
diff --git a/Source/AudibleUtilities/AccountsSettings.cs b/Source/AudibleUtilities/AccountsSettings.cs
index 8f1bc538..bdef1580 100644
--- a/Source/AudibleUtilities/AccountsSettings.cs
+++ b/Source/AudibleUtilities/AccountsSettings.cs
@@ -6,6 +6,7 @@ using AudibleApi.Authorization;
using Dinah.Core;
using Newtonsoft.Json;
+#nullable enable
namespace AudibleUtilities
{
// 'AccountsSettings' is intentionally NOT IEnumerable<> so that properties can be added/extended
@@ -14,8 +15,8 @@ namespace AudibleUtilities
// JSON : Array (properties on the collection will not be serialized)
public class AccountsSettings : IUpdatable
{
- public event EventHandler Updated;
- private void update(object sender = null, EventArgs e = null)
+ public event EventHandler? Updated;
+ private void update(object? sender = null, EventArgs? e = null)
{
foreach (var account in Accounts)
validate(account);
@@ -48,9 +49,9 @@ namespace AudibleUtilities
}
}
- private string _cdm;
+ private string? _cdm;
[JsonProperty]
- public string Cdm
+ public string? Cdm
{
get => _cdm;
set
@@ -68,7 +69,7 @@ namespace AudibleUtilities
#endregion
#region de/serialize
- public static AccountsSettings FromJson(string json)
+ public static AccountsSettings? FromJson(string json)
=> JsonConvert.DeserializeObject(json, Identity.GetJsonSerializerSettings());
public string ToJson(Formatting formatting = Formatting.Indented)
@@ -107,7 +108,7 @@ namespace AudibleUtilities
account.Updated += update;
}
- public Account GetAccount(string accountId, string locale)
+ public Account? GetAccount(string accountId, string? locale)
{
if (locale is null)
return null;
diff --git a/Source/AudibleUtilities/AudibleUtilities.csproj b/Source/AudibleUtilities/AudibleUtilities.csproj
index 506a45d6..4b62e37d 100644
--- a/Source/AudibleUtilities/AudibleUtilities.csproj
+++ b/Source/AudibleUtilities/AudibleUtilities.csproj
@@ -5,8 +5,8 @@
-
-
+
+
diff --git a/Source/DataLayer.Postgres/DataLayer.Postgres.csproj b/Source/DataLayer.Postgres/DataLayer.Postgres.csproj
index d23ec26b..4ee33922 100644
--- a/Source/DataLayer.Postgres/DataLayer.Postgres.csproj
+++ b/Source/DataLayer.Postgres/DataLayer.Postgres.csproj
@@ -12,12 +12,12 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Source/DataLayer.Sqlite/DataLayer.Sqlite.csproj b/Source/DataLayer.Sqlite/DataLayer.Sqlite.csproj
index f1546adc..e1128635 100644
--- a/Source/DataLayer.Sqlite/DataLayer.Sqlite.csproj
+++ b/Source/DataLayer.Sqlite/DataLayer.Sqlite.csproj
@@ -12,12 +12,12 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Source/DataLayer/DataLayer.csproj b/Source/DataLayer/DataLayer.csproj
index 916c545e..e2c3699a 100644
--- a/Source/DataLayer/DataLayer.csproj
+++ b/Source/DataLayer/DataLayer.csproj
@@ -13,12 +13,12 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Source/DataLayer/MockLibraryBook.cs b/Source/DataLayer/MockLibraryBook.cs
new file mode 100644
index 00000000..f3b5dc02
--- /dev/null
+++ b/Source/DataLayer/MockLibraryBook.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+#nullable enable
+namespace DataLayer;
+public class MockLibraryBook : LibraryBook
+{
+ protected MockLibraryBook(Book book, DateTime dateAdded, string account, DateTime? includedUntil)
+ : base(book, dateAdded, account)
+ {
+ SetIncludedUntil(includedUntil);
+ }
+
+ public MockLibraryBook AddSeries(string seriesName, int order)
+ {
+ var series = new Series(new AudibleSeriesId(CalculateAsin(seriesName)), seriesName);
+ Book.UpsertSeries(series, order.ToString());
+ return this;
+ }
+
+ public MockLibraryBook AddCategoryLadder(params string[] ladder)
+ {
+ var newLadder = new CategoryLadder(ladder.Select(c => new Category(new AudibleCategoryId(CalculateAsin(c)), c)).ToList());
+ Book.SetCategoryLadders(Book.Categories.Select(c => c.CategoryLadder).Append(newLadder));
+ return this;
+ }
+
+ public MockLibraryBook AddNarrator(string name)
+ {
+ var newNarrator = new Contributor(name, CalculateAsin(name));
+ Book.ReplaceNarrators(Book.Narrators.Append(newNarrator));
+ return this;
+ }
+
+ public MockLibraryBook AddAuthor(string name)
+ {
+ var newAuthor = new Contributor(name, CalculateAsin(name));
+ Book.ReplaceAuthors(Book.Authors.Append(newAuthor));
+ return this;
+ }
+
+ public MockLibraryBook WithBookStatus(LiberatedStatus liberatedStatus)
+ {
+ //Set the backing field directly to preserve LiberatedStatus.PartialDownload
+ typeof(UserDefinedItem)
+ .GetField("_bookStatus", BindingFlags.NonPublic | BindingFlags.Instance)
+ ?.SetValue(Book.UserDefinedItem, liberatedStatus);
+ return this;
+ }
+
+ public MockLibraryBook WithPdfStatus(LiberatedStatus liberatedStatus)
+ {
+ Book.UserDefinedItem.PdfStatus = liberatedStatus;
+ return this;
+ }
+
+ public MockLibraryBook WithLastDownloaded(Version? lastVersion = null, AudioFormat? format = null, string audioVersion = "1")
+ {
+ lastVersion ??= new Version(10, 0, 0, 0);
+ format ??= AudioFormat.Default;
+ Book.UserDefinedItem.SetLastDownloaded(lastVersion, format, audioVersion);
+ return this;
+ }
+
+ public MockLibraryBook WithMyRating(float overallRating = 4, float performanceRating = 4.5f, float storyRating = 5)
+ {
+ Book.UserDefinedItem.UpdateRating(overallRating, performanceRating, storyRating);
+ return this;
+ }
+
+ public static MockLibraryBook CreateBook(
+ string account = "someone@email.co",
+ bool absetFromLastScan = false,
+ DateTime? dateAdded = null,
+ DateTime? datePublished = null,
+ DateTime? includedUntil = null,
+ string title = "Mock Book Title",
+ string subtitle = "Mock Book Subtitle",
+ string description = "This is a mock book description.",
+ int lengthInMinutes = 1400,
+ ContentType contentType = ContentType.Product,
+ string firstAuthor = "Author One",
+ string firstNarrator = "Narrator One",
+ string localeName = "us",
+ bool isAbridged = false,
+ bool isSpatial = false,
+ string language = "English")
+ {
+ var book = new Book(
+ new AudibleProductId(CalculateAsin(title + subtitle)),
+ title,
+ subtitle,
+ description,
+ lengthInMinutes,
+ contentType,
+ [new Contributor(firstAuthor, CalculateAsin(firstAuthor))],
+ [new Contributor(firstNarrator, CalculateAsin(firstNarrator))],
+ localeName);
+
+ book.UpdateBookDetails(isAbridged, isSpatial, datePublished ?? DateTime.Now, language);
+
+ return new MockLibraryBook(
+ book,
+ dateAdded ?? DateTime.Now,
+ account,
+ includedUntil)
+ {
+ AbsentFromLastScan = absetFromLastScan
+ };
+ }
+
+ private static string CalculateAsin(string name)
+ => Convert.ToHexString(System.Security.Cryptography.MD5.HashData(Encoding.UTF8.GetBytes(name))).Substring(0, 10);
+}
diff --git a/Source/FileLiberator/DownloadOptions.Factory.cs b/Source/FileLiberator/DownloadOptions.Factory.cs
index d560886c..f6485c3f 100644
--- a/Source/FileLiberator/DownloadOptions.Factory.cs
+++ b/Source/FileLiberator/DownloadOptions.Factory.cs
@@ -3,6 +3,7 @@ using AudibleApi;
using AudibleApi.Common;
using AudibleUtilities.Widevine;
using DataLayer;
+using Dinah.Core;
using LibationFileManager;
using NAudio.Lame;
using System;
@@ -24,14 +25,29 @@ public partial class DownloadOptions
public static async Task InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook, CancellationToken token)
{
var license = await ChooseContent(api, libraryBook, config, token);
+ Serilog.Log.Logger.Debug("Content License {@License}", new
+ {
+ license.DrmType,
+ license.ContentMetadata.ContentReference.Codec,
+ license.ContentMetadata.ContentReference.Marketplace,
+ license.ContentMetadata.ContentReference.ContentSizeInBytes,
+ license.ContentMetadata.ContentReference.Version,
+ license.ContentMetadata.ContentReference.FileVersion
+ });
+
token.ThrowIfCancellationRequested();
//Some audiobooks will have incorrect chapters in the metadata returned from the license request,
//but the metadata returned by the content metadata endpoint will be correct. Call the content
- //metadata endpoint and use its chapters. Only replace the license request chapters if the total
- //lengths match (defensive against different audio formats having slightly different lengths).
- var metadata = await api.GetContentMetadataAsync(libraryBook.Book.AudibleProductId);
- if (metadata.ChapterInfo.RuntimeLengthMs == license.ContentMetadata.ChapterInfo.RuntimeLengthMs)
+ //metadata endpoint and use its chapters. Only replace the license request chapters if the content
+ //references match (defensive against different audio formats having slightly different lengths).
+ var metadata = await api.GetContentMetadataAsync(
+ libraryBook.Book.AudibleProductId,
+ license.DrmType,
+ license.ContentMetadata.ContentReference.Acr,
+ license.ContentMetadata.ContentReference.FileVersion);
+
+ if (metadata is not null && metadata.ContentReference == license.ContentMetadata.ContentReference)
license.ContentMetadata.ChapterInfo = metadata.ChapterInfo;
token.ThrowIfCancellationRequested();
@@ -41,7 +57,7 @@ public partial class DownloadOptions
private class LicenseInfo
{
public DrmType DrmType { get; }
- public ContentMetadata ContentMetadata { get; set; }
+ public ContentMetadata ContentMetadata { get; }
public KeyData[]? DecryptionKeys { get; }
public LicenseInfo(ContentLicense license, IEnumerable? keys = null)
{
@@ -56,10 +72,28 @@ public partial class DownloadOptions
private static async Task ChooseContent(Api api, LibraryBook libraryBook, Configuration config, CancellationToken token)
{
+ Serilog.Log.Logger.Debug("Download Settings {@Settings}", new
+ {
+ config.FileDownloadQuality,
+ config.UseWidevine,
+ config.Request_xHE_AAC,
+ config.RequestSpatial,
+ config.SpatialAudioCodec
+ });
+
var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High;
- if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm)
+ bool canUseWidevine = api.SupportsWidevine();
+ if (!config.UseWidevine || !canUseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm)
{
+ if (config.UseWidevine)
+ {
+ if (canUseWidevine)
+ Serilog.Log.Logger.Information("Unable to get a Widevine CDM. Falling back to ADRM.");
+ else
+ Serilog.Log.Logger.Information("Account {@account} is not registered as an android device, so content will not be downloaded with Widevine DRM. Remove and re-add the account in Libation to fix.", libraryBook.Account.ToMask());
+ }
+
token.ThrowIfCancellationRequested();
var license = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
return new LicenseInfo(license);
@@ -254,8 +288,11 @@ public partial class DownloadOptions
*/
- public static List flattenChapters(IList chapters, string? titleConcat = ": ")
+ public static List flattenChapters(IList? chapters, string? titleConcat = ": ")
{
+ if (chapters is null)
+ return [];
+
List chaps = new();
foreach (var c in chapters)
diff --git a/Source/FileLiberator/UtilityExtensions.cs b/Source/FileLiberator/UtilityExtensions.cs
index 73ac5bbc..d563924c 100644
--- a/Source/FileLiberator/UtilityExtensions.cs
+++ b/Source/FileLiberator/UtilityExtensions.cs
@@ -7,6 +7,7 @@ using DataLayer;
using Dinah.Core;
using LibationFileManager;
using LibationFileManager.Templates;
+using System.Security.Authentication;
#nullable enable
namespace FileLiberator
@@ -25,14 +26,22 @@ namespace FileLiberator
public static async Task GetApiAsync(this LibraryBook libraryBook)
{
- Account account;
- using (var accounts = AudibleApiStorage.GetAccountsSettingsPersister())
- account = accounts.AccountsSettings.GetAccount(libraryBook.Account, libraryBook.Book.Locale);
+ using var accounts = AudibleApiStorage.GetAccountsSettingsPersister();
+ var account = accounts.AccountsSettings.GetAccount(libraryBook.Account, libraryBook.Book.Locale)
+ ?? throw new InvalidCredentialException($"No account found for '{libraryBook.Account}' and locale '{libraryBook.Book.Locale}'");
var apiExtended = await ApiExtended.CreateAsync(account);
return apiExtended.Api;
}
+ public static bool SupportsWidevine(this AudibleApi.Api api)
+ {
+ //TODO: Expose Api's identity maintainer directly instead of using reflection.
+ var identityProperty = api.GetType().GetProperty("_identityMaintainer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ return identityProperty?.GetValue(api) is AudibleApi.Authorization.IIdentityMaintainer identityMaintainer
+ && identityMaintainer.DeviceType == AudibleApi.Resources.DeviceType;
+ }
+
public static LibraryBookDto ToDto(this LibraryBook libraryBook)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
diff --git a/Source/FileManager/BackgroundFileSystem.cs b/Source/FileManager/BackgroundFileSystem.cs
index e4ec7db2..4dec3b8a 100644
--- a/Source/FileManager/BackgroundFileSystem.cs
+++ b/Source/FileManager/BackgroundFileSystem.cs
@@ -3,15 +3,16 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
#nullable enable
namespace FileManager
{
- ///
- /// Tracks actual locations of files.
- ///
- public class BackgroundFileSystem
+ ///
+ /// Tracks actual locations of files.
+ ///
+ public class BackgroundFileSystem : IDisposable
{
public LongPath RootDirectory { get; private set; }
public string SearchPattern { get; private set; }
@@ -21,7 +22,7 @@ namespace FileManager
private BlockingCollection? directoryChangesEvents { get; set; }
private Task? backgroundScanner { get; set; }
- private object fsCacheLocker { get; } = new();
+ private Lock fsCacheLocker { get; } = new();
private List fsCache { get; } = new();
public BackgroundFileSystem(LongPath rootDirectory, string searchPattern, SearchOption searchOptions)
@@ -100,7 +101,6 @@ namespace FileManager
private void FileSystemWatcher_Error(object sender, ErrorEventArgs e)
{
- Stop();
Init();
}
@@ -181,8 +181,12 @@ namespace FileManager
fsCache.Add(newFile);
}
- #endregion
+ #endregion
- ~BackgroundFileSystem() => Stop();
- }
+ public void Dispose()
+ {
+ Stop();
+ GC.SuppressFinalize(this);
+ }
+ }
}
diff --git a/Source/FileManager/FileManager.csproj b/Source/FileManager/FileManager.csproj
index 8077a239..03d96351 100644
--- a/Source/FileManager/FileManager.csproj
+++ b/Source/FileManager/FileManager.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/Source/FileManager/IPersistentDictionary.cs b/Source/FileManager/IPersistentDictionary.cs
new file mode 100644
index 00000000..865733d1
--- /dev/null
+++ b/Source/FileManager/IPersistentDictionary.cs
@@ -0,0 +1,40 @@
+using Newtonsoft.Json.Linq;
+using System;
+using System.Linq;
+
+#nullable enable
+namespace FileManager;
+
+public interface IPersistentDictionary
+{
+ bool Exists(string propertyName);
+ string? GetString(string propertyName, string? defaultValue = null);
+ T? GetNonString(string propertyName, T? defaultValue = default);
+ object? GetObject(string propertyName);
+ void SetString(string propertyName, string? newValue);
+ void SetNonString(string propertyName, object? newValue);
+ bool RemoveProperty(string propertyName);
+ bool SetWithJsonPath(string jsonPath, string propertyName, string? newValue, bool suppressLogging = false);
+ string? GetStringFromJsonPath(string jsonPath);
+
+ string? GetStringFromJsonPath(string jsonPath, string propertyName)
+ => GetStringFromJsonPath($"{jsonPath}.{propertyName}");
+
+ static T? UpCast(object obj)
+ {
+ if (obj.GetType().IsAssignableTo(typeof(T))) return (T)obj;
+ if (obj is JObject jObject) return jObject.ToObject();
+ if (obj is JValue jValue)
+ {
+ if (typeof(T).IsAssignableTo(typeof(Enum)))
+ {
+ return
+ Enum.TryParse(typeof(T), jValue.Value(), out var enumVal)
+ ? (T)enumVal
+ : Enum.GetValues(typeof(T)).Cast().First();
+ }
+ return jValue.Value();
+ }
+ throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}");
+ }
+}
diff --git a/Source/FileManager/PersistentDictionary.cs b/Source/FileManager/PersistentDictionary.cs
index 3ebbd1cc..9bc86746 100644
--- a/Source/FileManager/PersistentDictionary.cs
+++ b/Source/FileManager/PersistentDictionary.cs
@@ -2,14 +2,13 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
-using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
#nullable enable
namespace FileManager
{
- public class PersistentDictionary
+ public class PersistentDictionary : IPersistentDictionary
{
public string Filepath { get; }
public bool IsReadOnly { get; }
@@ -60,21 +59,8 @@ namespace FileManager
objectCache[propertyName] = defaultValue;
return defaultValue;
}
- if (obj.GetType().IsAssignableTo(typeof(T))) return (T)obj;
- if (obj is JObject jObject) return jObject.ToObject();
- if (obj is JValue jValue)
- {
- if (typeof(T).IsAssignableTo(typeof(Enum)))
- {
- return
- Enum.TryParse(typeof(T), jValue.Value(), out var enumVal)
- ? (T)enumVal
- : Enum.GetValues(typeof(T)).Cast().First();
- }
- return jValue.Value();
- }
- throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}");
- }
+ return IPersistentDictionary.UpCast(obj);
+ }
public object? GetObject(string propertyName)
{
@@ -89,7 +75,6 @@ namespace FileManager
return objectCache[propertyName];
}
- public string? GetStringFromJsonPath(string jsonPath, string propertyName) => GetStringFromJsonPath($"{jsonPath}.{propertyName}");
public string? GetStringFromJsonPath(string jsonPath)
{
if (!stringCache.ContainsKey(jsonPath))
diff --git a/Source/HangoverAvalonia/HangoverAvalonia.csproj b/Source/HangoverAvalonia/HangoverAvalonia.csproj
index da86862c..fdff7595 100644
--- a/Source/HangoverAvalonia/HangoverAvalonia.csproj
+++ b/Source/HangoverAvalonia/HangoverAvalonia.csproj
@@ -70,13 +70,11 @@
-
-
-
+
-
-
-
+
+
+
diff --git a/Source/HangoverAvalonia/Program.cs b/Source/HangoverAvalonia/Program.cs
index 037d6c57..fc42d4e9 100644
--- a/Source/HangoverAvalonia/Program.cs
+++ b/Source/HangoverAvalonia/Program.cs
@@ -1,5 +1,5 @@
using Avalonia;
-using Avalonia.ReactiveUI;
+using ReactiveUI.Avalonia;
using System;
namespace HangoverAvalonia
diff --git a/Source/LibationAvalonia/App.axaml.cs b/Source/LibationAvalonia/App.axaml.cs
index bbff098b..db969cf4 100644
--- a/Source/LibationAvalonia/App.axaml.cs
+++ b/Source/LibationAvalonia/App.axaml.cs
@@ -1,261 +1,261 @@
using ApplicationServices;
using Avalonia;
+using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Styling;
+using Avalonia.Threading;
+using Dinah.Core;
using LibationAvalonia.Dialogs;
+using LibationAvalonia.Themes;
using LibationAvalonia.Views;
using LibationFileManager;
+using LibationUiBase.Forms;
using System;
using System.Collections.Generic;
using System.IO;
-using System.Threading.Tasks;
-using Avalonia.Threading;
-using Dinah.Core;
-using LibationAvalonia.Themes;
-using Avalonia.Data.Core.Plugins;
using System.Linq;
-using LibationUiBase.Forms;
-using Avalonia.Controls;
+using System.Threading.Tasks;
#nullable enable
-namespace LibationAvalonia
+namespace LibationAvalonia;
+
+public class App : Application
{
- public class App : Application
+ public static Task>? LibraryTask { get; set; }
+ public static ChardonnayTheme? DefaultThemeColors { get; private set; }
+ public static MainWindow? MainWindow { get; private set; }
+ public static Uri AssetUriBase { get; } = new("avares://Libation/Assets/");
+ public static new Application Current => Application.Current ?? throw new InvalidOperationException("The Avalonia app hasn't started yet.");
+
+ public static Stream OpenAsset(string assetRelativePath)
+ => AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
+
+ public override void Initialize() => AvaloniaXamlLoader.Load(this);
+
+ public override void OnFrameworkInitializationCompleted()
{
- public static Task>? LibraryTask { get; set; }
- public static ChardonnayTheme? DefaultThemeColors { get; private set; }
- public static MainWindow? MainWindow { get; private set; }
- public static Uri AssetUriBase { get; } = new("avares://Libation/Assets/");
- public static new Application Current => Application.Current ?? throw new InvalidOperationException("The Avalonia app hasn't started yet.");
+ DefaultThemeColors = ChardonnayTheme.GetLiveTheme();
- public static Stream OpenAsset(string assetRelativePath)
- => AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
-
- public override void Initialize()
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
- AvaloniaXamlLoader.Load(this);
- }
+ // Chardonnay uses the OnLastWindowClose shutdown mode. As long as the application lifetime
+ // has one active window, the application will stay alive. Setup windows must be daisy chained,
+ // each closing windows opens the next window before closing itself to prevent the app from exiting.
- public override void OnFrameworkInitializationCompleted()
- {
- DefaultThemeColors = ChardonnayTheme.GetLiveTheme();
+ MessageBoxBase.ShowAsyncImpl = (owner, message, caption, buttons, icon, defaultButton, saveAndRestorePosition) =>
+ MessageBox.Show(owner as Window, message, caption, buttons, icon, defaultButton, saveAndRestorePosition);
- if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ // Avoid duplicate validations from both Avalonia and the CommunityToolkit.
+ // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
+ DisableAvaloniaDataAnnotationValidation();
+
+ Configuration config = Configuration.Instance;
+
+ if (!config.LibationSettingsAreValid)
{
- MessageBoxBase.ShowAsyncImpl = (owner, message, caption, buttons, icon, defaultButton, saveAndRestorePosition) =>
- MessageBox.Show(owner as Window, message, caption, buttons, icon, defaultButton, saveAndRestorePosition);
+ string defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory;
- // Avoid duplicate validations from both Avalonia and the CommunityToolkit.
- // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
- DisableAvaloniaDataAnnotationValidation();
+ // check for existing settings in default location
+ string defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
+ if (Configuration.SettingsFileIsValid(defaultSettingsFile))
+ Configuration.SetLibationFiles(defaultLibationFilesDir);
- var config = Configuration.Instance;
-
- if (!config.LibationSettingsAreValid)
+ if (config.LibationSettingsAreValid)
{
- var defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory;
-
- // check for existing settings in default location
- var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
- if (Configuration.SettingsFileIsValid(defaultSettingsFile))
- Configuration.SetLibationFiles(defaultLibationFilesDir);
-
- if (config.LibationSettingsAreValid)
- {
- LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
- ShowMainWindow(desktop);
- }
- else
- {
- var setupDialog = new SetupDialog { Config = config };
- setupDialog.Closing += Setup_Closing;
- desktop.MainWindow = setupDialog;
- }
+ LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
+ ShowMainWindow(desktop);
}
else
- ShowMainWindow(desktop);
- }
-
- base.OnFrameworkInitializationCompleted();
- }
- private void DisableAvaloniaDataAnnotationValidation()
- {
- // Get an array of plugins to remove
- var dataValidationPluginsToRemove =
- BindingPlugins.DataValidators.OfType().ToArray();
-
- // remove each entry found
- foreach (var plugin in dataValidationPluginsToRemove)
- {
- BindingPlugins.DataValidators.Remove(plugin);
- }
- }
-
- private async void Setup_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
- {
- if (sender is not SetupDialog setupDialog || ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
- return;
-
- try
- {
- // all returns should be preceded by either:
- // - if config.LibationSettingsAreValid
- // - error message, Exit()
- if (setupDialog.IsNewUser)
{
- Configuration.SetLibationFiles(Configuration.DefaultLibationFilesDirectory);
- setupDialog.Config.Books = Configuration.DefaultBooksDirectory;
-
- if (setupDialog.Config.LibationSettingsAreValid)
- {
- string? theme = setupDialog.SelectedTheme.Content as string;
-
- setupDialog.Config.SetString(theme, nameof(ThemeVariant));
-
- await RunMigrationsAsync(setupDialog.Config);
- LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
- AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
- ShowMainWindow(desktop);
- }
- else
- {
- e.Cancel = true;
- await CancelInstallation(setupDialog);
- }
+ SetupDialog setupDialog = new() { Config = config };
+ setupDialog.Closing += (_, e) => SetupClosing(setupDialog, desktop, e);
+ desktop.MainWindow = setupDialog;
}
- else if (setupDialog.IsReturningUser)
+ }
+ else
+ {
+ ShowMainWindow(desktop);
+ }
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+
+ private void DisableAvaloniaDataAnnotationValidation()
+ {
+ // Get an array of plugins to remove
+ DataAnnotationsValidationPlugin[] dataValidationPluginsToRemove =
+ BindingPlugins.DataValidators.OfType().ToArray();
+
+ // remove each entry found
+ foreach (DataAnnotationsValidationPlugin? plugin in dataValidationPluginsToRemove)
+ {
+ BindingPlugins.DataValidators.Remove(plugin);
+ }
+ }
+
+ private async void SetupClosing(SetupDialog setupDialog, IClassicDesktopStyleApplicationLifetime desktop, System.ComponentModel.CancelEventArgs e)
+ {
+ try
+ {
+ if (setupDialog.IsNewUser)
+ {
+ Configuration.SetLibationFiles(Configuration.DefaultLibationFilesDirectory);
+ setupDialog.Config.Books = Configuration.DefaultBooksDirectory;
+
+ if (setupDialog.Config.LibationSettingsAreValid)
{
- ShowLibationFilesDialog(desktop, setupDialog.Config, OnLibationFilesCompleted);
+ string? theme = setupDialog.SelectedTheme.Content as string;
+ setupDialog.Config.SetString(theme, nameof(ThemeVariant));
+
+ await RunMigrationsAsync(setupDialog.Config);
+ LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
+ AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
+ ShowMainWindow(desktop);
}
else
{
e.Cancel = true;
await CancelInstallation(setupDialog);
- return;
}
-
}
- catch (Exception ex)
+ else if (setupDialog.IsReturningUser)
{
- var title = "Fatal error, pre-logging";
- var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.";
- try
- {
- await MessageBox.ShowAdminAlert(setupDialog, body, title, ex);
- }
- catch
- {
- await MessageBox.Show(setupDialog, $"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error);
- }
- return;
- }
- }
-
- private async Task RunMigrationsAsync(Configuration config)
- {
- // most migrations go in here
- AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config);
-
- await MessageBox.VerboseLoggingWarning_ShowIfTrue();
-
- // logging is init'd here
- AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(AppScaffolding.Variety.Chardonnay, config);
- }
-
- private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action OnClose)
- {
- var libationFilesDialog = new LibationFilesDialog();
- desktop.MainWindow = libationFilesDialog;
- libationFilesDialog.Show();
-
- void WindowClosing(object? sender, System.ComponentModel.CancelEventArgs e)
- {
- libationFilesDialog.Closing -= WindowClosing;
- e.Cancel = true;
- OnClose?.Invoke(desktop, libationFilesDialog, config);
- }
- libationFilesDialog.Closing += WindowClosing;
- }
-
- private async void OnLibationFilesCompleted(IClassicDesktopStyleApplicationLifetime desktop, LibationFilesDialog libationFilesDialog, Configuration config)
- {
- Configuration.SetLibationFiles(libationFilesDialog.SelectedDirectory);
- if (config.LibationSettingsAreValid)
- {
- await RunMigrationsAsync(config);
-
- LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
- AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
- ShowMainWindow(desktop);
+ ShowLibationFilesDialog(desktop, setupDialog.Config);
}
else
{
- // path did not result in valid settings
- var continueResult = await MessageBox.Show(
- libationFilesDialog,
- $"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
- "New install?",
- MessageBoxButtons.YesNo,
- MessageBoxIcon.Question);
+ e.Cancel = true;
+ await CancelInstallation(setupDialog);
+ }
+ }
+ catch (Exception ex)
+ {
+ string title = "Fatal error, pre-logging";
+ string body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.";
- if (continueResult == DialogResult.Yes)
+ MessageBoxAlertAdminDialog alert = new(body, title, ex);
+ desktop.MainWindow = alert;
+ alert.Show();
+ }
+ }
+
+ private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config)
+ {
+ LibationFilesDialog libationFilesDialog = new();
+ desktop.MainWindow = libationFilesDialog;
+ libationFilesDialog.Show();
+
+ async void WindowClosing(object? sender, System.ComponentModel.CancelEventArgs e)
+ {
+ libationFilesDialog.Closing -= WindowClosing;
+ e.Cancel = true;
+ if (libationFilesDialog.DialogResult == DialogResult.OK)
+ OnLibationFilesCompleted(desktop, libationFilesDialog, config);
+ else
+ await CancelInstallation(libationFilesDialog);
+ }
+ libationFilesDialog.Closing += WindowClosing;
+ }
+
+ private async void OnLibationFilesCompleted(IClassicDesktopStyleApplicationLifetime desktop, LibationFilesDialog libationFilesDialog, Configuration config)
+ {
+ Configuration.SetLibationFiles(libationFilesDialog.SelectedDirectory);
+ if (config.LibationSettingsAreValid)
+ {
+ await RunMigrationsAsync(config);
+
+ LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
+ AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
+ ShowMainWindow(desktop);
+ }
+ else
+ {
+ // path did not result in valid settings
+ DialogResult continueResult = await MessageBox.Show(
+ libationFilesDialog,
+ $"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
+ "New install?",
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Question);
+
+ if (continueResult == DialogResult.Yes)
+ {
+ config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books));
+
+ if (config.LibationSettingsAreValid)
{
- config.Books = Configuration.DefaultBooksDirectory;
-
- if (config.LibationSettingsAreValid)
- {
- await RunMigrationsAsync(config);
- LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
- AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
- ShowMainWindow(desktop);
- }
- else
- await CancelInstallation(libationFilesDialog);
+ await RunMigrationsAsync(config);
+ LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
+ AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
+ ShowMainWindow(desktop);
}
else
+ {
await CancelInstallation(libationFilesDialog);
+ }
}
-
- libationFilesDialog.Close();
- }
-
- static async Task CancelInstallation(Window window)
- {
- await MessageBox.Show(window, "Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
- Environment.Exit(0);
- }
-
- private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
- {
- Configuration.Instance.PropertyChanged += ThemeVariant_PropertyChanged;
- OpenAndApplyTheme(Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)));
-
- var mainWindow = new MainWindow();
- desktop.MainWindow = MainWindow = mainWindow;
- mainWindow.Loaded += MainWindow_Loaded;
- mainWindow.RestoreSizeAndLocation(Configuration.Instance);
- mainWindow.Show();
- }
-
- [PropertyChangeFilter(nameof(ThemeVariant))]
- private static void ThemeVariant_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
- => OpenAndApplyTheme(e.NewValue as string);
-
- private static void OpenAndApplyTheme(string? themeVariant)
- {
- using var themePersister = ChardonnayThemePersister.Create();
- themePersister?.Target.ApplyTheme(themeVariant);
- }
-
- private static async void MainWindow_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
- {
- if (LibraryTask is not null && MainWindow is not null)
+ else
{
- var library = await LibraryTask;
- await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
+ await CancelInstallation(libationFilesDialog);
}
}
+
+ libationFilesDialog.Close();
+ }
+
+ private static async Task CancelInstallation(Window window)
+ {
+ await MessageBox.Show(window, "Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ Environment.Exit(-1);
+ }
+
+ private async Task RunMigrationsAsync(Configuration config)
+ {
+ // most migrations go in here
+ AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config);
+
+ await MessageBox.VerboseLoggingWarning_ShowIfTrue();
+
+ // logging is init'd here
+ AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(AppScaffolding.Variety.Chardonnay, config);
+ }
+
+ private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ Configuration.Instance.PropertyChanged += ThemeVariant_PropertyChanged;
+ Current.ActualThemeVariantChanged += OnActualThemeVariantChanged;
+ OnActualThemeVariantChanged(Current, EventArgs.Empty);
+
+ MainWindow mainWindow = new();
+ desktop.MainWindow = MainWindow = mainWindow;
+ mainWindow.Loaded += MainWindow_Loaded;
+ mainWindow.RestoreSizeAndLocation(Configuration.Instance);
+ mainWindow.Show();
+ }
+
+ [PropertyChangeFilter(nameof(ThemeVariant))]
+ private static void ThemeVariant_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
+ => OpenAndApplyTheme(e.NewValue as string);
+
+ private static void OnActualThemeVariantChanged(object? sender, EventArgs e)
+ => OpenAndApplyTheme(Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)));
+
+ private static void OpenAndApplyTheme(string? themeVariant)
+ {
+ using ChardonnayThemePersister? themePersister = ChardonnayThemePersister.Create();
+ themePersister?.Target.ApplyTheme(themeVariant);
+ }
+
+ private static async void MainWindow_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ if (LibraryTask is not null && MainWindow is not null)
+ {
+ List library = await LibraryTask;
+ await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
+ }
}
}
diff --git a/Source/LibationAvalonia/AvaloniaUtils.cs b/Source/LibationAvalonia/AvaloniaUtils.cs
index 4bfc1aa7..82a360ba 100644
--- a/Source/LibationAvalonia/AvaloniaUtils.cs
+++ b/Source/LibationAvalonia/AvaloniaUtils.cs
@@ -23,11 +23,6 @@ namespace LibationAvalonia
? dialogWindow.ShowDialog(window)
: Task.FromResult(DialogResult.None);
- public static Task ShowDialogAsync(this Dialogs.Login.WebLoginDialog dialogWindow, Window? owner = null)
- => ((owner ?? App.MainWindow) is Window window)
- ? dialogWindow.ShowDialog(window)
- : Task.FromResult(DialogResult.None);
-
public static Window? GetParentWindow(this Control control) => control.GetVisualRoot() as Window;
diff --git a/Source/LibationAvalonia/Controls/NativeWebView.cs b/Source/LibationAvalonia/Controls/NativeWebView.cs
deleted file mode 100644
index 0aad165d..00000000
--- a/Source/LibationAvalonia/Controls/NativeWebView.cs
+++ /dev/null
@@ -1,176 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Input;
-using Avalonia.Platform;
-using Avalonia;
-using LibationFileManager;
-using System;
-using System.Threading.Tasks;
-
-namespace LibationAvalonia.Controls;
-
-#nullable enable
-public class NativeWebView : NativeControlHost, IWebView
-{
- private IWebViewAdapter? _webViewAdapter;
- private Uri? _delayedSource;
- private TaskCompletionSource _webViewReadyCompletion = new();
-
- public event EventHandler? NavigationCompleted;
-
- public event EventHandler? NavigationStarted;
- public event EventHandler? DOMContentLoaded;
-
- public bool CanGoBack => _webViewAdapter?.CanGoBack ?? false;
-
- public bool CanGoForward => _webViewAdapter?.CanGoForward ?? false;
-
- public Uri? Source
- {
- get => _webViewAdapter?.Source ?? throw new InvalidOperationException("Control was not initialized");
- set
- {
- if (_webViewAdapter is null)
- {
- _delayedSource = value;
- return;
- }
- _webViewAdapter.Source = value;
- }
- }
-
-
- public bool GoBack()
- {
- return _webViewAdapter?.GoBack() ?? throw new InvalidOperationException("Control was not initialized");
- }
-
- public bool GoForward()
- {
- return _webViewAdapter?.GoForward() ?? throw new InvalidOperationException("Control was not initialized");
- }
-
- public Task InvokeScriptAsync(string scriptName)
- {
- return _webViewAdapter is null
- ? throw new InvalidOperationException("Control was not initialized")
- : _webViewAdapter.InvokeScriptAsync(scriptName);
- }
-
- public void Navigate(Uri url)
- {
- (_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
- .Navigate(url);
- }
-
- public Task NavigateToString(string text)
- {
- return (_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
- .NavigateToString(text);
- }
-
- public void Refresh()
- {
- (_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
- .Refresh();
- }
-
- public void Stop()
- {
- (_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
- .Stop();
- }
-
- public Task WaitForNativeHost()
- {
- return _webViewReadyCompletion.Task;
- }
-
- private class PlatformHandle : IPlatformHandle
- {
- public nint Handle { get; init; }
-
- public string? HandleDescriptor { get; init; }
- }
-
- protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
- {
- _webViewAdapter = InteropFactory.Create().CreateWebViewAdapter();
-
- if (_webViewAdapter is null)
- return base.CreateNativeControlCore(parent);
- else
- {
- SubscribeOnEvents();
- var handle = new PlatformHandle
- {
- Handle = _webViewAdapter.PlatformHandle.Handle,
- HandleDescriptor = _webViewAdapter.PlatformHandle.HandleDescriptor
- };
-
- if (_delayedSource is not null)
- {
- _webViewAdapter.Source = _delayedSource;
- }
-
- _webViewReadyCompletion.TrySetResult();
-
- return handle;
- }
- }
-
- private void SubscribeOnEvents()
- {
- if (_webViewAdapter is not null)
- {
- _webViewAdapter.NavigationStarted += WebViewAdapterOnNavigationStarted;
- _webViewAdapter.NavigationCompleted += WebViewAdapterOnNavigationCompleted;
- _webViewAdapter.DOMContentLoaded += _webViewAdapter_DOMContentLoaded;
- }
- }
-
- private void _webViewAdapter_DOMContentLoaded(object? sender, EventArgs e)
- {
- DOMContentLoaded?.Invoke(this, e);
- }
-
- private void WebViewAdapterOnNavigationStarted(object? sender, WebViewNavigationEventArgs e)
- {
- NavigationStarted?.Invoke(this, e);
- }
-
- private void WebViewAdapterOnNavigationCompleted(object? sender, WebViewNavigationEventArgs e)
- {
- NavigationCompleted?.Invoke(this, e);
- }
-
- protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
- {
- base.OnPropertyChanged(change);
- if (change.Property == BoundsProperty && change.NewValue is Rect rect)
- {
- var scaling = (float)(VisualRoot?.RenderScaling ?? 1.0f);
- _webViewAdapter?.HandleResize((int)(rect.Width * scaling), (int)(rect.Height * scaling), scaling);
- }
- }
-
- protected override void OnKeyDown(KeyEventArgs e)
- {
- if (_webViewAdapter != null)
- {
- e.Handled = _webViewAdapter.HandleKeyDown((uint)e.Key, (uint)e.KeyModifiers);
- }
-
- base.OnKeyDown(e);
- }
-
- protected override void DestroyNativeControlCore(IPlatformHandle control)
- {
- if (_webViewAdapter is not null)
- {
- _webViewReadyCompletion = new TaskCompletionSource();
- _webViewAdapter.NavigationStarted -= WebViewAdapterOnNavigationStarted;
- _webViewAdapter.NavigationCompleted -= WebViewAdapterOnNavigationCompleted;
- (_webViewAdapter as IDisposable)?.Dispose();
- }
- }
-}
diff --git a/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs b/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs
index 63758b69..d87e9401 100644
--- a/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs
+++ b/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs
@@ -19,8 +19,7 @@ namespace LibationAvalonia.Controls.Settings
InitializeComponent();
if (Design.IsDesignMode)
{
- _ = Configuration.Instance.LibationFiles;
- DataContext = new AudioSettingsVM(Configuration.Instance);
+ DataContext = new AudioSettingsVM(Configuration.CreateMockInstance());
}
}
@@ -39,7 +38,7 @@ namespace LibationAvalonia.Controls.Settings
{
using var accounts = AudibleApiStorage.GetAccountsSettingsPersister();
- if (!accounts.AccountsSettings.Accounts.Any(a => a.IdentityTokens.DeviceType == AudibleApi.Resources.DeviceType))
+ if (!accounts.AccountsSettings.Accounts.All(a => a.IdentityTokens.DeviceType == AudibleApi.Resources.DeviceType))
{
if (VisualRoot is Window parent)
{
diff --git a/Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml.cs b/Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml.cs
index 2be2bb5b..aa49d94d 100644
--- a/Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml.cs
+++ b/Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml.cs
@@ -16,8 +16,7 @@ namespace LibationAvalonia.Controls.Settings
InitializeComponent();
if (Design.IsDesignMode)
{
- _ = Configuration.Instance.LibationFiles;
- DataContext = new DownloadDecryptSettingsVM(Configuration.Instance);
+ DataContext = new DownloadDecryptSettingsVM(Configuration.CreateMockInstance());
}
}
diff --git a/Source/LibationAvalonia/Controls/Settings/Import.axaml.cs b/Source/LibationAvalonia/Controls/Settings/Import.axaml.cs
index cd6faaf3..163a7d38 100644
--- a/Source/LibationAvalonia/Controls/Settings/Import.axaml.cs
+++ b/Source/LibationAvalonia/Controls/Settings/Import.axaml.cs
@@ -12,8 +12,7 @@ namespace LibationAvalonia.Controls.Settings
if (Design.IsDesignMode)
{
- _ = Configuration.Instance.LibationFiles;
- DataContext = new ImportSettingsVM(Configuration.Instance);
+ DataContext = new ImportSettingsVM(Configuration.CreateMockInstance());
}
}
}
diff --git a/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs b/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs
index 272f5da7..66148fc8 100644
--- a/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs
+++ b/Source/LibationAvalonia/Controls/Settings/Important.axaml.cs
@@ -18,8 +18,7 @@ namespace LibationAvalonia.Controls.Settings
InitializeComponent();
if (Design.IsDesignMode)
{
- _ = Configuration.Instance.LibationFiles;
- DataContext = new ImportantSettingsVM(Configuration.Instance);
+ DataContext = new ImportantSettingsVM(Configuration.CreateMockInstance());
}
ThemeComboBox.SelectionChanged += ThemeComboBox_SelectionChanged;
diff --git a/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs b/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs
index 029b46f3..5966103f 100644
--- a/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs
+++ b/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs
@@ -2,11 +2,8 @@ using Avalonia.Controls;
using DataLayer;
using Dinah.Core.ErrorHandling;
using LibationAvalonia.ViewModels;
-using LibationFileManager;
using LibationUiBase.ProcessQueue;
-using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
@@ -32,9 +29,7 @@ public partial class ThemePreviewControl : UserControl
if (Design.IsDesignMode)
{
- using var ms1 = new MemoryStream();
- App.OpenAsset("img-coverart-prod-unavailable_80x80.jpg").CopyTo(ms1);
- PictureStorage.SetDefaultImage(PictureSize._80x80, ms1.ToArray());
+ MainVM.Configure_NonUI();
}
QueuedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Queued };
@@ -56,32 +51,12 @@ public partial class ThemePreviewControl : UserControl
private IEnumerable CreateMockBooks()
{
- var author = new Contributor("Some Author", "asin_contributor");
- var narrator = new Contributor("Some Narrator", "asin_narrator");
-
- var book1 = new Book(new AudibleProductId("asin_book1"), "Some Book 1", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
- var book2 = new Book(new AudibleProductId("asin_book2"), "Some Book 2", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
- var book3 = new Book(new AudibleProductId("asin_book3"), "Some Book 3", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
- var book4 = new Book(new AudibleProductId("asin_book4"), "Some Book 4", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
- var seriesParent = new Book(new AudibleProductId("asin_series"), "Some Series", "", "Demo Series Entry", 0, ContentType.Parent, [author], [narrator], "us");
- var episode = new Book(new AudibleProductId("asin_episode"), "Some Episode", "Episode 1", "Demo Episode Entry", 56, ContentType.Episode, [author], [narrator], "us");
-
- var series = new Series(new AudibleSeriesId(seriesParent.AudibleProductId), seriesParent.Title);
-
- seriesParent.UpsertSeries(series, "");
- episode.UpsertSeries(series, "1");
-
- book1.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
- book4.UserDefinedItem.BookStatus = LiberatedStatus.Error;
- //Set the backing field directly to preserve LiberatedStatus.PartialDownload
- typeof(UserDefinedItem).GetField("_bookStatus", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(book2.UserDefinedItem, LiberatedStatus.PartialDownload);
-
- yield return new LibraryBook(book1, System.DateTime.Now.AddDays(4), "someone@email.co");
- yield return new LibraryBook(book2, System.DateTime.Now.AddDays(3), "someone@email.co");
- yield return new LibraryBook(book3, System.DateTime.Now.AddDays(2), "someone@email.co") { AbsentFromLastScan = true };
- yield return new LibraryBook(book4, System.DateTime.Now.AddDays(1), "someone@email.co");
- yield return new LibraryBook(seriesParent, System.DateTime.Now, "someone@email.co");
- yield return new LibraryBook(episode, System.DateTime.Now, "someone@email.co");
+ yield return MockLibraryBook.CreateBook(title: "Some Book 1", subtitle: "The Theming", dateAdded: System.DateTime.Now.AddDays(4)).WithBookStatus(LiberatedStatus.Liberated);
+ yield return MockLibraryBook.CreateBook(title: "Some Book 2", dateAdded: System.DateTime.Now.AddDays(3)).WithBookStatus(LiberatedStatus.PartialDownload);
+ yield return MockLibraryBook.CreateBook(title: "Some Book 3", dateAdded: System.DateTime.Now.AddDays(2), absetFromLastScan: true).WithPdfStatus(LiberatedStatus.NotLiberated);
+ yield return MockLibraryBook.CreateBook(title: "Some Book 4", dateAdded: System.DateTime.Now.AddDays(1)).WithBookStatus(LiberatedStatus.Error);
+ yield return MockLibraryBook.CreateBook(title: "Some Series", subtitle: "", contentType: ContentType.Parent).AddSeries("Some Series", 0);
+ yield return MockLibraryBook.CreateBook(title: "Some Episode", subtitle: "Episode 1", contentType: ContentType.Episode).AddSeries("Some Series", 1);
}
private class MockProcessable : FileLiberator.Processable
diff --git a/Source/LibationAvalonia/Dialogs/AboutDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/AboutDialog.axaml.cs
index 8218882f..30ed6303 100644
--- a/Source/LibationAvalonia/Dialogs/AboutDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/AboutDialog.axaml.cs
@@ -16,9 +16,6 @@ namespace LibationAvalonia.Dialogs
private readonly AboutVM _viewModel;
public AboutDialog() : base(saveAndRestorePosition:false)
{
- if (Design.IsDesignMode)
- _ = Configuration.Instance.LibationFiles;
-
InitializeComponent();
DataContext = _viewModel = new AboutVM();
diff --git a/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs
index fe30623c..b0dabb41 100644
--- a/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs
@@ -37,8 +37,17 @@ namespace LibationAvalonia.Dialogs
if (Design.IsDesignMode)
{
- using var context = DbContexts.GetContext();
- LibraryBook = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
+ MainVM.Configure_NonUI();
+ LibraryBook
+ = MockLibraryBook
+ .CreateBook()
+ .AddAuthor("Author 2")
+ .AddNarrator("Narrator 2")
+ .AddSeries("Series Name", 1)
+ .AddCategoryLadder("Parent", "Child Category")
+ .AddCategoryLadder("Parent", "Child Category 2")
+ .WithBookStatus(LiberatedStatus.NotLiberated)
+ .WithPdfStatus(LiberatedStatus.Liberated);
}
}
public BookDetailsDialog(LibraryBook libraryBook) : this()
diff --git a/Source/LibationAvalonia/Dialogs/DialogWindow.cs b/Source/LibationAvalonia/Dialogs/DialogWindow.cs
index 016bbd3a..1bb07286 100644
--- a/Source/LibationAvalonia/Dialogs/DialogWindow.cs
+++ b/Source/LibationAvalonia/Dialogs/DialogWindow.cs
@@ -16,6 +16,7 @@ namespace LibationAvalonia.Dialogs
public bool SaveAndRestorePosition { get; set; }
public Control ControlToFocusOnShow { get; set; }
protected override Type StyleKeyOverride => typeof(DialogWindow);
+ public DialogResult DialogResult { get; private set; } = DialogResult.None;
public DialogWindow(bool saveAndRestorePosition = true)
{
@@ -27,7 +28,15 @@ namespace LibationAvalonia.Dialogs
Closing += DialogWindow_Closing;
if (Design.IsDesignMode)
- RequestedThemeVariant = ThemeVariant.Dark;
+ {
+ var themeVariant = Configuration.CreateMockInstance().GetString(propertyName: nameof(ThemeVariant));
+ RequestedThemeVariant = themeVariant switch
+ {
+ nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
+ nameof(ThemeVariant.Light) => ThemeVariant.Light,
+ _ => ThemeVariant.Default,
+ };
+ }
}
private void DialogWindow_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -66,6 +75,12 @@ namespace LibationAvalonia.Dialogs
ControlToFocusOnShow?.Focus();
}
+ public void Close(DialogResult dialogResult)
+ {
+ DialogResult = dialogResult;
+ base.Close(dialogResult);
+ }
+
protected virtual void SaveAndClose() => Close(DialogResult.OK);
protected virtual async Task SaveAndCloseAsync() => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(SaveAndClose);
protected virtual void CancelAndClose() => Close(DialogResult.Cancel);
diff --git a/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs
index 07725b3c..fe82746a 100644
--- a/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs
@@ -25,10 +25,11 @@ public partial class EditTemplateDialog : DialogWindow
if (Design.IsDesignMode)
{
- _ = Configuration.Instance.LibationFiles;
+ var mockInstance = Configuration.CreateMockInstance();
+ mockInstance.Books = Configuration.DefaultBooksDirectory;
RequestedThemeVariant = ThemeVariant.Dark;
- var editor = TemplateEditor.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
- _viewModel = new(Configuration.Instance, editor);
+ var editor = TemplateEditor.CreateFilenameEditor(mockInstance.Books, mockInstance.FileTemplate);
+ _viewModel = new(mockInstance, editor);
_viewModel.ResetTextBox(editor.EditingTemplate.TemplateText);
Title = $"Edit {editor.TemplateName}";
DataContext = _viewModel;
diff --git a/Source/LibationAvalonia/Dialogs/Login/ApprovalNeededDialog.axaml b/Source/LibationAvalonia/Dialogs/Login/ApprovalNeededDialog.axaml
deleted file mode 100644
index 8d52c43c..00000000
--- a/Source/LibationAvalonia/Dialogs/Login/ApprovalNeededDialog.axaml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Source/LibationAvalonia/Dialogs/Login/ApprovalNeededDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/Login/ApprovalNeededDialog.axaml.cs
deleted file mode 100644
index 5bab6bb3..00000000
--- a/Source/LibationAvalonia/Dialogs/Login/ApprovalNeededDialog.axaml.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System.Threading.Tasks;
-
-namespace LibationAvalonia.Dialogs.Login
-{
- public partial class ApprovalNeededDialog : DialogWindow
- {
- public ApprovalNeededDialog() : base(saveAndRestorePosition: false)
- {
- InitializeComponent();
- }
-
- protected override Task SaveAndCloseAsync()
- {
- Serilog.Log.Logger.Information("Approve button clicked");
-
- return base.SaveAndCloseAsync();
- }
-
- public async void Approve_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
- => await SaveAndCloseAsync();
- }
-}
diff --git a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginCallback.cs b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginCallback.cs
index 3e5a5725..377e32df 100644
--- a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginCallback.cs
+++ b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginCallback.cs
@@ -1,63 +1,19 @@
using AudibleApi;
-using AudibleUtilities;
-using Avalonia.Threading;
-using LibationUiBase.Forms;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public class AvaloniaLoginCallback : ILoginCallback
{
- private Account _account { get; }
-
public string DeviceName { get; } = "Libation";
- public AvaloniaLoginCallback(Account account)
- {
- _account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
- }
-
- public async Task Get2faCodeAsync(string prompt)
- => await Dispatcher.UIThread.InvokeAsync(async () =>
- {
- var dialog = new _2faCodeDialog(prompt);
- if (await dialog.ShowDialogAsync() is DialogResult.OK)
- return dialog.Code;
- return null;
- });
-
- public async Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
- => await Dispatcher.UIThread.InvokeAsync(async () =>
- {
- var dialog = new CaptchaDialog(password, captchaImage);
- if (await dialog.ShowDialogAsync() is DialogResult.OK)
- return (dialog.Password, dialog.Answer);
- return (null, null);
- });
-
- public async Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
- => await Dispatcher.UIThread.InvokeAsync(async () =>
- {
- var dialog = new MfaDialog(mfaConfig);
- if (await dialog.ShowDialogAsync() is DialogResult.OK)
- return (dialog.SelectedName, dialog.SelectedValue);
- return (null, null);
- });
-
- public async Task<(string email, string password)> GetLoginAsync()
- => await Dispatcher.UIThread.InvokeAsync(async () =>
- {
- var dialog = new LoginCallbackDialog(_account);
- if (await dialog.ShowDialogAsync() is DialogResult.OK)
- return (_account.AccountId, dialog.Password);
- return (null, null);
- });
-
- public async Task ShowApprovalNeededAsync()
- => await Dispatcher.UIThread.InvokeAsync(async () =>
- {
- var dialog = new ApprovalNeededDialog();
- await dialog.ShowDialogAsync();
- });
+ public Task Get2faCodeAsync(string prompt) => throw new System.NotSupportedException();
+ public Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
+ => throw new System.NotSupportedException();
+ public Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
+ => throw new System.NotSupportedException();
+ public Task<(string email, string password)> GetLoginAsync()
+ => throw new System.NotSupportedException();
+ public Task ShowApprovalNeededAsync() => throw new System.NotSupportedException();
}
}
\ No newline at end of file
diff --git a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs
index d6745ad5..65c1404b 100644
--- a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs
+++ b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs
@@ -1,5 +1,6 @@
using AudibleApi;
using AudibleUtilities;
+using Avalonia.Controls;
using Avalonia.Threading;
using LibationFileManager;
using LibationUiBase.Forms;
@@ -11,14 +12,13 @@ namespace LibationAvalonia.Dialogs.Login
{
public class AvaloniaLoginChoiceEager : ILoginChoiceEager
{
- public ILoginCallback LoginCallback { get; }
+ public ILoginCallback LoginCallback { get; } = new AvaloniaLoginCallback();
private readonly Account _account;
public AvaloniaLoginChoiceEager(Account account)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
- LoginCallback = new AvaloniaLoginCallback(_account);
}
public async Task StartAsync(ChoiceIn choiceIn)
@@ -26,41 +26,84 @@ namespace LibationAvalonia.Dialogs.Login
private async Task StartAsyncInternal(ChoiceIn choiceIn)
{
- if (Configuration.IsWindows && Environment.OSVersion.Version.Major >= 10)
+ try
{
- try
- {
- var weblogin = new WebLoginDialog(_account.AccountId, choiceIn.LoginUrl);
- if (await weblogin.ShowDialogAsync(App.MainWindow) is DialogResult.OK)
- return ChoiceOut.External(weblogin.ResponseUrl);
- }
- catch (Exception ex)
- {
- Serilog.Log.Logger.Error(ex, $"Failed to run {nameof(WebLoginDialog)}");
- }
+ if (await BrowserLoginAsync(choiceIn.LoginUrl) is ChoiceOut external)
+ return external;
+ }
+ catch (Exception ex)
+ {
+ Serilog.Log.Logger.Error(ex, $"Failed to use the {nameof(NativeWebDialog)}");
}
- var dialog = new LoginChoiceEagerDialog(_account);
+ var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
+ return await externalDialog.ShowDialogAsync() is DialogResult.OK
+ ? ChoiceOut.External(externalDialog.ResponseUrl)
+ : null;
+ }
- if (await dialog.ShowDialogAsync() is not DialogResult.OK ||
- (dialog.LoginMethod is LoginMethod.Api && string.IsNullOrWhiteSpace(dialog.Password)))
- return null;
+ private async Task BrowserLoginAsync(string url)
+ {
+ TaskCompletionSource tcs = new();
- switch (dialog.LoginMethod)
+ NativeWebDialog dialog = new()
{
- case LoginMethod.Api:
- return ChoiceOut.WithApi(dialog.Account.AccountId, dialog.Password);
- case LoginMethod.External:
- {
- var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
- return await externalDialog.ShowDialogAsync() is DialogResult.OK
- ? ChoiceOut.External(externalDialog.ResponseUrl)
- : null;
- }
- default:
- throw new Exception($"Unknown {nameof(LoginMethod)} value");
+ Title = "Audible Login",
+ CanUserResize = true,
+ Source = new Uri(url)
+ };
+
+ dialog.AdapterCreated += Dialog_AdapterCreated;
+ dialog.NavigationCompleted += Dialog_NavigationCompleted;
+ dialog.Closing += (_, _) => tcs.TrySetResult(null);
+ dialog.NavigationStarted += (_, e) =>
+ {
+ if (e.Request?.AbsolutePath.StartsWith("/ap/maplanding") is true)
+ {
+ tcs.TrySetResult(ChoiceOut.External(e.Request.ToString()));
+ dialog.Close();
+ }
+ };
+
+ if (!Configuration.IsLinux && App.MainWindow is TopLevel topLevel)
+ dialog.Show(topLevel);
+ else
+ dialog.Show();
+
+ return await tcs.Task;
+ }
+
+ private async void Dialog_NavigationCompleted(object? sender, WebViewNavigationCompletedEventArgs e)
+ {
+ if (e.IsSuccess && sender is NativeWebDialog dialog)
+ {
+ await dialog.InvokeScript(getScript(_account.AccountId));
}
}
+ private void Dialog_AdapterCreated(object? sender, WebViewAdapterEventArgs e)
+ {
+ if ((sender as NativeWebDialog)?.TryGetWindow() is Window window)
+ {
+ window.Width = 450;
+ window.Height = 700;
+ }
+ }
+
+ private static string getScript(string accountID) => $$"""
+ (function() {
+ function populateForm(){
+ var email = document.querySelector("input[id='ap_email_login']");
+ if (email !== null)
+ email.value = '{{accountID}}';
+
+ var pass = document.querySelector("input[name='password']");
+ if (pass !== null)
+ pass.focus();
+ }
+ window.addEventListener("load", (event) => { populateForm(); });
+ populateForm();
+ })()
+ """;
}
}
diff --git a/Source/LibationAvalonia/Dialogs/Login/CaptchaDialog.axaml b/Source/LibationAvalonia/Dialogs/Login/CaptchaDialog.axaml
deleted file mode 100644
index 4c4ab245..00000000
--- a/Source/LibationAvalonia/Dialogs/Login/CaptchaDialog.axaml
+++ /dev/null
@@ -1,72 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Source/LibationAvalonia/Dialogs/Login/CaptchaDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/Login/CaptchaDialog.axaml.cs
deleted file mode 100644
index c079332b..00000000
--- a/Source/LibationAvalonia/Dialogs/Login/CaptchaDialog.axaml.cs
+++ /dev/null
@@ -1,120 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Media.Imaging;
-using LibationAvalonia.ViewModels;
-using ReactiveUI;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace LibationAvalonia.Dialogs.Login
-{
- public partial class CaptchaDialog : DialogWindow
- {
- public string Password => _viewModel.Password;
- public string Answer => _viewModel.Answer;
-
- private readonly CaptchaDialogViewModel _viewModel;
- public CaptchaDialog() : base(saveAndRestorePosition: false)
- {
- InitializeComponent();
- passwordBox = this.FindControl(nameof(passwordBox));
- captchaBox = this.FindControl(nameof(captchaBox));
- }
-
- public CaptchaDialog(string password, byte[] captchaImage) : this()
- {
- //Avalonia doesn't support animated gifs.
- //Deconstruct gifs into frames and manually switch them.
- using var gif = SixLabors.ImageSharp.Image.Load(captchaImage);
- var gifEncoder = new SixLabors.ImageSharp.Formats.Gif.GifEncoder();
- var gifFrames = new Bitmap[gif.Frames.Count];
- var frameDelayMs = new int[gif.Frames.Count];
-
- for (int i = 0; i < gif.Frames.Count; i++)
- {
- var frameMetadata = gif.Frames[i].Metadata.GetFormatMetadata(SixLabors.ImageSharp.Formats.Gif.GifFormat.Instance);
-
- using var clonedFrame = gif.Frames.CloneFrame(i);
- using var framems = new MemoryStream();
-
- clonedFrame.Save(framems, gifEncoder);
- framems.Position = 0;
-
- gifFrames[i] = new Bitmap(framems);
- frameDelayMs[i] = frameMetadata.FrameDelay * 10;
- }
-
- DataContext = _viewModel = new(password, gifFrames, frameDelayMs);
-
- Opened += (_, _) => (string.IsNullOrEmpty(password) ? passwordBox : captchaBox).Focus();
- }
-
- protected override async Task SaveAndCloseAsync()
- {
- if (string.IsNullOrWhiteSpace(_viewModel.Password))
- {
- await MessageBox.Show(this, "Please re-enter your password");
- return;
- }
-
- Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { _viewModel.Answer });
-
- await _viewModel.StopAsync();
- await base.SaveAndCloseAsync();
- }
-
- protected override async Task CancelAndCloseAsync()
- {
- await _viewModel.StopAsync();
- await base.CancelAndCloseAsync();
- }
-
- public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
- => await SaveAndCloseAsync();
- }
-
- public class CaptchaDialogViewModel : ViewModelBase
- {
- public string Answer { get; set; }
- public string Password { get; set; }
- public Bitmap CaptchaImage { get => _captchaImage; private set => this.RaiseAndSetIfChanged(ref _captchaImage, value); }
-
- private Bitmap _captchaImage;
- private bool keepSwitching = true;
- private readonly Task FrameSwitch;
-
- public CaptchaDialogViewModel(string password, Bitmap[] gifFrames, int[] frameDelayMs)
- {
- Password = password;
- if (gifFrames.Length == 1)
- {
- FrameSwitch = Task.CompletedTask;
- CaptchaImage = gifFrames[0];
- }
- else
- {
- FrameSwitch = SwitchFramesAsync(gifFrames, frameDelayMs);
- }
- }
-
- public async Task StopAsync()
- {
- keepSwitching = false;
- await FrameSwitch;
- }
-
- private async Task SwitchFramesAsync(Bitmap[] gifFrames, int[] frameDelayMs)
- {
- int index = 0;
- while (keepSwitching)
- {
- CaptchaImage = gifFrames[index];
- await Task.Delay(frameDelayMs[index++]);
-
- index %= gifFrames.Length;
- }
-
- foreach (var frame in gifFrames)
- frame.Dispose();
- }
- }
-}
diff --git a/Source/LibationAvalonia/Dialogs/Login/LoginCallbackDialog.axaml b/Source/LibationAvalonia/Dialogs/Login/LoginCallbackDialog.axaml
deleted file mode 100644
index 51b3cfe2..00000000
--- a/Source/LibationAvalonia/Dialogs/Login/LoginCallbackDialog.axaml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Source/LibationAvalonia/Dialogs/Login/LoginCallbackDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/Login/LoginCallbackDialog.axaml.cs
deleted file mode 100644
index faf919fc..00000000
--- a/Source/LibationAvalonia/Dialogs/Login/LoginCallbackDialog.axaml.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using AudibleUtilities;
-using Avalonia.Controls;
-using Dinah.Core;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace LibationAvalonia.Dialogs.Login
-{
- public partial class LoginCallbackDialog : DialogWindow
- {
- public Account Account { get; }
- public string Password { get; set; }
-
- public LoginCallbackDialog() : base(saveAndRestorePosition: false)
- {
- InitializeComponent();
-
- if (Design.IsDesignMode)
- {
- using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
- var accounts = persister.AccountsSettings.Accounts;
- Account = accounts.FirstOrDefault();
- DataContext = this;
- }
- }
- public LoginCallbackDialog(Account account) : this()
- {
- Account = account;
- DataContext = this;
- }
-
- protected override Task SaveAndCloseAsync()
- {
- Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { email = Account?.AccountId?.ToMask(), passwordLength = Password?.Length });
-
- return base.SaveAndCloseAsync();
- }
-
- public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
- => await SaveAndCloseAsync();
- }
-}
diff --git a/Source/LibationAvalonia/Dialogs/Login/LoginChoiceEagerDialog.axaml b/Source/LibationAvalonia/Dialogs/Login/LoginChoiceEagerDialog.axaml
deleted file mode 100644
index d8adce88..00000000
--- a/Source/LibationAvalonia/Dialogs/Login/LoginChoiceEagerDialog.axaml
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Source/LibationAvalonia/Dialogs/Login/LoginChoiceEagerDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/Login/LoginChoiceEagerDialog.axaml.cs
deleted file mode 100644
index a72fed0d..00000000
--- a/Source/LibationAvalonia/Dialogs/Login/LoginChoiceEagerDialog.axaml.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using AudibleApi;
-using AudibleUtilities;
-using Avalonia.Controls;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace LibationAvalonia.Dialogs.Login
-{
- public partial class LoginChoiceEagerDialog : DialogWindow
- {
- public Account Account { get; }
- public string Password { get; set; }
- public LoginMethod LoginMethod { get; private set; }
-
- public LoginChoiceEagerDialog() : base(saveAndRestorePosition: false)
- {
- InitializeComponent();
-
- if (Design.IsDesignMode)
- {
- using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
- var accounts = persister.AccountsSettings.Accounts;
- Account = accounts.FirstOrDefault();
- DataContext = this;
- }
- }
- public LoginChoiceEagerDialog(Account account) : this()
- {
- Account = account;
- DataContext = this;
- }
-
- protected override async Task SaveAndCloseAsync()
- {
- if (LoginMethod is LoginMethod.Api && string.IsNullOrWhiteSpace(Password))
- {
- await MessageBox.Show(this, "Please enter your password");
- return;
- }
-
- await base.SaveAndCloseAsync();
- }
-
- public async void ExternalLoginLink_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
- {
- LoginMethod = LoginMethod.External;
- await SaveAndCloseAsync();
- }
- }
-}
diff --git a/Source/LibationAvalonia/Dialogs/Login/LoginExternalDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/Login/LoginExternalDialog.axaml.cs
index 4d6c230b..77268722 100644
--- a/Source/LibationAvalonia/Dialogs/Login/LoginExternalDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/Login/LoginExternalDialog.axaml.cs
@@ -20,9 +20,10 @@ namespace LibationAvalonia.Dialogs.Login
if (Design.IsDesignMode)
{
- using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
- var accounts = persister.AccountsSettings.Accounts;
- Account = accounts.FirstOrDefault();
+ Account = new Account("someemail.somedomain.co")
+ {
+ IdentityTokens = new AudibleApi.Authorization.Identity(AudibleApi.Localization.Locales.First())
+ };
ExternalLoginUrl = "ht" + "tps://us.audible.com/Test_url";
DataContext = this;
}
diff --git a/Source/LibationAvalonia/Dialogs/Login/MfaDialog.axaml b/Source/LibationAvalonia/Dialogs/Login/MfaDialog.axaml
deleted file mode 100644
index 7304cea3..00000000
--- a/Source/LibationAvalonia/Dialogs/Login/MfaDialog.axaml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/Source/LibationAvalonia/Dialogs/Login/MfaDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/Login/MfaDialog.axaml.cs
deleted file mode 100644
index 90e348dc..00000000
--- a/Source/LibationAvalonia/Dialogs/Login/MfaDialog.axaml.cs
+++ /dev/null
@@ -1,137 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Data;
-using LibationUiBase.Forms;
-using ReactiveUI;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace LibationAvalonia.Dialogs.Login
-{
- public partial class MfaDialog : DialogWindow
- {
- public string SelectedName { get; private set; }
- public string SelectedValue { get; private set; }
- private RbValues Values { get; } = new();
-
- public MfaDialog() : base(saveAndRestorePosition: false)
- {
- InitializeComponent();
-
- if (Design.IsDesignMode)
- {
- var mfaConfig = new AudibleApi.MfaConfig { Title = "My title" };
- mfaConfig.Buttons.Add(new() { Text = "Enter the OTP from the authenticator app", Name = "otpDeviceContext", Value = "aAbBcC=, TOTP" });
- mfaConfig.Buttons.Add(new() { Text = "Send an SMS to my number ending with 123", Name = "otpDeviceContext", Value = "dDeEfE=, SMS" });
- mfaConfig.Buttons.Add(new() { Text = "Call me on my number ending with 123", Name = "otpDeviceContext", Value = "dDeEfE=, VOICE" });
-
- loadRadioButtons(mfaConfig);
- }
- }
-
- public MfaDialog(AudibleApi.MfaConfig mfaConfig) : this()
- {
- loadRadioButtons(mfaConfig);
- }
-
- private void loadRadioButtons(AudibleApi.MfaConfig mfaConfig)
- {
- if (!string.IsNullOrWhiteSpace(mfaConfig.Title))
- Title = mfaConfig.Title;
-
- rbStackPanel = this.Find(nameof(rbStackPanel));
-
- foreach (var conf in mfaConfig.Buttons)
- {
- var rb = new RbValue(conf);
- Values.AddButton(rb);
-
- RadioButton radioButton = new()
- {
- Content = new TextBlock { Text = conf.Text },
- Margin = new Thickness(0, 10, 0, 0),
- };
-
- radioButton.Bind(
- RadioButton.IsCheckedProperty,
- new Binding
- {
- Source = rb,
- Path = nameof(rb.IsChecked)
- });
-
- rbStackPanel.Children.Add(radioButton);
- }
- }
-
- protected override async Task SaveAndCloseAsync()
- {
- var selected = Values.CheckedButton;
-
- Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new
- {
- text = selected?.Text,
- name = selected?.Name,
- value = selected?.Value
- });
- if (selected is null)
- {
- await MessageBox.Show("No MFA option selected", "None selected", MessageBoxButtons.OK, MessageBoxIcon.Error);
- return;
- }
-
- SelectedName = selected.Name;
- SelectedValue = selected.Value;
-
- await base.SaveAndCloseAsync();
- }
-
- public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
- => await SaveAndCloseAsync();
-
-
- private class RbValue : ViewModels.ViewModelBase
- {
- private bool _isChecked;
- public bool IsChecked
- {
- get => _isChecked;
- set => this.RaiseAndSetIfChanged(ref _isChecked, value);
- }
- public AudibleApi.MfaConfigButton MfaConfigButton { get; }
- public RbValue(AudibleApi.MfaConfigButton mfaConfig)
- {
- MfaConfigButton = mfaConfig;
- }
- }
-
- private class RbValues
- {
- private List ButtonValues { get; } = new();
-
- public AudibleApi.MfaConfigButton CheckedButton => ButtonValues.SingleOrDefault(rb => rb.IsChecked)?.MfaConfigButton;
-
- public void AddButton(RbValue rbValue)
- {
- if (ButtonValues.Contains(rbValue))
- return;
-
- rbValue.PropertyChanged += RbValue_PropertyChanged;
- rbValue.IsChecked = ButtonValues.Count == 0;
- ButtonValues.Add(rbValue);
- }
-
- private void RbValue_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
- {
- var button = sender as RbValue;
-
- if (button.IsChecked)
- {
- foreach (var rb in ButtonValues.Where(rb => rb != button))
- rb.IsChecked = false;
- }
- }
- }
- }
-}
diff --git a/Source/LibationAvalonia/Dialogs/Login/WebLoginDialog.axaml b/Source/LibationAvalonia/Dialogs/Login/WebLoginDialog.axaml
deleted file mode 100644
index 3fa2efca..00000000
--- a/Source/LibationAvalonia/Dialogs/Login/WebLoginDialog.axaml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
diff --git a/Source/LibationAvalonia/Dialogs/Login/WebLoginDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/Login/WebLoginDialog.axaml.cs
deleted file mode 100644
index b2df7078..00000000
--- a/Source/LibationAvalonia/Dialogs/Login/WebLoginDialog.axaml.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using Avalonia.Controls;
-using Dinah.Core;
-using LibationUiBase.Forms;
-using System;
-
-namespace LibationAvalonia.Dialogs.Login
-{
- public partial class WebLoginDialog : Window
- {
- public string ResponseUrl { get; private set; }
- private readonly string accountID;
-
- public WebLoginDialog()
- {
- InitializeComponent();
- webView.NavigationStarted += WebView_NavigationStarted;
- webView.DOMContentLoaded += WebView_NavigationCompleted;
- }
-
- public WebLoginDialog(string accountID, string loginUrl) : this()
- {
- this.accountID = ArgumentValidator.EnsureNotNullOrWhiteSpace(accountID, nameof(accountID));
- webView.Source = new Uri(ArgumentValidator.EnsureNotNullOrWhiteSpace(loginUrl, nameof(loginUrl)));
- }
-
- private void WebView_NavigationStarted(object sender, LibationFileManager.WebViewNavigationEventArgs e)
- {
- if (e.Request?.AbsolutePath.Contains("/ap/maplanding") is true)
- {
- ResponseUrl = e.Request.ToString();
- Close(DialogResult.OK);
- }
- }
-
- private async void WebView_NavigationCompleted(object sender, EventArgs e)
- {
- await webView.InvokeScriptAsync(getScript(accountID));
- }
-
- private static string getScript(string accountID) => $$"""
- (function() {
- var inputs = document.getElementsByTagName('input');
- for (index = 0; index < inputs.length; ++index) {
- if (inputs[index].name.includes('email')) {
- inputs[index].value = '{{accountID}}';
- }
- if (inputs[index].name.includes('password')) {
- inputs[index].focus();
- }
- }
- })()
- """;
-
- }
-}
diff --git a/Source/LibationAvalonia/Dialogs/Login/_2faCodeDialog.axaml b/Source/LibationAvalonia/Dialogs/Login/_2faCodeDialog.axaml
deleted file mode 100644
index b8050f4c..00000000
--- a/Source/LibationAvalonia/Dialogs/Login/_2faCodeDialog.axaml
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Source/LibationAvalonia/Dialogs/Login/_2faCodeDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/Login/_2faCodeDialog.axaml.cs
deleted file mode 100644
index 7e89bbe3..00000000
--- a/Source/LibationAvalonia/Dialogs/Login/_2faCodeDialog.axaml.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using Avalonia.Controls;
-using System.Threading.Tasks;
-
-namespace LibationAvalonia.Dialogs.Login
-{
- public partial class _2faCodeDialog : DialogWindow
- {
- public string Code { get; set; }
- public string Prompt { get; } = "For added security, please enter the One Time Password (OTP) generated by your Authenticator App";
-
-
- public _2faCodeDialog() : base(saveAndRestorePosition: false)
- {
- InitializeComponent();
- _2FABox = this.FindControl(nameof(_2FABox));
- }
-
- public _2faCodeDialog(string prompt) : this()
- {
- Prompt = prompt;
- DataContext = this;
- Opened += (_, _) => _2FABox.Focus();
- }
-
- protected override Task SaveAndCloseAsync()
- {
- Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Code });
-
- return base.SaveAndCloseAsync();
- }
-
- public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
- => await SaveAndCloseAsync();
- }
-}
diff --git a/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml b/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml
index e8fdaed8..eebd8765 100644
--- a/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml
@@ -18,7 +18,7 @@
Margin="10,10,10,0"
ColumnDefinitions="Auto,*">
-
+
-
diff --git a/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs
index ddcfecf6..7307263b 100644
--- a/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs
@@ -37,33 +37,34 @@ namespace LibationAvalonia.Dialogs
}
catch
{
- await MessageBox.Show($"Error opening url\r\n{url}", "Error opening url", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ await MessageBox.Show(this, $"Error opening url\r\n{url}", "Error opening url", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private async void GoToLogs_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
{
- LongPath dir = "";
try
{
- dir = LibationFileManager.Configuration.Instance.LibationFiles;
- }
- catch { }
-
- try
- {
- Go.To.Folder(dir.ShortPathName);
+ Go.To.File(LibationFileManager.LogFileFilter.LogFilePath);
}
catch
{
- await MessageBox.Show($"Error opening folder\r\n{dir}", "Error opening folder", MessageBoxButtons.OK, MessageBoxIcon.Error);
- }
+ LongPath dir = "";
+ try
+ {
+ dir = LibationFileManager.Configuration.Instance.LibationFiles;
+ Go.To.Folder(dir.ShortPathName);
+ }
+ catch
+ {
+ await MessageBox.Show(this, $"Error opening folder\r\n{dir}", "Error opening folder", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
}
public void OkButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
SaveAndClose();
}
-
}
}
diff --git a/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml.cs b/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml.cs
index 1633112f..bd77f825 100644
--- a/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml.cs
@@ -19,12 +19,10 @@ namespace LibationAvalonia.Dialogs
protected override void SaveAndClose() { }
- public DialogResult DialogResult { get; private set; }
-
public void Button1_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var vm = DataContext as MessageBoxViewModel;
- DialogResult = vm.Buttons switch
+ var dialogResult = vm.Buttons switch
{
MessageBoxButtons.OK => DialogResult.OK,
MessageBoxButtons.OKCancel => DialogResult.OK,
@@ -35,12 +33,12 @@ namespace LibationAvalonia.Dialogs
MessageBoxButtons.CancelTryContinue => DialogResult.Cancel,
_ => DialogResult.None
};
- Close(DialogResult);
+ Close(dialogResult);
}
public void Button2_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var vm = DataContext as MessageBoxViewModel;
- DialogResult = vm.Buttons switch
+ var dialogResult = vm.Buttons switch
{
MessageBoxButtons.OKCancel => DialogResult.Cancel,
MessageBoxButtons.AbortRetryIgnore => DialogResult.Retry,
@@ -50,19 +48,19 @@ namespace LibationAvalonia.Dialogs
MessageBoxButtons.CancelTryContinue => DialogResult.TryAgain,
_ => DialogResult.None
};
- Close(DialogResult);
+ Close(dialogResult);
}
public void Button3_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var vm = DataContext as MessageBoxViewModel;
- DialogResult = vm.Buttons switch
+ var dialogResult = vm.Buttons switch
{
MessageBoxButtons.AbortRetryIgnore => DialogResult.Ignore,
MessageBoxButtons.YesNoCancel => DialogResult.Cancel,
MessageBoxButtons.CancelTryContinue => DialogResult.Continue,
_ => DialogResult.None
};
- Close(DialogResult);
+ Close(dialogResult);
}
}
}
diff --git a/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml b/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml
index ce5b79d5..7d0939ff 100644
--- a/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml
@@ -3,7 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="200"
- MinWidth="900" MinHeight="200"
+ MinWidth="900" MinHeight="750"
Width="900" Height="750"
x:Class="LibationAvalonia.Dialogs.SettingsDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
diff --git a/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml.cs
index 910d2f87..851a58b5 100644
--- a/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml.cs
@@ -12,11 +12,9 @@ namespace LibationAvalonia.Dialogs
{
private SettingsVM settingsDisp;
- private readonly Configuration config = Configuration.Instance;
+ private readonly Configuration config = Design.IsDesignMode ? Configuration.CreateMockInstance() : Configuration.Instance;
public SettingsDialog()
{
- if (Design.IsDesignMode)
- _ = Configuration.Instance.LibationFiles;
InitializeComponent();
DataContext = settingsDisp = new(config);
diff --git a/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml.cs
index c2dd2221..324aee12 100644
--- a/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/TrashBinDialog.axaml.cs
@@ -14,12 +14,11 @@ using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs
{
- public partial class TrashBinDialog : Window
+ public partial class TrashBinDialog : DialogWindow
{
public TrashBinDialog()
{
InitializeComponent();
- this.RestoreSizeAndLocation(Configuration.Instance);
DataContext = new TrashBinViewModel();
this.Closing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
diff --git a/Source/LibationAvalonia/LibationAvalonia.csproj b/Source/LibationAvalonia/LibationAvalonia.csproj
index 1e5e0f7d..ea243ee3 100644
--- a/Source/LibationAvalonia/LibationAvalonia.csproj
+++ b/Source/LibationAvalonia/LibationAvalonia.csproj
@@ -3,8 +3,7 @@
WinExe
- net9.0-windows7.0
- win-x64
+ net9.0
true
Assets/libation.ico
Libation
@@ -73,13 +72,13 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/Source/LibationAvalonia/Program.cs b/Source/LibationAvalonia/Program.cs
index ef5d6c61..c437fb36 100644
--- a/Source/LibationAvalonia/Program.cs
+++ b/Source/LibationAvalonia/Program.cs
@@ -5,18 +5,23 @@ using System.Threading.Tasks;
using ApplicationServices;
using AppScaffolding;
using Avalonia;
-using Avalonia.ReactiveUI;
+using ReactiveUI.Avalonia;
using LibationFileManager;
+using LibationAvalonia.Dialogs;
+using Avalonia.Threading;
+using FileManager;
+using System.Linq;
#nullable enable
namespace LibationAvalonia
{
static class Program
{
+ private static System.Threading.Lock SetupLock { get; } = new();
+ private static bool LoggingEnabled { get; set; }
[STAThread]
static void Main(string[] args)
{
-
if (Configuration.IsMacOs && args?.Length > 0 && args[0] == "hangover")
{
//Launch the Hangover app within the sandbox
@@ -34,9 +39,8 @@ namespace LibationAvalonia
$"\"{Configuration.ProcessDirectory}\"");
return;
}
- AppDomain.CurrentDomain.UnhandledException += (o, e) => LogError(e.ExceptionObject);
+ AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
- bool loggingEnabled = false;
//***********************************************//
// //
// do not use Configuration before this line //
@@ -51,30 +55,86 @@ namespace LibationAvalonia
// most migrations go in here
LibationScaffolding.RunPostConfigMigrations(config);
LibationScaffolding.RunPostMigrationScaffolding(Variety.Chardonnay, config);
- loggingEnabled = true;
+ LoggingEnabled = true;
//Start loading the library before loading the main form
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
}
-
- BuildAvaloniaApp().StartWithClassicDesktopLifetime([]);
+ BuildAvaloniaApp()?.StartWithClassicDesktopLifetime([]);
}
catch (Exception ex)
{
- if (loggingEnabled)
- Serilog.Log.Logger.Error(ex, "CRASH");
- else
- LogError(ex);
+ LogAndShowCrashMessage(ex);
}
}
- public static AppBuilder BuildAvaloniaApp()
- => AppBuilder.Configure()
- .UsePlatformDetect()
- .LogToTrace()
- .UseReactiveUI();
+ public static AppBuilder? BuildAvaloniaApp()
+ {
+ //Ensure that setup is only run once
+ SetupLock.Enter();
+ if (Application.Current is not null)
+ {
+ SetupLock.Exit();
+ return null;
+ }
+ else
+ {
+ return AppBuilder.Configure()
+ .UsePlatformDetect()
+ .LogToTrace()
+ .UseReactiveUI()
+ .AfterSetup(_ => SetupLock.Exit());
+ }
+ }
- private static void LogError(object exceptionObject)
+ private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
+ {
+ var ex = e.ExceptionObject as Exception ?? new Exception(e.ExceptionObject.ToString());
+ LogAndShowCrashMessage(ex);
+ }
+
+ private static void LogAndShowCrashMessage(Exception exception)
+ {
+ try
+ {
+ //Try to log the error message before displaying the crash dialog
+ if (LoggingEnabled)
+ Serilog.Log.Logger.Error(exception, "CRASH");
+ else
+ LogErrorWithoutSerilog(exception);
+ }
+ catch { /* continue to show the crash dialog even if logging fails */ }
+
+ //Run setup if needed so that we can show the crash dialog
+ BuildAvaloniaApp()?.SetupWithoutStarting();
+
+ try
+ {
+ Dispatcher.UIThread.Invoke(() => DisplayErrorMessage(exception));
+ }
+ catch (Exception ex)
+ {
+ Environment.FailFast("Fatal error displaying crash message", new AggregateException(ex, exception));
+ }
+ }
+
+ private static void DisplayErrorMessage(Exception exception)
+ {
+ var dispatcher = new DispatcherFrame();
+
+ var mbAlert = new MessageBoxAlertAdminDialog("""
+ Libation encountered a fatal error and must close.
+
+ Please consider reporting this issue on GitHub, including the contents of the LibationCrash.log file created in your user folder.
+ """,
+ "Libation Crash",
+ exception);
+ mbAlert.Closed += (_, _) => dispatcher.Continue = false;
+ mbAlert.Show();
+ Dispatcher.UIThread.PushFrame(dispatcher);
+ }
+
+ private static void LogErrorWithoutSerilog(object exceptionObject)
{
var logError = $"""
{DateTime.Now} - Libation Crash
@@ -88,9 +148,25 @@ namespace LibationAvalonia
{exceptionObject}
""";
- var crashLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LibationCrash.log");
+ LongPath logFile;
+ try
+ {
+ //Try to add crash message to the newest existing Libation log file
+ //then to LibationFiles/LibationCrash.log
+ //then to %UserProfile%/LibationCrash.log
+ string logDir = Configuration.Instance.LibationFiles;
+ var existingLogFiles = Directory.GetFiles(logDir, "Log*.log");
- using var sw = new StreamWriter(crashLog, true);
+ logFile = existingLogFiles.Length == 0 ? getFallbackLogFile()
+ : existingLogFiles.Select(f => new FileInfo(f)).OrderByDescending(f => f.CreationTimeUtc).First().FullName;
+ }
+ catch
+ {
+ logFile = getFallbackLogFile();
+ }
+
+
+ using var sw = new StreamWriter(logFile, true);
sw.WriteLine(logError);
static string getConfigValue(Func selector)
@@ -104,6 +180,23 @@ namespace LibationAvalonia
return ex.ToString();
}
}
+
+ static string getFallbackLogFile()
+ {
+ try
+ {
+
+ string logDir = Configuration.Instance.LibationFiles;
+ if (!Directory.Exists(logDir))
+ logDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+
+ return Path.Combine(logDir, "LibationCrash.log");
+ }
+ catch
+ {
+ return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LibationCrash.log");
+ }
+ }
}
}
}
diff --git a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs
index 473a8592..efeb25bd 100644
--- a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs
+++ b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs
@@ -481,6 +481,7 @@ namespace LibationAvalonia.ViewModels
public DataGridLength LastDownloadWidth { get => getColumnWidth("LastDownload", 100); set => setColumnWidth("LastDownload", value); }
public DataGridLength BookTagsWidth { get => getColumnWidth("BookTags", 100); set => setColumnWidth("BookTags", value); }
public DataGridLength IsSpatialWidth { get => getColumnWidth("IsSpatial", 100); set => setColumnWidth("IsSpatial", value); }
+ public DataGridLength AccountWidth { get => getColumnWidth(nameof(GridEntry.Account), 100); set => setColumnWidth(nameof(GridEntry.Account), value); }
private static DataGridLength getColumnWidth(string columnName, double defaultWidth)
=> Configuration.Instance.GridColumnsWidths.TryGetValue(columnName, out var val)
diff --git a/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs b/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs
index 28544c9f..7e88c783 100644
--- a/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs
+++ b/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs
@@ -29,7 +29,7 @@ namespace LibationAvalonia.ViewModels.Settings
GridScaleFactor = scaleFactorToLinearRange(config.GridScaleFactor);
GridFontScaleFactor = scaleFactorToLinearRange(config.GridFontScaleFactor);
- themeVariant = initialThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) ?? "";
+ themeVariant = initialThemeVariant = config.GetString(propertyName: nameof(ThemeVariant)) ?? "";
if (string.IsNullOrWhiteSpace(initialThemeVariant))
themeVariant = initialThemeVariant = "System";
}
diff --git a/Source/LibationAvalonia/Views/MainWindow.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.axaml.cs
index d2347fa9..8d08b264 100644
--- a/Source/LibationAvalonia/Views/MainWindow.axaml.cs
+++ b/Source/LibationAvalonia/Views/MainWindow.axaml.cs
@@ -1,7 +1,7 @@
using AudibleUtilities;
using Avalonia.Controls;
using Avalonia.Input;
-using Avalonia.ReactiveUI;
+using ReactiveUI.Avalonia;
using Avalonia.Threading;
using DataLayer;
using FileManager;
@@ -23,7 +23,7 @@ namespace LibationAvalonia.Views
public MainWindow()
{
if (Design.IsDesignMode)
- _ = Configuration.Instance.LibationFiles;
+ Configuration.CreateMockInstance();
DataContext = new MainVM(this);
ApiExtended.LoginChoiceFactory = account => Dispatcher.UIThread.Invoke(() => new Dialogs.Login.AvaloniaLoginChoiceEager(account));
diff --git a/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs
index 8e877518..8dacf91a 100644
--- a/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs
+++ b/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs
@@ -30,10 +30,8 @@ namespace LibationAvalonia.Views
if (Design.IsDesignMode)
{
- using var context = DbContexts.GetContext();
ViewModels.MainVM.Configure_NonUI();
- if (context.GetLibraryBook_Flat_NoTracking("B017V4IM1G") is LibraryBook book)
- DataContext = new ProcessBookViewModel(book);
+ DataContext = new ProcessBookViewModel(MockLibraryBook.CreateBook());
return;
}
}
diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
index 9a67905f..621eb092 100644
--- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
+++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
@@ -2,6 +2,7 @@
using Avalonia.Controls;
using Avalonia.Data.Converters;
using DataLayer;
+using LibationFileManager;
using LibationUiBase;
using LibationUiBase.ProcessQueue;
using System;
@@ -29,18 +30,12 @@ namespace LibationAvalonia.Views
#if DEBUG
if (Design.IsDesignMode)
{
- _ = LibationFileManager.Configuration.Instance.LibationFiles;
ViewModels.MainVM.Configure_NonUI();
+ Configuration.CreateMockInstance();
var vm = new ProcessQueueViewModel();
DataContext = vm;
- using var context = DbContexts.GetContext();
-
-
- var trialBook = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G") ?? context.GetLibrary_Flat_NoTracking().FirstOrDefault();
- if (trialBook is null)
- return;
-
+ var trialBook = MockLibraryBook.CreateBook();
List testList = new()
{
new ProcessBookViewModel(trialBook)
diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml b/Source/LibationAvalonia/Views/ProductsDisplay.axaml
index 287d2da0..712e2103 100644
--- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml
+++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml
@@ -24,6 +24,33 @@
CanUserReorderColumns="True">
+
+
+
+
+
+
+
+
+
@@ -236,12 +263,22 @@
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
index 735c22ad..aeaa4c92 100644
--- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
+++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
@@ -2,6 +2,7 @@ using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input.Platform;
+using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using DataLayer;
@@ -61,20 +62,14 @@ namespace LibationAvalonia.Views
#if DEBUG
if (Design.IsDesignMode)
{
- using var context = DbContexts.GetContext();
- LibraryBook?[] sampleEntries;
- try
- {
- sampleEntries = [
- context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"),
- context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"),
- context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"),
- context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"),
- context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"),
- context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"),
- context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")];
- }
- catch { sampleEntries = []; }
+ MainVM.Configure_NonUI();
+ LibraryBook[] sampleEntries = [
+ MockLibraryBook.CreateBook(title: "Book 1"),
+ MockLibraryBook.CreateBook(title: "Book 2"),
+ MockLibraryBook.CreateBook(title: "Book 3"),
+ MockLibraryBook.CreateBook(title: "Book 4"),
+ MockLibraryBook.CreateBook(title: "Book 5"),
+ MockLibraryBook.CreateBook(title: "Book 6")];
var pdvm = new ProductsDisplayViewModel();
_ = pdvm.BindToGridAsync(sampleEntries.OfType().ToList());
@@ -422,48 +417,23 @@ namespace LibationAvalonia.Views
productsGrid.ColumnDisplayIndexChanged += ProductsGrid_ColumnDisplayIndexChanged;
- var config = Configuration.Instance;
- var displayIndices = config.GridColumnsDisplayIndices;
-
- var contextMenu = new ContextMenu();
- contextMenu.Closed += ContextMenu_MenuClosed;
- contextMenu.Opening += ContextMenu_ContextMenuOpening;
- List menuItems = new();
- contextMenu.ItemsSource = menuItems;
-
- menuItems.Add(new MenuItem { Header = "Show / Hide Columns" });
- menuItems.Add(new MenuItem { Header = "-" });
-
- var HeaderCell_PI = typeof(DataGridColumn).GetProperty("HeaderCell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
-
- if (HeaderCell_PI is null)
- return;
-
foreach (var column in productsGrid.Columns)
{
var itemName = column.SortMemberPath;
-
if (itemName == nameof(GridEntry.Remove))
continue;
- menuItems.Add
- (
- new MenuItem
- {
- Header = ((string)column.Header).Replace('\n', ' '),
- Tag = column,
- Icon = new CheckBox(),
- }
- );
+ GridHeaderContextMenu.Items.Add(new MenuItem
+ {
+ Header = new CheckBox { Content = new TextBlock { Text = ((string)column.Header).Replace('\n', ' ') } },
+ Tag = column,
+ });
- var headerCell = HeaderCell_PI.GetValue(column) as DataGridColumnHeader;
- if (headerCell is not null)
- headerCell.ContextMenu = contextMenu;
-
- column.IsVisible = config.GetColumnVisibility(itemName);
+ column.IsVisible = Configuration.Instance.GetColumnVisibility(itemName);
}
//We must set DisplayIndex properties in ascending order
+ var displayIndices = Configuration.Instance.GridColumnsDisplayIndices;
foreach (var itemName in displayIndices.OrderBy(i => i.Value).Select(i => i.Key))
{
if (!productsGrid.Columns.Any(c => c.SortMemberPath == itemName))
@@ -476,20 +446,20 @@ namespace LibationAvalonia.Views
}
}
- private void ContextMenu_ContextMenuOpening(object? sender, System.ComponentModel.CancelEventArgs e)
+ public void ContextMenu_ContextMenuOpening(object? sender, System.ComponentModel.CancelEventArgs e)
{
if (sender is not ContextMenu contextMenu)
return;
foreach (var mi in contextMenu.Items.OfType