Compare commits

...

33 Commits

Author SHA1 Message Date
Robert McRackan
ce624399ba incr ver 2023-07-19 07:48:11 -04:00
rmcrackan
63e9700c4a Merge pull request #682 from Mbucari/master
Fix #680 and Add category ladders
2023-07-19 07:46:05 -04:00
Mbucari
914e574bf8 Improve GridView
- Remove LongDescription
- Description has the full description
- Better MyRating updating
2023-07-18 16:18:01 -06:00
Mbucari
b94f9bbc15 Fix grid update bug 2023-07-18 16:11:22 -06:00
Mbucari
4e34834c35 Fix category search indexing 2023-07-18 16:00:06 -06:00
Mbucari
3211b2dc85 Improved Category Ladders 2023-07-18 15:51:02 -06:00
Mbucari
ea6adeb58f Add category ladders 2023-07-17 16:50:45 -06:00
Mbucari
90eccbf2f6 Fix FilePathCache NRE (#680) 2023-07-17 08:55:55 -06:00
rmcrackan
668cd7dba8 Merge pull request #679 from Mbucari/master
Mp3 embedded cuesheet and raw metadata
2023-07-16 14:32:49 -04:00
MBucari
c08b2b575c UI Tweak 2023-07-16 10:57:17 -06:00
MBucari
07eaa48e10 Save raw json metadata 2023-07-16 10:54:05 -06:00
MBucari
3cf5fc1d99 Add mp3 embedded cuesheet (#677) 2023-07-15 10:44:31 -06:00
Robert McRackan
15ad753fa1 update dependencies 2023-07-14 20:58:26 -04:00
rmcrackan
75b984bdb2 Merge pull request #678 from Mbucari/master
Fix quick filter not being applied on startup
2023-07-14 20:53:57 -04:00
Mbucari
f586d1d59f Fix quick filter not being applied on startup 2023-07-13 11:00:05 -06:00
Mbucari
cb91a591f0 inc ver 2023-07-13 09:58:45 -06:00
Mbucari
0c0c556c6a Merge pull request #674 from Mbucari/master
Fix #673
2023-07-13 09:31:28 -06:00
MBucari
ff63b73c09 Fix #673 2023-07-13 09:30:02 -06:00
Mbucari
c1d56adbd2 Add groupbox title 2023-07-12 21:29:00 -06:00
rmcrackan
bcd99fd208 Merge pull request #670 from Mbucari/master
Add products grid scaling setting
2023-07-12 21:10:51 -04:00
Mbucari
d1df10d060 Add products grid scaling setting
- Add Grid Scaling Settings
- Add WinForms DPI migration to remove stored form sizes
- Add textbox clear button
2023-07-12 15:32:37 -06:00
Mbucari
1fa415628f Update ProductsGrid.cs 2023-07-10 11:39:33 -06:00
rmcrackan
a83fe9e532 Merge pull request #667 from Mbucari/master
Fix setting Panel2MinSize min width bug (#666)
2023-07-10 11:19:45 -04:00
Mbucari
f85462ffec Fix setting Panel2MinSize min width bug (#666) 2023-07-10 09:11:38 -06:00
Robert McRackan
156349c293 incr ver 2023-07-10 09:26:26 -04:00
rmcrackan
5976706e40 Merge pull request #664 from Mbucari/startup-2
New settings, context menu, and performance improvements
2023-07-10 09:25:13 -04:00
Mbucari
1e40180f0c Fix unit test 2023-07-09 16:42:08 -06:00
Mbucari
7d09728e6b Add Re-download context menu item 2023-07-09 16:26:58 -06:00
Mbucari
4899ef3007 Add new settings and settings dialog help tips
Add CombineNestedChapterTitles setting (#663)
Add SaveMetadataToFile setting
Add extended setting descriptions for select options
2023-07-09 16:07:13 -06:00
Mbucari
296c2b43eb Remove extra library load and move comments to Main 2023-07-09 10:10:00 -06:00
Mbucari
932472cb91 Add full context menu to call columns 2023-07-09 09:53:28 -06:00
Mbucari
1bf86b05ec Download high quality cover art 2023-07-09 09:35:40 -06:00
Mbucari
5d5e3a6671 improve startup time 2023-07-09 09:23:58 -06:00
98 changed files with 2933 additions and 1263 deletions

View File

@@ -81,14 +81,7 @@ namespace AaxDecrypter
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PART", part.ToString());
}
//Finishing configuring lame encoder.
if (DownloadOptions.OutputFormat == OutputFormat.Mp3)
MpegUtil.ConfigureLameOptions(
AaxFile,
DownloadOptions.LameConfig,
DownloadOptions.Downsample,
DownloadOptions.MatchSourceBitrate);
OnInitialized();
OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged);
OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor ?? "[unknown]");
OnRetrievedNarrators(AaxFile.AppleTags.Narrator ?? "[unknown]");
@@ -98,5 +91,7 @@ namespace AaxDecrypter
return !IsCanceled;
}
protected virtual void OnInitialized() { }
}
}

View File

