mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-23 22:17:52 -05:00
Merge pull request #1444 from Mbucari/master
Use C# 14 `field` and extension members.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
using FileManager;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class UnencryptedAudiobookDownloader : AudiobookDownloadBase
|
||||
|
||||
@@ -243,6 +243,7 @@ namespace AppScaffolding
|
||||
//Log.Logger.Here().Debug("Begin Libation. Debug with line numbers");
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
private static void logStartupState(Configuration config)
|
||||
{
|
||||
#if DEBUG
|
||||
@@ -256,9 +257,11 @@ namespace AppScaffolding
|
||||
// begin logging session with a form feed
|
||||
Log.Logger.Information("\r\n\f");
|
||||
|
||||
static int fileCount(FileManager.LongPath longPath)
|
||||
static int fileCount(FileManager.LongPath? longPath)
|
||||
{
|
||||
try { return FileManager.FileUtility.SaferEnumerateFiles(longPath).Count(); }
|
||||
if (longPath is null)
|
||||
return -1;
|
||||
try { return FileManager.FileUtility.SaferEnumerateFiles(longPath).Count(); }
|
||||
catch { return -1; }
|
||||
}
|
||||
|
||||
@@ -298,8 +301,8 @@ namespace AppScaffolding
|
||||
if (InteropFactory.InteropFunctionsType is null)
|
||||
Serilog.Log.Logger.Warning("WARNING: OSInteropProxy.InteropFunctionsType is null");
|
||||
}
|
||||
|
||||
private static void wireUpSystemEvents(Configuration configuration)
|
||||
#nullable restore
|
||||
private static void wireUpSystemEvents(Configuration configuration)
|
||||
{
|
||||
LibraryCommands.LibrarySizeChanged += (object _, List<DataLayer.LibraryBook> libraryBooks)
|
||||
=> SearchEngineCommands.FullReIndex(libraryBooks);
|
||||
|
||||
@@ -577,7 +577,7 @@ namespace ApplicationServices
|
||||
|
||||
// must be here instead of in db layer due to AaxcExists
|
||||
public static LiberatedStatus Liberated_Status(Book book)
|
||||
=> book.Audio_Exists() ? book.UserDefinedItem.BookStatus
|
||||
=> book.AudioExists ? book.UserDefinedItem.BookStatus
|
||||
: AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
|
||||
: LiberatedStatus.NotLiberated;
|
||||
|
||||
@@ -645,7 +645,7 @@ namespace ApplicationServices
|
||||
|
||||
var pdfResults = libraryBooks
|
||||
.AsParallel()
|
||||
.Where(lb => lb.Book.HasPdf())
|
||||
.Where(lb => lb.Book.HasPdf)
|
||||
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Pdf_Status(lb.Book) })
|
||||
.ToList();
|
||||
|
||||
|
||||
@@ -150,12 +150,12 @@ namespace ApplicationServices
|
||||
Locale = a.Book.Locale,
|
||||
Title = a.Book.Title,
|
||||
Subtitle = a.Book.Subtitle,
|
||||
AuthorNames = a.Book.AuthorNames(),
|
||||
NarratorNames = a.Book.NarratorNames(),
|
||||
AuthorNames = a.Book.AuthorNames,
|
||||
NarratorNames = a.Book.NarratorNames,
|
||||
LengthInMinutes = a.Book.LengthInMinutes,
|
||||
Description = a.Book.Description,
|
||||
Publisher = a.Book.Publisher,
|
||||
HasPdf = a.Book.HasPdf(),
|
||||
HasPdf = a.Book.HasPdf,
|
||||
SeriesNames = a.Book.SeriesNames(),
|
||||
SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Order} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "",
|
||||
CommunityRatingOverall = a.Book.Rating?.OverallRating.ZeroIsNull(),
|
||||
|
||||
@@ -18,68 +18,64 @@ namespace AudibleUtilities
|
||||
public string AccountId { get; }
|
||||
|
||||
// user-friendly, non-canonical name. mutable
|
||||
private string _accountName;
|
||||
public string AccountName
|
||||
{
|
||||
get => _accountName;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return;
|
||||
var v = value.Trim();
|
||||
if (v == _accountName)
|
||||
if (v == field)
|
||||
return;
|
||||
_accountName = v;
|
||||
field = v;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
// whether to include this account when scanning libraries.
|
||||
// technically this is an app setting; not an attribute of account. but since it's managed with accounts, it makes sense to put this exception-to-the-rule here
|
||||
private bool _libraryScan = true;
|
||||
public bool LibraryScan
|
||||
{
|
||||
get => _libraryScan;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (value == _libraryScan)
|
||||
if (value == field)
|
||||
return;
|
||||
_libraryScan = value;
|
||||
field = value;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
private string _decryptKey = "";
|
||||
/// <summary>aka: activation bytes</summary>
|
||||
public string DecryptKey
|
||||
{
|
||||
get => _decryptKey;
|
||||
get => field ?? "";
|
||||
set
|
||||
{
|
||||
var v = (value ?? "").Trim();
|
||||
if (v == _decryptKey)
|
||||
if (v == field)
|
||||
return;
|
||||
_decryptKey = v;
|
||||
field = v;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
private Identity _identity;
|
||||
public Identity IdentityTokens
|
||||
{
|
||||
get => _identity;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (_identity is null && value is null)
|
||||
if (field is null && value is null)
|
||||
return;
|
||||
|
||||
if (_identity is not null)
|
||||
_identity.Updated -= update;
|
||||
if (field is not null)
|
||||
field.Updated -= update;
|
||||
|
||||
if (value is not null)
|
||||
value.Updated += update;
|
||||
|
||||
_identity = value;
|
||||
field = value;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="10.0.0.1" />
|
||||
<PackageReference Include="AudibleApi" Version="10.1.0.1" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="10.0.0.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="10.0.0.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0-rtm-ci.20251120T065334" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -13,69 +13,74 @@ namespace DataLayer
|
||||
.Where(a => a.Role == role)
|
||||
.OrderBy(a => a.Order);
|
||||
|
||||
public static string TitleSortable(this Book book) => Formatters.GetSortName(book.Title + book.Subtitle);
|
||||
|
||||
public static string AuthorNames(this Book book) => string.Join(", ", book.Authors.Select(a => a.Name));
|
||||
public static string NarratorNames(this Book book) => string.Join(", ", book.Narrators.Select(n => n.Name));
|
||||
extension(Book book)
|
||||
{
|
||||
public string SeriesSortable() => Formatters.GetSortName(book.SeriesNames(true));
|
||||
public string TitleSortable() => Formatters.GetSortName(book.Title + book.Subtitle);
|
||||
|
||||
/// <summary>True if IsLiberated or Error. False if NotLiberated</summary>
|
||||
public static bool Audio_Exists(this Book book) => book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated;
|
||||
/// <summary>True if exists and IsLiberated. Else false</summary>
|
||||
public static bool PDF_Exists(this Book book) => book.UserDefinedItem.PdfStatus == LiberatedStatus.Liberated;
|
||||
public string AuthorNames => string.Join(", ", book.Authors.Select(a => a.Name));
|
||||
public string NarratorNames => string.Join(", ", book.Narrators.Select(n => n.Name));
|
||||
/// <summary>True if IsLiberated or Error. False if NotLiberated</summary>
|
||||
public bool AudioExists => book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated or LiberatedStatus.Error;
|
||||
/// <summary>True if exists and IsLiberated. Else false</summary>
|
||||
public bool PdfExists => book.UserDefinedItem.PdfStatus == LiberatedStatus.NotLiberated;
|
||||
/// <summary> Whether the book has any supplements </summary>
|
||||
public bool HasPdf => book.Supplements.Any();
|
||||
|
||||
public static string SeriesSortable(this Book book) => Formatters.GetSortName(book.SeriesNames(true));
|
||||
public static bool HasPdf(this Book book) => book.Supplements.Any();
|
||||
public static string SeriesNames(this Book book, bool includeIndex = false)
|
||||
{
|
||||
if (book.SeriesLink is null)
|
||||
return "";
|
||||
public string SeriesNames(bool includeIndex = false)
|
||||
{
|
||||
if (book.SeriesLink is null)
|
||||
return "";
|
||||
|
||||
// first: alphabetical by name
|
||||
var withNames = book.SeriesLink
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
.Select(getSeriesNameString)
|
||||
.OrderBy(a => a)
|
||||
.ToList();
|
||||
// then un-named are alpha by series id
|
||||
var nullNames = book.SeriesLink
|
||||
.Where(s => string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
.Select(s => s.Series.AudibleSeriesId)
|
||||
.OrderBy(a => a)
|
||||
.ToList();
|
||||
// first: alphabetical by name
|
||||
var withNames = book.SeriesLink
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
.Select(getSeriesNameString)
|
||||
.OrderBy(a => a)
|
||||
.ToList();
|
||||
// then un-named are alpha by series id
|
||||
var nullNames = book.SeriesLink
|
||||
.Where(s => string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
.Select(s => s.Series.AudibleSeriesId)
|
||||
.OrderBy(a => a)
|
||||
.ToList();
|
||||
|
||||
var all = withNames.Union(nullNames).ToList();
|
||||
return string.Join(", ", all);
|
||||
var all = withNames.Union(nullNames).ToList();
|
||||
return string.Join(", ", all);
|
||||
|
||||
string getSeriesNameString(SeriesBook sb)
|
||||
=> includeIndex && !string.IsNullOrWhiteSpace(sb.Order) && sb.Order != "-1"
|
||||
? $"{sb.Series.Name} (#{sb.Order})"
|
||||
: sb.Series.Name;
|
||||
string getSeriesNameString(SeriesBook sb)
|
||||
=> includeIndex && !string.IsNullOrWhiteSpace(sb.Order) && sb.Order != "-1"
|
||||
? $"{sb.Series.Name} (#{sb.Order})"
|
||||
: sb.Series.Name;
|
||||
}
|
||||
|
||||
public string[] LowestCategoryNames()
|
||||
=> book.CategoriesLink?.Any() is not true ? Array.Empty<string>()
|
||||
: book
|
||||
.CategoriesLink
|
||||
.Select(cl => cl.CategoryLadder.Categories.LastOrDefault()?.Name)
|
||||
.Where(c => c is not null)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
public string[] AllCategoryNames()
|
||||
=> book.CategoriesLink?.Any() is not true ? Array.Empty<string>()
|
||||
: book
|
||||
.CategoriesLink
|
||||
.SelectMany(cl => cl.CategoryLadder.Categories)
|
||||
.Select(c => c.Name)
|
||||
.ToArray();
|
||||
|
||||
public string[] AllCategoryIds()
|
||||
=> book.CategoriesLink?.Any() is not true ? null
|
||||
: book
|
||||
.CategoriesLink
|
||||
.SelectMany(cl => cl.CategoryLadder.Categories)
|
||||
.Select(c => c.AudibleCategoryId)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static string[] LowestCategoryNames(this Book book)
|
||||
=> book.CategoriesLink?.Any() is not true ? Array.Empty<string>()
|
||||
: book
|
||||
.CategoriesLink
|
||||
.Select(cl => cl.CategoryLadder.Categories.LastOrDefault()?.Name)
|
||||
.Where(c => c is not null)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
public static string[] AllCategoryNames(this Book book)
|
||||
=> book.CategoriesLink?.Any() is not true ? Array.Empty<string>()
|
||||
: book
|
||||
.CategoriesLink
|
||||
.SelectMany(cl => cl.CategoryLadder.Categories)
|
||||
.Select(c => c.Name)
|
||||
.ToArray();
|
||||
|
||||
public static string[] AllCategoryIds(this Book book)
|
||||
=> book.CategoriesLink?.Any() is not true ? null
|
||||
: book
|
||||
.CategoriesLink
|
||||
.SelectMany(cl => cl.CategoryLadder.Categories)
|
||||
.Select(c => c.AudibleCategoryId)
|
||||
.ToArray();
|
||||
|
||||
public static string AggregateTitles(this IEnumerable<LibraryBook> libraryBooks, int max = 5)
|
||||
{
|
||||
@@ -93,7 +98,7 @@ namespace DataLayer
|
||||
return titlesAgg;
|
||||
}
|
||||
|
||||
public static float FirstScore(this Rating rating)
|
||||
public static float FirstScore(this Rating rating)
|
||||
=> rating.OverallRating > 0 ? rating.OverallRating
|
||||
: rating.PerformanceRating > 0 ? rating.PerformanceRating
|
||||
: rating.StoryRating;
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace FileLiberator
|
||||
/// </summary>
|
||||
public DownloadOptions.LicenseInfo? LicenseInfo { get; set; }
|
||||
|
||||
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
|
||||
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.AudioExists;
|
||||
public override async Task CancelAsync()
|
||||
{
|
||||
if (abDownloader is not null) await abDownloader.CancelAsync();
|
||||
@@ -43,7 +43,7 @@ namespace FileLiberator
|
||||
|
||||
try
|
||||
{
|
||||
if (libraryBook.Book.Audio_Exists())
|
||||
if (libraryBook.Book.AudioExists)
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
|
||||
DownloadValidation(libraryBook);
|
||||
@@ -129,20 +129,21 @@ namespace FileLiberator
|
||||
|
||||
private async Task<AudiobookDecryptResult> DownloadAudiobookAsync(AudibleApi.Api api, DownloadOptions dlOptions, CancellationToken cancellationToken)
|
||||
{
|
||||
var outpoutDir = AudibleFileStorage.DecryptInProgressDirectory;
|
||||
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
|
||||
//Directories are validated prior to beginning download/decrypt
|
||||
var outputDir = AudibleFileStorage.DecryptInProgressDirectory!;
|
||||
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory!;
|
||||
var result = new AudiobookDecryptResult(false, [], []);
|
||||
|
||||
try
|
||||
{
|
||||
if (dlOptions.DrmType is not DrmType.Adrm and not DrmType.Widevine)
|
||||
abDownloader = new UnencryptedAudiobookDownloader(outpoutDir, cacheDir, dlOptions);
|
||||
abDownloader = new UnencryptedAudiobookDownloader(outputDir, cacheDir, dlOptions);
|
||||
else
|
||||
{
|
||||
AaxcDownloadConvertBase converter
|
||||
= dlOptions.Config.SplitFilesByChapter && dlOptions.ChapterInfo.Count > 1 ?
|
||||
new AaxcDownloadMultiConverter(outpoutDir, cacheDir, dlOptions) :
|
||||
new AaxcDownloadSingleConverter(outpoutDir, cacheDir, dlOptions);
|
||||
new AaxcDownloadMultiConverter(outputDir, cacheDir, dlOptions) :
|
||||
new AaxcDownloadSingleConverter(outputDir, cacheDir, dlOptions);
|
||||
|
||||
if (dlOptions.Config.AllowLibationFixup)
|
||||
converter.RetrievedMetadata += Converter_RetrievedMetadata;
|
||||
@@ -176,7 +177,7 @@ namespace FileLiberator
|
||||
|
||||
void AbDownloader_TempFileCreated(object? sender, TempFile e)
|
||||
{
|
||||
if (Path.GetDirectoryName(e.FilePath) == outpoutDir)
|
||||
if (Path.GetDirectoryName(e.FilePath) == outputDir)
|
||||
{
|
||||
result.ResultFiles.Add(e);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace FileLiberator
|
||||
public override string Name => "Download Pdf";
|
||||
public override bool Validate(LibraryBook libraryBook)
|
||||
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
|
||||
&& !libraryBook.Book.PDF_Exists();
|
||||
&& !libraryBook.Book.PdfExists;
|
||||
|
||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
|
||||
@@ -21,9 +21,7 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
public class CheckBoxViewModel : ViewModelBase
|
||||
{
|
||||
private bool _isChecked;
|
||||
public bool IsChecked { get => _isChecked; set => this.RaiseAndSetIfChanged(ref _isChecked, value); }
|
||||
private object _bookText;
|
||||
public object Item { get => _bookText; set => this.RaiseAndSetIfChanged(ref _bookText, value); }
|
||||
public bool IsChecked { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public object Item { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,19 +140,17 @@ namespace LibationAvalonia.Controls
|
||||
private class KnownDirectoryItem : ReactiveObject
|
||||
{
|
||||
public Configuration.KnownDirectories KnownDirectory { get; set; }
|
||||
private string? _directory;
|
||||
public string? Directory { get => _directory; private set => this.RaiseAndSetIfChanged(ref _directory, value); }
|
||||
public string? Directory { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? Name { get; }
|
||||
private string? _subDir;
|
||||
public string? SubDirectory
|
||||
{
|
||||
get => _subDir;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
_subDir = value;
|
||||
field = value;
|
||||
if (Configuration.GetKnownDirectoryPath(KnownDirectory) is string dir)
|
||||
{
|
||||
Directory = Path.Combine(dir, _subDir ?? "");
|
||||
Directory = Path.Combine(dir, field ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,12 +64,8 @@ namespace LibationAvalonia.Dialogs
|
||||
public class AboutVM : ViewModelBase
|
||||
{
|
||||
public string Version { get; }
|
||||
public bool CanCheckForUpgrade { get => canCheckForUpgrade; set => this.RaiseAndSetIfChanged(ref canCheckForUpgrade, value); }
|
||||
public string UpgradeButtonText { get => upgradeButtonText; set => this.RaiseAndSetIfChanged(ref upgradeButtonText, value); }
|
||||
|
||||
|
||||
private bool canCheckForUpgrade = true;
|
||||
private string upgradeButtonText = "Check for Upgrade";
|
||||
public bool CanCheckForUpgrade { get => field; set => this.RaiseAndSetIfChanged(ref field, value); } = true;
|
||||
public string UpgradeButtonText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }= "Check for Upgrade";
|
||||
|
||||
public IEnumerable<LibationContributor> PrimaryContributors => LibationContributor.PrimaryContributors;
|
||||
public IEnumerable<LibationContributor> AdditionalContributors => LibationContributor.AdditionalContributors;
|
||||
|
||||
@@ -19,15 +19,14 @@ namespace LibationAvalonia.Dialogs
|
||||
public AvaloniaList<AccountDto> Accounts { get; } = new();
|
||||
public class AccountDto : ViewModels.ViewModelBase
|
||||
{
|
||||
private string _accountId;
|
||||
public IReadOnlyList<Locale> Locales => AccountsDialog.Locales;
|
||||
public bool LibraryScan { get; set; } = true;
|
||||
public string AccountId
|
||||
{
|
||||
get => _accountId;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _accountId, value);
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
this.RaisePropertyChanged(nameof(IsDefault));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,15 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class BookDetailsDialog : DialogWindow
|
||||
{
|
||||
private LibraryBook _libraryBook;
|
||||
private BookDetailsDialogViewModel _viewModel;
|
||||
public LibraryBook LibraryBook
|
||||
{
|
||||
get => _libraryBook;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
_libraryBook = value;
|
||||
Title = _libraryBook.Book.TitleWithSubtitle;
|
||||
DataContext = _viewModel = new BookDetailsDialogViewModel(_libraryBook);
|
||||
field = value;
|
||||
Title = field.Book.TitleWithSubtitle;
|
||||
DataContext = _viewModel = new BookDetailsDialogViewModel(field);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,16 +115,16 @@ namespace LibationAvalonia.Dialogs
|
||||
var title = string.IsNullOrEmpty(Book.Subtitle) ? Book.Title : $"{Book.Title}\r\n {Book.Subtitle}";
|
||||
|
||||
//init book details
|
||||
DetailsText = @$"
|
||||
Title: {title}
|
||||
Author(s): {Book.AuthorNames()}
|
||||
Narrator(s): {Book.NarratorNames()}
|
||||
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
|
||||
Category: {string.Join(", ", Book.LowestCategoryNames())}
|
||||
Purchase Date: {libraryBook.DateAdded:d}
|
||||
Language: {Book.Language}
|
||||
Audible ID: {Book.AudibleProductId}
|
||||
".Trim();
|
||||
DetailsText = $"""
|
||||
Title: {title}
|
||||
Author(s): {Book.AuthorNames}
|
||||
Narrator(s): {Book.NarratorNames}
|
||||
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
|
||||
Category: {string.Join(", ", Book.LowestCategoryNames())}
|
||||
Purchase Date: {libraryBook.DateAdded:d}
|
||||
Language: {Book.Language}
|
||||
Audible ID: {Book.AudibleProductId}
|
||||
""";
|
||||
|
||||
var seriesNames = libraryBook.Book.SeriesNames();
|
||||
if (!string.IsNullOrWhiteSpace(seriesNames))
|
||||
|
||||
@@ -204,9 +204,8 @@ namespace LibationAvalonia.Dialogs
|
||||
private class BookRecordEntry : ViewModels.ViewModelBase
|
||||
{
|
||||
private const string DateFormat = "yyyy-MM-dd HH\\:mm";
|
||||
private bool _ischecked;
|
||||
public IRecord Record { get; }
|
||||
public bool IsChecked { get => _ischecked; set => this.RaiseAndSetIfChanged(ref _ischecked, value); }
|
||||
public bool IsChecked { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string Type => Record.GetType().Name;
|
||||
public string Start => formatTimeSpan(Record.Start);
|
||||
public string Created => Record.Created.ToString(DateFormat);
|
||||
|
||||
@@ -13,29 +13,25 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public class Filter : ViewModels.ViewModelBase
|
||||
{
|
||||
private string _name;
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => this.RaiseAndSetIfChanged(ref _name, value);
|
||||
get => field;
|
||||
set => this.RaiseAndSetIfChanged(ref field, value);
|
||||
}
|
||||
|
||||
private string _filterString;
|
||||
public string FilterString
|
||||
{
|
||||
get => _filterString;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
IsDefault = string.IsNullOrEmpty(value);
|
||||
this.RaiseAndSetIfChanged(ref _filterString, value);
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
this.RaisePropertyChanged(nameof(IsDefault));
|
||||
}
|
||||
}
|
||||
public bool IsDefault { get; private set; } = true;
|
||||
private bool _isTop;
|
||||
private bool _isBottom;
|
||||
public bool IsTop { get => _isTop; set => this.RaiseAndSetIfChanged(ref _isTop, value); }
|
||||
public bool IsBottom { get => _isBottom; set => this.RaiseAndSetIfChanged(ref _isBottom, value); }
|
||||
public bool IsTop { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool IsBottom { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
public QuickFilters.NamedFilter AsNamedFilter() => new(FilterString, Name);
|
||||
|
||||
|
||||
@@ -123,25 +123,22 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public ReplacementsExt()
|
||||
{
|
||||
_replacementText = string.Empty;
|
||||
_description = string.Empty;
|
||||
_characterToReplace = string.Empty;
|
||||
ReplacementText = string.Empty;
|
||||
Description = string.Empty;
|
||||
CharacterToReplace = string.Empty;
|
||||
}
|
||||
public ReplacementsExt(Replacement replacement)
|
||||
{
|
||||
_characterToReplace = replacement.CharacterToReplace == default ? "" : replacement.CharacterToReplace.ToString();
|
||||
_replacementText = replacement.ReplacementString;
|
||||
_description = replacement.Description;
|
||||
CharacterToReplace = replacement.CharacterToReplace == default ? "" : replacement.CharacterToReplace.ToString();
|
||||
ReplacementText = replacement.ReplacementString;
|
||||
Description = replacement.Description;
|
||||
Mandatory = replacement.Mandatory;
|
||||
}
|
||||
|
||||
private string _replacementText;
|
||||
private string _description;
|
||||
private string _characterToReplace;
|
||||
public string ReplacementText { get => _replacementText; set => this.RaiseAndSetIfChanged(ref _replacementText, value); }
|
||||
public string Description { get => _description; set => this.RaiseAndSetIfChanged(ref _description, value); }
|
||||
public string CharacterToReplace { get => _characterToReplace; set => this.RaiseAndSetIfChanged(ref _characterToReplace, value); }
|
||||
public char Character => string.IsNullOrEmpty(_characterToReplace) ? default : _characterToReplace[0];
|
||||
public string ReplacementText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string Description { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string CharacterToReplace { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public char Character => string.IsNullOrEmpty(CharacterToReplace) ? default : CharacterToReplace[0];
|
||||
public bool IsDefault => !Mandatory && string.IsNullOrEmpty(CharacterToReplace);
|
||||
public bool Mandatory { get; }
|
||||
|
||||
|
||||
@@ -101,19 +101,17 @@ public partial class EditTemplateDialog : DialogWindow
|
||||
=> Go.To.Url(@"ht" + "tps://github.com/rmcrackan/Libation/blob/master/Documentation/NamingTemplates.md");
|
||||
|
||||
// hold the work-in-progress value. not guaranteed to be valid
|
||||
private string _userTemplateText;
|
||||
public string UserTemplateText
|
||||
{
|
||||
get => _userTemplateText;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _userTemplateText, value);
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
templateTb_TextChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _warningText;
|
||||
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
||||
public string WarningText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
|
||||
@@ -61,15 +61,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public class BitmapHolder : ViewModels.ViewModelBase
|
||||
{
|
||||
private Bitmap _coverImage;
|
||||
public Bitmap CoverImage
|
||||
{
|
||||
get => _coverImage;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _coverImage, value);
|
||||
}
|
||||
}
|
||||
public Bitmap CoverImage { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +108,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public class LocatedAudiobooksViewModel : ViewModelBase
|
||||
{
|
||||
private int _foundAsins = 0;
|
||||
public AvaloniaList<Tuple<string, string>> FoundFiles { get; } = new();
|
||||
public int FoundAsins { get => _foundAsins; set => this.RaiseAndSetIfChanged(ref _foundAsins, value); }
|
||||
public int FoundAsins { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Threading;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.Forms;
|
||||
@@ -28,7 +29,7 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Configuration.Instance.UseWebView && await BrowserLoginAsync(choiceIn.LoginUrl) is ChoiceOut external)
|
||||
if (Configuration.Instance.UseWebView && await BrowserLoginAsync(choiceIn) is ChoiceOut external)
|
||||
return external;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -42,20 +43,19 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
: null;
|
||||
}
|
||||
|
||||
private async Task<ChoiceOut?> BrowserLoginAsync(string url)
|
||||
private async Task<ChoiceOut?> BrowserLoginAsync(ChoiceIn shoiceIn)
|
||||
{
|
||||
TaskCompletionSource<ChoiceOut?> tcs = new();
|
||||
|
||||
NativeWebDialog dialog = new()
|
||||
{
|
||||
Title = "Audible Login",
|
||||
CanUserResize = true,
|
||||
Source = new Uri(url)
|
||||
CanUserResize = true
|
||||
};
|
||||
|
||||
dialog.EnvironmentRequested += Dialog_EnvironmentRequested;
|
||||
dialog.NavigationCompleted += Dialog_NavigationCompleted;
|
||||
dialog.Closing += (_, _) => tcs.TrySetResult(null);
|
||||
dialog.NavigationStarted += (_, e) =>
|
||||
dialog.NavigationStarted += async (_, e) =>
|
||||
{
|
||||
if (e.Request?.AbsolutePath.StartsWith("/ap/maplanding") is true)
|
||||
{
|
||||
@@ -63,6 +63,16 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
dialog.Close();
|
||||
}
|
||||
};
|
||||
dialog.AdapterCreated += (s, e) =>
|
||||
{
|
||||
if (dialog.TryGetCookieManager() is NativeWebViewCookieManager cookieManager)
|
||||
{
|
||||
foreach (System.Net.Cookie c in shoiceIn.SignInCookies)
|
||||
cookieManager.AddOrUpdateCookie(c);
|
||||
}
|
||||
//Set the source only after loading cookies
|
||||
dialog.Source = new Uri(shoiceIn.LoginUrl);
|
||||
};
|
||||
|
||||
if (!Configuration.IsLinux && App.MainWindow is TopLevel topLevel)
|
||||
dialog.Show(topLevel);
|
||||
@@ -70,7 +80,27 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
dialog.Show();
|
||||
|
||||
return await tcs.Task;
|
||||
}
|
||||
|
||||
void Dialog_EnvironmentRequested(object? sender, WebViewEnvironmentRequestedEventArgs e)
|
||||
{
|
||||
// Private browsing & user agent setting
|
||||
switch (e)
|
||||
{
|
||||
case WindowsWebView2EnvironmentRequestedEventArgs webView2Args:
|
||||
webView2Args.IsInPrivateModeEnabled = true;
|
||||
webView2Args.AdditionalBrowserArguments = "--user-agent=\"" + Resources.User_Agent + "\"";
|
||||
break;
|
||||
case AppleWKWebViewEnvironmentRequestedEventArgs appleArgs:
|
||||
appleArgs.NonPersistentDataStore = true;
|
||||
appleArgs.ApplicationNameForUserAgent = Resources.User_Agent;
|
||||
break;
|
||||
case GtkWebViewEnvironmentRequestedEventArgs gtkArgs:
|
||||
gtkArgs.EphemeralDataManager = true;
|
||||
gtkArgs.ApplicationNameForUserAgent = Resources.User_Agent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void Dialog_NavigationCompleted(object? sender, WebViewNavigationCompletedEventArgs e)
|
||||
{
|
||||
|
||||
@@ -180,16 +180,15 @@ public partial class ThemePickerDialog : DialogWindow
|
||||
public required string ThemeItemName { get; init; }
|
||||
public required Action<Color, string>? ColorSetter { get; set; }
|
||||
|
||||
private Color _themeColor;
|
||||
public Color ThemeColor
|
||||
{
|
||||
get => _themeColor;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
var setColors = !_themeColor.Equals(value);
|
||||
this.RaiseAndSetIfChanged(ref _themeColor, value);
|
||||
var setColors = !field.Equals(value);
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
if (setColors)
|
||||
ColorSetter?.Invoke(_themeColor, ThemeItemName);
|
||||
ColorSetter?.Invoke(field, ThemeItemName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,7 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public AvaloniaList<CheckBoxViewModel> DeletedBooks { get; }
|
||||
public string CheckedCountText => $"Checked : {_checkedBooksCount} of {_totalBooksCount}";
|
||||
|
||||
private bool _controlsEnabled = true;
|
||||
public bool ControlsEnabled { get => _controlsEnabled; set => this.RaiseAndSetIfChanged(ref _controlsEnabled, value); }
|
||||
public bool ControlsEnabled { get => field; set => this.RaiseAndSetIfChanged(ref field, value); } = true;
|
||||
|
||||
private bool? everythingChecked = false;
|
||||
public bool? EverythingChecked
|
||||
|
||||
@@ -5,8 +5,7 @@ namespace LibationAvalonia.ViewModels.Dialogs
|
||||
{
|
||||
public class MessageBoxViewModel
|
||||
{
|
||||
private string _message;
|
||||
public string Message { get { return _message; } set { _message = value; } }
|
||||
public string Message { get => field; set => field = value; }
|
||||
public string Caption { get; } = "Message Box";
|
||||
private MessageBoxButtons _button;
|
||||
private MessageBoxIcon _icon;
|
||||
|
||||
@@ -5,24 +5,14 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public class LiberateStatusButtonViewModel : ViewModelBase
|
||||
{
|
||||
private bool isSeries;
|
||||
private bool isError;
|
||||
private bool isButtonEnabled;
|
||||
private bool expanded;
|
||||
private bool redVisible = true;
|
||||
private bool yellowVisible;
|
||||
private bool greenVisible;
|
||||
private bool pdfNotDownloadedVisible;
|
||||
private bool pdfDownloadedVisible;
|
||||
|
||||
public bool IsError { get => isError; set => this.RaiseAndSetIfChanged(ref isError, value); }
|
||||
public bool IsButtonEnabled { get => isButtonEnabled; set => this.RaiseAndSetIfChanged(ref isButtonEnabled, value); }
|
||||
public bool IsSeries { get => isSeries; set => this.RaiseAndSetIfChanged(ref isSeries, value); }
|
||||
public bool Expanded { get => expanded; set => this.RaiseAndSetIfChanged(ref expanded, value); }
|
||||
public bool RedVisible { get => redVisible; set => this.RaiseAndSetIfChanged(ref redVisible, value); }
|
||||
public bool YellowVisible { get => yellowVisible; set => this.RaiseAndSetIfChanged(ref yellowVisible, value); }
|
||||
public bool GreenVisible { get => greenVisible; set => this.RaiseAndSetIfChanged(ref greenVisible, value); }
|
||||
public bool PdfDownloadedVisible { get => pdfDownloadedVisible; set => this.RaiseAndSetIfChanged(ref pdfDownloadedVisible, value); }
|
||||
public bool PdfNotDownloadedVisible { get => pdfNotDownloadedVisible; set => this.RaiseAndSetIfChanged(ref pdfNotDownloadedVisible, value); }
|
||||
public bool IsError { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool IsButtonEnabled { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool IsSeries { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool Expanded { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool RedVisible { get => field; set => this.RaiseAndSetIfChanged(ref field, value); } = true;
|
||||
public bool YellowVisible { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool GreenVisible { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool PdfDownloadedVisible { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool PdfNotDownloadedVisible { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace LibationAvalonia.ViewModels
|
||||
partial class MainVM
|
||||
{
|
||||
private Task<LibraryCommands.LibraryStats>? updateCountsTask;
|
||||
private LibraryCommands.LibraryStats? _libraryStats;
|
||||
|
||||
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
|
||||
public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0";
|
||||
@@ -22,10 +21,10 @@ namespace LibationAvalonia.ViewModels
|
||||
/// <summary> The user's library statistics </summary>
|
||||
public LibraryCommands.LibraryStats? LibraryStats
|
||||
{
|
||||
get => _libraryStats;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _libraryStats, value);
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
|
||||
BookBackupsToolStripText
|
||||
= LibraryStats?.HasPendingBooks ?? false
|
||||
|
||||
@@ -18,14 +18,11 @@ namespace LibationAvalonia.ViewModels
|
||||
private string lastGoodSearch = string.Empty;
|
||||
private QuickFilters.NamedFilter? lastGoodFilter => new(lastGoodSearch, null);
|
||||
|
||||
private QuickFilters.NamedFilter? _selectedNamedFilter = new(string.Empty, null);
|
||||
private bool _firstFilterIsDefault = true;
|
||||
|
||||
/// <summary> Library filterting query </summary>
|
||||
public QuickFilters.NamedFilter? SelectedNamedFilter { get => _selectedNamedFilter; set => this.RaiseAndSetIfChanged(ref _selectedNamedFilter, value); }
|
||||
public QuickFilters.NamedFilter? SelectedNamedFilter { get => field; set => this.RaiseAndSetIfChanged(ref field, value); } = new(string.Empty, null);
|
||||
public AvaloniaList<Control> QuickFilterMenuItems { get; } = new();
|
||||
/// <summary> Indicates if the first quick filter is the default filter </summary>
|
||||
public bool FirstFilterIsDefault { get => _firstFilterIsDefault; set => QuickFilters.UseDefault = this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value); }
|
||||
public bool FirstFilterIsDefault { get => field; set => QuickFilters.UseDefault = this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
private void Configure_Filters()
|
||||
{
|
||||
|
||||
@@ -14,30 +14,25 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public partial class MainVM
|
||||
{
|
||||
private bool _autoScanChecked = Configuration.Instance.AutoScan;
|
||||
private string _removeBooksButtonText = "Remove # Books from Libation";
|
||||
private bool _removeBooksButtonEnabled = Design.IsDesignMode;
|
||||
private bool _removeButtonsVisible = Design.IsDesignMode;
|
||||
private int _numAccountsScanning = 2;
|
||||
private int _accountsCount = 0;
|
||||
public string LocateAudiobooksTip => Configuration.GetHelpText("LocateAudiobooks");
|
||||
|
||||
/// <summary> Auto scanning accounts is enables </summary>
|
||||
public bool AutoScanChecked { get => _autoScanChecked; set => Configuration.Instance.AutoScan = this.RaiseAndSetIfChanged(ref _autoScanChecked, value); }
|
||||
public bool AutoScanChecked { get => field; set => Configuration.Instance.AutoScan = this.RaiseAndSetIfChanged(ref field, value); } = Configuration.Instance.AutoScan;
|
||||
/// <summary> Display text for the "Remove # Books from Libation" button </summary>
|
||||
public string RemoveBooksButtonText { get => _removeBooksButtonText; set => this.RaiseAndSetIfChanged(ref _removeBooksButtonText, value); }
|
||||
public string RemoveBooksButtonText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }= "Remove # Books from Libation";
|
||||
/// <summary> Indicates if the "Remove # Books from Libation" button is enabled </summary>
|
||||
public bool RemoveBooksButtonEnabled { get => _removeBooksButtonEnabled; set { this.RaiseAndSetIfChanged(ref _removeBooksButtonEnabled, value); } }
|
||||
public bool RemoveBooksButtonEnabled { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } } = Design.IsDesignMode;
|
||||
/// <summary> Indicates if the "Remove # Books from Libation" and "Done Removing" buttons should be visible </summary>
|
||||
public bool RemoveButtonsVisible
|
||||
{
|
||||
get => _removeButtonsVisible;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _removeButtonsVisible, value);
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
this.RaisePropertyChanged(nameof(RemoveMenuItemsEnabled));
|
||||
}
|
||||
}
|
||||
} = Design.IsDesignMode;
|
||||
/// <summary> Indicates if Libation is currently scanning account(s) </summary>
|
||||
public bool ActivelyScanning => _numAccountsScanning > 0;
|
||||
/// <summary> Indicates if the "Remove Books" menu items are enabled</summary>
|
||||
@@ -53,10 +48,10 @@ namespace LibationAvalonia.ViewModels
|
||||
/// <summary> The number of accounts added to Libation </summary>
|
||||
public int AccountsCount
|
||||
{
|
||||
get => _accountsCount;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _accountsCount, value);
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
this.RaisePropertyChanged(nameof(AnyAccounts));
|
||||
this.RaisePropertyChanged(nameof(OneAccount));
|
||||
this.RaisePropertyChanged(nameof(MultipleAccounts));
|
||||
|
||||
@@ -12,15 +12,13 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
{
|
||||
private bool _queueOpen = false;
|
||||
|
||||
/// <summary> The Process Queue panel is open </summary>
|
||||
public bool QueueOpen
|
||||
{
|
||||
get => _queueOpen;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _queueOpen, value);
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
QueueButtonAngle = value ? 180 : 0;
|
||||
this.RaisePropertyChanged(nameof(QueueButtonAngle));
|
||||
}
|
||||
@@ -40,7 +38,7 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
if (ProcessQueue.QueueDownloadDecrypt(libraryBooks))
|
||||
setQueueCollapseState(false);
|
||||
else if (libraryBooks.Length == 1 && libraryBooks[0].Book.Audio_Exists())
|
||||
else if (libraryBooks.Length == 1 && libraryBooks[0].Book.AudioExists)
|
||||
{
|
||||
// liberated: open explorer to file
|
||||
var filePath = AudibleFileStorage.Audio.GetPath(libraryBooks[0].Book.AudibleProductId);
|
||||
|
||||
@@ -9,8 +9,7 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
{
|
||||
private bool _menuBarVisible = !Configuration.IsMacOs;
|
||||
public bool MenuBarVisible { get => _menuBarVisible; set => this.RaiseAndSetIfChanged(ref _menuBarVisible, value); }
|
||||
public bool MenuBarVisible { get => field; set => this.RaiseAndSetIfChanged(ref field, value); } = !Configuration.IsMacOs;
|
||||
private void Configure_Settings()
|
||||
{
|
||||
if (App.Current is Avalonia.Application app &&
|
||||
|
||||
@@ -16,8 +16,7 @@ namespace LibationAvalonia.ViewModels
|
||||
public ProcessQueueViewModel ProcessQueue { get; } = new ProcessQueueViewModel();
|
||||
public ProductsDisplayViewModel ProductsDisplay { get; } = new ProductsDisplayViewModel();
|
||||
|
||||
private double? _downloadProgress = null;
|
||||
public double? DownloadProgress { get => _downloadProgress; set => this.RaiseAndSetIfChanged(ref _downloadProgress, value); }
|
||||
public double? DownloadProgress { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
|
||||
private readonly MainWindow MainWindow;
|
||||
|
||||
@@ -31,15 +31,8 @@ namespace LibationAvalonia.ViewModels
|
||||
private HashSet<GridEntry>? FilteredInGridEntries;
|
||||
public string? FilterString { get; private set; }
|
||||
|
||||
private DataGridCollectionView? _gridEntries;
|
||||
public DataGridCollectionView? GridEntries
|
||||
{
|
||||
get => _gridEntries;
|
||||
private set => this.RaiseAndSetIfChanged(ref _gridEntries, value);
|
||||
}
|
||||
|
||||
private bool _removeColumnVisible;
|
||||
public bool RemoveColumnVisible { get => _removeColumnVisible; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisible, value); }
|
||||
public DataGridCollectionView? GridEntries { get => field; private set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool RemoveColumnVisible { get => field; private set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
public List<LibraryBook> GetVisibleBookEntries()
|
||||
=> FilteredInGridEntries?
|
||||
@@ -104,8 +97,7 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
||||
{
|
||||
if (dbBooks == null)
|
||||
throw new ArgumentNullException(nameof(dbBooks));
|
||||
ArgumentNullException.ThrowIfNull(dbBooks, nameof(dbBooks));
|
||||
|
||||
//Get the UI thread's synchronization context and set it on the current thread to ensure
|
||||
//it's available for GetAllProductsAsync and GetAllSeriesEntriesAsync
|
||||
@@ -158,8 +150,7 @@ namespace LibationAvalonia.ViewModels
|
||||
/// </summary>
|
||||
internal async Task UpdateGridAsync(List<LibraryBook> dbBooks)
|
||||
{
|
||||
if (dbBooks == null)
|
||||
throw new ArgumentNullException(nameof(dbBooks));
|
||||
ArgumentNullException.ThrowIfNull(dbBooks, nameof(dbBooks));
|
||||
|
||||
if (GridEntries == null)
|
||||
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(UpdateGridAsync)}");
|
||||
|
||||
@@ -13,14 +13,6 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class AudioSettingsVM : ViewModelBase
|
||||
{
|
||||
private bool _downloadClipsBookmarks;
|
||||
private bool _decryptToLossy;
|
||||
private bool _splitFilesByChapter;
|
||||
private bool _allowLibationFixup;
|
||||
private bool _lameTargetBitrate;
|
||||
private bool _lameMatchSource;
|
||||
private int _lameBitrate;
|
||||
private int _lameVBRQuality;
|
||||
private string _chapterTitleTemplate;
|
||||
public EnumDisplay<SampleRate> SelectedSampleRate { get; set; }
|
||||
public NAudio.Lame.EncoderQuality SelectedEncoderQuality { get; set; }
|
||||
@@ -31,13 +23,11 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
.Select(v => new EnumDisplay<SampleRate>(v, $"{((int)v):N0} Hz")));
|
||||
|
||||
public AvaloniaList<NAudio.Lame.EncoderQuality> EncoderQualities { get; }
|
||||
= new(
|
||||
new[]
|
||||
{
|
||||
= new([
|
||||
NAudio.Lame.EncoderQuality.High,
|
||||
NAudio.Lame.EncoderQuality.Standard,
|
||||
NAudio.Lame.EncoderQuality.Fast,
|
||||
});
|
||||
]);
|
||||
|
||||
|
||||
public AudioSettingsVM(Configuration config)
|
||||
@@ -141,12 +131,10 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public bool DownloadCoverArt { get; set; }
|
||||
public bool RetainAaxFile { get; set; }
|
||||
public string RetainAaxFileTip => Configuration.GetHelpText(nameof(RetainAaxFile));
|
||||
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }
|
||||
|
||||
private bool _useWidevine, _requestSpatial, _request_xHE_AAC;
|
||||
public bool UseWidevine { get => _useWidevine; set => this.RaiseAndSetIfChanged(ref _useWidevine, value); }
|
||||
public bool Request_xHE_AAC { get => _request_xHE_AAC; set => this.RaiseAndSetIfChanged(ref _request_xHE_AAC, value); }
|
||||
public bool RequestSpatial { get => _requestSpatial; set => this.RaiseAndSetIfChanged(ref _requestSpatial, value); }
|
||||
public bool DownloadClipsBookmarks { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool UseWidevine { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool Request_xHE_AAC { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool RequestSpatial { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
public EnumDisplay<Configuration.DownloadQuality> FileDownloadQuality { get; set; }
|
||||
public EnumDisplay<Configuration.SpatialCodec> SpatialAudioCodec { get; set; }
|
||||
@@ -158,10 +146,10 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public bool StripUnabridged { get; set; }
|
||||
public string StripUnabridgedTip => Configuration.GetHelpText(nameof(StripUnabridged));
|
||||
public bool DecryptToLossy {
|
||||
get => _decryptToLossy;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _decryptToLossy, value);
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
if (DecryptToLossy && SpatialAudioCodec.Value is Configuration.SpatialCodec.AC_4)
|
||||
{
|
||||
SpatialAudioCodec = SpatialAudioCodecs[0];
|
||||
@@ -176,20 +164,20 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public string LameDownsampleMonoTip => Configuration.GetHelpText(nameof(LameDownsampleMono));
|
||||
public bool LameConstantBitrate { get; set; } = Design.IsDesignMode;
|
||||
|
||||
public bool SplitFilesByChapter { get => _splitFilesByChapter; set { this.RaiseAndSetIfChanged(ref _splitFilesByChapter, value); } }
|
||||
public bool LameTargetBitrate { get => _lameTargetBitrate; set { this.RaiseAndSetIfChanged(ref _lameTargetBitrate, value); } }
|
||||
public bool LameMatchSource { get => _lameMatchSource; set { this.RaiseAndSetIfChanged(ref _lameMatchSource, value); } }
|
||||
public int LameBitrate { get => _lameBitrate; set { this.RaiseAndSetIfChanged(ref _lameBitrate, value); } }
|
||||
public int LameVBRQuality { get => _lameVBRQuality; set { this.RaiseAndSetIfChanged(ref _lameVBRQuality, value); } }
|
||||
public bool SplitFilesByChapter { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
|
||||
public bool LameTargetBitrate { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
|
||||
public bool LameMatchSource { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
|
||||
public int LameBitrate { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
|
||||
public int LameVBRQuality { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
|
||||
|
||||
public string ChapterTitleTemplate { get => _chapterTitleTemplate; set { this.RaiseAndSetIfChanged(ref _chapterTitleTemplate, value); } }
|
||||
|
||||
public bool AllowLibationFixup
|
||||
{
|
||||
get => _allowLibationFixup;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (!this.RaiseAndSetIfChanged(ref _allowLibationFixup, value))
|
||||
if (!this.RaiseAndSetIfChanged(ref field, value))
|
||||
{
|
||||
SplitFilesByChapter = false;
|
||||
StripAudibleBrandAudio = false;
|
||||
|
||||
@@ -150,6 +150,8 @@ namespace LibationAvalonia.Views
|
||||
{
|
||||
productsDisplay?.CloseImageDisplay();
|
||||
this.SaveSizeAndLocation(Configuration.Instance);
|
||||
//This is double firing with 11.3.9
|
||||
Closing -= MainWindow_Closing;
|
||||
}
|
||||
|
||||
private void selectAndFocusSearchBox()
|
||||
|
||||
@@ -327,7 +327,7 @@ namespace LibationAvalonia.Views
|
||||
{
|
||||
//No need to persist these changes. They only needs to last long for the files to start downloading
|
||||
entry4.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
|
||||
if (entry4.Book.HasPdf())
|
||||
if (entry4.Book.HasPdf)
|
||||
entry4.Book.UserDefinedItem.SetPdfStatus(LiberatedStatus.NotLiberated);
|
||||
LiberateClicked?.Invoke(this, [entry4.LibraryBook]);
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Dinah.Core;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace LibationCli;
|
||||
@@ -12,10 +13,10 @@ internal class ConsoleProgressBar
|
||||
|
||||
public double? Progress
|
||||
{
|
||||
get => m_Progress;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
m_Progress = value ?? 0;
|
||||
field = value ?? 0;
|
||||
WriteProgress();
|
||||
}
|
||||
}
|
||||
@@ -30,7 +31,6 @@ internal class ConsoleProgressBar
|
||||
}
|
||||
}
|
||||
|
||||
private double m_Progress;
|
||||
private TimeSpan m_RemainingTime;
|
||||
private int m_LastWriteLength = 0;
|
||||
private const int MAX_ETA_LEN = 10;
|
||||
@@ -51,7 +51,7 @@ internal class ConsoleProgressBar
|
||||
|
||||
private void WriteProgress()
|
||||
{
|
||||
var numCompleted = (int)Math.Round(double.Min(100, m_Progress) * m_NumProgressPieces / 100);
|
||||
var numCompleted = (int)Math.Round(double.Min(100, Progress?? 0) * m_NumProgressPieces / 100);
|
||||
var numRemaining = m_NumProgressPieces - numCompleted;
|
||||
var progressBar = $"[{new string(ProgressChar, numCompleted)}{new string(NoProgressChar, numRemaining)}] ";
|
||||
|
||||
|
||||
@@ -19,8 +19,32 @@ namespace LibationFileManager
|
||||
protected abstract List<LongPath> GetFilePathsCustom(string productId);
|
||||
|
||||
#region static
|
||||
public static LongPath DownloadsInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
|
||||
public static LongPath DecryptInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
|
||||
|
||||
/*
|
||||
* Operations like LibraryCommands.GetCounts() hit the file system hard.
|
||||
* Since failing to create a directory and exception handling is expensive,
|
||||
* only retry creating InProgress subdirectories every RetryInProgressInterval.
|
||||
*/
|
||||
private static DateTime lastInProgressFail;
|
||||
private static readonly TimeSpan RetryInProgressInterval = TimeSpan.FromSeconds(2);
|
||||
|
||||
private static DirectoryInfo? CreateInProgressDirectory(string subDirectory)
|
||||
{
|
||||
try
|
||||
{
|
||||
return (DateTime.UtcNow - lastInProgressFail) < RetryInProgressInterval ? null
|
||||
: new DirectoryInfo(Configuration.Instance.InProgress).CreateSubdirectoryEx(subDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Error creating subdirectory in {@InProgress}", Configuration.Instance.InProgress);
|
||||
lastInProgressFail = DateTime.UtcNow;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static LongPath? DownloadsInProgressDirectory => CreateInProgressDirectory("DownloadsInProgress")?.FullName;
|
||||
public static LongPath? DecryptInProgressDirectory => CreateInProgressDirectory("DecryptInProgress")?.FullName;
|
||||
|
||||
static AudibleFileStorage()
|
||||
{
|
||||
@@ -28,7 +52,9 @@ namespace LibationFileManager
|
||||
//Do not clean DownloadsInProgressDirectory. Those files are resumable.
|
||||
try
|
||||
{
|
||||
foreach (var tempFile in FileUtility.SaferEnumerateFiles(DecryptInProgressDirectory))
|
||||
if (DecryptInProgressDirectory is not LongPath decryptDir)
|
||||
return;
|
||||
foreach (var tempFile in FileUtility.SaferEnumerateFiles(decryptDir))
|
||||
FileUtility.SaferDelete(tempFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -114,9 +140,12 @@ namespace LibationFileManager
|
||||
|
||||
protected override List<LongPath> GetFilePathsCustom(string productId)
|
||||
{
|
||||
if (DownloadsInProgressDirectory is not LongPath dlFolder)
|
||||
return [];
|
||||
|
||||
var regex = GetBookSearchRegex(productId);
|
||||
return FileUtility
|
||||
.SaferEnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories)
|
||||
.SaferEnumerateFiles(dlFolder, "*.*", SearchOption.AllDirectories)
|
||||
.Where(s => regex.IsMatch(s)).ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -86,11 +86,10 @@ namespace LibationFileManager.Templates
|
||||
=> NamingTemplate?.TagsInUse.Cast<TemplateTags>() ?? Enumerable.Empty<TemplateTags>();
|
||||
public string TemplateText => NamingTemplate?.TemplateText ?? "";
|
||||
|
||||
private readonly NamingTemplate? _namingTemplate;
|
||||
protected NamingTemplate NamingTemplate
|
||||
{
|
||||
get => _namingTemplate ?? throw new NullReferenceException(nameof(_namingTemplate));
|
||||
private init => _namingTemplate = value;
|
||||
get => field ?? throw new NullReferenceException(nameof(NamingTemplate));
|
||||
private init => field = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LuceneNet303r2" Version="3.0.3.10" />
|
||||
<PackageReference Include="LuceneNet303r2" Version="3.0.3.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -37,8 +37,8 @@ namespace LibationSearchEngine
|
||||
{ FieldType.ID, lb => lb.Book.AudibleProductId.ToLowerInvariant(), nameof(Book.AudibleProductId), "ProductId", "Id", "ASIN" },
|
||||
{ FieldType.Raw, lb => lb.Book.AudibleProductId, _ID_ },
|
||||
{ FieldType.String, lb => lb.Book.TitleWithSubtitle, "Title", "ProductId", "Id", "ASIN" },
|
||||
{ FieldType.String, lb => lb.Book.AuthorNames(), "AuthorNames", "Author", "Authors" },
|
||||
{ FieldType.String, lb => lb.Book.NarratorNames(), "NarratorNames", "Narrator", "Narrators" },
|
||||
{ FieldType.String, lb => lb.Book.AuthorNames, "AuthorNames", "Author", "Authors" },
|
||||
{ FieldType.String, lb => lb.Book.NarratorNames, "NarratorNames", "Narrator", "Narrators" },
|
||||
{ FieldType.String, lb => lb.Book.Publisher, nameof(Book.Publisher) },
|
||||
{ FieldType.String, lb => lb.Book.SeriesNames(), "SeriesNames", "Narrator", "Series" },
|
||||
{ FieldType.String, lb => string.Join(", ", lb.Book.SeriesLink.Select(s => s.Series.AudibleSeriesId)), "SeriesId" },
|
||||
@@ -48,7 +48,7 @@ namespace LibationSearchEngine
|
||||
{ FieldType.String, lb => lb.Book.Locale, "Locale", "Region" },
|
||||
{ FieldType.String, lb => lb.Account, "Account", "Email" },
|
||||
{ FieldType.String, lb => lb.Book.UserDefinedItem.LastDownloadedFormat?.CodecString, "Codec", "DownloadedCodec" },
|
||||
{ FieldType.Bool, lb => lb.Book.HasPdf().ToString(), "HasDownloads", "HasDownload", "Downloads" , "Download", "HasPDFs", "HasPDF" , "PDFs", "PDF" },
|
||||
{ FieldType.Bool, lb => lb.Book.HasPdf.ToString(), "HasDownloads", "HasDownload", "Downloads" , "Download", "HasPDFs", "HasPDF" , "PDFs", "PDF" },
|
||||
{ FieldType.Bool, lb => (lb.Book.UserDefinedItem.Rating.OverallRating > 0f).ToString(), "IsRated", "Rated" },
|
||||
{ FieldType.Bool, lb => isAuthorNarrated(lb.Book).ToString(), "IsAuthorNarrated", "AuthorNarrated" },
|
||||
{ FieldType.Bool, lb => lb.Book.IsAbridged.ToString(), nameof(Book.IsAbridged), "Abridged" },
|
||||
|
||||
@@ -32,12 +32,12 @@ namespace LibationUiBase.GridView
|
||||
|
||||
public bool Expanded
|
||||
{
|
||||
get => expanded;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (value != expanded)
|
||||
if (value != field)
|
||||
{
|
||||
expanded = value;
|
||||
field = value;
|
||||
Invalidate(nameof(Expanded), nameof(ButtonImage));
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,6 @@ namespace LibationUiBase.GridView
|
||||
|
||||
private DateTime lastBookUpdate;
|
||||
private LiberatedStatus bookStatus;
|
||||
private bool expanded;
|
||||
private readonly bool isAbsent;
|
||||
private static readonly Dictionary<string, object> iconCache = new();
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ public class GridContextMenu
|
||||
.UpdateUserDefinedItemAsync(udi =>
|
||||
{
|
||||
udi.BookStatus = LiberatedStatus.Liberated;
|
||||
if (udi.Book.HasPdf())
|
||||
if (udi.Book.HasPdf)
|
||||
udi.SetPdfStatus(LiberatedStatus.Liberated);
|
||||
});
|
||||
}
|
||||
@@ -71,7 +71,7 @@ public class GridContextMenu
|
||||
.UpdateUserDefinedItemAsync(udi =>
|
||||
{
|
||||
udi.BookStatus = LiberatedStatus.NotLiberated;
|
||||
if (udi.Book.HasPdf())
|
||||
if (udi.Book.HasPdf)
|
||||
udi.SetPdfStatus(LiberatedStatus.NotLiberated);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,44 +32,27 @@ namespace LibationUiBase.GridView
|
||||
#region Model properties exposed to the view
|
||||
|
||||
protected bool? remove = false;
|
||||
private EntryStatus _liberate;
|
||||
private string _purchasedate;
|
||||
private string _length;
|
||||
private LastDownloadStatus _lastDownload;
|
||||
private Lazy<object> _lazyCover;
|
||||
private string _series;
|
||||
private SeriesOrder _seriesOrder;
|
||||
private string _title;
|
||||
private string _authors;
|
||||
private string _narrators;
|
||||
private string _category;
|
||||
private string _misc;
|
||||
private string _description;
|
||||
private Rating _productrating;
|
||||
private string _bookTags;
|
||||
private Rating _myRating;
|
||||
private bool _isSpatial;
|
||||
private string _includedUntil;
|
||||
private string _account;
|
||||
public abstract bool? Remove { get; set; }
|
||||
public EntryStatus Liberate { get => _liberate; private set => RaiseAndSetIfChanged(ref _liberate, value); }
|
||||
public string PurchaseDate { get => _purchasedate; protected set => RaiseAndSetIfChanged(ref _purchasedate, value); }
|
||||
public string Length { get => _length; protected set => RaiseAndSetIfChanged(ref _length, value); }
|
||||
public LastDownloadStatus LastDownload { get => _lastDownload; protected set => RaiseAndSetIfChanged(ref _lastDownload, value); }
|
||||
public EntryStatus Liberate { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string PurchaseDate { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string Length { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public LastDownloadStatus LastDownload { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public object Cover { get => _lazyCover.Value; }
|
||||
public string Series { get => _series; private set => RaiseAndSetIfChanged(ref _series, value); }
|
||||
public SeriesOrder SeriesOrder { get => _seriesOrder; private set => RaiseAndSetIfChanged(ref _seriesOrder, value); }
|
||||
public string Title { get => _title; private set => RaiseAndSetIfChanged(ref _title, value); }
|
||||
public string Authors { get => _authors; private set => RaiseAndSetIfChanged(ref _authors, value); }
|
||||
public string Narrators { get => _narrators; private set => RaiseAndSetIfChanged(ref _narrators, value); }
|
||||
public string Category { get => _category; private set => RaiseAndSetIfChanged(ref _category, value); }
|
||||
public string Misc { get => _misc; private set => RaiseAndSetIfChanged(ref _misc, value); }
|
||||
public string Description { get => _description; private set => RaiseAndSetIfChanged(ref _description, value); }
|
||||
public Rating ProductRating { get => _productrating; private set => RaiseAndSetIfChanged(ref _productrating, value); }
|
||||
public string BookTags { get => _bookTags; private set => RaiseAndSetIfChanged(ref _bookTags, value); }
|
||||
public bool IsSpatial { get => _isSpatial; protected set => RaiseAndSetIfChanged(ref _isSpatial, value); }
|
||||
public string IncludedUntil { get => _includedUntil; protected set => RaiseAndSetIfChanged(ref _includedUntil, value); }
|
||||
public string Account { get => _account; protected set => RaiseAndSetIfChanged(ref _account, value); }
|
||||
public string Series { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public SeriesOrder SeriesOrder { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string Title { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string Authors { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string Narrators { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string Category { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string Misc { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string Description { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public Rating ProductRating { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string BookTags { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool IsSpatial { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string IncludedUntil { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string Account { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
public Rating MyRating
|
||||
{
|
||||
@@ -115,8 +98,8 @@ namespace LibationUiBase.GridView
|
||||
RaiseAndSetIfChanged(ref _myRating, Book.UserDefinedItem.Rating, nameof(MyRating));
|
||||
PurchaseDate = GetPurchaseDateString();
|
||||
ProductRating = Book.Rating ?? new Rating(0, 0, 0);
|
||||
Authors = Book.AuthorNames();
|
||||
Narrators = Book.NarratorNames();
|
||||
Authors = Book.AuthorNames;
|
||||
Narrators = Book.NarratorNames;
|
||||
Category = string.Join(", ", Book.LowestCategoryNames());
|
||||
Misc = GetMiscDisplay(libraryBook);
|
||||
LastDownload = new(Book.UserDefinedItem);
|
||||
@@ -323,7 +306,7 @@ namespace LibationUiBase.GridView
|
||||
|
||||
details.Add($"Account: {locale} - {acct}");
|
||||
|
||||
if (libraryBook.Book.HasPdf())
|
||||
if (libraryBook.Book.HasPdf)
|
||||
details.Add("Has PDF");
|
||||
if (libraryBook.Book.IsAbridged)
|
||||
details.Add("Abridged");
|
||||
|
||||
@@ -44,26 +44,16 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
{
|
||||
public LibraryBook LibraryBook { get; protected set; }
|
||||
|
||||
private ProcessBookResult _result = ProcessBookResult.None;
|
||||
private ProcessBookStatus _status = ProcessBookStatus.Queued;
|
||||
private string? _narrator;
|
||||
private string? _author;
|
||||
private string? _title;
|
||||
private int _progress;
|
||||
private string? _eta;
|
||||
private object? _cover;
|
||||
private TimeSpan _timeRemaining;
|
||||
|
||||
#region Properties exposed to the view
|
||||
public ProcessBookResult Result { get => _result; set { RaiseAndSetIfChanged(ref _result, value); RaisePropertyChanged(nameof(StatusText)); } }
|
||||
public ProcessBookStatus Status { get => _status; set { RaiseAndSetIfChanged(ref _status, value); RaisePropertyChanged(nameof(IsFinished)); RaisePropertyChanged(nameof(IsDownloading)); RaisePropertyChanged(nameof(Queued)); } }
|
||||
public string? Narrator { get => _narrator; set => RaiseAndSetIfChanged(ref _narrator, value); }
|
||||
public string? Author { get => _author; set => RaiseAndSetIfChanged(ref _author, value); }
|
||||
public string? Title { get => _title; set => RaiseAndSetIfChanged(ref _title, value); }
|
||||
public int Progress { get => _progress; protected set => RaiseAndSetIfChanged(ref _progress, value); }
|
||||
public TimeSpan TimeRemaining { get => _timeRemaining; set { RaiseAndSetIfChanged(ref _timeRemaining, value); ETA = $"ETA: {value:mm\\:ss}"; } }
|
||||
public string? ETA { get => _eta; private set => RaiseAndSetIfChanged(ref _eta, value); }
|
||||
public object? Cover { get => _cover; protected set => RaiseAndSetIfChanged(ref _cover, value); }
|
||||
public ProcessBookResult Result { get => field; set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(StatusText)); } }
|
||||
public ProcessBookStatus Status { get => field; set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(IsFinished)); RaisePropertyChanged(nameof(IsDownloading)); RaisePropertyChanged(nameof(Queued)); } }
|
||||
public string? Narrator { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? Author { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? Title { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public int Progress { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public TimeSpan TimeRemaining { get => field; set { RaiseAndSetIfChanged(ref field, value); ETA = $"ETA: {value:mm\\:ss}"; } }
|
||||
public string? ETA { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public object? Cover { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
|
||||
public bool IsDownloading => Status is ProcessBookStatus.Working;
|
||||
public bool Queued => Status is ProcessBookStatus.Queued;
|
||||
@@ -109,17 +99,16 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
{
|
||||
LibraryBook = libraryBook;
|
||||
|
||||
_title = LibraryBook.Book.TitleWithSubtitle;
|
||||
_author = LibraryBook.Book.AuthorNames();
|
||||
_narrator = LibraryBook.Book.NarratorNames();
|
||||
Title = LibraryBook.Book.TitleWithSubtitle;
|
||||
Author = LibraryBook.Book.AuthorNames;
|
||||
Narrator = LibraryBook.Book.NarratorNames;
|
||||
|
||||
(bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._80x80));
|
||||
|
||||
if (isDefault)
|
||||
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
||||
|
||||
// Mutable property. Set the field so PropertyChanged isn't fired.
|
||||
_cover = BaseUtil.LoadImage(picture, PictureSize._80x80);
|
||||
Cover = BaseUtil.LoadImage(picture, PictureSize._80x80);
|
||||
}
|
||||
|
||||
private void PictureStorage_PictureCached(object? sender, PictureCachedEventArgs e)
|
||||
@@ -307,8 +296,8 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
LogInfo($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
|
||||
|
||||
Title = libraryBook.Book.TitleWithSubtitle;
|
||||
Author = libraryBook.Book.AuthorNames();
|
||||
Narrator = libraryBook.Book.NarratorNames();
|
||||
Author = libraryBook.Book.AuthorNames;
|
||||
Narrator = libraryBook.Book.NarratorNames;
|
||||
}
|
||||
|
||||
private async void Processable_Completed(object? sender, LibraryBook libraryBook)
|
||||
@@ -386,8 +375,8 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
details = $"""
|
||||
Title: {libraryBook.Book.TitleWithSubtitle}
|
||||
ID: {libraryBook.Book.AudibleProductId}
|
||||
Author: {trunc(libraryBook.Book.AuthorNames())}
|
||||
Narr: {trunc(libraryBook.Book.NarratorNames())}
|
||||
Author: {trunc(libraryBook.Book.AuthorNames)}
|
||||
Narr: {trunc(libraryBook.Book.NarratorNames)}
|
||||
""";
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -30,23 +30,18 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
SpeedLimit = LibationFileManager.Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
||||
}
|
||||
|
||||
private int _completedCount;
|
||||
private int _errorCount;
|
||||
private int _queuedCount;
|
||||
private string? _runningTime;
|
||||
private bool _progressBarVisible;
|
||||
private decimal _speedLimit;
|
||||
|
||||
public int CompletedCount { get => _completedCount; private set { RaiseAndSetIfChanged(ref _completedCount, value); RaisePropertyChanged(nameof(AnyCompleted)); } }
|
||||
public int QueuedCount { get => _queuedCount; private set { this.RaiseAndSetIfChanged(ref _queuedCount, value); RaisePropertyChanged(nameof(AnyQueued)); } }
|
||||
public int ErrorCount { get => _errorCount; private set { RaiseAndSetIfChanged(ref _errorCount, value); RaisePropertyChanged(nameof(AnyErrors)); } }
|
||||
public string? RunningTime { get => _runningTime; set => RaiseAndSetIfChanged(ref _runningTime, value); }
|
||||
public bool ProgressBarVisible { get => _progressBarVisible; set => RaiseAndSetIfChanged(ref _progressBarVisible, value); }
|
||||
public int CompletedCount { get => field; private set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(AnyCompleted)); } }
|
||||
public int QueuedCount { get => field; private set { this.RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(AnyQueued)); } }
|
||||
public int ErrorCount { get => field; private set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(AnyErrors)); } }
|
||||
public string? RunningTime { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool ProgressBarVisible { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool AnyCompleted => CompletedCount > 0;
|
||||
public bool AnyQueued => QueuedCount > 0;
|
||||
public bool AnyErrors => ErrorCount > 0;
|
||||
public double Progress => 100d * Queue.Completed.Count / Queue.Count;
|
||||
public decimal SpeedLimitIncrement { get; private set; }
|
||||
|
||||
private decimal _speedLimit;
|
||||
public decimal SpeedLimit
|
||||
{
|
||||
get => _speedLimit;
|
||||
@@ -189,6 +184,26 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
else if (AudibleFileStorage.DownloadsInProgressDirectory is null)
|
||||
{
|
||||
Serilog.Log.Logger.Error("Failed to create DownloadsInProgressDirectory in {@InProgress}", Configuration.Instance.InProgress);
|
||||
MessageBoxBase.Show(
|
||||
$"Libation was unable to create the \"Downloads In Progress\" folder in:\n{Configuration.Instance.InProgress}\n\nPlease change the In Progress location in the settings menu.",
|
||||
"Failed to Create Downloads In Progress Directory",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
else if (AudibleFileStorage.DecryptInProgressDirectory is null)
|
||||
{
|
||||
Serilog.Log.Logger.Error("Failed to create DecryptInProgressDirectory in {@InProgress}", Configuration.Instance.InProgress);
|
||||
MessageBoxBase.Show(
|
||||
$"Libation was unable to create the \"Decrypt In Progress\" folder in:\n{Configuration.Instance.InProgress}\n\nPlease change the In Progress location in the settings menu.",
|
||||
"Failed to Create Decrypt In Progress Directory",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ namespace LibationWinForms
|
||||
{
|
||||
public class AccessibleDataGridViewButtonCell : DataGridViewButtonCell
|
||||
{
|
||||
private string accessibilityDescription;
|
||||
|
||||
protected string AccessibilityName { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -13,10 +11,10 @@ namespace LibationWinForms
|
||||
/// </summary>
|
||||
protected string AccessibilityDescription
|
||||
{
|
||||
get => accessibilityDescription;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
accessibilityDescription = value;
|
||||
field = value;
|
||||
ToolTipText = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ namespace LibationWinForms
|
||||
{
|
||||
internal class AccessibleDataGridViewTextBoxCell : DataGridViewTextBoxCell
|
||||
{
|
||||
private string accessibilityDescription;
|
||||
|
||||
protected string AccessibilityName { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -13,10 +11,10 @@ namespace LibationWinForms
|
||||
/// </summary>
|
||||
protected string AccessibilityDescription
|
||||
{
|
||||
get => accessibilityDescription;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
accessibilityDescription = value;
|
||||
field = value;
|
||||
ToolTipText = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,16 +45,16 @@ namespace LibationWinForms.Dialogs
|
||||
this.coverPb.Image = WinFormsUtil.TryLoadImageOrDefault(picture, PictureSize._80x80);
|
||||
|
||||
var title = string.IsNullOrEmpty(Book.Subtitle) ? Book.Title : $"{Book.Title}\r\n {Book.Subtitle}";
|
||||
var t = @$"
|
||||
Title: {title}
|
||||
Author(s): {Book.AuthorNames()}
|
||||
Narrator(s): {Book.NarratorNames()}
|
||||
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
|
||||
Category: {string.Join(", ", Book.LowestCategoryNames())}
|
||||
Purchase Date: {_libraryBook.DateAdded:d}
|
||||
Language: {Book.Language}
|
||||
Audible ID: {Book.AudibleProductId}
|
||||
".Trim();
|
||||
var t = $"""
|
||||
Title: {title}
|
||||
Author(s): {Book.AuthorNames}
|
||||
Narrator(s): {Book.NarratorNames}
|
||||
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
|
||||
Category: {string.Join(", ", Book.LowestCategoryNames())}
|
||||
Purchase Date: {_libraryBook.DateAdded:d}
|
||||
Language: {Book.Language}
|
||||
Audible ID: {Book.AudibleProductId}
|
||||
""";
|
||||
|
||||
var seriesNames = Book.SeriesNames();
|
||||
if (!string.IsNullOrWhiteSpace(seriesNames))
|
||||
|
||||
@@ -253,9 +253,8 @@ namespace LibationWinForms.Dialogs
|
||||
private class BookRecordEntry : LibationUiBase.ReactiveObject
|
||||
{
|
||||
private const string DateFormat = "yyyy-MM-dd HH\\:mm";
|
||||
private bool _ischecked;
|
||||
public IRecord Record { get; }
|
||||
public bool IsChecked { get => _ischecked; set => RaiseAndSetIfChanged(ref _ischecked, value); }
|
||||
public bool IsChecked { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string Type => Record.GetType().Name;
|
||||
public string Start => formatTimeSpan(Record.Start);
|
||||
public string Created => Record.Created.ToString(DateFormat);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Dinah.Core;
|
||||
using AudibleApi;
|
||||
using Dinah.Core;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Microsoft.Web.WebView2.WinForms;
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
@@ -19,15 +21,32 @@ namespace LibationWinForms.Login
|
||||
Controls.Add(webView);
|
||||
|
||||
webView.NavigationStarting += WebView_NavigationStarting;
|
||||
webView.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted;
|
||||
this.SetLibationIcon();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public WebLoginDialog(string accountID, string loginUrl) : this()
|
||||
public WebLoginDialog(string accountID, ChoiceIn choiceIn) : this()
|
||||
{
|
||||
this.accountID = ArgumentValidator.EnsureNotNullOrWhiteSpace(accountID, nameof(accountID));
|
||||
webView.Source = new Uri(ArgumentValidator.EnsureNotNullOrWhiteSpace(loginUrl, nameof(loginUrl)));
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(choiceIn?.LoginUrl, nameof(choiceIn));
|
||||
this.Load += async (_, _) =>
|
||||
{
|
||||
//enable private browsing
|
||||
var env = await CoreWebView2Environment.CreateAsync();
|
||||
var options = env.CreateCoreWebView2ControllerOptions();
|
||||
options.IsInPrivateModeEnabled = true;
|
||||
await webView.EnsureCoreWebView2Async(env, options);
|
||||
|
||||
webView.CoreWebView2.Settings.UserAgent = Resources.User_Agent;
|
||||
|
||||
//Load init cookies
|
||||
foreach (System.Net.Cookie cookie in choiceIn.SignInCookies ?? [])
|
||||
{
|
||||
webView.CoreWebView2.CookieManager.AddOrUpdateCookie(webView.CoreWebView2.CookieManager.CreateCookieWithSystemNetCookie(cookie));
|
||||
}
|
||||
|
||||
webView.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
|
||||
Invoke(() => webView.Source = new Uri(choiceIn.LoginUrl));
|
||||
};
|
||||
}
|
||||
|
||||
private void WebView_NavigationStarting(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs e)
|
||||
@@ -40,13 +59,7 @@ namespace LibationWinForms.Login
|
||||
}
|
||||
}
|
||||
|
||||
private void WebView_CoreWebView2InitializationCompleted(object sender, Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs e)
|
||||
{
|
||||
webView.CoreWebView2.DOMContentLoaded -= CoreWebView2_DOMContentLoaded;
|
||||
webView.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
|
||||
}
|
||||
|
||||
private async void CoreWebView2_DOMContentLoaded(object sender, Microsoft.Web.WebView2.Core.CoreWebView2DOMContentLoadedEventArgs e)
|
||||
private async void CoreWebView2_DOMContentLoaded(object sender, CoreWebView2DOMContentLoadedEventArgs e)
|
||||
{
|
||||
await webView.ExecuteScriptAsync(getScript(accountID));
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace LibationWinForms.Login
|
||||
{
|
||||
try
|
||||
{
|
||||
using var weblogin = new WebLoginDialog(_account.AccountId, choiceIn.LoginUrl);
|
||||
using var weblogin = new WebLoginDialog(_account.AccountId, choiceIn);
|
||||
if (ShowDialog(weblogin))
|
||||
return Task.FromResult(ChoiceOut.External(weblogin.ResponseUrl));
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace LibationWinForms
|
||||
{
|
||||
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(libraryBooks))
|
||||
SetQueueCollapseState(false);
|
||||
else if (libraryBooks.Length == 1 && libraryBooks[0].Book.Audio_Exists())
|
||||
else if (libraryBooks.Length == 1 && libraryBooks[0].Book.AudioExists)
|
||||
{
|
||||
// liberated: open explorer to file
|
||||
var filePath = AudibleFileStorage.Audio.GetPath(libraryBooks[0].Book.AudibleProductId);
|
||||
|
||||
@@ -7,18 +7,16 @@ namespace LibationWinForms.GridView
|
||||
{
|
||||
public partial class DescriptionDisplay : Form
|
||||
{
|
||||
private int borderThickness = 5;
|
||||
|
||||
public int BorderThickness
|
||||
{
|
||||
get => borderThickness;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
borderThickness = value;
|
||||
textBox1.Location = new Point(borderThickness, borderThickness);
|
||||
textBox1.Size = new Size(Width - 2 * borderThickness, Height - 2 * borderThickness);
|
||||
field = value;
|
||||
textBox1.Location = new Point(field, field);
|
||||
textBox1.Size = new Size(Width - 2 * field, Height - 2 * field);
|
||||
}
|
||||
}
|
||||
} = 5;
|
||||
public string DescriptionText { get => textBox1.Text; set => textBox1.Text = value; }
|
||||
public Point SpawnLocation { get; set; }
|
||||
public DescriptionDisplay()
|
||||
|
||||
@@ -13,24 +13,23 @@ namespace LibationWinForms.GridView
|
||||
private const string SOLID_STAR = "★";
|
||||
private const string HOLLOW_STAR = "☆";
|
||||
|
||||
private Rating _rating;
|
||||
public Rating Rating
|
||||
{
|
||||
get => _rating;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
_rating = value;
|
||||
field = value;
|
||||
int rating = 0;
|
||||
foreach (NoBorderLabel star in panelOverall.Controls)
|
||||
star.Tag = star.Text = _rating.OverallRating > rating++ ? SOLID_STAR : HOLLOW_STAR;
|
||||
star.Tag = star.Text = field.OverallRating > rating++ ? SOLID_STAR : HOLLOW_STAR;
|
||||
|
||||
rating = 0;
|
||||
foreach (NoBorderLabel star in panelPerform.Controls)
|
||||
star.Tag = star.Text = _rating.PerformanceRating > rating++ ? SOLID_STAR : HOLLOW_STAR;
|
||||
star.Tag = star.Text = field.PerformanceRating > rating++ ? SOLID_STAR : HOLLOW_STAR;
|
||||
|
||||
rating = 0;
|
||||
foreach (NoBorderLabel star in panelStory.Controls)
|
||||
star.Tag = star.Text = _rating.StoryRating > rating++ ? SOLID_STAR : HOLLOW_STAR;
|
||||
star.Tag = star.Text = field.StoryRating > rating++ ? SOLID_STAR : HOLLOW_STAR;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ namespace LibationWinForms.GridView
|
||||
{
|
||||
//No need to persist these changes. They only needs to last long for the files to start downloading
|
||||
entry4.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
|
||||
if (entry4.Book.HasPdf())
|
||||
if (entry4.Book.HasPdf)
|
||||
entry4.Book.UserDefinedItem.SetPdfStatus(LiberatedStatus.NotLiberated);
|
||||
|
||||
LiberateClicked?.Invoke(s, [entry4.LibraryBook]);
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
|
||||
|
||||
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
|
||||
<!-- Needed for RTM build of Npgsql.EntityFrameworkCore.PostgreSQL -->
|
||||
<add key="myget" value="https://www.myget.org/F/npgsql-vnext/api/v3/index.json " />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user