@@ -21,6 +21,18 @@ namespace AaxDecrypter
AsyncSteps["Step 3: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync;
}
protected override void OnInitialized()
{
//Finishing configuring lame encoder.
if (DownloadOptions.OutputFormat == OutputFormat.Mp3)
MpegUtil.ConfigureLameOptions(
AaxFile,
DownloadOptions.LameConfig,
DownloadOptions.Downsample,
DownloadOptions.MatchSourceBitrate,
chapters: null);
}
/*
https://github.com/rmcrackan/Libation/pull/127#issuecomment-939088489

View File

@@ -3,7 +3,6 @@ using AAXClean.Codecs;
using Dinah.Core.Net.Http;
using FileManager;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
@@ -26,6 +25,18 @@ namespace AaxDecrypter
AsyncSteps[$"Step {step++}: Create Cue"] = Step_CreateCueAsync;
}
protected override void OnInitialized()
{
//Finishing configuring lame encoder.
if (DownloadOptions.OutputFormat == OutputFormat.Mp3)
MpegUtil.ConfigureLameOptions(
AaxFile,
DownloadOptions.LameConfig,
DownloadOptions.Downsample,
DownloadOptions.MatchSourceBitrate,
DownloadOptions.ChapterInfo);
}
protected async override Task<bool> Step_DownloadAndDecryptAudiobookAsync()
{
FileUtility.SaferDelete(OutputFileName);

View File

@@ -118,11 +118,7 @@ namespace AaxDecrypter
public abstract Task CancelAsync();
protected abstract Task<bool> Step_DownloadAndDecryptAudiobookAsync();
public virtual void SetCoverArt(byte[] coverArt)
{
if (coverArt is not null)
OnRetrievedCoverArt(coverArt);
}
public virtual void SetCoverArt(byte[] coverArt) { }
protected void OnRetrievedTitle(string title)
=> RetrievedTitle?.Invoke(this, title);

View File

@@ -8,7 +8,12 @@ namespace AaxDecrypter
public static class MpegUtil
{
private const string TagDomain = "com.pilabor.tone";
public static void ConfigureLameOptions(Mp4File mp4File, LameConfig lameConfig, bool downsample, bool matchSourceBitrate)
public static void ConfigureLameOptions(
Mp4File mp4File,
LameConfig lameConfig,
bool downsample,
bool matchSourceBitrate,
ChapterInfo chapters)
{
double bitrateMultiple = 1;
@@ -53,6 +58,12 @@ namespace AaxDecrypter
if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "PART") is string part)
lameConfig.ID3.UserDefinedText.Add("PART", part);
if (chapters?.Count > 0)
{
var cue = Cue.CreateContents(lameConfig.ID3.Title + ".mp3", chapters);
lameConfig.ID3.UserDefinedText.Add("CUESHEET", cue);
}
}
}
}

View File

@@ -127,6 +127,8 @@ namespace AaxDecrypter
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile));
if (Path.GetFileName(uriToSameFile.LocalPath) != Path.GetFileName(Uri.LocalPath))
throw new ArgumentException($"New uri to the same file must have the same file name.");
if (uriToSameFile.Host != Uri.Host)
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
if (DownloadTask is not null)

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>10.5.2.1</Version>
<Version>10.6.3.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Octokit" Version="6.0.0" />

View File

@@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="NPOI" Version="2.6.0" />
<PackageReference Include="NPOI" Version="2.6.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -141,7 +141,7 @@ namespace ApplicationServices
PictureId = a.Book.PictureId,
IsAbridged = a.Book.IsAbridged,
DatePublished = a.Book.DatePublished,
CategoriesNames = a.Book.CategoriesNames().Any() ? a.Book.CategoriesNames().Aggregate((a, b) => $"{a}, {b}") : "",
CategoriesNames = a.Book.LowestCategoryNames().Any() ? a.Book.LowestCategoryNames().Aggregate((a, b) => $"{a}, {b}") : "",
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="8.4.1.1" />
<PackageReference Include="AudibleApi" Version="8.4.2.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore;
namespace DataLayer.Configurations
{
internal class BookCategoryConfig : IEntityTypeConfiguration<BookCategory>
{
public void Configure(EntityTypeBuilder<BookCategory> entity)
{
entity.HasKey(bc => new { bc.BookId, bc.CategoryLadderId });
entity.HasIndex(bc => bc.BookId);
entity.HasIndex(bc => bc.CategoryLadderId);
entity
.HasOne(bc => bc.Book)
.WithMany(b => b.CategoriesLink)
.HasForeignKey(bc => bc.BookId);
entity
.HasOne(bc => bc.CategoryLadder)
.WithMany(c => c.BooksLink)
.HasForeignKey(bc => bc.CategoryLadderId);
}
}
}

View File

@@ -21,10 +21,7 @@ namespace DataLayer.Configurations
entity.Ignore(nameof(Book.Narrators));
entity.Ignore(nameof(Book.AudioFormat));
entity.Ignore(nameof(Book.TitleWithSubtitle));
//// these don't seem to matter
//entity.Ignore(nameof(Book.AuthorNames));
//entity.Ignore(nameof(Book.NarratorNames));
//entity.Ignore(nameof(Book.HasPdfs));
entity.Ignore(b => b.Categories);
// OwnsMany: "Can only ever appear on navigation properties of other entity types.
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner."
@@ -58,23 +55,6 @@ namespace DataLayer.Configurations
// owns it 1:1, store in same table
b_udi.OwnsOne(udi => udi.Rating);
});
entity
.Metadata
.FindNavigation(nameof(Book.ContributorsLink))
// PropertyAccessMode.Field : Contributions is a get-only property, not a field, so use its backing field
.SetPropertyAccessMode(PropertyAccessMode.Field);
entity
.Metadata
.FindNavigation(nameof(Book.SeriesLink))
// PropertyAccessMode.Field : Series is a get-only property, not a field, so use its backing field
.SetPropertyAccessMode(PropertyAccessMode.Field);
entity
.HasOne(b => b.Category)
.WithMany()
.HasForeignKey(b => b.CategoryId);
}
}
}

View File

@@ -10,8 +10,11 @@ namespace DataLayer.Configurations
entity.HasKey(c => c.CategoryId);
entity.HasIndex(c => c.AudibleCategoryId);
// seeds go here. examples in Dinah.EntityFrameworkCore.Tests\DbContextFactoryExample.cs
entity.HasData(Category.GetEmpty());
entity.Ignore(c => c.CategoryLadders);
entity
.HasMany(e => e._categoryLadders)
.WithMany(e => e._categories);
}
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore;
namespace DataLayer.Configurations
{
internal class CategoryLadderConfig : IEntityTypeConfiguration<CategoryLadder>
{
public void Configure(EntityTypeBuilder<CategoryLadder> entity)
{
entity.HasKey(cl => cl.CategoryLadderId);
entity.Ignore(cl => cl.Categories);
entity
.HasMany(cl => cl._categories)
.WithMany(c => c._categoryLadders);
entity
.Metadata
.FindNavigation(nameof(CategoryLadder.BooksLink))
.SetPropertyAccessMode(PropertyAccessMode.Field);
}
}
}

View File

@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="7.2.2.1" />
<PackageReference Include="Dinah.Core" Version="7.2.3.1" />
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.1.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
<PrivateAssets>all</PrivateAssets>

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Dinah.Core;
using Microsoft.EntityFrameworkCore;
@@ -37,7 +38,7 @@ namespace DataLayer
public string Subtitle { get; private set; }
private string _titleWithSubtitle;
public string TitleWithSubtitle => _titleWithSubtitle ??= string.IsNullOrEmpty(Subtitle) ? Title : $"{Title}: {Subtitle}";
public string Description { get; private set; }
public string Description { get; private set; }
public int LengthInMinutes { get; private set; }
public ContentType ContentType { get; private set; }
public string Locale { get; private set; }
@@ -55,10 +56,6 @@ namespace DataLayer
public DateTime? DatePublished { get; private set; }
public string Language { get; private set; }
// non-null. use "empty pattern"
internal int CategoryId { get; private set; }
public Category Category { get; private set; }
// is owned, not optional 1:1
public UserDefinedItem UserDefinedItem { get; private set; }
@@ -77,9 +74,8 @@ namespace DataLayer
string description,
int lengthInMinutes,
ContentType contentType,
IEnumerable<Contributor> authors,
IEnumerable<Contributor> narrators,
Category category,
IEnumerable<Contributor> authors,
IEnumerable<Contributor> narrators,
string localeName)
{
// validate
@@ -87,7 +83,7 @@ namespace DataLayer
var productId = audibleProductId.Id;
ArgumentValidator.EnsureNotNullOrWhiteSpace(productId, nameof(productId));
// assign as soon as possible. stuff below relies on this
// assign as soon as possible. stuff below relies on this
AudibleProductId = productId;
Locale = localeName;
@@ -95,43 +91,34 @@ namespace DataLayer
// non-ef-ctor init.s
UserDefinedItem = new UserDefinedItem(this);
_contributorsLink = new HashSet<BookContributor>();
ContributorsLink = new HashSet<BookContributor>();
CategoriesLink = new HashSet<BookCategory>();
_seriesLink = new HashSet<SeriesBook>();
_supplements = new HashSet<Supplement>();
Category = category;
// simple assigns
UpdateTitle(title, subtitle);
Description = description?.Trim() ?? "";
Description = description?.Trim() ?? "";
LengthInMinutes = lengthInMinutes;
ContentType = contentType;
// assigns with biz logic
ReplaceAuthors(authors);
ReplaceNarrators(narrators);
}
public void UpdateTitle(string title, string subtitle)
}
public void UpdateTitle(string title, string subtitle)
{
Title = title?.Trim() ?? "";
Subtitle = subtitle?.Trim() ?? "";
_titleWithSubtitle = null;
}
}
#region contributors, authors, narrators
// use uninitialised backing fields - this means we can detect if the collection was loaded
private HashSet<BookContributor> _contributorsLink;
// i'd like this to be internal but migration throws this exception when i try:
// Value cannot be null.
// Parameter name: property
public IEnumerable<BookContributor> ContributorsLink
=> _contributorsLink?
.OrderBy(bc => bc.Order)
.ToList();
#region contributors, authors, narrators
internal HashSet<BookContributor> ContributorsLink { get; private set; }
public IEnumerable<Contributor> Authors => getContributions(Role.Author).Select(bc => bc.Contributor).ToList();
public IEnumerable<Contributor> Narrators => getContributions(Role.Narrator).Select(bc => bc.Contributor).ToList();
public string Publisher => getContributions(Role.Publisher).SingleOrDefault()?.Contributor.Name;
public IEnumerable<Contributor> Authors => ContributorsLink.ByRole(Role.Author).Select(bc => bc.Contributor).ToList();
public IEnumerable<Contributor> Narrators => ContributorsLink.ByRole(Role.Narrator).Select(bc => bc.Contributor).ToList();
public string Publisher => ContributorsLink.ByRole(Role.Publisher).SingleOrDefault()?.Contributor.Name;
public void ReplaceAuthors(IEnumerable<Contributor> authors, DbContext context = null)
=> replaceContributors(authors, Role.Author, context);
@@ -144,47 +131,70 @@ namespace DataLayer
ArgumentValidator.EnsureEnumerableNotNullOrEmpty(newContributors, nameof(newContributors));
// the edge cases of doing local-loaded vs remote-only got weird. just load it
if (_contributorsLink is null)
getEntry(context).Collection(s => s.ContributorsLink).Load();
if (ContributorsLink is null)
getEntry(context).Collection(s => s.ContributorsLink).Load();
var isIdentical
= ContributorsLink
.ByRole(role)
.Select(c => c.Contributor)
.SequenceEqual(newContributors);
var roleContributions = getContributions(role);
var isIdentical = roleContributions.Select(c => c.Contributor).SequenceEqual(newContributors);
if (isIdentical)
return;
_contributorsLink.RemoveWhere(bc => bc.Role == role);
ContributorsLink.RemoveWhere(bc => bc.Role == role);
addNewContributors(newContributors, role);
}
private void addNewContributors(IEnumerable<Contributor> newContributors, Role role)
private void addNewContributors(IEnumerable<Contributor> newContributors, Role role)
{
byte order = 0;
var newContributionsEnum = newContributors.Select(c => new BookContributor(this, c, role, order++));
var newContributions = new HashSet<BookContributor>(newContributionsEnum);
_contributorsLink.UnionWith(newContributions);
ContributorsLink.UnionWith(newContributions);
}
private List<BookContributor> getContributions(Role role)
=> ContributorsLink
.Where(a => a.Role == role)
.OrderBy(a => a.Order)
.ToList();
#endregion
private Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<Book> getEntry(DbContext context)
private Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<Book> getEntry(DbContext context)
{
ArgumentValidator.EnsureNotNull(context, nameof(context));
var entry = context.Entry(this);
if (!entry.IsKeySet)
throw new InvalidOperationException("Could not load a valid Book from database");
return entry;
}
#region categories
internal HashSet<BookCategory> CategoriesLink { get; private set; }
private ReadOnlyCollection<BookCategory> _categoriesReadOnly;
public ReadOnlyCollection<BookCategory> Categories
{
get
{
if (_categoriesReadOnly?.SequenceEqual(CategoriesLink) is not true)
_categoriesReadOnly = CategoriesLink.ToList().AsReadOnly();
return _categoriesReadOnly;
}
}
public void SetCategoryLadders(IEnumerable<CategoryLadder> ladders)
{
ArgumentValidator.EnsureNotNull(context, nameof(context));
ArgumentValidator.EnsureNotNull(ladders, nameof(ladders));
var entry = context.Entry(this);
if (!entry.IsKeySet)
throw new InvalidOperationException("Could not load a valid Book from database");
return entry;
//Replace all existing category ladders.
//Some books make have duplocate ladders
CategoriesLink.Clear();
CategoriesLink.UnionWith(ladders.Distinct().Select(l => new BookCategory(this, l)));
}
#endregion
#region series
private HashSet<SeriesBook> _seriesLink;
#region series
private HashSet<SeriesBook> _seriesLink;
public IEnumerable<SeriesBook> SeriesLink => _seriesLink?.ToList();
public void UpsertSeries(Series series, string order, DbContext context = null)
@@ -234,15 +244,6 @@ namespace DataLayer
Language = language?.FirstCharToUpper() ?? Language;
}
public void UpdateCategory(Category category, DbContext context = null)
{
// since category is never null, nullity means it hasn't been loaded
if (Category is null)
getEntry(context).Reference(s => s.Category).Load();
Category = category;
}
public override string ToString() => $"[{AudibleProductId}] {TitleWithSubtitle}";
}
}

View File

@@ -0,0 +1,20 @@
using Dinah.Core;
namespace DataLayer
{
public class BookCategory
{
internal int BookId { get; private set; }
internal int CategoryLadderId { get; private set; }
public Book Book { get; private set; }
public CategoryLadder CategoryLadder { get; private set; }
private BookCategory() { }
internal BookCategory(Book book, CategoryLadder categoriesList)
{
Book = ArgumentValidator.EnsureNotNull(book, nameof(book));
CategoryLadder = ArgumentValidator.EnsureNotNull(categoriesList, nameof(categoriesList));
}
}
}

View File

@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Dinah.Core;
using Microsoft.EntityFrameworkCore;
#nullable enable
namespace DataLayer
{
public class AudibleCategoryId
@@ -15,20 +15,29 @@ namespace DataLayer
Id = id;
}
}
public class Category
{
// Empty is a special case. use private ctor w/o validation
public static Category GetEmpty() => new() { CategoryId = -1, AudibleCategoryId = "", Name = "" };
internal int CategoryId { get; private set; }
public string AudibleCategoryId { get; private set; }
public string? AudibleCategoryId { get; private set; }
public string Name { get; private set; }
public Category ParentCategory { get; private set; }
public string? Name { get; internal set; }
private Category() { }
internal List<CategoryLadder> _categoryLadders = new();
private ReadOnlyCollection<CategoryLadder>? _categoryLaddersReadOnly;
public ReadOnlyCollection<CategoryLadder> CategoryLadders
{
get
{
if (_categoryLaddersReadOnly?.SequenceEqual(_categoryLadders) is not true)
_categoryLaddersReadOnly = _categoryLadders.AsReadOnly();
return _categoryLaddersReadOnly;
}
}
private Category() { }
/// <summary>special id class b/c it's too easy to get string order mixed up</summary>
public Category(AudibleCategoryId audibleSeriesId, string name, Category parentCategory = null)
public Category(AudibleCategoryId audibleSeriesId, string name)
{
ArgumentValidator.EnsureNotNull(audibleSeriesId, nameof(audibleSeriesId));
var id = audibleSeriesId.Id;
@@ -37,15 +46,6 @@ namespace DataLayer
AudibleCategoryId = id;
Name = name;
UpdateParentCategory(parentCategory);
}
public void UpdateParentCategory(Category parentCategory)
{
// don't overwrite with null but not an error
if (parentCategory is not null)
ParentCategory = parentCategory;
}
public override string ToString() => $"[{AudibleCategoryId}] {Name}";

View File

@@ -0,0 +1,58 @@
using Dinah.Core;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
#nullable enable
namespace DataLayer
{
public class CategoryLadder : IEquatable<CategoryLadder>
{
internal int CategoryLadderId { get; private set; }
internal List<Category> _categories;
private ReadOnlyCollection<Category>? _categoriesReadOnly;
public ReadOnlyCollection<Category> Categories
{
get
{
if (_categoriesReadOnly?.SequenceEqual(_categories) is not true)
_categoriesReadOnly = _categories.AsReadOnly();
return _categoriesReadOnly;
}
}
private HashSet<BookCategory>? _booksLink;
public IEnumerable<BookCategory>? BooksLink => _booksLink?.ToList();
private CategoryLadder() { _categories = new(); }
public CategoryLadder(List<Category> categories)
{
ArgumentValidator.EnsureNotNull(categories, nameof(categories));
ArgumentValidator.EnsureGreaterThan(categories.Count, nameof(categories), 0);
_booksLink = new HashSet<BookCategory>();
_categories = categories;
}
public override int GetHashCode()
{
HashCode hashCode = default;
foreach (var category in _categories)
hashCode.Add(category.AudibleCategoryId);
return hashCode.ToHashCode();
}
public bool Equals(CategoryLadder? other)
=> other?._categories is not null
&& Equals(other._categories.Select(c => c.AudibleCategoryId));
public bool Equals(IEnumerable<string?>? categoryIds)
=> categoryIds is not null
&& _categories.Select(c => c.AudibleCategoryId).SequenceEqual(categoryIds);
public override bool Equals(object? obj)
=> obj is CategoryLadder other && Equals(other);
public override string ToString() => string.Join(" > ", _categories.Select(c => c.Name));
}
}

View File

@@ -7,8 +7,13 @@ using System.Threading.Tasks;
namespace DataLayer
{
public static class EntityExtensions
{
public static string TitleSortable(this Book book) => Formatters.GetSortName(book.Title + book.Subtitle);
{
public static IEnumerable<BookContributor> ByRole(this IEnumerable<BookContributor> contributors, Role role)
=> contributors
.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));
@@ -46,14 +51,31 @@ namespace DataLayer
? $"{sb.Series.Name} (#{sb.Order})"
: sb.Series.Name;
}
public static string[] CategoriesNames(this Book book)
=> book.Category is null ? new string[0]
: book.Category.ParentCategory is null ? new[] { book.Category.Name }
: new[] { book.Category.ParentCategory.Name, book.Category.Name };
public static string[] CategoriesIds(this Book book)
=> book.Category is null ? null
: book.Category.ParentCategory is null ? new[] { book.Category.AudibleCategoryId }
: new[] { book.Category.ParentCategory.AudibleCategoryId, book.Category.AudibleCategoryId };
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)
{

View File

@@ -23,6 +23,7 @@ namespace DataLayer
public DbSet<Contributor> Contributors { get; private set; }
public DbSet<Series> Series { get; private set; }
public DbSet<Category> Categories { get; private set; }
public DbSet<CategoryLadder> CategoryLadders { get; private set; }
public static LibationContext Create(string connectionString)
{
@@ -39,13 +40,15 @@ namespace DataLayer
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new BookConfig());
modelBuilder.ApplyConfiguration(new BookConfig());
modelBuilder.ApplyConfiguration(new ContributorConfig());
modelBuilder.ApplyConfiguration(new BookContributorConfig());
modelBuilder.ApplyConfiguration(new LibraryBookConfig());
modelBuilder.ApplyConfiguration(new SeriesConfig());
modelBuilder.ApplyConfiguration(new SeriesBookConfig());
modelBuilder.ApplyConfiguration(new CategoryConfig());
modelBuilder.ApplyConfiguration(new CategoryLadderConfig());
modelBuilder.ApplyConfiguration(new BookCategoryConfig());
// views are now supported via "keyless entity types" (instead of "entity types" or the prev "query types"):
// https://docs.microsoft.com/en-us/ef/core/modeling/keyless-entity-types

View File

@@ -0,0 +1,465 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20230718214617_AddCategoryLadder")]
partial class AddCategoryLadder
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
modelBuilder.Entity("CategoryCategoryLadder", b =>
{
b.Property<int>("_categoriesCategoryId")
.HasColumnType("INTEGER");
b.Property<int>("_categoryLaddersCategoryLadderId")
.HasColumnType("INTEGER");
b.HasKey("_categoriesCategoryId", "_categoryLaddersCategoryLadderId");
b.HasIndex("_categoryLaddersCategoryLadderId");
b.ToTable("CategoryCategoryLadder");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("TEXT");
b.Property<int>("ContentType")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("INTEGER");
b.Property<string>("Language")
.HasColumnType("TEXT");
b.Property<int>("LengthInMinutes")
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.HasColumnType("TEXT");
b.Property<string>("PictureId")
.HasColumnType("TEXT");
b.Property<string>("PictureLarge")
.HasColumnType("TEXT");
b.Property<string>("Subtitle")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<long>("_audioFormat")
.HasColumnType("INTEGER");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookCategory", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("CategoryLadderId")
.HasColumnType("INTEGER");
b.HasKey("BookId", "CategoryLadderId");
b.HasIndex("BookId");
b.HasIndex("CategoryLadderId");
b.ToTable("BookCategory");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.ToTable("Categories");
});
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
{
b.Property<int>("CategoryLadderId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.HasKey("CategoryLadderId");
b.ToTable("CategoryLadders");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
b.HasData(
new
{
ContributorId = -1,
Name = ""
});
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<bool>("AbsentFromLastScan")
.HasColumnType("INTEGER");
b.Property<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.HasKey("BookId");
b.ToTable("LibraryBooks");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<string>("Order")
.HasColumnType("TEXT");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("CategoryCategoryLadder", b =>
{
b.HasOne("DataLayer.Category", null)
.WithMany()
.HasForeignKey("_categoriesCategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.CategoryLadder", null)
.WithMany()
.HasForeignKey("_categoryLaddersCategoryLadderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("REAL");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.WithOwner()
.HasForeignKey("BookId");
});
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("Url")
.HasColumnType("TEXT");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.Navigation("Book");
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<int>("BookStatus")
.HasColumnType("INTEGER");
b1.Property<DateTime?>("LastDownloaded")
.HasColumnType("TEXT");
b1.Property<string>("LastDownloadedVersion")
.HasColumnType("TEXT");
b1.Property<int?>("PdfStatus")
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("TEXT");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem", (string)null);
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b2.Property<float>("StoryRating")
.HasColumnType("REAL");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.WithOwner()
.HasForeignKey("UserDefinedItemBookId");
});
b1.Navigation("Book");
b1.Navigation("Rating");
});
b.Navigation("Rating");
b.Navigation("Supplements");
b.Navigation("UserDefinedItem");
});
modelBuilder.Entity("DataLayer.BookCategory", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("CategoriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.CategoryLadder", "CategoryLadder")
.WithMany("BooksLink")
.HasForeignKey("CategoryLadderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("CategoryLadder");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Contributor");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Series");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Navigation("CategoriesLink");
b.Navigation("ContributorsLink");
b.Navigation("SeriesLink");
});
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
{
b.Navigation("BooksLink");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Navigation("BooksLink");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Navigation("BooksLink");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,174 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DataLayer.Migrations
{
/// <inheritdoc />
public partial class AddCategoryLadder : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Books_Categories_CategoryId",
table: "Books");
migrationBuilder.DropForeignKey(
name: "FK_Categories_Categories_ParentCategoryCategoryId",
table: "Categories");
migrationBuilder.DropIndex(
name: "IX_Categories_ParentCategoryCategoryId",
table: "Categories");
migrationBuilder.DropIndex(
name: "IX_Books_CategoryId",
table: "Books");
migrationBuilder.DeleteData(
table: "Categories",
keyColumn: "CategoryId",
keyValue: -1);
migrationBuilder.DropColumn(
name: "ParentCategoryCategoryId",
table: "Categories");
migrationBuilder.DropColumn(
name: "CategoryId",
table: "Books");
migrationBuilder.CreateTable(
name: "CategoryLadders",
columns: table => new
{
CategoryLadderId = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true)
},
constraints: table =>
{
table.PrimaryKey("PK_CategoryLadders", x => x.CategoryLadderId);
});
migrationBuilder.CreateTable(
name: "BookCategory",
columns: table => new
{
BookId = table.Column<int>(type: "INTEGER", nullable: false),
CategoryLadderId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BookCategory", x => new { x.BookId, x.CategoryLadderId });
table.ForeignKey(
name: "FK_BookCategory_Books_BookId",
column: x => x.BookId,
principalTable: "Books",
principalColumn: "BookId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BookCategory_CategoryLadders_CategoryLadderId",
column: x => x.CategoryLadderId,
principalTable: "CategoryLadders",
principalColumn: "CategoryLadderId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "CategoryCategoryLadder",
columns: table => new
{
_categoriesCategoryId = table.Column<int>(type: "INTEGER", nullable: false),
_categoryLaddersCategoryLadderId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CategoryCategoryLadder", x => new { x._categoriesCategoryId, x._categoryLaddersCategoryLadderId });
table.ForeignKey(
name: "FK_CategoryCategoryLadder_Categories__categoriesCategoryId",
column: x => x._categoriesCategoryId,
principalTable: "Categories",
principalColumn: "CategoryId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_CategoryCategoryLadder_CategoryLadders__categoryLaddersCategoryLadderId",
column: x => x._categoryLaddersCategoryLadderId,
principalTable: "CategoryLadders",
principalColumn: "CategoryLadderId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_BookCategory_BookId",
table: "BookCategory",
column: "BookId");
migrationBuilder.CreateIndex(
name: "IX_BookCategory_CategoryLadderId",
table: "BookCategory",
column: "CategoryLadderId");
migrationBuilder.CreateIndex(
name: "IX_CategoryCategoryLadder__categoryLaddersCategoryLadderId",
table: "CategoryCategoryLadder",
column: "_categoryLaddersCategoryLadderId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BookCategory");
migrationBuilder.DropTable(
name: "CategoryCategoryLadder");
migrationBuilder.DropTable(
name: "CategoryLadders");
migrationBuilder.AddColumn<int>(
name: "ParentCategoryCategoryId",
table: "Categories",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "CategoryId",
table: "Books",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.InsertData(
table: "Categories",
columns: new[] { "CategoryId", "AudibleCategoryId", "Name", "ParentCategoryCategoryId" },
values: new object[] { -1, "", "", null });
migrationBuilder.CreateIndex(
name: "IX_Categories_ParentCategoryCategoryId",
table: "Categories",
column: "ParentCategoryCategoryId");
migrationBuilder.CreateIndex(
name: "IX_Books_CategoryId",
table: "Books",
column: "CategoryId");
migrationBuilder.AddForeignKey(
name: "FK_Books_Categories_CategoryId",
table: "Books",
column: "CategoryId",
principalTable: "Categories",
principalColumn: "CategoryId",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Categories_Categories_ParentCategoryCategoryId",
table: "Categories",
column: "ParentCategoryCategoryId",
principalTable: "Categories",
principalColumn: "CategoryId");
}
}
}

View File

@@ -17,6 +17,21 @@ namespace DataLayer.Migrations
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
modelBuilder.Entity("CategoryCategoryLadder", b =>
{
b.Property<int>("_categoriesCategoryId")
.HasColumnType("INTEGER");
b.Property<int>("_categoryLaddersCategoryLadderId")
.HasColumnType("INTEGER");
b.HasKey("_categoriesCategoryId", "_categoryLaddersCategoryLadderId");
b.HasIndex("_categoryLaddersCategoryLadderId");
b.ToTable("CategoryCategoryLadder");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
@@ -26,9 +41,6 @@ namespace DataLayer.Migrations
b.Property<string>("AudibleProductId")
.HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<int>("ContentType")
.HasColumnType("INTEGER");
@@ -69,11 +81,26 @@ namespace DataLayer.Migrations
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookCategory", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("CategoryLadderId")
.HasColumnType("INTEGER");
b.HasKey("BookId", "CategoryLadderId");
b.HasIndex("BookId");
b.HasIndex("CategoryLadderId");
b.ToTable("BookCategory");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
@@ -109,24 +136,22 @@ namespace DataLayer.Migrations
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("ParentCategoryCategoryId")
.HasColumnType("INTEGER");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
});
b.HasData(
new
{
CategoryId = -1,
AudibleCategoryId = "",
Name = ""
});
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
{
b.Property<int>("CategoryLadderId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.HasKey("CategoryLadderId");
b.ToTable("CategoryLadders");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
@@ -216,14 +241,23 @@ namespace DataLayer.Migrations
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
modelBuilder.Entity("CategoryCategoryLadder", b =>
{
b.HasOne("DataLayer.Category", "Category")
b.HasOne("DataLayer.Category", null)
.WithMany()
.HasForeignKey("CategoryId")
.HasForeignKey("_categoriesCategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.CategoryLadder", null)
.WithMany()
.HasForeignKey("_categoryLaddersCategoryLadderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
@@ -324,8 +358,6 @@ namespace DataLayer.Migrations
b1.Navigation("Rating");
});
b.Navigation("Category");
b.Navigation("Rating");
b.Navigation("Supplements");
@@ -333,6 +365,25 @@ namespace DataLayer.Migrations
b.Navigation("UserDefinedItem");
});
modelBuilder.Entity("DataLayer.BookCategory", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("CategoriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.CategoryLadder", "CategoryLadder")
.WithMany("BooksLink")
.HasForeignKey("CategoryLadderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("CategoryLadder");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
@@ -352,15 +403,6 @@ namespace DataLayer.Migrations
b.Navigation("Contributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
b.Navigation("ParentCategory");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
@@ -393,11 +435,18 @@ namespace DataLayer.Migrations
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Navigation("CategoriesLink");
b.Navigation("ContributorsLink");
b.Navigation("SeriesLink");
});
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
{
b.Navigation("BooksLink");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Navigation("BooksLink");

View File

@@ -34,7 +34,7 @@ namespace DataLayer
// owned items are always loaded. eg: book.UserDefinedItem, book.Supplements
.Include(b => b.SeriesLink).ThenInclude(sb => sb.Series)
.Include(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
.Include(b => b.Category).ThenInclude(c => c.ParentCategory);
.Include(b => b.CategoriesLink).ThenInclude(c => c.CategoryLadder).ThenInclude(c => c._categories);
public static bool IsProduct(this Book book)
=> book.ContentType is not ContentType.Episode and not ContentType.Parent;

View File

@@ -0,0 +1,11 @@
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace DataLayer
{
public static class CategoryQueries
{
public static IQueryable<CategoryLadder> GetCategoryLadders(this LibationContext context)
=> context.CategoryLadders.Include(c => c._categories);
}
}

View File

@@ -55,7 +55,7 @@ namespace DataLayer
// owned items are always loaded. eg: book.UserDefinedItem, book.Supplements
.Include(le => le.Book).ThenInclude(b => b.SeriesLink).ThenInclude(sb => sb.Series)
.Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
.Include(le => le.Book).ThenInclude(b => b.Category).ThenInclude(c => c.ParentCategory);
.Include(le => le.Book).ThenInclude(b => b.CategoriesLink).ThenInclude(c => c.CategoryLadder).ThenInclude(c => c._categories);
public static IEnumerable<LibraryBook> ParentedEpisodes(this IEnumerable<LibraryBook> libraryBooks)
=> libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).SelectMany(libraryBooks.FindChildren);

View File

@@ -99,20 +99,6 @@ namespace DtoImporterService
.Select(n => contributorImporter.Cache[n.Name])
.ToList();
// categories are laid out for a breadcrumb. category is 1st, subcategory is 2nd
// absence of categories is also possible
// CATEGORY HACK: only use the 1st 2 categories
// after we support full arbitrary-depth category trees and multiple categories per book, the real impl will be something like this
// var lastCategory = item.Categories.LastOrDefault()?.CategoryId ?? "";
var lastCategory
= item.Categories.Length == 0 ? ""
: item.Categories.Length == 1 ? item.Categories[0].CategoryId
// 2+
: item.Categories[1].CategoryId;
var category = categoryImporter.Cache[lastCategory];
Book book;
try
{
@@ -125,7 +111,6 @@ namespace DtoImporterService
contentType,
authors,
narrators,
category,
importItem.LocaleName)
).Entity;
Cache.Add(book.AudibleProductId, book);
@@ -140,7 +125,6 @@ namespace DtoImporterService
contentType,
QtyAuthors = authors?.Count,
QtyNarrators = narrators?.Count,
Category = category?.Name,
importItem.LocaleName
});
throw;
@@ -201,6 +185,19 @@ namespace DtoImporterService
book.UpsertSeries(series, seriesEntry.Sequence);
}
}
if (item.CategoryLadders is not null)
{
var ladders = new List<DataLayer.CategoryLadder>();
foreach (var ladder in item.CategoryLadders.Select(cl => cl.Ladder).Where(l => l?.Length > 0))
{
var categoryIds = ladder.Select(l => l.CategoryId).ToList();
ladders.Add(categoryImporter.LadderCache.Single(c => c.Equals(categoryIds)));
}
//Set all ladders at once so ladders that have been
//removed by audible can be removed from the DB
book.SetCategoryLadders(ladders);
}
}
private static DataLayer.ContentType GetContentType(Item item)

View File

@@ -12,76 +12,84 @@ namespace DtoImporterService
{
protected override IValidator Validator => new CategoryValidator();
public Dictionary<string, Category> Cache { get; private set; } = new();
private Dictionary<string, Category> CategoryCache { get; set; } = new();
public HashSet<DataLayer.CategoryLadder> LadderCache { get; private set; } = new();
public CategoryImporter(LibationContext context) : base(context) { }
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
// get distinct
var categoryIds = importItems
.Select(i => i.DtoItem)
.GetCategoriesDistinct()
.Select(c => c.CategoryId)
.Distinct()
.ToList();
// load db existing => .Local
loadLocal_categories(categoryIds);
loadLocal_categories();
// upsert
var categoryPairs = importItems
.Select(i => i.DtoItem)
.GetCategoryPairsDistinct()
var categoryLadders = importItems
.SelectMany(i => i.DtoItem.CategoryLadders)
.Select(cl => cl.Ladder)
.Where(l => l?.Length > 0)
.ToList();
var qtyNew = upsertCategories(categoryPairs);
var qtyNew = upsertCategories(categoryLadders);
return qtyNew;
}
private void loadLocal_categories(List<string> categoryIds)
private void loadLocal_categories()
{
// must include default/empty/missing
categoryIds.Add(Category.GetEmpty().AudibleCategoryId);
// load existing => local
Cache = DbContext.Categories
.Where(c => categoryIds.Contains(c.AudibleCategoryId))
.ToDictionarySafe(c => c.AudibleCategoryId);
LadderCache = DbContext.GetCategoryLadders().ToHashSet();
CategoryCache = LadderCache.SelectMany(cl => cl.Categories).ToDictionarySafe(c => c.AudibleCategoryId);
}
// only use after loading contributors => local
private int upsertCategories(List<Ladder[]> categoryPairs)
private int upsertCategories(List<Ladder[]> ladders)
{
var qtyNew = 0;
foreach (var pair in categoryPairs)
foreach (var ladder in ladders)
{
for (var i = 0; i < pair.Length; i++)
var categories = new List<Category>(ladder.Length);
for (var i = 0; i < ladder.Length; i++)
{
// CATEGORY HACK: not yet supported: depth beyond 0 and 1
if (i > 1)
break;
var id = ladder[i].CategoryId;
var name = ladder[i].CategoryName;
var id = pair[i].CategoryId;
var name = pair[i].CategoryName;
Category parentCategory = null;
if (i == 1)
Cache.TryGetValue(pair[0].CategoryId, out parentCategory);
if (!Cache.TryGetValue(id, out var category))
if (!CategoryCache.TryGetValue(id, out var category))
{
category = addCategory(id, name);
qtyNew++;
}
category.UpdateParentCategory(parentCategory);
categories.Add(category);
}
var categoryLadder = new DataLayer.CategoryLadder(categories);
if (!LadderCache.Contains(categoryLadder))
{
addCategoryLadder(categoryLadder);
qtyNew++;
}
}
return qtyNew;
}
private DataLayer.CategoryLadder addCategoryLadder(DataLayer.CategoryLadder categoryList)
{
try
{
var entityEntry = DbContext.CategoryLadders.Add(categoryList);
var entity = entityEntry.Entity;
LadderCache.Add(entity);
return entity;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error adding category ladder. {@DebugInfo}", categoryList);
throw;
}
}
private Category addCategory(string id, string name)
{
try
@@ -91,7 +99,7 @@ namespace DtoImporterService
var entityEntry = DbContext.Categories.Add(category);
var entity = entityEntry.Entity;
Cache.Add(entity.AudibleCategoryId, entity);
CategoryCache.Add(entity.AudibleCategoryId, entity);
return entity;
}
catch (Exception ex)

View File

@@ -51,18 +51,19 @@ namespace FileLiberator
var config = Configuration.Instance;
var lameConfig = GetLameOptions(config);
var chapters = m4bBook.GetChaptersFromMetadata();
//Finishing configuring lame encoder.
AaxDecrypter.MpegUtil.ConfigureLameOptions(
m4bBook,
lameConfig,
config.LameDownsampleMono,
config.LameMatchSourceBR);
config.LameMatchSourceBR,
chapters);
using var mp3File = File.OpenWrite(Path.GetTempFileName());
using var mp3File = File.Open(Path.GetTempFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite);
try
{
Mp4Operation = m4bBook.ConvertToMp3Async(mp3File, lameConfig);
Mp4Operation = m4bBook.ConvertToMp3Async(mp3File, lameConfig, chapters);
Mp4Operation.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;
await Mp4Operation;

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using AaxDecrypter;
using ApplicationServices;
using AudibleApi.Common;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
@@ -124,12 +125,12 @@ namespace FileLiberator
var quality = (AudibleApi.DownloadQuality)config.FileDownloadQuality;
var api = await libraryBook.GetApiAsync();
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, quality);
using var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic);
using var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic);
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower());
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
if (contentLic.DrmType != AudibleApi.Common.DrmType.Adrm)
if (contentLic.DrmType != DrmType.Adrm)
abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, dlOptions);
else
{
@@ -139,7 +140,7 @@ namespace FileLiberator
new AaxcDownloadSingleConverter(outFileName, cacheDir, dlOptions);
if (config.AllowLibationFixup)
converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.CategoriesNames());
converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.LowestCategoryNames());
abDownloader = converter;
}
@@ -152,16 +153,29 @@ namespace FileLiberator
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
// REAL WORK DONE HERE
return await abDownloader.RunAsync();
// REAL WORK DONE HERE
var success = await abDownloader.RunAsync();
if (success && config.SaveMetadataToFile)
{
var metadataFile = Templates.File.GetFilename(dlOptions.LibraryBookDto, Path.GetDirectoryName(outFileName), ".metadata.json");
var item = await api.GetCatalogProductAsync(libraryBook.Book.AudibleProductId, AudibleApi.CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
item.SourceJson.Add(nameof(ContentMetadata.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(contentLic.ContentMetadata.ChapterInfo));
item.SourceJson.Add(nameof(ContentMetadata.ContentReference), Newtonsoft.Json.Linq.JObject.FromObject(contentLic.ContentMetadata.ContentReference));
File.WriteAllText(metadataFile, item.SourceJson.ToString());
OnFileCreated(libraryBook, metadataFile);
}
return success;
}
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic)
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, ContentLicense contentLic)
{
//If DrmType != Adrm the delivered file is an unencrypted mp3.
var outputFormat
= contentLic.DrmType != AudibleApi.Common.DrmType.Adrm || (config.AllowLibationFixup && config.DecryptToLossy)
= contentLic.DrmType != DrmType.Adrm || (config.AllowLibationFixup && config.DecryptToLossy)
? OutputFormat.Mp3
: OutputFormat.M4b;
@@ -183,7 +197,11 @@ namespace FileLiberator
RuntimeLength = TimeSpan.FromMilliseconds(contentLic?.ContentMetadata?.ChapterInfo?.RuntimeLengthMs ?? 0),
};
var chapters = flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters).OrderBy(c => c.StartOffsetMs).ToList();
var titleConcat = config.CombineNestedChapterTitles ? ": " : null;
var chapters
= flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters, titleConcat)
.OrderBy(c => c.StartOffsetMs)
.ToList();
if (config.MergeOpeningAndEndCredits)
combineCredits(chapters);
@@ -280,14 +298,19 @@ namespace FileLiberator
*/
public static List<AudibleApi.Common.Chapter> flattenChapters(IList<AudibleApi.Common.Chapter> chapters, string titleConcat = ": ")
public static List<Chapter> flattenChapters(IList<Chapter> chapters, string titleConcat = ": ")
{
List<AudibleApi.Common.Chapter> chaps = new();
List<Chapter> chaps = new();
foreach (var c in chapters)
{
if (c.Chapters is null)
chaps.Add(c);
else if (titleConcat is null)
{
chaps.Add(c);
chaps.AddRange(flattenChapters(c.Chapters));
}
else
{
if (c.LengthMs < 10000)
@@ -305,13 +328,12 @@ namespace FileLiberator
child.Title = $"{c.Title}{titleConcat}{child.Title}";
chaps.AddRange(children);
c.Chapters = null;
}
}
return chaps;
}
public static void combineCredits(IList<AudibleApi.Common.Chapter> chapters)
public static void combineCredits(IList<Chapter> chapters)
{
if (chapters.Count > 1 && chapters[0].Title == "Opening Credits")
{
@@ -351,11 +373,15 @@ namespace FileLiberator
private void AaxcDownloader_RetrievedCoverArt(object _, byte[] e)
{
if (Configuration.Instance.AllowLibationFixup)
{
e = OnRequestCoverArt();
abDownloader.SetCoverArt(e);
}
if (e is not null)
OnCoverImageDiscovered(e);
else if (Configuration.Instance.AllowLibationFixup)
abDownloader.SetCoverArt(OnRequestCoverArt());
}
}
/// <summary>Move new files to 'Books' directory</summary>
/// <returns>Return directory if audiobook file(s) were successfully created and can be located on disk. Else null.</returns>

View File

@@ -5,8 +5,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="7.2.2.1" />
<PackageReference Include="Polly" Version="7.2.3" />
<PackageReference Include="Dinah.Core" Version="7.2.3.1" />
<PackageReference Include="Polly" Version="7.2.4" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@@ -131,6 +131,35 @@ namespace FileManager
writeFile(propertyName, parsedNewValue);
}
public bool RemoveProperty(string propertyName)
{
if (IsReadOnly)
return false;
var success = false;
try
{
lock (locker)
{
var jObject = readFile();
if (!jObject.ContainsKey(propertyName))
return false;
jObject.Remove(propertyName);
var endContents = JsonConvert.SerializeObject(jObject, Formatting.Indented);
File.WriteAllText(Filepath, endContents);
success = true;
}
Serilog.Log.Logger.Information("Removed property. {@DebugInfo}", propertyName);
}
catch { }
return success;
}
private void writeFile(string propertyName, JToken newValue)
{
if (IsReadOnly)

View File

@@ -8,9 +8,6 @@
<Panel Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid Name="ratingsGrid" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="3,0,0,0" ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto">
<Grid.Styles>
<Style Selector="TextBlock">
<Setter Property="FontSize" Value="11" />
</Style>
<Style Selector="StackPanel > TextBlock">
<Setter Property="Padding" Value="0,0,-2,0" />
</Style>

View File

@@ -73,7 +73,15 @@
<TextBlock Text="{CompiledBinding MergeOpeningEndCreditsText}" />
</CheckBox>
<CheckBox IsChecked="{CompiledBinding AllowLibationFixup, Mode=TwoWay}">
<CheckBox
ToolTip.Tip="{CompiledBinding CombineNestedChapterTitlesTip}"
IsChecked="{CompiledBinding CombineNestedChapterTitles, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding CombineNestedChapterTitlesText}" />
</CheckBox>
<CheckBox
ToolTip.Tip="{CompiledBinding AllowLibationFixupTip}"
IsChecked="{CompiledBinding AllowLibationFixup, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding AllowLibationFixupText}" />
</CheckBox>
</StackPanel>

View File

@@ -165,18 +165,32 @@
</StackPanel>
</controls:GroupBox>
<CheckBox
<StackPanel
Grid.Row="3"
Margin="5"
VerticalAlignment="Top"
IsVisible="{CompiledBinding !Config.IsLinux}"
IsChecked="{CompiledBinding UseCoverAsFolderIcon, Mode=TwoWay}">
Orientation="Horizontal">
<TextBlock
TextWrapping="Wrap"
Text="{CompiledBinding UseCoverAsFolderIconText}" />
<CheckBox
Margin="5"
VerticalAlignment="Top"
IsVisible="{CompiledBinding !Config.IsLinux}"
IsChecked="{CompiledBinding UseCoverAsFolderIcon, Mode=TwoWay}">
</CheckBox>
<TextBlock
TextWrapping="Wrap"
Text="{CompiledBinding UseCoverAsFolderIconText}" />
</CheckBox>
<CheckBox
Margin="5"
VerticalAlignment="Top"
IsChecked="{CompiledBinding SaveMetadataToFile, Mode=TwoWay}">
<TextBlock
TextWrapping="Wrap"
Text="{CompiledBinding SaveMetadataToFileText}" />
</CheckBox>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -2,18 +2,18 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="600"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
x:DataType="vm:ImportantSettingsVM"
x:Class="LibationAvalonia.Controls.Settings.Important">
<Grid RowDefinitions="Auto,Auto,*">
<Grid RowDefinitions="Auto,Auto,Auto,*">
<controls:GroupBox
Grid.Row="0"
Margin="5"
Label="Books Location">
<StackPanel>
<TextBlock
Margin="5"
@@ -35,7 +35,7 @@
<Grid
RowDefinitions="Auto,Auto"
ColumnDefinitions="Auto,*">
<TextBlock
VerticalAlignment="Center"
Margin="0,0,10,0"
@@ -95,8 +95,63 @@
</StackPanel>
<Grid
<controls:GroupBox
Grid.Row="2"
Margin="5"
Label="Display Settings">
<Grid
RowDefinitions="Auto,Auto"
ColumnDefinitions="Auto,Auto,*">
<TextBlock
Margin="0,0,10,0"
VerticalAlignment="Center"
Text="{CompiledBinding GridScaleFactorText}"/>
<Slider
Grid.Column="1"
Width="200"
Value="{CompiledBinding GridScaleFactor, Mode=TwoWay}"
VerticalAlignment="Center"
Minimum="-100"
Maximum="100"
IsSnapToTickEnabled="False"
TickFrequency="25"
TickPlacement="BottomRight">
</Slider>
<TextBlock
Margin="0,0,10,0"
Grid.Row="1"
VerticalAlignment="Center"
Text="{CompiledBinding GridFontScaleFactorText}"/>
<Slider
Grid.Column="1"
Grid.Row="1"
Width="200"
Value="{CompiledBinding GridFontScaleFactor, Mode=TwoWay}"
VerticalAlignment="Center"
Minimum="-100"
Maximum="100"
IsSnapToTickEnabled="False"
TickFrequency="25"
TickPlacement="BottomRight">
</Slider>
<Button
Grid.Column="2"
Grid.Row="1"
HorizontalAlignment="Right"
Margin="0,5"
Padding="20,0"
VerticalAlignment="Stretch"
Content="Apply Display Settings"
Command="{CompiledBinding ApplyDisplaySettings}"/>
</Grid>
</controls:GroupBox>
<Grid
Grid.Row="3"
ColumnDefinitions="Auto,Auto,*"
Margin="10"
VerticalAlignment="Bottom">
@@ -111,7 +166,7 @@
MinWidth="80"
SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}"
ItemsSource="{CompiledBinding Themes}"/>
<TextBlock
Grid.Column="2"
FontSize="16"

View File

@@ -115,7 +115,7 @@ Author(s): {Book.AuthorNames()}
Narrator(s): {Book.NarratorNames()}
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
Audio Bitrate: {Book.AudioFormat}
Category: {string.Join(" > ", Book.CategoriesNames())}
Category: {string.Join(", ", Book.LowestCategoryNames())}
Purchase Date: {libraryBook.DateAdded:d}
Language: {Book.Language}
Audible ID: {Book.AudibleProductId}

View File

@@ -36,6 +36,7 @@ namespace LibationAvalonia
}
AppDomain.CurrentDomain.UnhandledException += (o, e) => LogError(e.ExceptionObject);
bool loggingEnabled = false;
//***********************************************//
// //
// do not use Configuration before this line //
@@ -55,6 +56,7 @@ namespace LibationAvalonia
// most migrations go in here
LibationScaffolding.RunPostConfigMigrations(config);
LibationScaffolding.RunPostMigrationScaffolding(Variety.Chardonnay, config);
loggingEnabled = true;
//Start loading the library before loading the main form
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
@@ -64,9 +66,12 @@ namespace LibationAvalonia
classicLifetimeTask.Result.Start(null);
}
catch(Exception e)
catch (Exception ex)
{
LogError(e);
if (loggingEnabled)
Serilog.Log.Logger.Error(ex, "CRASH");
else
LogError(ex);
}
}

View File

@@ -1,7 +1,10 @@
using ApplicationServices;
using Avalonia.Threading;
using DataLayer;
using LibationFileManager;
using ReactiveUI;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.ViewModels
@@ -41,16 +44,17 @@ namespace LibationAvalonia.ViewModels
private void Configure_BackupCounts()
{
MainWindow.Loaded += setBackupCounts;
LibraryCommands.LibrarySizeChanged += setBackupCounts;
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
MainWindow.LibraryLoaded += (_, e) => setBackupCounts(e.Where(l => !l.Book.IsEpisodeParent()));
LibraryCommands.LibrarySizeChanged += (_,_) => setBackupCounts();
LibraryCommands.BookUserDefinedItemCommitted += (_, _) => setBackupCounts();
}
private async void setBackupCounts(object _, object __)
private async void setBackupCounts(IEnumerable<LibraryBook> libraryBooks = null)
{
if (updateCountsTask?.IsCompleted ?? true)
{
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts());
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
var stats = await updateCountsTask;
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);

View File

@@ -264,9 +264,12 @@ namespace LibationAvalonia.ViewModels
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
{
byte[] coverData = PictureStorage
.GetPictureSynchronously(
new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500));
var quality
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High
? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
: new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
byte[] coverData = PictureStorage.GetPictureSynchronously(quality);
AudioDecodable_CoverImageDiscovered(this, coverData);
return coverData;

View File

@@ -91,37 +91,21 @@ namespace LibationAvalonia.ViewModels
#region Display Functions
internal void BindToGrid(List<LibraryBook> dbBooks)
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
{
GridEntries = new(SOURCE) { Filter = CollectionFilter };
var geList = dbBooks
.Where(lb => lb.Book.IsProduct())
.Select(b => new LibraryBookEntry<AvaloniaEntryStatus>(b))
.ToList<IGridEntry>();
var geList = await LibraryBookEntry<AvaloniaEntryStatus>.GetAllProductsAsync(dbBooks);
var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild()).ToList();
var seriesBooks = dbBooks.Where(lb => lb.Book.IsEpisodeParent()).ToList();
foreach (var parent in seriesBooks)
{
var seriesEpisodes = episodes.FindChildren(parent);
if (!seriesEpisodes.Any()) continue;
var seriesEntry = new SeriesEntry<AvaloniaEntryStatus>(parent, seriesEpisodes);
seriesEntry.Liberate.Expanded = false;
geList.Add(seriesEntry);
}
var seriesEntries = await SeriesEntry<AvaloniaEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
//Create the filtered-in list before adding entries to avoid a refresh
FilteredInGridEntries = geList.Union(geList.OfType<ISeriesEntry>().SelectMany(s => s.Children)).FilterEntries(FilterString);
SOURCE.AddRange(geList.OrderDescending(new RowComparer(null)));
FilteredInGridEntries = geList.Union(seriesEntries.SelectMany(s => s.Children)).FilterEntries(FilterString);
//Adding entries to the Source list will invoke CollectionFilter
SOURCE.AddRange(geList.Concat(seriesEntries).OrderDescending(new RowComparer(null)));
//Add all children beneath their parent
foreach (var series in SOURCE.OfType<ISeriesEntry>().ToList())
foreach (var series in seriesEntries)
{
var seriesIndex = SOURCE.IndexOf(series);
foreach (var child in series.Children)

View File

@@ -44,6 +44,7 @@ namespace LibationAvalonia.ViewModels.Settings
public void LoadSettings(Configuration config)
{
CreateCueSheet = config.CreateCueSheet;
CombineNestedChapterTitles = config.CombineNestedChapterTitles;
AllowLibationFixup = config.AllowLibationFixup;
DownloadCoverArt = config.DownloadCoverArt;
RetainAaxFile = config.RetainAaxFile;
@@ -71,6 +72,7 @@ namespace LibationAvalonia.ViewModels.Settings
public void SaveSettings(Configuration config)
{
config.CreateCueSheet = CreateCueSheet;
config.CombineNestedChapterTitles = CombineNestedChapterTitles;
config.AllowLibationFixup = AllowLibationFixup;
config.DownloadCoverArt = DownloadCoverArt;
config.RetainAaxFile = RetainAaxFile;
@@ -99,7 +101,10 @@ namespace LibationAvalonia.ViewModels.Settings
public AvaloniaList<Configuration.ClipBookmarkFormat> ClipBookmarkFormats { get; } = new(Enum<Configuration.ClipBookmarkFormat>.GetValues());
public string FileDownloadQualityText { get; } = Configuration.GetDescription(nameof(Configuration.FileDownloadQuality));
public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet));
public string CombineNestedChapterTitlesText { get; } = Configuration.GetDescription(nameof(Configuration.CombineNestedChapterTitles));
public string CombineNestedChapterTitlesTip => Configuration.GetHelpText(nameof(CombineNestedChapterTitles));
public string AllowLibationFixupText { get; } = Configuration.GetDescription(nameof(Configuration.AllowLibationFixup));
public string AllowLibationFixupTip => Configuration.GetHelpText(nameof(AllowLibationFixup));
public string DownloadCoverArtText { get; } = Configuration.GetDescription(nameof(Configuration.DownloadCoverArt));
public string RetainAaxFileText { get; } = Configuration.GetDescription(nameof(Configuration.RetainAaxFile));
public string SplitFilesByChapterText { get; } = Configuration.GetDescription(nameof(Configuration.SplitFilesByChapter));
@@ -110,6 +115,7 @@ namespace LibationAvalonia.ViewModels.Settings
public string MoveMoovToBeginningText { get; } = Configuration.GetDescription(nameof(Configuration.MoveMoovToBeginning));
public bool CreateCueSheet { get; set; }
public bool CombineNestedChapterTitles { get; set; }
public bool DownloadCoverArt { get; set; }
public bool RetainAaxFile { get; set; }
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }

View File

@@ -38,6 +38,7 @@ namespace LibationAvalonia.ViewModels.Settings
ChapterFileTemplate = config.ChapterFileTemplate;
InProgressDirectory = config.InProgress;
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
SaveMetadataToFile = config.SaveMetadataToFile;
}
public void SaveSettings(Configuration config)
@@ -54,9 +55,11 @@ namespace LibationAvalonia.ViewModels.Settings
config.InProgress = InProgressDirectory;
config.UseCoverAsFolderIcon = UseCoverAsFolderIcon;
config.SaveMetadataToFile = SaveMetadataToFile;
}
public string UseCoverAsFolderIconText { get; } = Configuration.GetDescription(nameof(Configuration.UseCoverAsFolderIcon));
public string SaveMetadataToFileText { get; } = Configuration.GetDescription(nameof(Configuration.SaveMetadataToFile));
public string BadBookGroupboxText { get; } = Configuration.GetDescription(nameof(Configuration.BadBook));
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription();
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription();
@@ -72,6 +75,7 @@ namespace LibationAvalonia.ViewModels.Settings
public string FileTemplate { get => _fileTemplate; set { this.RaiseAndSetIfChanged(ref _fileTemplate, value); } }
public string ChapterFileTemplate { get => _chapterFileTemplate; set { this.RaiseAndSetIfChanged(ref _chapterFileTemplate, value); } }
public bool UseCoverAsFolderIcon { get; set; }
public bool SaveMetadataToFile { get; set; }
public bool BadBookAsk { get; set; }
public bool BadBookAbort { get; set; }

View File

@@ -13,9 +13,11 @@ namespace LibationAvalonia.ViewModels.Settings
{
private string themeVariant;
private string initialThemeVariant;
private readonly Configuration config;
public ImportantSettingsVM(Configuration config)
{
this.config = config;
LoadSettings(config);
}
@@ -27,6 +29,8 @@ namespace LibationAvalonia.ViewModels.Settings
CreationTime = DateTimeSources.SingleOrDefault(v => v.Value == config.CreationTime) ?? DateTimeSources[0];
LastWriteTime = DateTimeSources.SingleOrDefault(v => v.Value == config.LastWriteTime) ?? DateTimeSources[0];
LoggingLevel = config.LogLevel;
GridScaleFactor = scaleFactorToLinearRange(config.GridScaleFactor);
GridFontScaleFactor = scaleFactorToLinearRange(config.GridFontScaleFactor);
ThemeVariant = initialThemeVariant
= Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) is nameof(Avalonia.Styling.ThemeVariant.Dark)
? nameof(Avalonia.Styling.ThemeVariant.Dark)
@@ -47,6 +51,16 @@ namespace LibationAvalonia.ViewModels.Settings
Configuration.Instance.SetString(ThemeVariant, nameof(ThemeVariant));
}
private static float scaleFactorToLinearRange(float scaleFactor)
=> float.Round(100 * MathF.Log2(scaleFactor));
private static float linearRangeToScaleFactor(float value)
=> MathF.Pow(2, value / 100f);
public void ApplyDisplaySettings()
{
config.GridFontScaleFactor = linearRangeToScaleFactor(GridFontScaleFactor);
config.GridScaleFactor = linearRangeToScaleFactor(GridScaleFactor);
}
public void OpenLogFolderButton() => Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
@@ -66,12 +80,16 @@ namespace LibationAvalonia.ViewModels.Settings
.Select(v => new EnumDiaplay<Configuration.DateTimeSource>(v))
.ToArray();
public Serilog.Events.LogEventLevel[] LoggingLevels { get; } = Enum.GetValues<Serilog.Events.LogEventLevel>();
public string GridScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridScaleFactor));
public string GridFontScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridFontScaleFactor));
public string BetaOptInText { get; } = Configuration.GetDescription(nameof(Configuration.BetaOptIn));
public string[] Themes { get; } = { nameof(Avalonia.Styling.ThemeVariant.Light), nameof(Avalonia.Styling.ThemeVariant.Dark) };
public string BooksDirectory { get; set; }
public bool SavePodcastsToParentFolder { get; set; }
public bool OverwriteExisting { get; set; }
public float GridScaleFactor { get; set; }
public float GridFontScaleFactor { get; set; }
public EnumDiaplay<Configuration.DateTimeSource> CreationTime { get; set; }
public EnumDiaplay<Configuration.DateTimeSource> LastWriteTime { get; set; }
public Serilog.Events.LogEventLevel LoggingLevel { get; set; }

View File

@@ -3,7 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels"
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="200" MinWidth="64" MinHeight="64"
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="200" MinWidth="37" MinHeight="40"
Background="Transparent"
x:DataType="vm:LiberateStatusButtonViewModel"
x:Class="LibationAvalonia.Views.LiberateStatusButton">
@@ -35,48 +35,51 @@
Name="button"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsEnabled="{CompiledBinding IsButtonEnabled}" Padding="0" Click="Button_Click" >
<Panel>
<Panel
Width="64" Height="64"
IsVisible="{CompiledBinding !IsError}">
Padding="0"
IsEnabled="{CompiledBinding IsButtonEnabled}" Click="Button_Click" >
<Panel IsVisible="{CompiledBinding IsSeries}">
<Path IsVisible="{CompiledBinding Expanded}" Data="{StaticResource CollapseIcon}" />
<Path IsVisible="{CompiledBinding !Expanded}" Data="{StaticResource ExpandIcon}" />
<Grid RowDefinitions="*,8*,*">
<Viewbox
Grid.Row="1"
Stretch="Uniform"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Panel>
<Panel IsVisible="{CompiledBinding !IsError}">
<Panel IsVisible="{CompiledBinding IsSeries}">
<Path IsVisible="{CompiledBinding Expanded}" Data="{StaticResource CollapseIcon}" />
<Path IsVisible="{CompiledBinding !Expanded}" Data="{StaticResource ExpandIcon}" />
</Panel>
<Grid
IsVisible="{CompiledBinding !IsSeries}"
HorizontalAlignment="Center"
ColumnDefinitions="Auto,Auto">
<Canvas Width="29.44" Height="64">
<Rectangle Canvas.Left="5" Canvas.Top="5" IsVisible="{CompiledBinding RedVisible}" Fill="{DynamicResource StoplightRed}" />
<Rectangle Canvas.Left="5" Canvas.Top="23" IsVisible="{CompiledBinding YellowVisible}" Fill="{DynamicResource StoplightYellow}" />
<Rectangle Canvas.Left="5" Canvas.Top="42" IsVisible="{CompiledBinding GreenVisible}" Fill="{DynamicResource StoplightGreen}" />
<Path Height="64" Stretch="Uniform" Data="{StaticResource StoplightBodyIcon}"/>
</Canvas>
<Path Grid.Column="1" IsVisible="{CompiledBinding PdfDownloadedVisible}" Data="{StaticResource PdfDownloadedIcon}"/>
<Path Grid.Column="1" IsVisible="{CompiledBinding PdfNotDownloadedVisible}" Data="{StaticResource PdfNotDownloadedIcon}"/>
</Grid>
</Panel>
<Path
Stretch="None" Width="64"
IsVisible="{CompiledBinding IsError}"
Fill="{DynamicResource CancelRed}"
Data="{StaticResource BookErrorIcon}" />
<Path
Stretch="Fill"
IsVisible="{CompiledBinding !IsButtonEnabled}"
Fill="{DynamicResource DisabledGrayBrush}"
Data="M0,0 H1 V1 H0" />
</Panel>
<Grid
IsVisible="{CompiledBinding !IsSeries}"
HorizontalAlignment="Center"
ColumnDefinitions="Auto,Auto">
<Canvas Width="29.44" Height="64">
<Rectangle Canvas.Left="5" Canvas.Top="5" IsVisible="{CompiledBinding RedVisible}" Fill="{DynamicResource StoplightRed}" />
<Rectangle Canvas.Left="5" Canvas.Top="23" IsVisible="{CompiledBinding YellowVisible}" Fill="{DynamicResource StoplightYellow}" />
<Rectangle Canvas.Left="5" Canvas.Top="42" IsVisible="{CompiledBinding GreenVisible}" Fill="{DynamicResource StoplightGreen}" />
<Path Height="64" Stretch="Uniform" Data="{StaticResource StoplightBodyIcon}"/>
</Canvas>
<Path Grid.Column="1" IsVisible="{CompiledBinding PdfDownloadedVisible}" Data="{StaticResource PdfDownloadedIcon}"/>
<Path Grid.Column="1" IsVisible="{CompiledBinding PdfNotDownloadedVisible}" Data="{StaticResource PdfNotDownloadedIcon}"/>
</Grid>
</Panel>
<Path
Stretch="None" Width="64"
IsVisible="{CompiledBinding IsError}"
Fill="{DynamicResource CancelRed}"
Data="{StaticResource BookErrorIcon}" />
<Path
Stretch="Fill"
IsVisible="{CompiledBinding !IsButtonEnabled}"
Fill="{DynamicResource DisabledGrayBrush}"
Data="M0,0 H1 V1 H0" />
</Panel>
</Viewbox>
</Grid>
</Button>
</UserControl>

View File

@@ -61,7 +61,7 @@ namespace LibationAvalonia.Views
if (QuickFilters.UseDefault)
await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault());
ViewModel.ProductsDisplay.BindToGrid(dbBooks);
await ViewModel.ProductsDisplay.BindToGridAsync(dbBooks);
}
private void selectAndFocusSearchBox()

View File

@@ -20,9 +20,6 @@
CanUserReorderColumns="True">
<DataGrid.Styles>
<Style Selector="DataGridCell">
<Setter Property="Height" Value="80"/>
</Style>
<Style Selector="DataGridCell > Panel">
<Setter Property="VerticalAlignment" Value="Stretch"/>
</Style>
@@ -31,7 +28,6 @@
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="FontSize" Value="12"/>
</Style>
<Style Selector="DataGridCell Path">
<Setter Property="Stretch" Value="Uniform" />
@@ -69,7 +65,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<controls:DataGridTemplateColumnExt CanUserSort="True" Width="75" Header="Liberate" SortMemberPath="Liberate" ClipboardContentBinding="{Binding Liberate.ToolTip}">
<controls:DataGridTemplateColumnExt CanUserSort="True" Header="Liberate" SortMemberPath="Liberate" ClipboardContentBinding="{Binding Liberate.ToolTip}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<views:LiberateStatusButton
@@ -84,19 +80,19 @@
</DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumnExt>
<DataGridTemplateColumn CanUserSort="False" Width="80" Header="Cover" SortMemberPath="Cover" ClipboardContentBinding="{Binding LibraryBook.Book.PictureLarge}">
<controls:DataGridTemplateColumnExt CanUserSort="False" Header="Cover" SortMemberPath="Cover" ClipboardContentBinding="{Binding LibraryBook.Book.PictureLarge}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Image Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Cover_Click" Height="80" Source="{CompiledBinding Cover}" ToolTip.Tip="Click to see full size" />
<DataTemplate x:DataType="uibase:IGridEntry">
<Image Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Cover_Click" Source="{CompiledBinding Cover}" ToolTip.Tip="Click to see full size" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</controls:DataGridTemplateColumnExt>
<controls:DataGridTemplateColumnExt MinWidth="150" Width="2*" Header="Title" CanUserSort="True" SortMemberPath="Title" ClipboardContentBinding="{Binding Title}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<TextBlock FontSize="14" Text="{CompiledBinding Title}" />
<TextBlock Classes="h1" Text="{CompiledBinding Title}" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -152,11 +148,11 @@
</DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumnExt>
<controls:DataGridTemplateColumnExt MinWidth="100" Width="1*" Header="Description" CanUserSort="True" SortMemberPath="Description" ClipboardContentBinding="{Binding LongDescription}">
<controls:DataGridTemplateColumnExt MinWidth="100" Width="1*" Header="Description" CanUserSort="True" SortMemberPath="Description" ClipboardContentBinding="{Binding Description}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}" Tapped="Description_Click" ToolTip.Tip="Click to see full description" >
<TextBlock Text="{CompiledBinding Description}" FontSize="11" VerticalAlignment="Top" />
<TextBlock Text="{CompiledBinding Description}" VerticalAlignment="Top" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -208,7 +204,7 @@
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<TextBlock Text="{CompiledBinding Misc}" TextWrapping="WrapWithOverflow" FontSize="10" />
<TextBlock Text="{CompiledBinding Misc}" TextWrapping="WrapWithOverflow" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -218,7 +214,7 @@
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}" ToolTip.Tip="{CompiledBinding LastDownload.ToolTipText}" DoubleTapped="Version_DoubleClick">
<TextBlock Text="{CompiledBinding LastDownload}" TextWrapping="WrapWithOverflow" FontSize="10" />
<TextBlock Text="{CompiledBinding LastDownload}" TextWrapping="WrapWithOverflow" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -227,13 +223,30 @@
<controls:DataGridTemplateColumnExt CanUserSort="True" Width="100" Header="Tags" SortMemberPath="BookTags" ClipboardContentBinding="{Binding BookTags}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Button IsVisible="{CompiledBinding !Liberate.IsSeries}" Width="100" Height="80" Click="OnTagsButtonClick" ToolTip.Tip="Click to edit tags" >
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
<Panel Width="24" Height="24" IsVisible="{CompiledBinding BookTags, Converter={x:Static StringConverters.IsNullOrEmpty}}">
<Path Stretch="Uniform" Fill="{DynamicResource IconFill}" Data="{StaticResource EditTagsIcon}" />
</Panel>
<TextBlock IsVisible="{CompiledBinding BookTags, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" FontSize="12" TextWrapping="WrapWithOverflow" HorizontalAlignment="Center" VerticalAlignment="Center" Text="{CompiledBinding BookTags}"/>
</Panel>
<Button
IsVisible="{CompiledBinding !Liberate.IsSeries}"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Click="OnTagsButtonClick"
ToolTip.Tip="Click to edit tags">
<Grid
RowDefinitions="*,*,*"
Opacity="{CompiledBinding Liberate.Opacity}">
<Viewbox
Grid.Row="1"
Stretch="Uniform"
IsVisible="{CompiledBinding BookTags, Converter={x:Static StringConverters.IsNullOrEmpty}}">
<Path Fill="{DynamicResource IconFill}" Data="{StaticResource EditTagsIcon}" />
</Viewbox>
<TextBlock
Classes="h2"
Grid.RowSpan="3"
IsVisible="{CompiledBinding BookTags, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" TextWrapping="WrapWithOverflow" HorizontalAlignment="Center" VerticalAlignment="Center" Text="{CompiledBinding BookTags}"/>
</Grid>
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>

View File

@@ -2,7 +2,9 @@ using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using DataLayer;
using Dinah.Core;
using FileLiberator;
using LibationAvalonia.Controls;
using LibationAvalonia.Dialogs;
@@ -30,6 +32,25 @@ namespace LibationAvalonia.Views
InitializeComponent();
DataGridContextMenus.CellContextMenuStripNeeded += ProductsGrid_CellContextMenuStripNeeded;
var cellSelector = Selectors.Is<DataGridCell>(null);
rowHeightStyle = new Style(_ => cellSelector);
rowHeightStyle.Setters.Add(rowHeightSetter);
var tboxSelector = cellSelector.Descendant().Is<TextBlock>();
fontSizeStyle = new Style(_ => tboxSelector);
fontSizeStyle.Setters.Add(fontSizeSetter);
var tboxH1Selector = cellSelector.Child().Is<Panel>().Child().Is<TextBlock>().Class("h1");
fontSizeH1Style = new Style(_ => tboxH1Selector);
fontSizeH1Style.Setters.Add(fontSizeH1Setter);
var tboxH2Selector = cellSelector.Child().Is<Panel>().Child().Is<TextBlock>().Class("h2");
fontSizeH2Style = new Style(_ => tboxH2Selector);
fontSizeH2Style.Setters.Add(fontSizeH2Setter);
Configuration.Instance.PropertyChanged += Configuration_GridScaleChanged;
Configuration.Instance.PropertyChanged += Configuration_FontChanged;
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
@@ -51,12 +72,16 @@ namespace LibationAvalonia.Views
catch { sampleEntries = new(); }
var pdvm = new ProductsDisplayViewModel();
pdvm.BindToGrid(sampleEntries);
_ = pdvm.BindToGridAsync(sampleEntries);
DataContext = pdvm;
setGridScale(1);
setFontScale(1);
return;
}
setGridScale(Configuration.Instance.GridScaleFactor);
setFontScale(Configuration.Instance.GridFontScaleFactor);
Configure_ColumnCustomization();
foreach (var column in productsGrid.Columns)
@@ -67,174 +92,257 @@ namespace LibationAvalonia.Views
private void RemoveColumn_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender is DataGridColumn col && e.Property.Name == nameof(DataGridColumn.IsVisible))
if (sender is DataGridColumn col && e.Property == DataGridColumn.IsVisibleProperty)
{
col.DisplayIndex = 0;
col.CanUserReorder = false;
}
}
#region Scaling
[PropertyChangeFilter(nameof(Configuration.GridScaleFactor))]
private void Configuration_GridScaleChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
{
setGridScale((float)e.NewValue);
}
[PropertyChangeFilter(nameof(Configuration.GridFontScaleFactor))]
private void Configuration_FontChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
{
setFontScale((float)e.NewValue);
}
private readonly Style rowHeightStyle;
private readonly Setter rowHeightSetter = new() { Property = DataGridCell.HeightProperty };
private readonly Style fontSizeStyle;
private readonly Setter fontSizeSetter = new() { Property = TextBlock.FontSizeProperty };
private readonly Style fontSizeH1Style;
private readonly Setter fontSizeH1Setter = new() { Property = TextBlock.FontSizeProperty };
private readonly Style fontSizeH2Style;
private readonly Setter fontSizeH2Setter = new() { Property = TextBlock.FontSizeProperty };
private void setFontScale(double scaleFactor)
{
const double TextBlockFontSize = 11;
const double H1FontSize = 14;
const double H2FontSize = 12;
fontSizeSetter.Value = TextBlockFontSize * scaleFactor;
fontSizeH1Setter.Value = H1FontSize * scaleFactor;
fontSizeH2Setter.Value = H2FontSize * scaleFactor;
productsGrid.Styles.Remove(fontSizeStyle);
productsGrid.Styles.Remove(fontSizeH1Style);
productsGrid.Styles.Remove(fontSizeH2Style);
productsGrid.Styles.Add(fontSizeStyle);
productsGrid.Styles.Add(fontSizeH1Style);
productsGrid.Styles.Add(fontSizeH2Style);
}
private void setGridScale(double scaleFactor)
{
const float BaseRowHeight = 80;
const float BaseLiberateWidth = 75;
const float BaseCoverWidth = 80;
foreach (var column in productsGrid.Columns)
{
switch (column.SortMemberPath)
{
case nameof(IGridEntry.Liberate):
column.Width = new DataGridLength(BaseLiberateWidth * scaleFactor);
break;
case nameof(IGridEntry.Cover):
column.Width = new DataGridLength(BaseCoverWidth * scaleFactor);
break;
}
}
rowHeightSetter.Value = BaseRowHeight * scaleFactor;
productsGrid.Styles.Remove(rowHeightStyle);
productsGrid.Styles.Add(rowHeightStyle);
}
#endregion
#region Cell Context Menu
public void ProductsGrid_CellContextMenuStripNeeded(object sender, DataGridCellContextMenuStripNeededEventArgs args)
{
// stop light
if (args.Column.SortMemberPath == "Liberate")
if (args.Column.SortMemberPath is not "Liberate" and not "Cover")
{
var entry = args.GridEntry;
#region Liberate all Episodes
if (entry.Liberate.IsSeries)
{
var liberateEpisodesMenuItem = new MenuItem()
{
Header = "_Liberate All Episodes",
IsEnabled = ((ISeriesEntry)entry).Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
};
args.ContextMenuItems.Add(liberateEpisodesMenuItem);
liberateEpisodesMenuItem.Click += (_, _) => LiberateSeriesClicked?.Invoke(this, ((ISeriesEntry)entry));
}
#endregion
#region Set Download status to Downloaded
var setDownloadMenuItem = new MenuItem()
{
Header = "Set Download status to '_Downloaded'",
IsEnabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || entry.Liberate.IsSeries
};
args.ContextMenuItems.Add(setDownloadMenuItem);
if (entry.Liberate.IsSeries)
setDownloadMenuItem.Click += (_, __) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.Liberated);
else
setDownloadMenuItem.Click += (_, __) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.Liberated);
#endregion
#region Set Download status to Not Downloaded
var setNotDownloadMenuItem = new MenuItem()
{
Header = "Set Download status to '_Not Downloaded'",
IsEnabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || entry.Liberate.IsSeries
};
args.ContextMenuItems.Add(setNotDownloadMenuItem);
if (entry.Liberate.IsSeries)
setNotDownloadMenuItem.Click += (_, __) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.NotLiberated);
else
setNotDownloadMenuItem.Click += (_, __) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.NotLiberated);
#endregion
#region Remove from library
var removeMenuItem = new MenuItem() { Header = "_Remove from library" };
args.ContextMenuItems.Add(removeMenuItem);
if (entry.Liberate.IsSeries)
removeMenuItem.Click += async (_, __) => await ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).RemoveBooksAsync();
else
removeMenuItem.Click += async (_, __) => await Task.Run(entry.LibraryBook.RemoveBook);
#endregion
if (!entry.Liberate.IsSeries)
{
#region Locate file
var locateFileMenuItem = new MenuItem() { Header = "_Locate file..." };
args.ContextMenuItems.Add(locateFileMenuItem);
locateFileMenuItem.Click += async (_, __) =>
{
try
{
var window = this.GetParentWindow();
var openFileDialogOptions = new FilePickerOpenOptions
{
Title = $"Locate the audio file for '{entry.Book.TitleWithSubtitle}'",
AllowMultiple = false,
SuggestedStartLocation = await window.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix),
FileTypeFilter = new FilePickerFileType[]
{
new("All files (*.*)") { Patterns = new[] { "*" } },
}
};
var selectedFiles = await window.StorageProvider.OpenFilePickerAsync(openFileDialogOptions);
var selectedFile = selectedFiles.SingleOrDefault()?.TryGetLocalPath();
if (selectedFile is not null)
FilePathCache.Insert(entry.AudibleProductId, selectedFile);
}
catch (Exception ex)
{
var msg = "Error saving book's location";
await MessageBox.ShowAdminAlert(null, msg, msg, ex);
}
};
#endregion
#region Convert to Mp3
var convertToMp3MenuItem = new MenuItem
{
Header = "_Convert to Mp3",
IsEnabled = entry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated
};
args.ContextMenuItems.Add(convertToMp3MenuItem);
convertToMp3MenuItem.Click += (_, _) => ConvertToMp3Clicked?.Invoke(this, entry.LibraryBook);
#endregion
}
args.ContextMenuItems.Add(new Separator());
#region View Bookmarks/Clips
if (!entry.Liberate.IsSeries)
{
var bookRecordMenuItem = new MenuItem { Header = "View _Bookmarks/Clips" };
args.ContextMenuItems.Add(bookRecordMenuItem);
bookRecordMenuItem.Click += async (_, _) => await new BookRecordsDialog(entry.LibraryBook).ShowDialog(VisualRoot as Window);
}
#endregion
#region View All Series
if (entry.Book.SeriesLink.Any())
{
var header = entry.Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series";
var viewSeriesMenuItem = new MenuItem { Header = header };
args.ContextMenuItems.Add(viewSeriesMenuItem);
viewSeriesMenuItem.Click += (_, _) => new SeriesViewDialog(entry.LibraryBook).Show();
}
#endregion
}
else
{
// any non-stop light column
// (except for the Cover column which does not have a context menu)
var menuItem = new MenuItem { Header = "_Copy Cell Contents" };
menuItem.Click += async (s, e)
=> await App.MainWindow.Clipboard.SetTextAsync(args.CellClipboardContents);
args.ContextMenuItems.Add(menuItem);
args.ContextMenuItems.Add(new Separator());
}
var entry = args.GridEntry;
#region Liberate all Episodes
if (entry.Liberate.IsSeries)
{
var liberateEpisodesMenuItem = new MenuItem()
{
Header = "_Liberate All Episodes",
IsEnabled = ((ISeriesEntry)entry).Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
};
args.ContextMenuItems.Add(liberateEpisodesMenuItem);
liberateEpisodesMenuItem.Click += (_, _) => LiberateSeriesClicked?.Invoke(this, ((ISeriesEntry)entry));
}
#endregion
#region Set Download status to Downloaded
var setDownloadMenuItem = new MenuItem()
{
Header = "Set Download status to '_Downloaded'",
IsEnabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || entry.Liberate.IsSeries
};
args.ContextMenuItems.Add(setDownloadMenuItem);
if (entry.Liberate.IsSeries)
setDownloadMenuItem.Click += (_, __) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.Liberated);
else
setDownloadMenuItem.Click += (_, __) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.Liberated);
#endregion
#region Set Download status to Not Downloaded
var setNotDownloadMenuItem = new MenuItem()
{
Header = "Set Download status to '_Not Downloaded'",
IsEnabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || entry.Liberate.IsSeries
};
args.ContextMenuItems.Add(setNotDownloadMenuItem);
if (entry.Liberate.IsSeries)
setNotDownloadMenuItem.Click += (_, __) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.NotLiberated);
else
setNotDownloadMenuItem.Click += (_, __) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.NotLiberated);
#endregion
#region Remove from library
var removeMenuItem = new MenuItem() { Header = "_Remove from library" };
args.ContextMenuItems.Add(removeMenuItem);
if (entry.Liberate.IsSeries)
removeMenuItem.Click += async (_, __) => await ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).RemoveBooksAsync();
else
removeMenuItem.Click += async (_, __) => await Task.Run(entry.LibraryBook.RemoveBook);
#endregion
if (!entry.Liberate.IsSeries)
{
#region Locate file
var locateFileMenuItem = new MenuItem() { Header = "_Locate file..." };
args.ContextMenuItems.Add(locateFileMenuItem);
locateFileMenuItem.Click += async (_, __) =>
{
try
{
var window = this.GetParentWindow();
var openFileDialogOptions = new FilePickerOpenOptions
{
Title = $"Locate the audio file for '{entry.Book.TitleWithSubtitle}'",
AllowMultiple = false,
SuggestedStartLocation = await window.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix),
FileTypeFilter = new FilePickerFileType[]
{
new("All files (*.*)") { Patterns = new[] { "*" } },
}
};
var selectedFiles = await window.StorageProvider.OpenFilePickerAsync(openFileDialogOptions);
var selectedFile = selectedFiles.SingleOrDefault()?.TryGetLocalPath();
if (selectedFile is not null)
FilePathCache.Insert(entry.AudibleProductId, selectedFile);
}
catch (Exception ex)
{
var msg = "Error saving book's location";
await MessageBox.ShowAdminAlert(null, msg, msg, ex);
}
};
#endregion
#region Convert to Mp3
var convertToMp3MenuItem = new MenuItem
{
Header = "_Convert to Mp3",
IsEnabled = entry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated
};
args.ContextMenuItems.Add(convertToMp3MenuItem);
convertToMp3MenuItem.Click += (_, _) => ConvertToMp3Clicked?.Invoke(this, entry.LibraryBook);
#endregion
}
#region Force Re-Download
if (!entry.Liberate.IsSeries)
{
var reDownloadMenuItem = new MenuItem()
{
Header = "Re-download this audiobook",
IsEnabled = entry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated
};
args.ContextMenuItems.Add(reDownloadMenuItem);
reDownloadMenuItem.Click += (s, _) =>
{
//No need to persist this change. It only needs to last long for the file to start downloading
entry.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
LiberateClicked?.Invoke(s, entry.LibraryBook);
};
}
#endregion
args.ContextMenuItems.Add(new Separator());
#region View Bookmarks/Clips
if (!entry.Liberate.IsSeries)
{
var bookRecordMenuItem = new MenuItem { Header = "View _Bookmarks/Clips" };
args.ContextMenuItems.Add(bookRecordMenuItem);
bookRecordMenuItem.Click += async (_, _) => await new BookRecordsDialog(entry.LibraryBook).ShowDialog(VisualRoot as Window);
}
#endregion
#region View All Series
if (entry.Book.SeriesLink.Any())
{
var header = entry.Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series";
var viewSeriesMenuItem = new MenuItem { Header = header };
args.ContextMenuItems.Add(viewSeriesMenuItem);
viewSeriesMenuItem.Click += (_, _) => new SeriesViewDialog(entry.LibraryBook).Show();
}
#endregion
}
#endregion
@@ -428,7 +536,7 @@ namespace LibationAvalonia.Views
var displayWindow = new DescriptionDisplayDialog
{
SpawnLocation = new Point(pt.X, pt.Y),
DescriptionText = gEntry.LongDescription,
DescriptionText = gEntry.Description,
};
void CloseWindow(object o, DataGridRowEventArgs e)

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace LibationFileManager
{
public partial class Configuration
{
public static ReadOnlyDictionary<string, string> HelpText { get; } = new Dictionary<string, string>
{
{ nameof(CombineNestedChapterTitles),"""
If the book has nested chapters, e.g. a chapter named "Part 1"
that contains chapters "Chapter 1" and "Chapter 2", then combine
the chapter titles like the following example:
Part 1: Chapter 1
Part 1: Chapter 2
"""},
{nameof(AllowLibationFixup), """
In addition to the options that are enabled if you allow
"fixing up" the audiobook, it does the following:
* Sets the ©gen metadata tag for the genres.
* Adds the TCOM (@wrt in M4B files) metadata tag for the narrators.
* Unescapes the copyright symbol (replace &#169; with ©)
* Replaces the recording copyright (P) string with
* Adds various other metadata tags recognized by AudiobookShelf
* Sets the embedded cover art image with cover art retrieved from Audible
""" },
}
.AsReadOnly();
public static string GetHelpText(string settingName)
=> HelpText.TryGetValue(settingName, out var value) ? value : null;
}
}

View File

@@ -20,6 +20,8 @@ namespace LibationFileManager
private PersistentDictionary persistentDictionary;
public bool RemoveProperty(string propertyName) => persistentDictionary.RemoveProperty(propertyName);
public T GetNonString<T>(T defaultValue, [CallerMemberName] string propertyName = "") => persistentDictionary.GetNonString(propertyName, defaultValue);
public object GetObject([CallerMemberName] string propertyName = "") => persistentDictionary.GetObject(propertyName);
public string GetString(string defaultValue = null, [CallerMemberName] string propertyName = "") => persistentDictionary.GetString(propertyName, defaultValue);
@@ -73,9 +75,18 @@ namespace LibationFileManager
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
[Description("Set cover art as the folder's icon. (Windows and macOS only)")]
[Description("Set cover art as the folder's icon.")]
public bool UseCoverAsFolderIcon { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Save audiobook metadata to metadata.json")]
public bool SaveMetadataToFile { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Book display grid size")]
public float GridScaleFactor { get => float.Min(2, float.Max(0.5f, GetNonString(defaultValue: 1f))); set => SetNonString(value); }
[Description("Book display font size")]
public float GridFontScaleFactor { get => float.Min(2, float.Max(0.5f, GetNonString(defaultValue: 1f))); set => SetNonString(value); }
[Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")]
public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); }
@@ -164,6 +175,9 @@ namespace LibationFileManager
[Description("Save cover image alongside audiobook?")]
public bool DownloadCoverArt { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Combine nested chapter titles")]
public bool CombineNestedChapterTitles { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Download clips and bookmarks?")]
public bool DownloadClipsBookmarks { get => GetNonString(defaultValue: false); set => SetNonString(value); }

View File

@@ -56,11 +56,11 @@ namespace LibationFileManager
?.FirstOrDefault()
?.Path;
private static List<CacheEntry> getEntries(Func<CacheEntry, bool> predicate)
private static IEnumerable<CacheEntry> getEntries(Func<CacheEntry, bool> predicate)
{
var entries = cache.Where(predicate).ToList();
if (entries is null || !entries.Any())
return null;
return Enumerable.Empty<CacheEntry>();
remove(entries.Where(e => !File.Exists(e.Path)).ToList());

View File

@@ -41,7 +41,8 @@ namespace LibationSearchEngine
{ 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" },
{ FieldType.String, lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()), nameof(Book.Category), "Categories", "CategoriesId", "CategoryId", "CategoriesNames" },
{ FieldType.String, lb => lb.Book.AllCategoryIds() is null ? null : string.Join(", ", lb.Book.AllCategoryIds()), "CategoriesId", "CategoryId" },
{ FieldType.String, lb => lb.Book.AllCategoryNames() is null ? null : string.Join(", ", lb.Book.AllCategoryNames()), "Category", "Categories", "CategoriesNames" },
{ FieldType.String, lb => lb.Book.UserDefinedItem.Tags, TAGS.FirstCharToUpper() },
{ FieldType.String, lb => lb.Book.Locale, "Locale", "Region" },
{ FieldType.String, lb => lb.Account, "Account", "Email" },

View File

@@ -27,7 +27,6 @@ namespace LibationUiBase.GridView
[Browsable(false)] public string AudibleProductId => Book.AudibleProductId;
[Browsable(false)] public LibraryBook LibraryBook { get; protected set; }
[Browsable(false)] public float SeriesIndex { get; protected set; }
[Browsable(false)] public string LongDescription { get; protected set; }
[Browsable(false)] public abstract DateTime DateAdded { get; }
[Browsable(false)] public Book Book => LibraryBook.Book;
@@ -109,23 +108,18 @@ namespace LibationUiBase.GridView
Series = Book.SeriesNames(includeIndex: true);
SeriesOrder = new SeriesOrder(Book.SeriesLink);
Length = GetBookLengthString();
//Ratings are changed using Update(), which is a problem for Avalonia data bindings because
//the reference doesn't change. Clone the rating so that it updates within Avalonia properly.
_myRating = new Rating(Book.UserDefinedItem.Rating.OverallRating, Book.UserDefinedItem.Rating.PerformanceRating, Book.UserDefinedItem.Rating.StoryRating);
RaiseAndSetIfChanged(ref _myRating, Book.UserDefinedItem.Rating, nameof(MyRating));
PurchaseDate = GetPurchaseDateString();
ProductRating = Book.Rating ?? new Rating(0, 0, 0);
Authors = Book.AuthorNames();
Narrators = Book.NarratorNames();
Category = string.Join(" > ", Book.CategoriesNames());
Category = string.Join(", ", Book.LowestCategoryNames());
Misc = GetMiscDisplay(libraryBook);
LastDownload = new(Book.UserDefinedItem);
LongDescription = GetDescriptionDisplay(Book);
Description = TrimTextToWord(LongDescription, 62);
Description = GetDescriptionDisplay(Book);
SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0;
BookTags = GetBookTags();
RaisePropertyChanged(nameof(MyRating));
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
}
@@ -182,6 +176,8 @@ namespace LibationUiBase.GridView
break;
case nameof(udi.Rating):
_myRating = udi.Rating;
//Ratings are changed using Update(), which is a problem for Avalonia data bindings because
//the reference doesn't change. Must call RaisePropertyChanged instead of RaiseAndSetIfChanged
RaisePropertyChanged(nameof(MyRating));
break;
}

View File

@@ -10,7 +10,6 @@ namespace LibationUiBase.GridView
EntryStatus Liberate { get; }
float SeriesIndex { get; }
string AudibleProductId { get; }
string LongDescription { get; }
LibraryBook LibraryBook { get; }
Book Book { get; }
DateTime DateAdded { get; }

View File

@@ -1,6 +1,10 @@
using DataLayer;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Threading;
using System.Linq;
namespace LibationUiBase.GridView
{
@@ -29,6 +33,41 @@ namespace LibationUiBase.GridView
LoadCover();
}
public static async Task<List<IGridEntry>> GetAllProductsAsync(IEnumerable<LibraryBook> libraryBooks)
{
var products = libraryBooks.Where(lb => lb.Book.IsProduct()).ToArray();
int parallelism = int.Max(1, Environment.ProcessorCount - 1);
(int numPer, int rem) = int.DivRem(products.Length, parallelism);
if (rem != 0) numPer++;
var tasks = new Task<IGridEntry[]>[parallelism];
var syncContext = SynchronizationContext.Current;
for (int i = 0; i < parallelism; i++)
{
int start = i * numPer;
tasks[i] = Task.Run(() =>
{
SynchronizationContext.SetSynchronizationContext(syncContext);
int length = int.Min(numPer, products.Length - start);
if (length < 1) return Array.Empty<IGridEntry>();
var result = new IGridEntry[length];
for (int j = 0; j < length; j++)
result[j] = new LibraryBookEntry<TStatus>(products[start + j]);
return result;
});
}
return (await Task.WhenAll(tasks)).SelectMany(a => a).ToList();
}
protected override string GetBookTags() => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
}
}

View File

@@ -1,7 +1,10 @@
using DataLayer;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace LibationUiBase.GridView
{
@@ -54,6 +57,60 @@ namespace LibationUiBase.GridView
LoadCover();
}
public static async Task<List<ISeriesEntry>> GetAllSeriesEntriesAsync(IEnumerable<LibraryBook> libraryBooks)
{
var seriesBooks = libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).ToArray();
var allEpisodes = libraryBooks.Where(lb => lb.Book.IsEpisodeChild()).ToArray();
int parallelism = int.Max(1, Environment.ProcessorCount - 1);
var tasks = new Task[parallelism];
var syncContext = SynchronizationContext.Current;
var q = new BlockingCollection<(int, LibraryBook episode)>();
var seriesEntries = new ISeriesEntry[seriesBooks.Length];
var seriesEpisodes = new ConcurrentBag<ILibraryBookEntry>[seriesBooks.Length];
for (int i = 0; i < parallelism; i++)
{
tasks[i] = Task.Run(() =>
{
SynchronizationContext.SetSynchronizationContext(syncContext);
while (q.TryTake(out var entry, -1))
{
var parent = seriesEntries[entry.Item1];
var episodeBag = seriesEpisodes[entry.Item1];
episodeBag.Add(new LibraryBookEntry<TStatus>(entry.episode, parent));
}
});
}
for (int i = 0; i <seriesBooks.Length; i++)
{
var series = seriesBooks[i];
seriesEntries[i] = new SeriesEntry<TStatus>(series, Enumerable.Empty<LibraryBook>());
seriesEpisodes[i] = new ConcurrentBag<ILibraryBookEntry>();
foreach (var ep in allEpisodes.FindChildren(series))
q.Add((i, ep));
}
q.CompleteAdding();
await Task.WhenAll(tasks);
for (int i = 0; i < seriesBooks.Length; i++)
{
var series = seriesEntries[i];
series.Children.AddRange(seriesEpisodes[i].OrderByDescending(c => c.SeriesOrder));
series.UpdateLibraryBook(series.LibraryBook);
}
return seriesEntries.Where(s => s.Children.Count != 0).ToList();
}
public void RemoveChild(ILibraryBookEntry lbe)
{
Children.Remove(lbe);

View File

@@ -0,0 +1,73 @@
namespace LibationWinForms
{
partial class ClearableTextBox
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
textBox1 = new System.Windows.Forms.TextBox();
button1 = new System.Windows.Forms.Button();
SuspendLayout();
//
// textBox1
//
textBox1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
textBox1.Location = new System.Drawing.Point(0, 0);
textBox1.Margin = new System.Windows.Forms.Padding(0);
textBox1.Name = "textBox1";
textBox1.Size = new System.Drawing.Size(625, 23);
textBox1.TabIndex = 0;
//
// button1
//
button1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
button1.Location = new System.Drawing.Point(623, 0);
button1.Margin = new System.Windows.Forms.Padding(0);
button1.Name = "button1";
button1.Size = new System.Drawing.Size(20, 20);
button1.TabIndex = 1;
button1.Text = "X";
button1.UseVisualStyleBackColor = true;
button1.Click += button1_Click;
//
// ClearableTextBox
//
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
Controls.Add(button1);
Controls.Add(textBox1);
Name = "ClearableTextBox";
Size = new System.Drawing.Size(642, 20);
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.TextBox textBox1;
private System.Windows.Forms.Button button1;
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace LibationWinForms
{
public partial class ClearableTextBox : UserControl
{
public event EventHandler TextCleared;
public override string Text { get => textBox1.Text; set => textBox1.Text = value; }
public override Font Font
{
get => textBox1.Font;
set
{
base.Font = textBox1.Font = button1.Font = value;
OnSizeChanged(EventArgs.Empty);
}
}
public ClearableTextBox()
{
InitializeComponent();
textBox1.KeyDown += (_, e) => OnKeyDown(e);
textBox1.KeyUp += (_, e) => OnKeyUp(e);
textBox1.KeyPress += (_, e) => OnKeyPress(e);
textBox1.TextChanged += (_, e) => OnTextChanged(e);
}
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
Height = button1.Width = button1.Height = textBox1.Height;
textBox1.Width = Width - button1.Width;
button1.Location = new Point(textBox1.Width, 0);
}
private void button1_Click(object sender, System.EventArgs e)
{
textBox1.Clear();
TextCleared?.Invoke(this, EventArgs.Empty);
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing"">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -50,7 +50,7 @@ Author(s): {Book.AuthorNames()}
Narrator(s): {Book.NarratorNames()}
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
Audio Bitrate: {Book.AudioFormat}
Category: {string.Join(" > ", Book.CategoriesNames())}
Category: {string.Join(", ", Book.LowestCategoryNames())}
Purchase Date: {_libraryBook.DateAdded:d}
Language: {Book.Language}
Audible ID: {Book.AudibleProductId}

View File

@@ -39,10 +39,9 @@ namespace LibationWinForms.Dialogs
// knownDirectoryRb
//
knownDirectoryRb.AutoSize = true;
knownDirectoryRb.Location = new System.Drawing.Point(6, 6);
knownDirectoryRb.Margin = new System.Windows.Forms.Padding(6);
knownDirectoryRb.Location = new System.Drawing.Point(3, 3);
knownDirectoryRb.Name = "knownDirectoryRb";
knownDirectoryRb.Size = new System.Drawing.Size(27, 26);
knownDirectoryRb.Size = new System.Drawing.Size(14, 13);
knownDirectoryRb.TabIndex = 0;
knownDirectoryRb.UseVisualStyleBackColor = true;
knownDirectoryRb.CheckedChanged += radioButton_CheckedChanged;
@@ -50,10 +49,9 @@ namespace LibationWinForms.Dialogs
// customDirectoryRb
//
customDirectoryRb.AutoSize = true;
customDirectoryRb.Location = new System.Drawing.Point(4, 124);
customDirectoryRb.Margin = new System.Windows.Forms.Padding(6);
customDirectoryRb.Location = new System.Drawing.Point(2, 62);
customDirectoryRb.Name = "customDirectoryRb";
customDirectoryRb.Size = new System.Drawing.Size(27, 26);
customDirectoryRb.Size = new System.Drawing.Size(14, 13);
customDirectoryRb.TabIndex = 2;
customDirectoryRb.UseVisualStyleBackColor = true;
customDirectoryRb.CheckedChanged += radioButton_CheckedChanged;
@@ -61,19 +59,17 @@ namespace LibationWinForms.Dialogs
// customTb
//
customTb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
customTb.Location = new System.Drawing.Point(44, 116);
customTb.Margin = new System.Windows.Forms.Padding(6);
customTb.Location = new System.Drawing.Point(22, 58);
customTb.Name = "customTb";
customTb.Size = new System.Drawing.Size(1172, 39);
customTb.Size = new System.Drawing.Size(588, 23);
customTb.TabIndex = 3;
//
// customBtn
//
customBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
customBtn.Location = new System.Drawing.Point(1232, 116);
customBtn.Margin = new System.Windows.Forms.Padding(6);
customBtn.Location = new System.Drawing.Point(616, 58);
customBtn.Name = "customBtn";
customBtn.Size = new System.Drawing.Size(82, 54);
customBtn.Size = new System.Drawing.Size(41, 27);
customBtn.TabIndex = 4;
customBtn.Text = "...";
customBtn.UseVisualStyleBackColor = true;
@@ -83,24 +79,23 @@ namespace LibationWinForms.Dialogs
//
directorySelectControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
directorySelectControl.AutoSize = true;
directorySelectControl.Location = new System.Drawing.Point(46, 0);
directorySelectControl.Margin = new System.Windows.Forms.Padding(12);
directorySelectControl.Location = new System.Drawing.Point(23, 0);
directorySelectControl.Margin = new System.Windows.Forms.Padding(6, 6, 6, 6);
directorySelectControl.Name = "directorySelectControl";
directorySelectControl.Size = new System.Drawing.Size(1270, 104);
directorySelectControl.Size = new System.Drawing.Size(635, 55);
directorySelectControl.TabIndex = 5;
//
// DirectoryOrCustomSelectControl
//
AutoScaleDimensions = new System.Drawing.SizeF(192F, 192F);
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
Controls.Add(directorySelectControl);
Controls.Add(customBtn);
Controls.Add(customTb);
Controls.Add(customDirectoryRb);
Controls.Add(knownDirectoryRb);
Margin = new System.Windows.Forms.Padding(6);
Name = "DirectoryOrCustomSelectControl";
Size = new System.Drawing.Size(1320, 176);
Size = new System.Drawing.Size(660, 88);
Load += DirectoryOrCustomSelectControl_Load;
ResumeLayout(false);
PerformLayout();

View File

@@ -37,6 +37,12 @@ namespace LibationWinForms.Dialogs
if (directory != Configuration.KnownDirectories.None)
selectDir(directory, null);
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
//For some reason anchors don't work when the parent form scales up, even with AutoScale
directorySelectControl.Width = customTb.Width = Width;
}
/// <summary>set selection</summary>
public void SelectDirectory(string directory)

View File

@@ -39,32 +39,29 @@ namespace LibationWinForms.Dialogs
directoryComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
directoryComboBox.FormattingEnabled = true;
directoryComboBox.Location = new System.Drawing.Point(0, 0);
directoryComboBox.Margin = new System.Windows.Forms.Padding(6, 6, 6, 6);
directoryComboBox.Name = "directoryComboBox";
directoryComboBox.Size = new System.Drawing.Size(810, 40);
directoryComboBox.Size = new System.Drawing.Size(814, 23);
directoryComboBox.TabIndex = 0;
directoryComboBox.SelectedIndexChanged += directoryComboBox_SelectedIndexChanged;
//
// textBox1
//
textBox1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
textBox1.Location = new System.Drawing.Point(0, 58);
textBox1.Margin = new System.Windows.Forms.Padding(6, 6, 6, 6);
textBox1.Location = new System.Drawing.Point(0, 29);
textBox1.Name = "textBox1";
textBox1.ReadOnly = true;
textBox1.Size = new System.Drawing.Size(810, 39);
textBox1.Size = new System.Drawing.Size(814, 23);
textBox1.TabIndex = 1;
//
// DirectorySelectControl
//
AutoScaleDimensions = new System.Drawing.SizeF(192F, 192F);
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
AutoSize = true;
Controls.Add(textBox1);
Controls.Add(directoryComboBox);
Margin = new System.Windows.Forms.Padding(6, 6, 6, 6);
Name = "DirectorySelectControl";
Size = new System.Drawing.Size(814, 104);
Size = new System.Drawing.Size(814, 55);
Load += DirectorySelectControl_Load;
ResumeLayout(false);
PerformLayout();

View File

@@ -50,6 +50,12 @@ namespace LibationWinForms.Dialogs
return path;
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
//For some reason anchors don't work when the parent form scales up, even with AutoScale
directoryComboBox.Width = textBox1.Width = Width;
}
private DirectoryComboBoxItem selectedItem => (DirectoryComboBoxItem)this.directoryComboBox.SelectedItem;

View File

@@ -37,10 +37,10 @@
// libationFilesDescLbl
//
libationFilesDescLbl.AutoSize = true;
libationFilesDescLbl.Location = new System.Drawing.Point(28, 20);
libationFilesDescLbl.Margin = new System.Windows.Forms.Padding(8, 0, 8, 0);
libationFilesDescLbl.Location = new System.Drawing.Point(14, 10);
libationFilesDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
libationFilesDescLbl.Name = "libationFilesDescLbl";
libationFilesDescLbl.Size = new System.Drawing.Size(76, 32);
libationFilesDescLbl.Size = new System.Drawing.Size(39, 15);
libationFilesDescLbl.TabIndex = 0;
libationFilesDescLbl.Text = "[desc]";
//
@@ -48,10 +48,10 @@
//
cancelBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
cancelBtn.Location = new System.Drawing.Point(1664, 236);
cancelBtn.Margin = new System.Windows.Forms.Padding(8, 6, 8, 6);
cancelBtn.Location = new System.Drawing.Point(832, 118);
cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
cancelBtn.Name = "cancelBtn";
cancelBtn.Size = new System.Drawing.Size(176, 54);
cancelBtn.Size = new System.Drawing.Size(88, 27);
cancelBtn.TabIndex = 3;
cancelBtn.Text = "Cancel";
cancelBtn.UseVisualStyleBackColor = true;
@@ -60,10 +60,10 @@
// saveBtn
//
saveBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
saveBtn.Location = new System.Drawing.Point(1428, 236);
saveBtn.Margin = new System.Windows.Forms.Padding(8, 6, 8, 6);
saveBtn.Location = new System.Drawing.Point(714, 118);
saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
saveBtn.Name = "saveBtn";
saveBtn.Size = new System.Drawing.Size(176, 54);
saveBtn.Size = new System.Drawing.Size(88, 27);
saveBtn.TabIndex = 2;
saveBtn.Text = "Save";
saveBtn.UseVisualStyleBackColor = true;
@@ -72,24 +72,24 @@
// libationFilesSelectControl
//
libationFilesSelectControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
libationFilesSelectControl.Location = new System.Drawing.Point(28, 56);
libationFilesSelectControl.Margin = new System.Windows.Forms.Padding(12);
libationFilesSelectControl.Location = new System.Drawing.Point(14, 28);
libationFilesSelectControl.Margin = new System.Windows.Forms.Padding(6, 6, 6, 6);
libationFilesSelectControl.Name = "libationFilesSelectControl";
libationFilesSelectControl.Size = new System.Drawing.Size(1818, 176);
libationFilesSelectControl.Size = new System.Drawing.Size(906, 88);
libationFilesSelectControl.TabIndex = 1;
//
// LibationFilesDialog
//
AutoScaleDimensions = new System.Drawing.SizeF(192F, 192F);
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
AutoSize = true;
ClientSize = new System.Drawing.Size(1866, 328);
ClientSize = new System.Drawing.Size(933, 164);
Controls.Add(libationFilesSelectControl);
Controls.Add(cancelBtn);
Controls.Add(saveBtn);
Controls.Add(libationFilesDescLbl);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
Margin = new System.Windows.Forms.Padding(8, 6, 8, 6);
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
Name = "LibationFilesDialog";
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "Libation Files location";

View File

@@ -14,12 +14,16 @@ namespace LibationWinForms.Dialogs
this.createCueSheetCbox.Text = desc(nameof(config.CreateCueSheet));
this.downloadCoverArtCbox.Text = desc(nameof(config.DownloadCoverArt));
this.retainAaxFileCbox.Text = desc(nameof(config.RetainAaxFile));
this.combineNestedChapterTitlesCbox.Text = desc(nameof(config.CombineNestedChapterTitles));
this.splitFilesByChapterCbox.Text = desc(nameof(config.SplitFilesByChapter));
this.mergeOpeningEndCreditsCbox.Text = desc(nameof(config.MergeOpeningAndEndCredits));
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
this.moveMoovAtomCbox.Text = desc(nameof(config.MoveMoovToBeginning));
toolTip.SetToolTip(combineNestedChapterTitlesCbox, Configuration.GetHelpText(nameof(config.CombineNestedChapterTitles)));
toolTip.SetToolTip(allowLibationFixupCbox, Configuration.GetHelpText(nameof(config.AllowLibationFixup)));
fileDownloadQualityCb.Items.AddRange(
new object[]
{
@@ -55,6 +59,7 @@ namespace LibationWinForms.Dialogs
fileDownloadQualityCb.SelectedItem = config.FileDownloadQuality;
clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat;
retainAaxFileCbox.Checked = config.RetainAaxFile;
combineNestedChapterTitlesCbox.Checked = config.CombineNestedChapterTitles;
splitFilesByChapterCbox.Checked = config.SplitFilesByChapter;
mergeOpeningEndCreditsCbox.Checked = config.MergeOpeningAndEndCredits;
stripUnabridgedCbox.Checked = config.StripUnabridged;
@@ -99,6 +104,7 @@ namespace LibationWinForms.Dialogs
config.FileDownloadQuality = (Configuration.DownloadQuality)fileDownloadQualityCb.SelectedItem;
config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem;
config.RetainAaxFile = retainAaxFileCbox.Checked;
config.CombineNestedChapterTitles = combineNestedChapterTitlesCbox.Checked;
config.SplitFilesByChapter = splitFilesByChapterCbox.Checked;
config.MergeOpeningAndEndCredits = mergeOpeningEndCreditsCbox.Checked;
config.StripUnabridged = stripUnabridgedCbox.Checked;

View File

File diff suppressed because it is too large Load Diff

View File

@@ -32,6 +32,7 @@ namespace LibationWinForms.Dialogs
badBookRetryRb.Text = Configuration.BadBookAction.Retry.GetDescription();
badBookIgnoreRb.Text = Configuration.BadBookAction.Ignore.GetDescription();
useCoverAsFolderIconCb.Text = desc(nameof(config.UseCoverAsFolderIcon));
saveMetadataToFileCbox.Text = desc(nameof(config.SaveMetadataToFile));
inProgressSelectControl.SetDirectoryItems(new()
{
@@ -60,6 +61,7 @@ namespace LibationWinForms.Dialogs
fileTemplateTb.Text = config.FileTemplate;
chapterFileTemplateTb.Text = config.ChapterFileTemplate;
useCoverAsFolderIconCb.Checked = config.UseCoverAsFolderIcon;
saveMetadataToFileCbox.Checked = config.SaveMetadataToFile;
}
private void Save_DownloadDecrypt(Configuration config)
@@ -77,6 +79,7 @@ namespace LibationWinForms.Dialogs
config.FileTemplate = fileTemplateTb.Text;
config.ChapterFileTemplate = chapterFileTemplateTb.Text;
config.UseCoverAsFolderIcon = useCoverAsFolderIconCb.Checked;
config.SaveMetadataToFile = saveMetadataToFileCbox.Checked;
}
}
}

View File

@@ -23,11 +23,12 @@ namespace LibationWinForms.Dialogs
}
booksLocationDescLbl.Text = desc(nameof(config.Books));
betaOptInCbox.Text = desc(nameof(config.BetaOptIn));
saveEpisodesToSeriesFolderCbox.Text = desc(nameof(config.SavePodcastsToParentFolder));
overwriteExistingCbox.Text = desc(nameof(config.OverwriteExisting));
creationTimeLbl.Text = desc(nameof(config.CreationTime));
lastWriteTimeLbl.Text = desc(nameof(config.LastWriteTime));
gridScaleFactorLbl.Text = desc(nameof(config.GridScaleFactor));
gridFontScaleFactorLbl.Text = desc(nameof(config.GridFontScaleFactor));
var dateTimeSources = Enum.GetValues<Configuration.DateTimeSource>().Select(v => new EnumDiaplay<Configuration.DateTimeSource>(v)).ToArray();
creationTimeCb.Items.AddRange(dateTimeSources);
@@ -51,11 +52,8 @@ namespace LibationWinForms.Dialogs
saveEpisodesToSeriesFolderCbox.Checked = config.SavePodcastsToParentFolder;
overwriteExistingCbox.Checked = config.OverwriteExisting;
betaOptInCbox.Checked = config.BetaOptIn;
if (!betaOptInCbox.Checked)
betaOptInCbox.CheckedChanged += betaOptInCbox_CheckedChanged;
gridScaleFactorTbar.Value = scaleFactorToLinearRange(config.GridScaleFactor);
gridFontScaleFactorTbar.Value = scaleFactorToLinearRange(config.GridFontScaleFactor);
}
private void Save_Important(Configuration config)
@@ -92,39 +90,20 @@ namespace LibationWinForms.Dialogs
config.SavePodcastsToParentFolder = saveEpisodesToSeriesFolderCbox.Checked;
config.OverwriteExisting = overwriteExistingCbox.Checked;
config.BetaOptIn = betaOptInCbox.Checked;
config.CreationTime = ((EnumDiaplay<Configuration.DateTimeSource>)creationTimeCb.SelectedItem).Value;
config.LastWriteTime = ((EnumDiaplay<Configuration.DateTimeSource>)lastWriteTimeCb.SelectedItem).Value;
}
private static int scaleFactorToLinearRange(float scaleFactor)
=> (int)float.Round(100 * MathF.Log2(scaleFactor));
private static float linearRangeToScaleFactor(int value)
=> MathF.Pow(2, value / 100f);
private void betaOptInCbox_CheckedChanged(object sender, EventArgs e)
private void applyDisplaySettingsBtn_Click(object sender, EventArgs e)
{
if (!betaOptInCbox.Checked)
return;
var result = MessageBox.Show(this, @"
You've chosen to opt-in to Libation's beta releases. Thank you! We need all the testers we can get.
These features are works in progress and potentially very buggy. Libation may crash unexpectedly, and your library database may even be corruted. We suggest you back up your LibationContext.db file before proceding.
If bad/weird things happen, please report them at getlibation.com.
".Trim(), "A word of warning...", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2);
if (result == DialogResult.Yes)
{
betaOptInCbox.CheckedChanged -= betaOptInCbox_CheckedChanged;
}
else
{
betaOptInCbox.Checked = false;
}
config.GridFontScaleFactor = linearRangeToScaleFactor(gridFontScaleFactorTbar.Value);
config.GridScaleFactor = linearRangeToScaleFactor(gridScaleFactorTbar.Value);
}
}
}

View File

@@ -9,6 +9,12 @@ namespace LibationWinForms.Dialogs
{
private Configuration config { get; } = Configuration.Instance;
private Func<string, string> desc { get; } = Configuration.GetDescription;
private readonly ToolTip toolTip = new ToolTip
{
InitialDelay = 300,
AutoPopDelay = 10000,
ReshowDelay = 0
};
public SettingsDialog()
{

View File

@@ -1,6 +1,8 @@
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using Dinah.Core.Threading;
using System.Collections.Generic;
namespace LibationWinForms
{
@@ -14,7 +16,6 @@ namespace LibationWinForms
beginBookBackupsToolStripMenuItem.Format(0);
beginPdfBackupsToolStripMenuItem.Format(0);
Load += setBackupCounts;
LibraryCommands.LibrarySizeChanged += setBackupCounts;
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
@@ -40,7 +41,11 @@ namespace LibationWinForms
while (runBackupCountsAgain)
{
runBackupCountsAgain = false;
e.Result = LibraryCommands.GetCounts();
if (e.Argument is not IEnumerable<LibraryBook> lbs)
lbs = DbContexts.GetLibrary_Flat_NoTracking();
e.Result = LibraryCommands.GetCounts(lbs);
}
}

View File

@@ -31,7 +31,7 @@
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.filterHelpBtn = new System.Windows.Forms.Button();
this.filterBtn = new System.Windows.Forms.Button();
this.filterSearchTb = new System.Windows.Forms.TextBox();
this.filterSearchTb = new ClearableTextBox();
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
this.importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.autoScanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
@@ -129,10 +129,11 @@
this.filterSearchTb.Size = new System.Drawing.Size(681, 25);
this.filterSearchTb.TabIndex = 1;
this.filterSearchTb.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress);
//
// menuStrip1
//
this.menuStrip1.ImageScalingSize = new System.Drawing.Size(40, 40);
this.filterSearchTb.TextCleared += filterSearchTb_TextCleared;
//
// menuStrip1
//
this.menuStrip1.ImageScalingSize = new System.Drawing.Size(40, 40);
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.importToolStripMenuItem,
this.liberateToolStripMenuItem,
@@ -315,7 +316,7 @@
this.scanningToolStripMenuItem.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
this.scanningToolStripMenuItem.Enabled = false;
this.scanningToolStripMenuItem.Image = global::LibationWinForms.Properties.Resources.import_16x16;
this.scanningToolStripMenuItem.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.SizeToFit;
this.scanningToolStripMenuItem.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None;
this.scanningToolStripMenuItem.Name = "scanningToolStripMenuItem";
this.scanningToolStripMenuItem.Size = new System.Drawing.Size(93, 20);
this.scanningToolStripMenuItem.Text = "Scanning...";
@@ -651,7 +652,7 @@
private System.Windows.Forms.ToolStripStatusLabel backupsCountsLbl;
private LibationWinForms.FormattableToolStripMenuItem beginBookBackupsToolStripMenuItem;
private LibationWinForms.FormattableToolStripMenuItem beginPdfBackupsToolStripMenuItem;
public System.Windows.Forms.TextBox filterSearchTb;
public ClearableTextBox filterSearchTb;
public System.Windows.Forms.Button filterBtn;
public System.Windows.Forms.Button filterHelpBtn;
public System.Windows.Forms.ToolStripMenuItem settingsToolStripMenuItem;

View File

@@ -10,6 +10,10 @@ namespace LibationWinForms
private void filterHelpBtn_Click(object sender, EventArgs e) => new SearchSyntaxDialog().ShowDialog();
private void filterSearchTb_TextCleared(object sender, EventArgs e)
{
performFilter(string.Empty);
}
private void filterSearchTb_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)Keys.Return)

View File

@@ -4,7 +4,6 @@ using LibationFileManager;
using LibationUiBase.GridView;
using LibationWinForms.ProcessQueue;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
@@ -12,15 +11,14 @@ namespace LibationWinForms
{
public partial class Form1
{
int WidthChange = 0;
private void Configure_ProcessQueue()
{
processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
splitContainer1.Panel2MinSize = this.DpiScale(350);
var coppalseState = Configuration.Instance.GetNonString(defaultValue: false, nameof(splitContainer1.Panel2Collapsed));
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
int width = this.Width;
var coppalseState = Configuration.Instance.GetNonString(defaultValue: false, nameof(splitContainer1.Panel2Collapsed));
SetQueueCollapseState(coppalseState);
this.Width = width;
}

View File

@@ -4,10 +4,8 @@ using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using Dinah.Core;
using Dinah.Core.Threading;
using DataLayer;
using LibationFileManager;
using LibationWinForms.Dialogs;
namespace LibationWinForms
{
@@ -16,11 +14,8 @@ namespace LibationWinForms
public Form1()
{
InitializeComponent();
// Pre-requisite:
// Before calling anything else, including subscribing to events, ensure database exists. If we wait and let it happen lazily, race conditions and errors are likely during new installs
using var _ = DbContexts.GetContext();
//Set this size before restoring form size and position
splitContainer1.Panel2MinSize = this.DpiScale(350);
this.RestoreSizeAndLocation(Configuration.Instance);
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
@@ -57,8 +52,7 @@ namespace LibationWinForms
// Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1'
{
this.Load += (_, __) => productsDisplay.Display();
LibraryCommands.LibrarySizeChanged += (_, __) => this.UIThreadAsync(() => productsDisplay.Display());
LibraryCommands.LibrarySizeChanged += (_, __) => Invoke(() => productsDisplay.DisplayAsync());
}
Shown += Form1_Shown;
}
@@ -78,6 +72,13 @@ namespace LibationWinForms
}
}
public async Task InitLibraryAsync(List<LibraryBook> libraryBooks)
{
runBackupCountsAgain = true;
updateCountsBw.RunWorkerAsync(libraryBooks.Where(b => !b.Book.IsEpisodeParent()));
await productsDisplay.DisplayAsync(libraryBooks);
}
private void Form1_Load(object sender, EventArgs e)
{
if (this.DesignMode)

View File

@@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForms.GridView
{
internal class CoverGridViewColumn : DataGridViewImageColumn
{
public CoverGridViewColumn()
{
CellTemplate = new CoverGridViewCell();
}
}
public class CoverGridViewCell : DataGridViewImageCell
{
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
if (value is Image image)
{
var w = graphics.ScaleX(image.Width);
var h = graphics.ScaleY(image.Height);
var x = cellBounds.Left + (cellBounds.Width - w) / 2;
var y = cellBounds.Top + (cellBounds.Height - h) / 2;
graphics.DrawImage(image, new Rectangle(x, y, w, h));
}
}
}
}

View File

@@ -7,8 +7,10 @@ namespace LibationWinForms.GridView
{
protected void DrawButtonImage(Graphics graphics, Image image, Rectangle cellBounds)
{
var w = graphics.ScaleX(image.Width);
var h = graphics.ScaleY(image.Height);
var scaleFactor = OwningColumn is IDataGridScaleColumn scCol ? scCol.ScaleFactor : 1f;
var w = (int)float.Round(graphics.ScaleX(image.Width) * scaleFactor);
var h = (int)float.Round(graphics.ScaleY(image.Height) * scaleFactor);
var x = cellBounds.Left + (cellBounds.Width - w) / 2;
var y = cellBounds.Top + (cellBounds.Height - h) / 2;

View File

@@ -5,12 +5,18 @@ using LibationUiBase.GridView;
namespace LibationWinForms.GridView
{
public class EditTagsDataGridViewImageButtonColumn : DataGridViewButtonColumn
public interface IDataGridScaleColumn
{
float ScaleFactor { get; set; }
}
public class EditTagsDataGridViewImageButtonColumn : DataGridViewButtonColumn, IDataGridScaleColumn
{
public EditTagsDataGridViewImageButtonColumn()
{
CellTemplate = new EditTagsDataGridViewImageButtonCell();
}
public float ScaleFactor { get; set; }
}
internal class EditTagsDataGridViewImageButtonCell : DataGridViewImageButtonCell

View File

@@ -30,7 +30,6 @@ namespace LibationWinForms.GridView
{
SearchEngineCommands.SearchEngineUpdated += SearchEngineCommands_SearchEngineUpdated;
ListChanged += GridEntryBindingList_ListChanged;
refreshEntries();
}
/// <returns>All items in the list, including those filtered out.</returns>

View File

@@ -4,12 +4,14 @@ using System.Windows.Forms;
namespace LibationWinForms.GridView
{
public class LiberateDataGridViewImageButtonColumn : DataGridViewButtonColumn
public class LiberateDataGridViewImageButtonColumn : DataGridViewButtonColumn, IDataGridScaleColumn
{
public LiberateDataGridViewImageButtonColumn()
{
CellTemplate = new LiberateDataGridViewImageButtonCell();
}
public float ScaleFactor { get; set; }
}
internal class LiberateDataGridViewImageButtonCell : DataGridViewImageButtonCell

View File

@@ -69,7 +69,6 @@
//
this.lblPerform.Anchor = System.Windows.Forms.AnchorStyles.Left;
this.lblPerform.AutoSize = true;
this.lblPerform.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.lblPerform.Location = new System.Drawing.Point(0, 16);
this.lblPerform.Margin = new System.Windows.Forms.Padding(0);
this.lblPerform.Name = "lblPerform";
@@ -103,7 +102,6 @@
//
// noBorderLabel1
//
this.noBorderLabel1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel1.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel1.Location = new System.Drawing.Point(0, 0);
this.noBorderLabel1.Name = "noBorderLabel1";
@@ -116,7 +114,6 @@
//
// noBorderLabel2
//
this.noBorderLabel2.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel2.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel2.Location = new System.Drawing.Point(10, 0);
this.noBorderLabel2.Name = "noBorderLabel2";
@@ -129,7 +126,6 @@
//
// noBorderLabel3
//
this.noBorderLabel3.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel3.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel3.Location = new System.Drawing.Point(20, 0);
this.noBorderLabel3.Name = "noBorderLabel3";
@@ -142,7 +138,6 @@
//
// noBorderLabel4
//
this.noBorderLabel4.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel4.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel4.Location = new System.Drawing.Point(30, 0);
this.noBorderLabel4.Name = "noBorderLabel4";
@@ -155,7 +150,6 @@
//
// noBorderLabel5
//
this.noBorderLabel5.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel5.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel5.Location = new System.Drawing.Point(40, 0);
this.noBorderLabel5.Name = "noBorderLabel5";
@@ -181,7 +175,6 @@
//
// noBorderLabel6
//
this.noBorderLabel6.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel6.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel6.Location = new System.Drawing.Point(0, 0);
this.noBorderLabel6.Name = "noBorderLabel6";
@@ -194,7 +187,6 @@
//
// noBorderLabel7
//
this.noBorderLabel7.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel7.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel7.Location = new System.Drawing.Point(10, 0);
this.noBorderLabel7.Name = "noBorderLabel7";
@@ -207,7 +199,6 @@
//
// noBorderLabel8
//
this.noBorderLabel8.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel8.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel8.Location = new System.Drawing.Point(20, 0);
this.noBorderLabel8.Name = "noBorderLabel8";
@@ -220,7 +211,6 @@
//
// noBorderLabel9
//
this.noBorderLabel9.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel9.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel9.Location = new System.Drawing.Point(30, 0);
this.noBorderLabel9.Name = "noBorderLabel9";
@@ -233,7 +223,6 @@
//
// noBorderLabel10
//
this.noBorderLabel10.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel10.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel10.Location = new System.Drawing.Point(40, 0);
this.noBorderLabel10.Name = "noBorderLabel10";
@@ -259,7 +248,6 @@
//
// noBorderLabel11
//
this.noBorderLabel11.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel11.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel11.Location = new System.Drawing.Point(0, 0);
this.noBorderLabel11.Name = "noBorderLabel11";
@@ -272,7 +260,6 @@
//
// noBorderLabel12
//
this.noBorderLabel12.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel12.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel12.Location = new System.Drawing.Point(10, 0);
this.noBorderLabel12.Name = "noBorderLabel12";
@@ -285,7 +272,6 @@
//
// noBorderLabel13
//
this.noBorderLabel13.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel13.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel13.Location = new System.Drawing.Point(20, 0);
this.noBorderLabel13.Name = "noBorderLabel13";
@@ -298,7 +284,6 @@
//
// noBorderLabel14
//
this.noBorderLabel14.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel14.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel14.Location = new System.Drawing.Point(30, 0);
this.noBorderLabel14.Name = "noBorderLabel14";
@@ -311,7 +296,6 @@
//
// noBorderLabel15
//
this.noBorderLabel15.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBorderLabel15.LabelOffset = new System.Drawing.Point(-3, -3);
this.noBorderLabel15.Location = new System.Drawing.Point(40, 0);
this.noBorderLabel15.Name = "noBorderLabel15";

View File

@@ -37,6 +37,13 @@ namespace LibationWinForms.GridView
public MyRatingCellEditor()
{
InitializeComponent();
this.FontChanged += MyRatingCellEditor_FontChanged;
}
private void MyRatingCellEditor_FontChanged(object sender, EventArgs e)
{
var scale = Font.Size / 9;
Scale(new SizeF(scale, scale));
}
private void Star_MouseEnter(object sender, EventArgs e)

View File

@@ -75,7 +75,7 @@ namespace LibationWinForms.GridView
var displayWindow = new DescriptionDisplay
{
SpawnLocation = PointToScreen(cellRectangle.Location + new Size(cellRectangle.Width, 0)),
DescriptionText = liveGridEntry.LongDescription,
DescriptionText = liveGridEntry.Description,
BorderThickness = 2,
};
@@ -206,6 +206,25 @@ namespace LibationWinForms.GridView
#endregion
}
#region Force Re-Download
if (!entry.Liberate.IsSeries)
{
var reDownloadMenuItem = new ToolStripMenuItem()
{
Text = "Re-download this audiobook",
Enabled = entry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated
};
ctxMenu.Items.Add(reDownloadMenuItem);
reDownloadMenuItem.Click += (s, _) =>
{
//No need to persist this change. It only needs to last long for the file to start downloading
entry.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
LiberateClicked?.Invoke(s, entry.LibraryBook);
};
}
#endregion
ctxMenu.Items.Add(new ToolStripSeparator());
#region View Bookmarks/Clips
@@ -306,22 +325,22 @@ namespace LibationWinForms.GridView
#region UI display functions
public void Display()
public async Task DisplayAsync(List<LibraryBook> libraryBooks = null)
{
try
{
// don't return early if lib size == 0. this will not update correctly if all books are removed
var lib = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true);
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking(includeParents: true);
if (!hasBeenDisplayed)
{
// bind
productsGrid.BindToGrid(lib);
await productsGrid.BindToGridAsync(libraryBooks);
hasBeenDisplayed = true;
InitialLoaded?.Invoke(this, new());
}
else
productsGrid.UpdateGrid(lib);
productsGrid.UpdateGrid(libraryBooks);
}
catch (Exception ex)
{

View File

@@ -2,7 +2,7 @@
namespace LibationWinForms.GridView
{
partial class ProductsGrid
partial class ProductsGrid
{
/// <summary>
/// Required designer variable.
@@ -30,254 +30,260 @@ namespace LibationWinForms.GridView
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
components = new System.ComponentModel.Container();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
this.removeGVColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
this.liberateGVColumn = new LibationWinForms.GridView.LiberateDataGridViewImageButtonColumn();
this.coverGVColumn = new CoverGridViewColumn();
this.titleGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.authorsGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.narratorsGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.lengthGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.seriesGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.seriesOrderGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.descriptionGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.categoryGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.productRatingGVColumn = new LibationWinForms.GridView.MyRatingGridViewColumn();
this.purchaseDateGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.myRatingGVColumn = new LibationWinForms.GridView.MyRatingGridViewColumn();
this.miscGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.lastDownloadedGVColumn = new LastDownloadedGridViewColumn();
this.tagAndDetailsGVColumn = new LibationWinForms.GridView.EditTagsDataGridViewImageButtonColumn();
this.showHideColumnsContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components);
this.syncBindingSource = new LibationWinForms.GridView.SyncBindingSource(this.components);
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).BeginInit();
this.SuspendLayout();
gridEntryDataGridView = new System.Windows.Forms.DataGridView();
removeGVColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
liberateGVColumn = new LiberateDataGridViewImageButtonColumn();
coverGVColumn = new System.Windows.Forms.DataGridViewImageColumn();
titleGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
authorsGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
narratorsGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
lengthGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
seriesGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
seriesOrderGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
descriptionGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
categoryGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
productRatingGVColumn = new MyRatingGridViewColumn();
purchaseDateGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
myRatingGVColumn = new MyRatingGridViewColumn();
miscGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
lastDownloadedGVColumn = new LastDownloadedGridViewColumn();
tagAndDetailsGVColumn = new EditTagsDataGridViewImageButtonColumn();
showHideColumnsContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(components);
syncBindingSource = new SyncBindingSource(components);
((System.ComponentModel.ISupportInitialize)gridEntryDataGridView).BeginInit();
((System.ComponentModel.ISupportInitialize)syncBindingSource).BeginInit();
SuspendLayout();
//
// gridEntryDataGridView
//
this.gridEntryDataGridView.AllowUserToAddRows = false;
this.gridEntryDataGridView.AllowUserToDeleteRows = false;
this.gridEntryDataGridView.AllowUserToOrderColumns = true;
this.gridEntryDataGridView.AllowUserToResizeRows = false;
this.gridEntryDataGridView.AutoGenerateColumns = false;
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.removeGVColumn,
this.liberateGVColumn,
this.coverGVColumn,
this.titleGVColumn,
this.authorsGVColumn,
this.narratorsGVColumn,
this.lengthGVColumn,
this.seriesGVColumn,
this.seriesOrderGVColumn,
this.descriptionGVColumn,
this.categoryGVColumn,
this.productRatingGVColumn,
this.purchaseDateGVColumn,
this.myRatingGVColumn,
this.miscGVColumn,
this.lastDownloadedGVColumn,
this.tagAndDetailsGVColumn});
this.gridEntryDataGridView.ContextMenuStrip = this.showHideColumnsContextMenuStrip;
this.gridEntryDataGridView.DataSource = this.syncBindingSource;
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Window;
dataGridViewCellStyle1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.ControlText;
dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight;
dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
this.gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle1;
this.gridEntryDataGridView.Dock = System.Windows.Forms.DockStyle.Fill;
this.gridEntryDataGridView.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnEnter;
this.gridEntryDataGridView.Location = new System.Drawing.Point(0, 0);
this.gridEntryDataGridView.Name = "gridEntryDataGridView";
this.gridEntryDataGridView.RowHeadersVisible = false;
this.gridEntryDataGridView.RowTemplate.Height = 82;
this.gridEntryDataGridView.Size = new System.Drawing.Size(1570, 380);
this.gridEntryDataGridView.TabIndex = 0;
this.gridEntryDataGridView.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView_CellContentClick);
this.gridEntryDataGridView.CellToolTipTextNeeded += new System.Windows.Forms.DataGridViewCellToolTipTextNeededEventHandler(this.gridEntryDataGridView_CellToolTipTextNeeded);
gridEntryDataGridView.AllowUserToAddRows = false;
gridEntryDataGridView.AllowUserToDeleteRows = false;
gridEntryDataGridView.AllowUserToOrderColumns = true;
gridEntryDataGridView.AllowUserToResizeRows = false;
gridEntryDataGridView.AutoGenerateColumns = false;
gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { removeGVColumn, liberateGVColumn, coverGVColumn, titleGVColumn, authorsGVColumn, narratorsGVColumn, lengthGVColumn, seriesGVColumn, seriesOrderGVColumn, descriptionGVColumn, categoryGVColumn, productRatingGVColumn, purchaseDateGVColumn, myRatingGVColumn, miscGVColumn, lastDownloadedGVColumn, tagAndDetailsGVColumn });
gridEntryDataGridView.ContextMenuStrip = showHideColumnsContextMenuStrip;
gridEntryDataGridView.DataSource = syncBindingSource;
dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle2.BackColor = System.Drawing.SystemColors.Window;
dataGridViewCellStyle2.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
dataGridViewCellStyle2.ForeColor = System.Drawing.SystemColors.ControlText;
dataGridViewCellStyle2.SelectionBackColor = System.Drawing.SystemColors.Highlight;
dataGridViewCellStyle2.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
dataGridViewCellStyle2.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle2;
gridEntryDataGridView.Dock = System.Windows.Forms.DockStyle.Fill;
gridEntryDataGridView.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnEnter;
gridEntryDataGridView.Location = new System.Drawing.Point(0, 0);
gridEntryDataGridView.Margin = new System.Windows.Forms.Padding(6);
gridEntryDataGridView.Name = "gridEntryDataGridView";
gridEntryDataGridView.RowHeadersVisible = false;
gridEntryDataGridView.RowHeadersWidth = 82;
gridEntryDataGridView.RowTemplate.Height = 82;
gridEntryDataGridView.Size = new System.Drawing.Size(3140, 760);
gridEntryDataGridView.TabIndex = 0;
gridEntryDataGridView.CellContentClick += DataGridView_CellContentClick;
gridEntryDataGridView.CellToolTipTextNeeded += gridEntryDataGridView_CellToolTipTextNeeded;
//
// removeGVColumn
//
this.removeGVColumn.DataPropertyName = "Remove";
this.removeGVColumn.FalseValue = "";
this.removeGVColumn.Frozen = true;
this.removeGVColumn.HeaderText = "Remove";
this.removeGVColumn.IndeterminateValue = "";
this.removeGVColumn.MinimumWidth = 60;
this.removeGVColumn.Name = "removeGVColumn";
this.removeGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
this.removeGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
this.removeGVColumn.ThreeState = true;
this.removeGVColumn.TrueValue = "";
this.removeGVColumn.Width = 60;
removeGVColumn.DataPropertyName = "Remove";
removeGVColumn.FalseValue = "";
removeGVColumn.Frozen = true;
removeGVColumn.HeaderText = "Remove";
removeGVColumn.IndeterminateValue = "";
removeGVColumn.MinimumWidth = 60;
removeGVColumn.Name = "removeGVColumn";
removeGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
removeGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
removeGVColumn.ThreeState = true;
removeGVColumn.TrueValue = "";
removeGVColumn.Width = 60;
//
// liberateGVColumn
//
this.liberateGVColumn.DataPropertyName = "Liberate";
this.liberateGVColumn.HeaderText = "Liberate";
this.liberateGVColumn.Name = "liberateGVColumn";
this.liberateGVColumn.ReadOnly = true;
this.liberateGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
this.liberateGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
this.liberateGVColumn.Width = 75;
liberateGVColumn.DataPropertyName = "Liberate";
liberateGVColumn.HeaderText = "Liberate";
liberateGVColumn.MinimumWidth = 10;
liberateGVColumn.Name = "liberateGVColumn";
liberateGVColumn.ReadOnly = true;
liberateGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
liberateGVColumn.ScaleFactor = 0F;
liberateGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
liberateGVColumn.Width = 75;
//
// coverGVColumn
//
this.coverGVColumn.DataPropertyName = "Cover";
this.coverGVColumn.HeaderText = "Cover";
this.coverGVColumn.Name = "coverGVColumn";
this.coverGVColumn.ReadOnly = true;
this.coverGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
this.coverGVColumn.ToolTipText = "Cover Art";
this.coverGVColumn.Width = 80;
coverGVColumn.DataPropertyName = "Cover";
coverGVColumn.HeaderText = "Cover";
coverGVColumn.ImageLayout = System.Windows.Forms.DataGridViewImageCellLayout.Zoom;
coverGVColumn.MinimumWidth = 10;
coverGVColumn.Name = "coverGVColumn";
coverGVColumn.ReadOnly = true;
coverGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
coverGVColumn.ToolTipText = "Cover Art";
coverGVColumn.Width = 80;
//
// titleGVColumn
//
this.titleGVColumn.DataPropertyName = "Title";
this.titleGVColumn.HeaderText = "Title";
this.titleGVColumn.Name = "titleGVColumn";
this.titleGVColumn.ReadOnly = true;
this.titleGVColumn.Width = 200;
titleGVColumn.DataPropertyName = "Title";
titleGVColumn.HeaderText = "Title";
titleGVColumn.MinimumWidth = 10;
titleGVColumn.Name = "titleGVColumn";
titleGVColumn.ReadOnly = true;
titleGVColumn.Width = 200;
//
// authorsGVColumn
//
this.authorsGVColumn.DataPropertyName = "Authors";
this.authorsGVColumn.HeaderText = "Authors";
this.authorsGVColumn.Name = "authorsGVColumn";
this.authorsGVColumn.ReadOnly = true;
this.authorsGVColumn.Width = 100;
authorsGVColumn.DataPropertyName = "Authors";
authorsGVColumn.HeaderText = "Authors";
authorsGVColumn.MinimumWidth = 10;
authorsGVColumn.Name = "authorsGVColumn";
authorsGVColumn.ReadOnly = true;
authorsGVColumn.Width = 100;
//
// narratorsGVColumn
//
this.narratorsGVColumn.DataPropertyName = "Narrators";
this.narratorsGVColumn.HeaderText = "Narrators";
this.narratorsGVColumn.Name = "narratorsGVColumn";
this.narratorsGVColumn.ReadOnly = true;
this.narratorsGVColumn.Width = 100;
narratorsGVColumn.DataPropertyName = "Narrators";
narratorsGVColumn.HeaderText = "Narrators";
narratorsGVColumn.MinimumWidth = 10;
narratorsGVColumn.Name = "narratorsGVColumn";
narratorsGVColumn.ReadOnly = true;
narratorsGVColumn.Width = 100;
//
// lengthGVColumn
//
this.lengthGVColumn.DataPropertyName = "Length";
this.lengthGVColumn.HeaderText = "Length";
this.lengthGVColumn.Name = "lengthGVColumn";
this.lengthGVColumn.ReadOnly = true;
this.lengthGVColumn.Width = 100;
this.lengthGVColumn.ToolTipText = "Recording Length";
lengthGVColumn.DataPropertyName = "Length";
lengthGVColumn.HeaderText = "Length";
lengthGVColumn.MinimumWidth = 10;
lengthGVColumn.Name = "lengthGVColumn";
lengthGVColumn.ReadOnly = true;
lengthGVColumn.ToolTipText = "Recording Length";
lengthGVColumn.Width = 100;
//
// seriesGVColumn
//
this.seriesGVColumn.DataPropertyName = "Series";
this.seriesGVColumn.HeaderText = "Series";
this.seriesGVColumn.Name = "seriesGVColumn";
this.seriesGVColumn.ReadOnly = true;
this.seriesGVColumn.Width = 100;
seriesGVColumn.DataPropertyName = "Series";
seriesGVColumn.HeaderText = "Series";
seriesGVColumn.MinimumWidth = 10;
seriesGVColumn.Name = "seriesGVColumn";
seriesGVColumn.ReadOnly = true;
seriesGVColumn.Width = 100;
//
// seriesOrderGVColumn
//
this.seriesOrderGVColumn.DataPropertyName = "SeriesOrder";
this.seriesOrderGVColumn.HeaderText = "Series\r\nOrder";
this.seriesOrderGVColumn.Name = "seriesOrderGVColumn";
this.seriesOrderGVColumn.Width = 60;
this.seriesOrderGVColumn.ReadOnly = true;
this.seriesOrderGVColumn.DefaultCellStyle.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleCenter;
seriesOrderGVColumn.DataPropertyName = "SeriesOrder";
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleCenter;
seriesOrderGVColumn.DefaultCellStyle = dataGridViewCellStyle1;
seriesOrderGVColumn.HeaderText = "Series\r\nOrder";
seriesOrderGVColumn.MinimumWidth = 10;
seriesOrderGVColumn.Name = "seriesOrderGVColumn";
seriesOrderGVColumn.ReadOnly = true;
seriesOrderGVColumn.Width = 60;
//
// descriptionGVColumn
//
this.descriptionGVColumn.DataPropertyName = "Description";
this.descriptionGVColumn.HeaderText = "Description";
this.descriptionGVColumn.Name = "descriptionGVColumn";
this.descriptionGVColumn.ReadOnly = true;
this.descriptionGVColumn.Width = 100;
this.descriptionGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
descriptionGVColumn.DataPropertyName = "Description";
descriptionGVColumn.HeaderText = "Description";
descriptionGVColumn.MinimumWidth = 10;
descriptionGVColumn.Name = "descriptionGVColumn";
descriptionGVColumn.ReadOnly = true;
descriptionGVColumn.Width = 100;
//
// categoryGVColumn
//
this.categoryGVColumn.DataPropertyName = "Category";
this.categoryGVColumn.HeaderText = "Category";
this.categoryGVColumn.Name = "categoryGVColumn";
this.categoryGVColumn.ReadOnly = true;
this.categoryGVColumn.Width = 100;
categoryGVColumn.DataPropertyName = "Category";
categoryGVColumn.HeaderText = "Category";
categoryGVColumn.MinimumWidth = 10;
categoryGVColumn.Name = "categoryGVColumn";
categoryGVColumn.ReadOnly = true;
categoryGVColumn.Width = 100;
//
// productRatingGVColumn
//
this.productRatingGVColumn.DataPropertyName = "ProductRating";
this.productRatingGVColumn.HeaderText = "Product Rating";
this.productRatingGVColumn.Name = "productRatingGVColumn";
this.productRatingGVColumn.ReadOnly = true;
this.productRatingGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
this.productRatingGVColumn.Width = 112;
productRatingGVColumn.DataPropertyName = "ProductRating";
productRatingGVColumn.HeaderText = "Product Rating";
productRatingGVColumn.MinimumWidth = 10;
productRatingGVColumn.Name = "productRatingGVColumn";
productRatingGVColumn.ReadOnly = true;
productRatingGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
productRatingGVColumn.Width = 112;
//
// purchaseDateGVColumn
//
this.purchaseDateGVColumn.DataPropertyName = "PurchaseDate";
this.purchaseDateGVColumn.HeaderText = "Purchase Date";
this.purchaseDateGVColumn.Name = "purchaseDateGVColumn";
this.purchaseDateGVColumn.ReadOnly = true;
this.purchaseDateGVColumn.Width = 100;
purchaseDateGVColumn.DataPropertyName = "PurchaseDate";
purchaseDateGVColumn.HeaderText = "Purchase Date";
purchaseDateGVColumn.MinimumWidth = 10;
purchaseDateGVColumn.Name = "purchaseDateGVColumn";
purchaseDateGVColumn.ReadOnly = true;
purchaseDateGVColumn.Width = 100;
//
// myRatingGVColumn
//
this.myRatingGVColumn.DataPropertyName = "MyRating";
this.myRatingGVColumn.HeaderText = "My Rating";
this.myRatingGVColumn.Name = "myRatingGVColumn";
this.myRatingGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
this.myRatingGVColumn.Width = 112;
myRatingGVColumn.DataPropertyName = "MyRating";
myRatingGVColumn.HeaderText = "My Rating";
myRatingGVColumn.MinimumWidth = 10;
myRatingGVColumn.Name = "myRatingGVColumn";
myRatingGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
myRatingGVColumn.Width = 112;
//
// miscGVColumn
//
this.miscGVColumn.DataPropertyName = "Misc";
this.miscGVColumn.HeaderText = "Misc";
this.miscGVColumn.Name = "miscGVColumn";
this.miscGVColumn.ReadOnly = true;
this.miscGVColumn.Width = 140;
miscGVColumn.DataPropertyName = "Misc";
miscGVColumn.HeaderText = "Misc";
miscGVColumn.MinimumWidth = 10;
miscGVColumn.Name = "miscGVColumn";
miscGVColumn.ReadOnly = true;
miscGVColumn.Width = 140;
//
// lastDownloadedGVColumn
//
this.lastDownloadedGVColumn.DataPropertyName = "LastDownload";
this.lastDownloadedGVColumn.HeaderText = "Last Download";
this.lastDownloadedGVColumn.Name = "lastDownloadedGVColumn";
this.lastDownloadedGVColumn.ReadOnly = true;
this.lastDownloadedGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
this.lastDownloadedGVColumn.Width = 108;
lastDownloadedGVColumn.DataPropertyName = "LastDownload";
lastDownloadedGVColumn.HeaderText = "Last Download";
lastDownloadedGVColumn.MinimumWidth = 10;
lastDownloadedGVColumn.Name = "lastDownloadedGVColumn";
lastDownloadedGVColumn.ReadOnly = true;
lastDownloadedGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
lastDownloadedGVColumn.Width = 108;
//
// tagAndDetailsGVColumn
//
this.tagAndDetailsGVColumn.DataPropertyName = "BookTags";
this.tagAndDetailsGVColumn.HeaderText = "Tags and Details";
this.tagAndDetailsGVColumn.Name = "tagAndDetailsGVColumn";
this.tagAndDetailsGVColumn.ReadOnly = true;
this.tagAndDetailsGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
this.tagAndDetailsGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
this.tagAndDetailsGVColumn.Width = 100;
tagAndDetailsGVColumn.DataPropertyName = "BookTags";
tagAndDetailsGVColumn.HeaderText = "Tags and Details";
tagAndDetailsGVColumn.MinimumWidth = 10;
tagAndDetailsGVColumn.Name = "tagAndDetailsGVColumn";
tagAndDetailsGVColumn.ReadOnly = true;
tagAndDetailsGVColumn.ScaleFactor = 0F;
tagAndDetailsGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
tagAndDetailsGVColumn.Width = 100;
//
// showHideColumnsContextMenuStrip
//
this.showHideColumnsContextMenuStrip.Name = "contextMenuStrip1";
this.showHideColumnsContextMenuStrip.Size = new System.Drawing.Size(61, 4);
showHideColumnsContextMenuStrip.ImageScalingSize = new System.Drawing.Size(32, 32);
showHideColumnsContextMenuStrip.Name = "contextMenuStrip1";
showHideColumnsContextMenuStrip.ShowCheckMargin = true;
showHideColumnsContextMenuStrip.Size = new System.Drawing.Size(83, 4);
//
// syncBindingSource
//
this.syncBindingSource.DataSource = typeof(IGridEntry);
syncBindingSource.DataSource = typeof(IGridEntry);
//
// ProductsGrid
//
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.AutoScroll = true;
this.Controls.Add(this.gridEntryDataGridView);
this.Name = "ProductsGrid";
this.Size = new System.Drawing.Size(1570, 380);
this.Load += new System.EventHandler(this.ProductsGrid_Load);
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).EndInit();
this.ResumeLayout(false);
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
AutoScroll = true;
Controls.Add(gridEntryDataGridView);
Name = "ProductsGrid";
Size = new System.Drawing.Size(1570, 380);
Load += new System.EventHandler(ProductsGrid_Load);
((System.ComponentModel.ISupportInitialize)(gridEntryDataGridView)).EndInit();
((System.ComponentModel.ISupportInitialize)(syncBindingSource)).EndInit();
ResumeLayout(false);
}
@@ -288,7 +294,7 @@ namespace LibationWinForms.GridView
private SyncBindingSource syncBindingSource;
private System.Windows.Forms.DataGridViewCheckBoxColumn removeGVColumn;
private LiberateDataGridViewImageButtonColumn liberateGVColumn;
private CoverGridViewColumn coverGVColumn;
private System.Windows.Forms.DataGridViewImageColumn coverGVColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn titleGVColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn authorsGVColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn narratorsGVColumn;

View File

@@ -1,12 +1,15 @@
using DataLayer;
using Dinah.Core;
using Dinah.Core.WindowsDesktop.Forms;
using LibationFileManager;
using LibationUiBase.GridView;
using NPOI.SS.Formula.Functions;
using System;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForms.GridView
@@ -44,24 +47,76 @@ namespace LibationWinForms.GridView
gridEntryDataGridView.CellContextMenuStripNeeded += GridEntryDataGridView_CellContextMenuStripNeeded;
removeGVColumn.Frozen = false;
gridEntryDataGridView.RowTemplate.Height = this.DpiScale(gridEntryDataGridView.RowTemplate.Height);
defaultFont = gridEntryDataGridView.DefaultCellStyle.Font;
setGridFontScale(Configuration.Instance.GridFontScaleFactor);
setGridScale(Configuration.Instance.GridScaleFactor);
Configuration.Instance.PropertyChanged += Configuration_ScaleChanged;
Configuration.Instance.PropertyChanged += Configuration_FontScaleChanged;
gridEntryDataGridView.Disposed += (_, _) =>
{
Configuration.Instance.PropertyChanged -= Configuration_ScaleChanged;
Configuration.Instance.PropertyChanged -= Configuration_FontScaleChanged;
};
}
#region Scaling
[PropertyChangeFilter(nameof(Configuration.GridFontScaleFactor))]
private void Configuration_FontScaleChanged(object sender, PropertyChangedEventArgsEx e)
=> setGridFontScale((float)e.NewValue);
[PropertyChangeFilter(nameof(Configuration.GridScaleFactor))]
private void Configuration_ScaleChanged(object sender, PropertyChangedEventArgsEx e)
=> setGridScale((float)e.NewValue);
/// <summary>
/// Keep track of the original dimensions for rescaling
/// </summary>
private static readonly Dictionary<DataGridViewElement, int> originalDims = new();
private readonly Font defaultFont;
private void setGridScale(float scale)
{
foreach (var col in gridEntryDataGridView.Columns.Cast<DataGridViewColumn>())
{
//Only resize fixed-width columns. The rest can be adjusted by users.
if (col.Resizable is DataGridViewTriState.False)
{
if (!originalDims.ContainsKey(col))
originalDims[col] = col.Width;
col.Width = this.DpiScale(originalDims[col], scale);
}
if (col is IDataGridScaleColumn scCol)
scCol.ScaleFactor = scale;
}
if (!originalDims.ContainsKey(gridEntryDataGridView.RowTemplate))
originalDims[gridEntryDataGridView.RowTemplate] = gridEntryDataGridView.RowTemplate.Height;
var height = gridEntryDataGridView.RowTemplate.Height = this.DpiScale(originalDims[gridEntryDataGridView.RowTemplate], scale);
foreach (var row in gridEntryDataGridView.Rows.Cast<DataGridViewRow>())
row.Height = height;
}
private void setGridFontScale(float scale)
=> gridEntryDataGridView.DefaultCellStyle.Font = new Font(defaultFont.FontFamily, defaultFont.Size * scale);
#endregion
private void GridEntryDataGridView_CellContextMenuStripNeeded(object sender, DataGridViewCellContextMenuStripNeededEventArgs e)
{
// header
if (e.RowIndex < 0)
return;
// cover
else if (e.ColumnIndex == coverGVColumn.Index)
return;
e.ContextMenuStrip = new ContextMenuStrip();
// any non-stop light
if (e.ColumnIndex != liberateGVColumn.Index)
// any column except cover & stop light
if (e.ColumnIndex != liberateGVColumn.Index && e.ColumnIndex != coverGVColumn.Index)
{
e.ContextMenuStrip.Items.Add("Copy", null, (_, __) =>
e.ContextMenuStrip.Items.Add("Copy Cell Contents", null, (_, __) =>
{
try
{
@@ -70,14 +125,13 @@ namespace LibationWinForms.GridView
Clipboard.SetDataObject(text, false, 5, 150);
}
catch { }
});
}
else
{
var entry = getGridEntry(e.RowIndex);
var name = gridEntryDataGridView.Columns[e.ColumnIndex].DataPropertyName;
LiberateContextMenuStripNeeded?.Invoke(entry, e.ContextMenuStrip);
});
e.ContextMenuStrip.Items.Add(new ToolStripSeparator());
}
var entry = getGridEntry(e.RowIndex);
var name = gridEntryDataGridView.Columns[e.ColumnIndex].DataPropertyName;
LiberateContextMenuStripNeeded?.Invoke(entry, e.ContextMenuStrip);
}
private void EnableDoubleBuffering()
@@ -160,27 +214,23 @@ namespace LibationWinForms.GridView
}
}
internal void BindToGrid(List<LibraryBook> dbBooks)
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
{
var geList = dbBooks
.Where(lb => lb.Book.IsProduct())
.Select(b => new LibraryBookEntry<WinFormsEntryStatus>(b))
.ToList<IGridEntry>();
var geList = await LibraryBookEntry<WinFormsEntryStatus>.GetAllProductsAsync(dbBooks);
var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild());
var seriesEntries = await SeriesEntry<WinFormsEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
var seriesBooks = dbBooks.Where(lb => lb.Book.IsEpisodeParent()).ToList();
geList.AddRange(seriesEntries);
//Sort descending by date (default sort property)
var comparer = new RowComparer();
geList.Sort((a, b) => comparer.Compare(b, a));
foreach (var parent in seriesBooks)
//Add all children beneath their parent
foreach (var series in seriesEntries)
{
var seriesEpisodes = episodes.FindChildren(parent);
if (!seriesEpisodes.Any()) continue;
var seriesEntry = new SeriesEntry<WinFormsEntryStatus>(parent, seriesEpisodes);
geList.Add(seriesEntry);
geList.AddRange(seriesEntry.Children);
var seriesIndex = geList.IndexOf(series);
foreach (var child in series.Children)
geList.Insert(++seriesIndex, child);
}
bindingList = new GridEntryBindingList(geList);
@@ -307,17 +357,22 @@ namespace LibationWinForms.GridView
//Series exists. Create and add episode child then update the SeriesEntry
episodeEntry = new LibraryBookEntry<WinFormsEntryStatus>(episodeBook, seriesEntry);
seriesEntry.Children.Add(episodeEntry);
seriesEntry.Children.Sort((c1,c2) => c1.SeriesIndex.CompareTo(c2.SeriesIndex));
seriesEntry.Children.Sort((c1, c2) => c1.SeriesIndex.CompareTo(c2.SeriesIndex));
var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId);
seriesEntry.UpdateLibraryBook(seriesBook);
}
//Series entry must be expanded so its child can
//be placed in the correct position beneath it.
var isExpanded = seriesEntry.Liberate.Expanded;
bindingList.ExpandItem(seriesEntry);
//Add episode to the grid beneath the parent
int seriesIndex = bindingList.IndexOf(seriesEntry);
int episodeIndex = seriesEntry.Children.IndexOf(episodeEntry);
bindingList.Insert(seriesIndex + 1 + episodeIndex, episodeEntry);
if (seriesEntry.Liberate.Expanded)
if (isExpanded)
bindingList.ExpandItem(seriesEntry);
else
bindingList.CollapseItem(seriesEntry);
@@ -332,6 +387,8 @@ namespace LibationWinForms.GridView
public void Filter(string searchString)
{
if (bindingList is null) return;
int visibleCount = bindingList.Count;
if (string.IsNullOrEmpty(searchString))
@@ -371,16 +428,19 @@ namespace LibationWinForms.GridView
var itemName = column.DataPropertyName;
var visible = gridColumnsVisibilities.GetValueOrDefault(itemName, true);
var menuItem = new ToolStripMenuItem()
var menuItem = new ToolStripMenuItem(column.HeaderText)
{
Text = column.HeaderText,
Checked = visible,
Tag = itemName
};
menuItem.Click += HideMenuItem_Click;
showHideColumnsContextMenuStrip.Items.Add(menuItem);
column.Width = gridColumnsWidths.GetValueOrDefault(itemName, this.DpiScale(column.Width));
//Only set column widths for user resizable columns.
//Fixed column widths are set by setGridScale()
if (column.Resizable is not DataGridViewTriState.False)
column.Width = gridColumnsWidths.GetValueOrDefault(itemName, this.DpiScale(column.Width));
column.MinimumWidth = 10;
column.HeaderCell.ContextMenuStrip = showHideColumnsContextMenuStrip;
column.Visible = visible;
@@ -459,6 +519,7 @@ namespace LibationWinForms.GridView
var dictionary = config.GridColumnsWidths;
dictionary[e.Column.DataPropertyName] = e.Column.Width;
config.GridColumnsWidths = dictionary;
}
#endregion

View File

@@ -1,4 +1,64 @@
<root>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing"">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">

View File

@@ -37,7 +37,7 @@
<ItemGroup>
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="7.2.2.1" />
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="7.2.3.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -249,9 +249,12 @@ namespace LibationWinForms.ProcessQueue
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
{
byte[] coverData = PictureStorage
.GetPictureSynchronously(
new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500));
var quality
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High
? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
: new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
byte[] coverData = PictureStorage.GetPictureSynchronously(quality);
AudioDecodable_CoverImageDiscovered(this, coverData);
return coverData;

View File

@@ -2,12 +2,13 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using AppScaffolding;
using Dinah.Core;
using DataLayer;
using LibationFileManager;
using LibationWinForms.Dialogs;
using Serilog;
namespace LibationWinForms
{
@@ -20,6 +21,7 @@ namespace LibationWinForms
[STAThread]
static void Main()
{
Task<List<LibraryBook>> libraryLoadTask;
try
{
//// Uncomment to see Console. Must be called before anything writes to Console.
@@ -48,6 +50,17 @@ namespace LibationWinForms
// migrations which require Forms or are long-running
RunWindowsOnlyMigrations(config);
//*******************************************************************//
// //
// Start loading the library as soon as possible //
// //
// Before calling anything else, including subscribing to events, //
// to ensure database exists. If we wait and let it happen lazily, //
// race conditions and errors are likely during new installs //
// //
//*******************************************************************//
libraryLoadTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
MessageBoxLib.VerboseLoggingWarning_ShowIfTrue();
// logging is init'd here
@@ -71,7 +84,9 @@ namespace LibationWinForms
// global exception handling (ShowAdminAlert) attempts to use logging. only call it after logging has been init'd
postLoggingGlobalExceptionHandling();
Application.Run(new Form1());
var form1 = new Form1();
form1.Load += async (_, _) => await form1.InitLibraryAsync(await libraryLoadTask);
Application.Run(form1);
}
private static void RunInstaller(Configuration config)
@@ -153,6 +168,17 @@ namespace LibationWinForms
// examples:
// - only supported in winforms. don't move to app scaffolding
// - long running. won't get a chance to finish in cli. don't move to app scaffolding
const string hasMigratedKey = "hasMigratedToHighDPI";
if (!config.GetNonString(defaultValue: false, hasMigratedKey))
{
config.RemoveProperty(nameof(config.GridColumnsWidths));
foreach (var form in typeof(Program).Assembly.GetTypes().Where(t => t.IsSubclassOf(typeof(Form))))
config.RemoveProperty(form.Name);
config.SetNonString(true, hasMigratedKey);
}
}
private static void postLoggingGlobalExceptionHandling()

View File

@@ -23,15 +23,15 @@ namespace LibationWinForms
}
}
public static int DpiScale(this Control control, int value)
=> (int)(control.DeviceDpi / BaseDpi * value);
public static int DpiScale(this Control control, int value, float additionalScaleFactor = 1)
=> (int)float.Round(control.DeviceDpi / BaseDpi * value * additionalScaleFactor);
public static int DpiUnscale(this Control control, int value)
=> (int)(BaseDpi / control.DeviceDpi * value);
=> (int)float.Round(BaseDpi / control.DeviceDpi * value);
public static int ScaleX(this Graphics control, int value)
=> (int)(control.DpiX / BaseDpi * value);
=> (int)float.Round(control.DpiX / BaseDpi * value);
public static int ScaleY(this Graphics control, int value)
=> (int)(control.DpiY / BaseDpi * value);
=> (int)float.Round(control.DpiY / BaseDpi * value);
}
}

View File

@@ -19,7 +19,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="7.2.2.1" />
<PackageReference Include="Dinah.Core" Version="7.2.3.1" />
</ItemGroup>
</Project>

View File

@@ -7,11 +7,11 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -540,7 +540,6 @@ namespace FileLiberator.Tests
value[i].StartOffsetMs.Should().Be(expected[i].StartOffsetMs);
value[i].StartOffsetSec.Should().Be(expected[i].StartOffsetSec);
value[i].LengthMs.Should().Be(expected[i].LengthMs);
value[i].Chapters.Should().BeNull();
}
}
}

View File

@@ -7,10 +7,10 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -8,10 +8,10 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -8,10 +8,10 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -8,11 +8,11 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>