mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-11 23:38:53 -05:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f69c2b1cfc | ||
|
|
b11675c36a | ||
|
|
379c2ed62d | ||
|
|
7c8489b52f | ||
|
|
c61a863edd | ||
|
|
1d54f32ef3 | ||
|
|
fabe4afd94 | ||
|
|
61efa3c0c1 | ||
|
|
fe70daf0bc | ||
|
|
34033e7947 | ||
|
|
e8c63e9a6e | ||
|
|
9315165f80 | ||
|
|
ce624399ba | ||
|
|
63e9700c4a | ||
|
|
914e574bf8 | ||
|
|
b94f9bbc15 | ||
|
|
4e34834c35 | ||
|
|
3211b2dc85 | ||
|
|
ea6adeb58f | ||
|
|
90eccbf2f6 | ||
|
|
668cd7dba8 | ||
|
|
c08b2b575c | ||
|
|
07eaa48e10 | ||
|
|
3cf5fc1d99 | ||
|
|
15ad753fa1 | ||
|
|
75b984bdb2 | ||
|
|
f586d1d59f | ||
|
|
cb91a591f0 | ||
|
|
0c0c556c6a | ||
|
|
ff63b73c09 | ||
|
|
c1d56adbd2 | ||
|
|
bcd99fd208 | ||
|
|
d1df10d060 | ||
|
|
1fa415628f | ||
|
|
a83fe9e532 | ||
|
|
f85462ffec | ||
|
|
156349c293 | ||
|
|
5976706e40 | ||
|
|
1e40180f0c | ||
|
|
7d09728e6b | ||
|
|
4899ef3007 | ||
|
|
296c2b43eb | ||
|
|
932472cb91 | ||
|
|
1bf86b05ec | ||
|
|
5d5e3a6671 |
@@ -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() { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>10.5.2.1</Version>
|
||||
<Version>10.6.4.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="6.0.0" />
|
||||
<PackageReference Include="Octokit" Version="7.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.ZipFile" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="8.4.1.1" />
|
||||
<PackageReference Include="AudibleApi" Version="8.4.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
26
Source/DataLayer/Configurations/BookCategoryConfig.cs
Normal file
26
Source/DataLayer/Configurations/BookCategoryConfig.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Source/DataLayer/Configurations/CategoryLadderConfig.cs
Normal file
24
Source/DataLayer/Configurations/CategoryLadderConfig.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.2.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.1.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
|
||||
<PackageReference Include="Dinah.Core" Version="7.3.0.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.3.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.5">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
|
||||
20
Source/DataLayer/EfClasses/BookCategory.cs
Normal file
20
Source/DataLayer/EfClasses/BookCategory.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}";
|
||||
|
||||
58
Source/DataLayer/EfClasses/CategoryLadder.cs
Normal file
58
Source/DataLayer/EfClasses/CategoryLadder.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
465
Source/DataLayer/Migrations/20230718214617_AddCategoryLadder.Designer.cs
generated
Normal file
465
Source/DataLayer/Migrations/20230718214617_AddCategoryLadder.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
174
Source/DataLayer/Migrations/20230718214617_AddCategoryLadder.cs
Normal file
174
Source/DataLayer/Migrations/20230718214617_AddCategoryLadder.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
11
Source/DataLayer/QueryObjects/CategoryQueries.cs
Normal file
11
Source/DataLayer/QueryObjects/CategoryQueries.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace FileLiberator
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
var nickname
|
||||
= persister.AccountsSettings.Accounts
|
||||
.FirstOrDefault(a => a.AccountId == libraryBook.Account)
|
||||
.FirstOrDefault(a => a.AccountId == libraryBook.Account && a.Locale.Name == libraryBook.Book.Locale)
|
||||
?.AccountName;
|
||||
|
||||
return new()
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace FileManager
|
||||
{
|
||||
/// <summary>
|
||||
@@ -16,9 +17,9 @@ namespace FileManager
|
||||
public string SearchPattern { get; private set; }
|
||||
public SearchOption SearchOption { get; private set; }
|
||||
|
||||
private FileSystemWatcher fileSystemWatcher { get; set; }
|
||||
private BlockingCollection<FileSystemEventArgs> directoryChangesEvents { get; set; }
|
||||
private Task backgroundScanner { get; set; }
|
||||
private FileSystemWatcher? fileSystemWatcher { get; set; }
|
||||
private BlockingCollection<FileSystemEventArgs>? directoryChangesEvents { get; set; }
|
||||
private Task? backgroundScanner { get; set; }
|
||||
|
||||
private object fsCacheLocker { get; } = new();
|
||||
private List<LongPath> fsCache { get; } = new();
|
||||
@@ -32,7 +33,7 @@ namespace FileManager
|
||||
Init();
|
||||
}
|
||||
|
||||
public LongPath FindFile(System.Text.RegularExpressions.Regex regex)
|
||||
public LongPath? FindFile(System.Text.RegularExpressions.Regex regex)
|
||||
{
|
||||
lock (fsCacheLocker)
|
||||
return fsCache.FirstOrDefault(s => regex.IsMatch(s));
|
||||
@@ -105,13 +106,13 @@ namespace FileManager
|
||||
|
||||
private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
directoryChangesEvents.Add(e);
|
||||
directoryChangesEvents?.Add(e);
|
||||
}
|
||||
|
||||
#region Background Thread
|
||||
private void BackgroundScanner()
|
||||
{
|
||||
while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1))
|
||||
while (directoryChangesEvents?.TryTake(out var change, -1) is true)
|
||||
{
|
||||
lock (fsCacheLocker)
|
||||
UpdateLocalCache(change);
|
||||
|
||||
@@ -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.3.0.1" />
|
||||
<PackageReference Include="Polly" Version="7.2.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -7,20 +8,20 @@ using Dinah.Core;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
|
||||
#nullable enable
|
||||
namespace FileManager
|
||||
{
|
||||
public static class FileUtility
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// "txt" => ".txt"
|
||||
/// <br />".txt" => ".txt"
|
||||
/// <br />null or whitespace => ""
|
||||
/// </summary>
|
||||
public static string GetStandardizedExtension(string extension)
|
||||
[return: NotNull]
|
||||
public static string GetStandardizedExtension(string? extension)
|
||||
=> string.IsNullOrWhiteSpace(extension)
|
||||
? (extension ?? "")?.Trim()
|
||||
? string.Empty
|
||||
: '.' + extension.Trim().Trim('.');
|
||||
|
||||
/// <summary>
|
||||
@@ -48,18 +49,18 @@ namespace FileManager
|
||||
/// <br/>- ensure uniqueness
|
||||
/// <br/>- enforce max file length
|
||||
/// </summary>
|
||||
public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, string fileExtension, bool returnFirstExisting = false)
|
||||
public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, string? fileExtension, bool returnFirstExisting = false)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||
ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension));
|
||||
ArgumentValidator.EnsureNotNull(replacements, nameof(replacements));
|
||||
|
||||
fileExtension = GetStandardizedExtension(fileExtension);
|
||||
|
||||
// remove invalid chars
|
||||
path = GetSafePath(path, replacements);
|
||||
|
||||
// ensure uniqueness and check lengths
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
dir = dir?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;
|
||||
var dir = Path.GetDirectoryName(path)?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;
|
||||
|
||||
var fileName = Path.GetFileName(path);
|
||||
var extIndex = fileName.LastIndexOf(fileExtension, StringComparison.OrdinalIgnoreCase);
|
||||
@@ -84,6 +85,7 @@ namespace FileManager
|
||||
public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||
ArgumentValidator.EnsureNotNull(replacements, nameof(replacements));
|
||||
|
||||
var pathNoPrefix = path.PathWithoutPrefix;
|
||||
|
||||
@@ -159,7 +161,7 @@ namespace FileManager
|
||||
LongPath source,
|
||||
LongPath destination,
|
||||
ReplacementCharacters replacements,
|
||||
string extension = null,
|
||||
string? extension = null,
|
||||
bool overwrite = false)
|
||||
{
|
||||
extension ??= Path.GetExtension(source);
|
||||
@@ -213,6 +215,9 @@ namespace FileManager
|
||||
SaferDelete(destination);
|
||||
|
||||
var dir = Path.GetDirectoryName(destination);
|
||||
if (dir is null)
|
||||
throw new DirectoryNotFoundException();
|
||||
|
||||
Serilog.Log.Logger.Debug("Attempt to create directory: {@DebugText}", new { dir });
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace FileManager
|
||||
{
|
||||
public sealed class LogArchiver : IAsyncDisposable
|
||||
public sealed class LogArchiver : IAsyncDisposable, IDisposable
|
||||
{
|
||||
public Encoding Encoding { get; set; }
|
||||
public string FileName { get; }
|
||||
@@ -39,28 +40,28 @@ namespace FileManager
|
||||
e.Delete();
|
||||
}
|
||||
|
||||
public async Task AddFileAsync(string name, JObject contents, string comment = null)
|
||||
public async Task AddFileAsync(string name, JObject contents, string? comment = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(contents, nameof(contents));
|
||||
await AddFileAsync(name, Encoding.GetBytes(contents.ToString(Newtonsoft.Json.Formatting.Indented)), comment);
|
||||
}
|
||||
|
||||
public async Task AddFileAsync(string name, string contents, string comment = null)
|
||||
public async Task AddFileAsync(string name, string contents, string? comment = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(contents, nameof(contents));
|
||||
await AddFileAsync(name, Encoding.GetBytes(contents), comment);
|
||||
}
|
||||
|
||||
public Task AddFileAsync(string name, ReadOnlyMemory<byte> contents, string comment = null)
|
||||
public Task AddFileAsync(string name, ReadOnlyMemory<byte> contents, string? comment = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(name, nameof(name));
|
||||
|
||||
name = ReplacementCharacters.Barebones.ReplaceFilenameChars(name);
|
||||
return Task.Run(() => AddfileInternal(name, contents.Span, comment));
|
||||
return Task.Run(() => AddFileInternal(name, contents.Span, comment));
|
||||
}
|
||||
|
||||
private readonly object lockObj = new();
|
||||
private void AddfileInternal(string name, ReadOnlySpan<byte> contents, string comment)
|
||||
private void AddFileInternal(string name, ReadOnlySpan<byte> contents, string? comment)
|
||||
{
|
||||
lock (lockObj)
|
||||
{
|
||||
@@ -73,5 +74,7 @@ namespace FileManager
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync() => await Task.Run(archive.Dispose);
|
||||
|
||||
public void Dispose() => archive.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
#nullable enable
|
||||
namespace FileManager
|
||||
{
|
||||
public class LongPath
|
||||
@@ -15,9 +17,9 @@ namespace FileManager
|
||||
public static readonly int MaxPathLength;
|
||||
private const int WIN_MAX_PATH = 260;
|
||||
private const string WIN_LONG_PATH_PREFIX = @"\\?\";
|
||||
internal static readonly bool IsWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
internal static readonly bool IsLinux = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||
internal static readonly bool IsOSX = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
||||
internal static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
internal static readonly bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||
internal static readonly bool IsOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
@@ -60,7 +62,8 @@ namespace FileManager
|
||||
=> IsWindows ? filename.Length
|
||||
: Encoding.UTF8.GetByteCount(filename);
|
||||
|
||||
public static implicit operator LongPath(string path)
|
||||
[return: NotNullIfNotNull(nameof(path))]
|
||||
public static implicit operator LongPath?(string? path)
|
||||
{
|
||||
if (path is null) return null;
|
||||
|
||||
@@ -93,7 +96,8 @@ namespace FileManager
|
||||
}
|
||||
}
|
||||
|
||||
public static implicit operator string(LongPath path) => path?.Path;
|
||||
[return: NotNullIfNotNull(nameof(path))]
|
||||
public static implicit operator string?(LongPath? path) => path?.Path;
|
||||
|
||||
[JsonIgnore]
|
||||
public string ShortPathName
|
||||
@@ -127,8 +131,6 @@ namespace FileManager
|
||||
//for newly-created entries in ther file system. Existing entries made while
|
||||
//8dot3 names were disabled will not be reachable by short paths.
|
||||
|
||||
if (Path is null) return null;
|
||||
|
||||
StringBuilder shortPathBuffer = new(MaxPathLength);
|
||||
GetShortPathName(Path, shortPathBuffer, MaxPathLength);
|
||||
return shortPathBuffer.ToString();
|
||||
@@ -141,7 +143,6 @@ namespace FileManager
|
||||
get
|
||||
{
|
||||
if (!IsWindows) return Path;
|
||||
if (Path is null) return null;
|
||||
|
||||
StringBuilder longPathBuffer = new(MaxPathLength);
|
||||
GetLongPathName(Path, longPathBuffer, MaxPathLength);
|
||||
@@ -156,17 +157,18 @@ namespace FileManager
|
||||
{
|
||||
if (!IsWindows) return Path;
|
||||
return
|
||||
Path?.StartsWith(WIN_LONG_PATH_PREFIX) == true ? Path.Remove(0, WIN_LONG_PATH_PREFIX.Length)
|
||||
:Path;
|
||||
Path.StartsWith(WIN_LONG_PATH_PREFIX)
|
||||
? Path.Remove(0, WIN_LONG_PATH_PREFIX.Length)
|
||||
: Path;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => Path;
|
||||
|
||||
public override int GetHashCode() => Path.GetHashCode();
|
||||
public override bool Equals(object obj) => obj is LongPath other && Path == other.Path;
|
||||
public static bool operator ==(LongPath path1, LongPath path2) => path1.Equals(path2);
|
||||
public static bool operator !=(LongPath path1, LongPath path2) => !path1.Equals(path2);
|
||||
public override bool Equals(object? obj) => obj is LongPath other && Path == other.Path;
|
||||
public static bool operator ==(LongPath? path1, LongPath? path2) => path1?.Equals(path2) is true;
|
||||
public static bool operator !=(LongPath? path1, LongPath? path2) => path1 is null || path2 is null || !path1.Equals(path2);
|
||||
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
#nullable enable
|
||||
namespace FileManager.NamingTemplate;
|
||||
|
||||
internal interface IClosingPropertyTag : IPropertyTag
|
||||
@@ -17,7 +19,7 @@ internal interface IClosingPropertyTag : IPropertyTag
|
||||
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
||||
/// <param name="propertyTag">The registered <see cref="IPropertyTag"/></param>
|
||||
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
|
||||
bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag propertyTag);
|
||||
bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag);
|
||||
}
|
||||
|
||||
public class ConditionalTagCollection<TClass> : TagCollection
|
||||
@@ -37,6 +39,7 @@ public class ConditionalTagCollection<TClass> : TagCollection
|
||||
|
||||
private class ConditionalTag : TagBase, IClosingPropertyTag
|
||||
{
|
||||
public override Regex NameMatcher { get; }
|
||||
public Regex NameCloseMatcher { get; }
|
||||
|
||||
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, Expression conditionExpression)
|
||||
@@ -46,7 +49,7 @@ public class ConditionalTagCollection<TClass> : TagCollection
|
||||
NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options);
|
||||
}
|
||||
|
||||
public bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag propertyTag)
|
||||
public bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag)
|
||||
{
|
||||
var match = NameCloseMatcher.Match(templateString);
|
||||
if (match.Success)
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
#nullable enable
|
||||
namespace FileManager.NamingTemplate;
|
||||
|
||||
public class NamingTemplate
|
||||
{
|
||||
public string TemplateText { get; private set; }
|
||||
public string TemplateText { get; private set; } = string.Empty;
|
||||
public IEnumerable<ITemplateTag> TagsInUse => _tagsInUse;
|
||||
public IEnumerable<ITemplateTag> TagsRegistered => TagCollections.SelectMany(t => t).DistinctBy(t => t.TagName);
|
||||
public IEnumerable<string> Warnings => errors.Concat(warnings);
|
||||
public IEnumerable<string> Errors => errors;
|
||||
|
||||
private Delegate templateToString;
|
||||
private Delegate? templateToString;
|
||||
private readonly List<string> warnings = new();
|
||||
private readonly List<string> errors = new();
|
||||
private readonly IEnumerable<TagCollection> TagCollections;
|
||||
@@ -30,6 +32,9 @@ public class NamingTemplate
|
||||
/// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param>
|
||||
public TemplatePart Evaluate(params object[] propertyClasses)
|
||||
{
|
||||
if (templateToString is null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
// Match propertyClasses to the arguments required by templateToString.DynamicInvoke().
|
||||
// First parameter is "this", so ignore it.
|
||||
var delegateArgTypes = templateToString.Method.GetParameters().Skip(1);
|
||||
@@ -39,7 +44,7 @@ public class NamingTemplate
|
||||
if (args.Length != delegateArgTypes.Count())
|
||||
throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}");
|
||||
|
||||
return ((TemplatePart)templateToString.DynamicInvoke(args)).FirstPart;
|
||||
return (templateToString.DynamicInvoke(args) as TemplatePart)!.FirstPart;
|
||||
}
|
||||
|
||||
/// <summary>Parse a template string to a <see cref="NamingTemplate"/></summary>
|
||||
@@ -69,7 +74,7 @@ public class NamingTemplate
|
||||
}
|
||||
|
||||
/// <summary>Builds an <see cref="Expression"/> tree that will evaluate to a <see cref="TemplatePart"/></summary>
|
||||
private static Expression GetExpressionTree(BinaryNode node)
|
||||
private static Expression GetExpressionTree(BinaryNode? node)
|
||||
{
|
||||
if (node is null) return TemplatePart.Blank;
|
||||
else if (node.IsValue) return node.Expression;
|
||||
@@ -81,10 +86,10 @@ public class NamingTemplate
|
||||
}
|
||||
|
||||
/// <summary>Parse a template string into a <see cref="BinaryNode"/> tree</summary>
|
||||
private BinaryNode IntermediateParse(string templateString)
|
||||
private BinaryNode IntermediateParse(string? templateString)
|
||||
{
|
||||
if (templateString is null)
|
||||
throw new NullReferenceException(ERROR_NULL_IS_INVALID);
|
||||
throw new ArgumentException(ERROR_NULL_IS_INVALID);
|
||||
else if (string.IsNullOrEmpty(templateString))
|
||||
warnings.Add(WARNING_EMPTY);
|
||||
else if (string.IsNullOrWhiteSpace(templateString))
|
||||
@@ -93,12 +98,12 @@ public class NamingTemplate
|
||||
TemplateText = templateString;
|
||||
|
||||
BinaryNode topNode = BinaryNode.CreateRoot();
|
||||
BinaryNode currentNode = topNode;
|
||||
BinaryNode? currentNode = topNode;
|
||||
List<char> literalChars = new();
|
||||
|
||||
while (templateString.Length > 0)
|
||||
{
|
||||
if (StartsWith(templateString, out string exactPropertyName, out var propertyTag, out var valueExpression))
|
||||
if (StartsWith(templateString, out var exactPropertyName, out var propertyTag, out var valueExpression))
|
||||
{
|
||||
checkAndAddLiterals();
|
||||
|
||||
@@ -116,7 +121,7 @@ public class NamingTemplate
|
||||
{
|
||||
checkAndAddLiterals();
|
||||
|
||||
BinaryNode lastParenth = currentNode;
|
||||
BinaryNode? lastParenth = currentNode;
|
||||
|
||||
while (lastParenth?.IsConditional is false)
|
||||
lastParenth = lastParenth.Parent;
|
||||
@@ -168,7 +173,7 @@ public class NamingTemplate
|
||||
}
|
||||
}
|
||||
|
||||
private bool StartsWith(string template, out string exactName, out IPropertyTag propertyTag, out Expression valueExpression)
|
||||
private bool StartsWith(string template, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? valueExpression)
|
||||
{
|
||||
foreach (var pc in TagCollections)
|
||||
{
|
||||
@@ -182,7 +187,7 @@ public class NamingTemplate
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool StartsWithClosing(string template, out string exactName, out IClosingPropertyTag closingPropertyTag)
|
||||
private bool StartsWithClosing(string template, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? closingPropertyTag)
|
||||
{
|
||||
foreach (var pc in TagCollections)
|
||||
{
|
||||
@@ -198,36 +203,36 @@ public class NamingTemplate
|
||||
private class BinaryNode
|
||||
{
|
||||
public string Name { get; }
|
||||
public BinaryNode Parent { get; private set; }
|
||||
public BinaryNode RightChild { get; private set; }
|
||||
public BinaryNode LeftChild { get; private set; }
|
||||
public Expression Expression { get; private init; }
|
||||
public BinaryNode? Parent { get; private set; }
|
||||
public BinaryNode? RightChild { get; private set; }
|
||||
public BinaryNode? LeftChild { get; private set; }
|
||||
public Expression Expression { get; }
|
||||
public bool IsConditional { get; private init; } = false;
|
||||
public bool IsValue { get; private init; } = false;
|
||||
|
||||
public static BinaryNode CreateRoot() => new("Root");
|
||||
public static BinaryNode CreateRoot() => new("Root", Expression.Empty());
|
||||
|
||||
public static BinaryNode CreateValue(string literal) => new("Literal")
|
||||
{
|
||||
IsValue = true,
|
||||
Expression = TemplatePart.CreateLiteral(literal)
|
||||
};
|
||||
public static BinaryNode CreateValue(string literal)
|
||||
=> new("Literal", TemplatePart.CreateLiteral(literal))
|
||||
{
|
||||
IsValue = true
|
||||
};
|
||||
|
||||
public static BinaryNode CreateValue(ITemplateTag templateTag, Expression property) => new(templateTag.TagName)
|
||||
{
|
||||
IsValue = true,
|
||||
Expression = TemplatePart.CreateProperty(templateTag, property)
|
||||
};
|
||||
public static BinaryNode CreateValue(ITemplateTag templateTag, Expression property)
|
||||
=> new(templateTag.TagName, TemplatePart.CreateProperty(templateTag, property))
|
||||
{
|
||||
IsValue = true
|
||||
};
|
||||
|
||||
public static BinaryNode CreateConditional(ITemplateTag templateTag, Expression property) => new(templateTag.TagName)
|
||||
{
|
||||
IsConditional = true,
|
||||
Expression = property
|
||||
};
|
||||
public static BinaryNode CreateConditional(ITemplateTag templateTag, Expression property)
|
||||
=> new(templateTag.TagName, property)
|
||||
{
|
||||
IsConditional = true
|
||||
};
|
||||
|
||||
private static BinaryNode CreateConcatenation(BinaryNode left, BinaryNode right)
|
||||
{
|
||||
var newNode = new BinaryNode("Concatenation")
|
||||
var newNode = new BinaryNode("Concatenation", Expression.Empty())
|
||||
{
|
||||
LeftChild = left,
|
||||
RightChild = right
|
||||
@@ -237,7 +242,12 @@ public class NamingTemplate
|
||||
return newNode;
|
||||
}
|
||||
|
||||
private BinaryNode(string name) => Name = name;
|
||||
private BinaryNode(string name, Expression expression)
|
||||
{
|
||||
Name = name;
|
||||
Expression = expression;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
|
||||
public BinaryNode AddNewNode(BinaryNode newNode)
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
#nullable enable
|
||||
namespace FileManager.NamingTemplate;
|
||||
|
||||
public delegate string PropertyFormatter<T>(ITemplateTag templateTag, T value, string formatString);
|
||||
@@ -37,7 +38,7 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
|
||||
/// and a formatting string and returnes the value the formatted string. If <see cref="null"/>, use the default
|
||||
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
|
||||
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, PropertyFormatter<TProperty> formatter = null)
|
||||
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, PropertyFormatter<TProperty>? formatter = null)
|
||||
where TProperty : struct
|
||||
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
|
||||
|
||||
@@ -59,7 +60,7 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
|
||||
/// and a formatting string and returnes the value formatted to string. If <see cref="null"/>, use the default
|
||||
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
|
||||
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TProperty> formatter = null)
|
||||
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TProperty>? formatter = null)
|
||||
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
|
||||
|
||||
/// <summary>
|
||||
@@ -72,14 +73,15 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
=> RegisterWithToString(templateTag, propertyGetter, toString);
|
||||
|
||||
private void RegisterWithFormatter<TProperty, TPropertyValue>
|
||||
(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TPropertyValue> formatter)
|
||||
(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TPropertyValue>? formatter)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag));
|
||||
ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter));
|
||||
|
||||
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
|
||||
formatter ??= GetDefaultFormatter<TPropertyValue>();
|
||||
|
||||
if ((formatter ??= GetDefaultFormatter<TPropertyValue>()) is null)
|
||||
if (formatter is null)
|
||||
AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, ToStringFunc));
|
||||
else
|
||||
AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, formatter));
|
||||
@@ -97,7 +99,7 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
|
||||
private static string ToStringFunc<T>(T propertyValue) => propertyValue?.ToString() ?? "";
|
||||
|
||||
private PropertyFormatter<T> GetDefaultFormatter<T>()
|
||||
private PropertyFormatter<T>? GetDefaultFormatter<T>()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -109,6 +111,7 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
|
||||
private class PropertyTag<TPropertyValue> : TagBase
|
||||
{
|
||||
public override Regex NameMatcher { get; }
|
||||
private Func<Expression, string, Expression> CreateToStringExpression { get; }
|
||||
|
||||
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PropertyFormatter<TPropertyValue> formatter)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
#nullable enable
|
||||
namespace FileManager.NamingTemplate;
|
||||
|
||||
internal interface IPropertyTag
|
||||
@@ -22,13 +24,13 @@ internal interface IPropertyTag
|
||||
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
||||
/// <param name="propertyValue">The <see cref="Expression"/> that returns the property's value</param>
|
||||
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
|
||||
bool StartsWith(string templateString, out string exactName, out Expression propertyValue);
|
||||
bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue);
|
||||
}
|
||||
|
||||
internal abstract class TagBase : IPropertyTag
|
||||
{
|
||||
public ITemplateTag TemplateTag { get; }
|
||||
public Regex NameMatcher { get; protected init; }
|
||||
public abstract Regex NameMatcher { get; }
|
||||
public Type ReturnType => ValueExpression.Type;
|
||||
protected Expression ValueExpression { get; }
|
||||
|
||||
@@ -43,7 +45,7 @@ internal abstract class TagBase : IPropertyTag
|
||||
/// <param name="formatter">The optional format string in the match inside the square brackets</param>
|
||||
protected abstract Expression GetTagExpression(string exactName, string formatter);
|
||||
|
||||
public bool StartsWith(string templateString, out string exactName, out Expression propertyValue)
|
||||
public bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue)
|
||||
{
|
||||
var match = NameMatcher.Match(templateString);
|
||||
if (match.Success)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
#nullable enable
|
||||
namespace FileManager.NamingTemplate;
|
||||
|
||||
/// <summary>A collection of <see cref="IPropertyTag"/>s registered to a single <see cref="Type"/>.</summary>
|
||||
@@ -32,7 +34,7 @@ public abstract class TagCollection : IEnumerable<ITemplateTag>
|
||||
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
||||
/// <param name="propertyValue">The <see cref="Expression"/> that returns the <paramref name="propertyTag"/>'s value</param>
|
||||
/// <returns>True if the <paramref name="templateString"/> starts with a tag registered in this class.</returns>
|
||||
internal bool StartsWith(string templateString, out string exactName, out IPropertyTag propertyTag, out Expression propertyValue)
|
||||
internal bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? propertyValue)
|
||||
{
|
||||
foreach (var p in PropertyTags)
|
||||
{
|
||||
@@ -57,7 +59,7 @@ public abstract class TagCollection : IEnumerable<ITemplateTag>
|
||||
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
||||
/// <param name="closingPropertyTag">The registered <see cref="IClosingPropertyTag"/></param>
|
||||
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
|
||||
internal bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag closingPropertyTag)
|
||||
internal bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? closingPropertyTag)
|
||||
{
|
||||
foreach (var cg in PropertyTags.OfType<IClosingPropertyTag>())
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
#nullable enable
|
||||
namespace FileManager.NamingTemplate;
|
||||
|
||||
/// <summary>Represents one part of an evaluated <see cref="NamingTemplate"/>.</summary>
|
||||
@@ -15,13 +16,13 @@ public class TemplatePart : IEnumerable<TemplatePart>
|
||||
|
||||
/// <summary> The <see cref="IPropertyTag"/>'s <see cref="ITemplateTag"/> if <see cref="TemplatePart"/> is
|
||||
/// a registered property, otherwise <see cref="null"/> for string literals. </summary>
|
||||
public ITemplateTag TemplateTag { get; }
|
||||
public ITemplateTag? TemplateTag { get; }
|
||||
|
||||
/// <summary>The evaluated string.</summary>
|
||||
public string Value { get; }
|
||||
|
||||
private TemplatePart previous;
|
||||
private TemplatePart next;
|
||||
private TemplatePart? previous;
|
||||
private TemplatePart? next;
|
||||
private TemplatePart(string name, string value)
|
||||
{
|
||||
TagName = name;
|
||||
@@ -53,14 +54,33 @@ public class TemplatePart : IEnumerable<TemplatePart>
|
||||
private static Expression CreateExpression(string name, Expression value)
|
||||
=> Expression.New(constructorInfo, Expression.Constant(name), value);
|
||||
|
||||
private static readonly ConstructorInfo constructorInfo
|
||||
= typeof(TemplatePart).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(string), typeof(string) });
|
||||
private static readonly ConstructorInfo constructorInfo;
|
||||
private static readonly ConstructorInfo tagTemplateConstructorInfo;
|
||||
private static readonly MethodInfo addMethodInfo;
|
||||
static TemplatePart()
|
||||
{
|
||||
var type = typeof(TemplatePart);
|
||||
|
||||
private static readonly ConstructorInfo tagTemplateConstructorInfo
|
||||
= typeof(TemplatePart).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(ITemplateTag), typeof(string) });
|
||||
if (type.GetConstructor(
|
||||
BindingFlags.NonPublic | BindingFlags.Instance,
|
||||
new Type[] { typeof(string), typeof(string) }) is not ConstructorInfo c1)
|
||||
throw new MissingMethodException(nameof(TemplatePart));
|
||||
|
||||
private static readonly MethodInfo addMethodInfo
|
||||
= typeof(TemplatePart).GetMethod(nameof(Concatenate), BindingFlags.NonPublic | BindingFlags.Static, new Type[] { typeof(TemplatePart), typeof(TemplatePart) });
|
||||
if (type.GetConstructor(
|
||||
BindingFlags.NonPublic | BindingFlags.Instance,
|
||||
new Type[] { typeof(ITemplateTag), typeof(string) }) is not ConstructorInfo c2)
|
||||
throw new MissingMethodException(nameof(TemplatePart));
|
||||
|
||||
if (type.GetMethod(
|
||||
nameof(Concatenate),
|
||||
BindingFlags.NonPublic | BindingFlags.Static,
|
||||
new Type[] { typeof(TemplatePart), typeof(TemplatePart) }) is not MethodInfo m1)
|
||||
throw new MissingMethodException(nameof(Concatenate));
|
||||
|
||||
constructorInfo = c1;
|
||||
tagTemplateConstructorInfo = c2;
|
||||
addMethodInfo = m1;
|
||||
}
|
||||
|
||||
public IEnumerator<TemplatePart> GetEnumerator()
|
||||
{
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace FileManager
|
||||
{
|
||||
public class PersistentDictionary
|
||||
@@ -13,19 +15,19 @@ namespace FileManager
|
||||
public bool IsReadOnly { get; }
|
||||
|
||||
// optimize for strings. expectation is most settings will be strings and a rare exception will be something else
|
||||
private Dictionary<string, string> stringCache { get; } = new Dictionary<string, string>();
|
||||
private Dictionary<string, object> objectCache { get; } = new Dictionary<string, object>();
|
||||
private Dictionary<string, string?> stringCache { get; } = new();
|
||||
private Dictionary<string, object?> objectCache { get; } = new();
|
||||
|
||||
public PersistentDictionary(string filepath, bool isReadOnly = false)
|
||||
{
|
||||
Filepath = filepath;
|
||||
IsReadOnly = isReadOnly;
|
||||
|
||||
if (File.Exists(Filepath))
|
||||
if (File.Exists(Filepath) || Path.GetDirectoryName(Filepath) is not string dirName)
|
||||
return;
|
||||
|
||||
// will create any missing directories, incl subdirectories. if all already exist: no action
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(filepath));
|
||||
Directory.CreateDirectory(dirName);
|
||||
|
||||
if (IsReadOnly)
|
||||
return;
|
||||
@@ -33,13 +35,14 @@ namespace FileManager
|
||||
createNewFile();
|
||||
}
|
||||
|
||||
public string GetString(string propertyName, string defaultValue = null)
|
||||
[return: NotNullIfNotNull(nameof(defaultValue))]
|
||||
public string? GetString(string propertyName, string? defaultValue = null)
|
||||
{
|
||||
if (!stringCache.ContainsKey(propertyName))
|
||||
{
|
||||
var jObject = readFile();
|
||||
if (jObject.ContainsKey(propertyName))
|
||||
stringCache[propertyName] = jObject[propertyName].Value<string>();
|
||||
stringCache[propertyName] = jObject[propertyName]?.Value<string>();
|
||||
else
|
||||
stringCache[propertyName] = defaultValue;
|
||||
}
|
||||
@@ -47,7 +50,8 @@ namespace FileManager
|
||||
return stringCache[propertyName];
|
||||
}
|
||||
|
||||
public T GetNonString<T>(string propertyName, T defaultValue = default)
|
||||
[return: NotNullIfNotNull(nameof(defaultValue))]
|
||||
public T? GetNonString<T>(string propertyName, T? defaultValue = default)
|
||||
{
|
||||
var obj = GetObject(propertyName);
|
||||
|
||||
@@ -72,21 +76,21 @@ namespace FileManager
|
||||
throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}");
|
||||
}
|
||||
|
||||
public object GetObject(string propertyName)
|
||||
public object? GetObject(string propertyName)
|
||||
{
|
||||
if (!objectCache.ContainsKey(propertyName))
|
||||
{
|
||||
var jObject = readFile();
|
||||
if (!jObject.ContainsKey(propertyName))
|
||||
return null;
|
||||
objectCache[propertyName] = jObject[propertyName].Value<object>();
|
||||
objectCache[propertyName] = jObject[propertyName]?.Value<object>();
|
||||
}
|
||||
|
||||
return objectCache[propertyName];
|
||||
}
|
||||
|
||||
public string GetStringFromJsonPath(string jsonPath, string propertyName) => GetStringFromJsonPath($"{jsonPath}.{propertyName}");
|
||||
public string GetStringFromJsonPath(string jsonPath)
|
||||
public string? GetStringFromJsonPath(string jsonPath, string propertyName) => GetStringFromJsonPath($"{jsonPath}.{propertyName}");
|
||||
public string? GetStringFromJsonPath(string jsonPath)
|
||||
{
|
||||
if (!stringCache.ContainsKey(jsonPath))
|
||||
{
|
||||
@@ -96,7 +100,7 @@ namespace FileManager
|
||||
var token = jObject.SelectToken(jsonPath);
|
||||
if (token is null)
|
||||
return null;
|
||||
stringCache[jsonPath] = (string)token;
|
||||
stringCache[jsonPath] = token.Value<string>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -110,7 +114,7 @@ namespace FileManager
|
||||
public bool Exists(string propertyName) => readFile().ContainsKey(propertyName);
|
||||
|
||||
private object locker { get; } = new object();
|
||||
public void SetString(string propertyName, string newValue)
|
||||
public void SetString(string propertyName, string? newValue)
|
||||
{
|
||||
// only do this check in string cache, NOT object cache
|
||||
if (stringCache.ContainsKey(propertyName) && stringCache[propertyName] == newValue)
|
||||
@@ -122,7 +126,7 @@ namespace FileManager
|
||||
writeFile(propertyName, newValue);
|
||||
}
|
||||
|
||||
public void SetNonString(string propertyName, object newValue)
|
||||
public void SetNonString(string propertyName, object? newValue)
|
||||
{
|
||||
// set cache
|
||||
objectCache[propertyName] = newValue;
|
||||
@@ -131,7 +135,36 @@ namespace FileManager
|
||||
writeFile(propertyName, parsedNewValue);
|
||||
}
|
||||
|
||||
private void writeFile(string propertyName, JToken newValue)
|
||||
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)
|
||||
return;
|
||||
@@ -161,7 +194,7 @@ namespace FileManager
|
||||
|
||||
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
||||
/// <returns>Value was changed</returns>
|
||||
public bool SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
|
||||
public bool SetWithJsonPath(string jsonPath, string propertyName, string? newValue, bool suppressLogging = false)
|
||||
{
|
||||
if (IsReadOnly)
|
||||
return false;
|
||||
@@ -213,7 +246,7 @@ namespace FileManager
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string formatValueForLog(string value)
|
||||
private static string formatValueForLog(string? value)
|
||||
=> value is null ? "[null]"
|
||||
: string.IsNullOrEmpty(value) ? "[empty]"
|
||||
: string.IsNullOrWhiteSpace(value) ? $"[whitespace. Length={value.Length}]"
|
||||
@@ -254,7 +287,6 @@ namespace FileManager
|
||||
private void createNewFile()
|
||||
{
|
||||
File.WriteAllText(Filepath, "{}");
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace FileManager
|
||||
{
|
||||
public record Replacement
|
||||
@@ -59,7 +60,7 @@ namespace FileManager
|
||||
[JsonConverter(typeof(ReplacementCharactersConverter))]
|
||||
public class ReplacementCharacters
|
||||
{
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is ReplacementCharacters second && Replacements.Count == second.Replacements.Count)
|
||||
{
|
||||
@@ -173,7 +174,7 @@ namespace FileManager
|
||||
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar
|
||||
}).ToArray();
|
||||
|
||||
public IReadOnlyList<Replacement> Replacements { get; init; }
|
||||
required public IReadOnlyList<Replacement> Replacements { get; init; }
|
||||
private string DefaultReplacement => Replacements[0].ReplacementString;
|
||||
private Replacement ForwardSlash => Replacements[1];
|
||||
private Replacement BackSlash => Replacements[2];
|
||||
@@ -298,12 +299,14 @@ namespace FileManager
|
||||
public override bool CanConvert(Type objectType)
|
||||
=> objectType == typeof(ReplacementCharacters);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var jObj = JObject.Load(reader);
|
||||
var replaceArr = jObj[nameof(Replacement)];
|
||||
IReadOnlyList<Replacement> dict = replaceArr
|
||||
.ToObject<Replacement[]>().ToList();
|
||||
var dict
|
||||
= replaceArr?.ToObject<Replacement[]>()?.ToList()
|
||||
?? ReplacementCharacters.Default.Replacements;
|
||||
|
||||
|
||||
//Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid.
|
||||
//If not, reset to default.
|
||||
@@ -325,9 +328,10 @@ namespace FileManager
|
||||
return new ReplacementCharacters { Replacements = dict };
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
ReplacementCharacters replacements = (ReplacementCharacters)value;
|
||||
if (value is not ReplacementCharacters replacements)
|
||||
return;
|
||||
|
||||
var propertyNames = replacements.Replacements
|
||||
.Select(JObject.FromObject).ToList();
|
||||
|
||||
@@ -67,13 +67,13 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
<PackageReference Include="Avalonia" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia" Version="11.0.2" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.2" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0" />
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.2" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.2" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.2" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -70,13 +70,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
||||
<PackageReference Include="Avalonia" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.2" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
||||
<PackageReference Include="Avalonia" Version="11.0.2" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.2" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.2" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.2" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.2" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -3,7 +3,6 @@ using AudibleUtilities;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Dinah.Core.StepRunner;
|
||||
@@ -13,7 +12,6 @@ using LibationFileManager;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using static Avalonia.Threading.Dispatcher;
|
||||
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
@@ -36,12 +34,13 @@ namespace LibationAvalonia
|
||||
AutoScan = Configuration.Instance.AutoScan;
|
||||
Configuration.Instance.AutoScan = false;
|
||||
MainForm = mainForm;
|
||||
sequence[nameof(ShowAccountDialog)] = () => UIThread.InvokeAsync(ShowAccountDialog);
|
||||
sequence[nameof(ShowSettingsDialog)] = () => UIThread.InvokeAsync(ShowSettingsDialog);
|
||||
sequence[nameof(ShowAccountScanning)] = () => UIThread.InvokeAsync(ShowAccountScanning);
|
||||
sequence[nameof(ShowSearching)] = () => UIThread.InvokeAsync(ShowSearching);
|
||||
sequence[nameof(ShowQuickFilters)] = () => UIThread.InvokeAsync(ShowQuickFilters);
|
||||
sequence[nameof(ShowTourComplete)] = () => UIThread.InvokeAsync(ShowTourComplete);
|
||||
var uiDispatcher = Avalonia.Threading.Dispatcher.UIThread;
|
||||
sequence[nameof(ShowAccountDialog)] = () => uiDispatcher.InvokeAsync(ShowAccountDialog);
|
||||
sequence[nameof(ShowSettingsDialog)] = () => uiDispatcher.InvokeAsync(ShowSettingsDialog);
|
||||
sequence[nameof(ShowAccountScanning)] = () => uiDispatcher.InvokeAsync(ShowAccountScanning);
|
||||
sequence[nameof(ShowSearching)] = () => uiDispatcher.InvokeAsync(ShowSearching);
|
||||
sequence[nameof(ShowQuickFilters)] = () => uiDispatcher.InvokeAsync(ShowQuickFilters);
|
||||
sequence[nameof(ShowTourComplete)] = () => uiDispatcher.InvokeAsync(ShowTourComplete);
|
||||
}
|
||||
|
||||
public async Task RunAsync()
|
||||
@@ -60,7 +59,7 @@ namespace LibationAvalonia
|
||||
await displayControlAsync(MainForm.accountsToolStripMenuItem);
|
||||
|
||||
var accountSettings = new AccountsDialog();
|
||||
accountSettings.Loaded += async (_, _) => await MessageBox.Show(accountSettings, "Add your Audible account(s), then save.", "Add an Account");
|
||||
accountSettings.Opened += async (_, _) => await MessageBox.Show(accountSettings, "Add your Audible account(s), then save.", "Add an Account");
|
||||
await accountSettings.ShowDialog(MainForm);
|
||||
return true;
|
||||
}
|
||||
@@ -74,14 +73,14 @@ namespace LibationAvalonia
|
||||
await displayControlAsync(MainForm.settingsToolStripMenuItem);
|
||||
await displayControlAsync(MainForm.basicSettingsToolStripMenuItem);
|
||||
|
||||
var settingsDialog = await UIThread.InvokeAsync(() => new SettingsDialog());
|
||||
var settingsDialog = new SettingsDialog();
|
||||
|
||||
var tabsToVisit = settingsDialog.tabControl.Items.OfType<TabItem>().ToList();
|
||||
|
||||
foreach (var tab in tabsToVisit)
|
||||
tab.PropertyChanged += TabControl_PropertyChanged;
|
||||
|
||||
settingsDialog.Loaded += SettingsDialog_Loaded;
|
||||
settingsDialog.Opened += SettingsDialog_Opened;
|
||||
settingsDialog.Closing += SettingsDialog_FormClosing;
|
||||
settingsDialog.saveBtn.Content = "Next Tab";
|
||||
|
||||
@@ -103,7 +102,7 @@ namespace LibationAvalonia
|
||||
settingTabMessages.Remove(header.Text);
|
||||
}
|
||||
|
||||
async void SettingsDialog_Loaded(object sender, RoutedEventArgs e)
|
||||
async void SettingsDialog_Opened(object sender, System.EventArgs e)
|
||||
{
|
||||
await ShowTabPageMessageBoxAsync(tabsToVisit[0]);
|
||||
}
|
||||
@@ -227,7 +226,7 @@ namespace LibationAvalonia
|
||||
await displayControlAsync(editQuickFiltersToolStripMenuItem);
|
||||
|
||||
var editQuickFilters = new EditQuickFilters();
|
||||
editQuickFilters.Loaded += async (_, _) => await MessageBox.Show(editQuickFilters, "From here you can edit, delete, and change the order of Quick Filters", "Editing Quick Filters");
|
||||
editQuickFilters.Opened += async (_, _) => await MessageBox.Show(editQuickFilters, "From here you can edit, delete, and change the order of Quick Filters", "Editing Quick Filters");
|
||||
await editQuickFilters.ShowDialog(MainForm);
|
||||
|
||||
return true;
|
||||
@@ -247,12 +246,12 @@ namespace LibationAvalonia
|
||||
|
||||
private async Task displayControlAsync(TemplatedControl control)
|
||||
{
|
||||
await UIThread.InvokeAsync(() => control.IsEnabled = false);
|
||||
await UIThread.InvokeAsync(() => MainForm.productsDisplay.Focus());
|
||||
await UIThread.InvokeAsync(() => flashControlAsync(control));
|
||||
if (control is MenuItem menuItem) await UIThread.InvokeAsync(menuItem.Open);
|
||||
control.IsEnabled = false;
|
||||
MainForm.productsDisplay.Focus();
|
||||
await flashControlAsync(control);
|
||||
if (control is MenuItem menuItem) menuItem.Open();
|
||||
await Task.Delay(500);
|
||||
await UIThread.InvokeAsync(() => control.IsEnabled = true);
|
||||
control.IsEnabled = true;
|
||||
}
|
||||
|
||||
private static async Task flashControlAsync(TemplatedControl control, int flashCount = 3)
|
||||
|
||||
@@ -9,11 +9,12 @@ using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using FileManager;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public abstract class AudibleFileStorage
|
||||
{
|
||||
protected abstract LongPath GetFilePathCustom(string productId);
|
||||
protected abstract LongPath? GetFilePathCustom(string productId);
|
||||
protected abstract List<LongPath> GetFilePathsCustom(string productId);
|
||||
|
||||
#region static
|
||||
@@ -57,7 +58,7 @@ namespace LibationFileManager
|
||||
regexTemplate = $@"{{0}}.*?\.({extAggr})$";
|
||||
}
|
||||
|
||||
protected LongPath GetFilePath(string productId)
|
||||
protected LongPath? GetFilePath(string productId)
|
||||
{
|
||||
// primary lookup
|
||||
var cachedFile = FilePathCache.GetFirstPath(productId, FileType);
|
||||
@@ -87,7 +88,7 @@ namespace LibationFileManager
|
||||
{
|
||||
internal AaxcFileStorage() : base(FileType.AAXC) { }
|
||||
|
||||
protected override LongPath GetFilePathCustom(string productId)
|
||||
protected override LongPath? GetFilePathCustom(string productId)
|
||||
=> GetFilePathsCustom(productId).FirstOrDefault();
|
||||
|
||||
protected override List<LongPath> GetFilePathsCustom(string productId)
|
||||
@@ -104,9 +105,9 @@ namespace LibationFileManager
|
||||
public class AudioFileStorage : AudibleFileStorage
|
||||
{
|
||||
internal AudioFileStorage() : base(FileType.Audio)
|
||||
=> BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||
=> BookDirectoryFiles ??= newBookDirectoryFiles();
|
||||
|
||||
private static BackgroundFileSystem BookDirectoryFiles { get; set; }
|
||||
private static BackgroundFileSystem? BookDirectoryFiles { get; set; }
|
||||
private static object bookDirectoryFilesLocker { get; } = new();
|
||||
private static EnumerationOptions enumerationOptions { get; } = new()
|
||||
{
|
||||
@@ -115,17 +116,20 @@ namespace LibationFileManager
|
||||
MatchCasing = MatchCasing.CaseInsensitive
|
||||
};
|
||||
|
||||
protected override LongPath GetFilePathCustom(string productId)
|
||||
protected override LongPath? GetFilePathCustom(string productId)
|
||||
=> GetFilePathsCustom(productId).FirstOrDefault();
|
||||
|
||||
protected override List<LongPath> GetFilePathsCustom(string productId)
|
||||
private static BackgroundFileSystem newBookDirectoryFiles()
|
||||
=> new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||
|
||||
protected override List<LongPath> GetFilePathsCustom(string productId)
|
||||
{
|
||||
// If user changed the BooksDirectory: reinitialize
|
||||
lock (bookDirectoryFilesLocker)
|
||||
if (BooksDirectory != BookDirectoryFiles.RootDirectory)
|
||||
BookDirectoryFiles = new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||
if (BooksDirectory != BookDirectoryFiles?.RootDirectory)
|
||||
BookDirectoryFiles = newBookDirectoryFiles();
|
||||
|
||||
var regex = GetBookSearchRegex(productId);
|
||||
var regex = GetBookSearchRegex(productId);
|
||||
|
||||
//Find all extant files matching the productId
|
||||
//using both the file system and the file path cache
|
||||
@@ -138,9 +142,16 @@ namespace LibationFileManager
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public void Refresh() => BookDirectoryFiles.RefreshFiles();
|
||||
public void Refresh()
|
||||
{
|
||||
if (BookDirectoryFiles is null)
|
||||
lock (bookDirectoryFilesLocker)
|
||||
BookDirectoryFiles = newBookDirectoryFiles();
|
||||
else
|
||||
BookDirectoryFiles?.RefreshFiles();
|
||||
}
|
||||
|
||||
public LongPath GetPath(string productId) => GetFilePath(productId);
|
||||
public LongPath? GetPath(string productId) => GetFilePath(productId);
|
||||
|
||||
public static async IAsyncEnumerable<FilePathCache.CacheEntry> FindAudiobooksAsync(LongPath searchDirectory, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -151,7 +162,7 @@ namespace LibationFileManager
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
yield break;
|
||||
|
||||
FilePathCache.CacheEntry audioFile = default;
|
||||
FilePathCache.CacheEntry? audioFile = default;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
[Flags]
|
||||
@@ -20,7 +21,7 @@ namespace LibationFileManager
|
||||
public static bool IsWindows { get; } = OperatingSystem.IsWindows();
|
||||
public static bool IsLinux { get; } = OperatingSystem.IsLinux();
|
||||
public static bool IsMacOs { get; } = OperatingSystem.IsMacOS();
|
||||
public static Version LibationVersion { get; private set; }
|
||||
public static Version? LibationVersion { get; private set; }
|
||||
public static void SetLibationVersion(Version version) => LibationVersion = version;
|
||||
|
||||
public static OS OS { get; }
|
||||
|
||||
36
Source/LibationFileManager/Configuration.HelpText.cs
Normal file
36
Source/LibationFileManager/Configuration.HelpText.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
#nullable enable
|
||||
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 © 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;
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,12 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
{
|
||||
public static string ProcessDirectory { get; } = Path.GetDirectoryName(Exe.FileLocationOnDisk);
|
||||
public static string ProcessDirectory { get; } = Path.GetDirectoryName(Exe.FileLocationOnDisk)!;
|
||||
public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}";
|
||||
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY));
|
||||
public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
|
||||
@@ -36,7 +37,7 @@ namespace LibationFileManager
|
||||
LibationFiles = 5
|
||||
}
|
||||
// use func calls so we always get the latest value of LibationFiles
|
||||
private static List<(KnownDirectories directory, Func<string> getPathFunc)> directoryOptionsPaths { get; } = new()
|
||||
private static List<(KnownDirectories directory, Func<string?> getPathFunc)> directoryOptionsPaths { get; } = new()
|
||||
{
|
||||
(KnownDirectories.None, () => null),
|
||||
(KnownDirectories.UserProfile, () => UserProfile),
|
||||
@@ -47,7 +48,7 @@ namespace LibationFileManager
|
||||
// also, keep this at bottom of this list
|
||||
(KnownDirectories.LibationFiles, () => libationFilesPathCache)
|
||||
};
|
||||
public static string GetKnownDirectoryPath(KnownDirectories directory)
|
||||
public static string? GetKnownDirectoryPath(KnownDirectories directory)
|
||||
{
|
||||
var dirFunc = directoryOptionsPaths.SingleOrDefault(dirFunc => dirFunc.directory == directory);
|
||||
return dirFunc == default ? null : dirFunc.getPathFunc();
|
||||
|
||||
@@ -7,6 +7,7 @@ using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
using Dinah.Core.Logging;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
@@ -44,7 +45,7 @@ namespace LibationFileManager
|
||||
}
|
||||
}
|
||||
|
||||
private static string libationFilesPathCache { get; set; }
|
||||
private static string? libationFilesPathCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Try to find appsettings.json in the following locations:
|
||||
@@ -124,7 +125,10 @@ namespace LibationFileManager
|
||||
// do not check whether directory exists. special/meta directory (eg: AppDir) is valid
|
||||
// verify from live file. no try/catch. want failures to be visible
|
||||
var jObjFinal = JObject.Parse(File.ReadAllText(AppsettingsJsonFile));
|
||||
var valueFinal = jObjFinal[LIBATION_FILES_KEY].Value<string>();
|
||||
|
||||
if (jObjFinal[LIBATION_FILES_KEY]?.Value<string>() is not string valueFinal)
|
||||
throw new InvalidDataException($"{LIBATION_FILES_KEY} not found in {AppsettingsJsonFile}");
|
||||
|
||||
return valueFinal;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Logging;
|
||||
using FileManager;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
{
|
||||
private IConfigurationRoot configuration;
|
||||
private IConfigurationRoot? configuration;
|
||||
|
||||
public void ConfigureLogging()
|
||||
{
|
||||
@@ -31,20 +30,20 @@ namespace LibationFileManager
|
||||
{
|
||||
get
|
||||
{
|
||||
var logLevelStr = persistentDictionary.GetStringFromJsonPath("Serilog", "MinimumLevel");
|
||||
var logLevelStr = Settings.GetStringFromJsonPath("Serilog", "MinimumLevel");
|
||||
return Enum.TryParse<LogEventLevel>(logLevelStr, out var logLevelEnum) ? logLevelEnum : LogEventLevel.Information;
|
||||
}
|
||||
set
|
||||
{
|
||||
OnPropertyChanging(nameof(LogLevel), LogLevel, value);
|
||||
var valueWasChanged = persistentDictionary.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString());
|
||||
var valueWasChanged = Settings.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString());
|
||||
if (!valueWasChanged)
|
||||
{
|
||||
Log.Logger.Debug("LogLevel.set attempt. No change");
|
||||
return;
|
||||
}
|
||||
|
||||
configuration.Reload();
|
||||
configuration?.Reload();
|
||||
|
||||
OnPropertyChanged(nameof(LogLevel), value);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
@@ -8,6 +9,7 @@ using FileManager;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
@@ -18,32 +20,52 @@ namespace LibationFileManager
|
||||
// config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation
|
||||
// exceptions: appsettings.json, LibationFiles dir, Settings.json
|
||||
|
||||
private PersistentDictionary persistentDictionary;
|
||||
private PersistentDictionary? persistentDictionary;
|
||||
|
||||
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);
|
||||
public void SetNonString(object newValue, [CallerMemberName] string propertyName = "")
|
||||
private PersistentDictionary Settings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (persistentDictionary is null)
|
||||
throw new InvalidOperationException($"{nameof(persistentDictionary)} must first be set by accessing {nameof(LibationFiles)} or calling {nameof(SettingsFileIsValid)}");
|
||||
return persistentDictionary;
|
||||
}
|
||||
}
|
||||
|
||||
public bool RemoveProperty(string propertyName) => Settings.RemoveProperty(propertyName);
|
||||
|
||||
[return: NotNullIfNotNull(nameof(defaultValue))]
|
||||
public T? GetNonString<T>(T defaultValue, [CallerMemberName] string propertyName = "")
|
||||
=> Settings.GetNonString(propertyName, defaultValue);
|
||||
|
||||
|
||||
[return: NotNullIfNotNull(nameof(defaultValue))]
|
||||
public string? GetString(string? defaultValue = null, [CallerMemberName] string propertyName = "")
|
||||
=> Settings.GetString(propertyName, defaultValue);
|
||||
|
||||
public object? GetObject([CallerMemberName] string propertyName = "") => Settings.GetObject(propertyName);
|
||||
|
||||
public void SetNonString(object? newValue, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
var existing = getExistingValue(propertyName);
|
||||
if (existing?.Equals(newValue) is true) return;
|
||||
|
||||
OnPropertyChanging(propertyName, existing, newValue);
|
||||
persistentDictionary.SetNonString(propertyName, newValue);
|
||||
Settings.SetNonString(propertyName, newValue);
|
||||
OnPropertyChanged(propertyName, newValue);
|
||||
}
|
||||
|
||||
public void SetString(string newValue, [CallerMemberName] string propertyName = "")
|
||||
public void SetString(string? newValue, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
var existing = getExistingValue(propertyName);
|
||||
if (existing?.Equals(newValue) is true) return;
|
||||
|
||||
OnPropertyChanging(propertyName, existing, newValue);
|
||||
persistentDictionary.SetString(propertyName, newValue);
|
||||
Settings.SetString(propertyName, newValue);
|
||||
OnPropertyChanged(propertyName, newValue);
|
||||
}
|
||||
|
||||
private object getExistingValue(string propertyName)
|
||||
private object? getExistingValue(string propertyName)
|
||||
{
|
||||
var property = GetType().GetProperty(propertyName);
|
||||
if (property is not null) return property.GetValue(this);
|
||||
@@ -51,16 +73,16 @@ namespace LibationFileManager
|
||||
}
|
||||
|
||||
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
||||
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
|
||||
public void SetWithJsonPath(string jsonPath, string propertyName, string? newValue, bool suppressLogging = false)
|
||||
{
|
||||
var settingWasChanged = persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging);
|
||||
var settingWasChanged = Settings.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging);
|
||||
if (settingWasChanged)
|
||||
configuration?.Reload();
|
||||
}
|
||||
|
||||
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
||||
|
||||
public static string GetDescription(string propertyName)
|
||||
public static string? GetDescription(string propertyName)
|
||||
{
|
||||
var attribute = typeof(Configuration)
|
||||
.GetProperty(propertyName)
|
||||
@@ -71,16 +93,25 @@ namespace LibationFileManager
|
||||
return attribute?.Description;
|
||||
}
|
||||
|
||||
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
|
||||
public bool Exists(string propertyName) => Settings.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); }
|
||||
|
||||
[Description("Location for book storage. Includes destination of newly liberated books")]
|
||||
public LongPath Books { get => GetString(); set => SetString(value); }
|
||||
public LongPath? Books { get => GetString(); set => SetString(value); }
|
||||
|
||||
[Description("Overwrite existing files if they already exist?")]
|
||||
public bool OverwriteExisting { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
@@ -164,6 +195,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); }
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
@@ -9,17 +10,17 @@ namespace LibationFileManager
|
||||
* and be sure to clone it before returning. This allows Configuration to
|
||||
* accurately detect if any of the Dictionary's elements have changed.
|
||||
*/
|
||||
private class EquatableDictionary<TKey, TValue> : Dictionary<TKey, TValue>
|
||||
private class EquatableDictionary<TKey, TValue> : Dictionary<TKey, TValue> where TKey : notnull
|
||||
{
|
||||
public EquatableDictionary() { }
|
||||
public EquatableDictionary(IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs) : base(keyValuePairs) { }
|
||||
public EquatableDictionary<TKey, TValue> Clone() => new(this);
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is Dictionary<TKey, TValue> dic && Count == dic.Count)
|
||||
{
|
||||
foreach (var pair in this)
|
||||
if (!dic.TryGetValue(pair.Key, out var value) || !pair.Value.Equals(value))
|
||||
if (!dic.TryGetValue(pair.Key, out var value) || pair.Value?.Equals(value) is not true)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Linq;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration : PropertyChangeFilter
|
||||
@@ -24,9 +24,12 @@ namespace LibationFileManager
|
||||
|
||||
if (!Directory.Exists(booksDir))
|
||||
{
|
||||
if (Path.GetDirectoryName(settingsFile) is not string dir)
|
||||
throw new DirectoryNotFoundException(settingsFile);
|
||||
|
||||
//"Books" is not null, so setup has already been run.
|
||||
//Since Books can't be found, try to create it in Libation settings folder
|
||||
booksDir = Path.Combine(Path.GetDirectoryName(settingsFile), nameof(Books));
|
||||
booksDir = Path.Combine(dir, nameof(Books));
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(booksDir);
|
||||
|
||||
@@ -6,6 +6,7 @@ using Dinah.Core.Collections.Immutable;
|
||||
using FileManager;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public static class FilePathCache
|
||||
@@ -14,8 +15,8 @@ namespace LibationFileManager
|
||||
|
||||
private const string FILENAME = "FileLocations.json";
|
||||
|
||||
public static event EventHandler<CacheEntry> Inserted;
|
||||
public static event EventHandler<CacheEntry> Removed;
|
||||
public static event EventHandler<CacheEntry>? Inserted;
|
||||
public static event EventHandler<CacheEntry>? Removed;
|
||||
|
||||
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
|
||||
|
||||
@@ -51,16 +52,16 @@ namespace LibationFileManager
|
||||
.Select(entry => (entry.FileType, entry.Path))
|
||||
.ToList();
|
||||
|
||||
public static LongPath GetFirstPath(string id, FileType type)
|
||||
public static LongPath? GetFirstPath(string id, FileType type)
|
||||
=> getEntries(entry => entry.Id == id && entry.FileType == type)
|
||||
?.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());
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
#nullable enable
|
||||
public interface IInteropFunctions
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -5,11 +5,12 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using Dinah.Core;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public static class InteropFactory
|
||||
{
|
||||
public static Type InteropFunctionsType { get; }
|
||||
public static Type? InteropFunctionsType { get; }
|
||||
|
||||
public static IInteropFunctions Create() => _create();
|
||||
|
||||
@@ -17,13 +18,17 @@ namespace LibationFileManager
|
||||
//public static IInteropFunctions Create(string str, int i) => _create(str, i);
|
||||
//public static IInteropFunctions Create(params object[] values) => _create(values);
|
||||
|
||||
private static IInteropFunctions instance { get; set; }
|
||||
private static IInteropFunctions? instance { get; set; }
|
||||
private static IInteropFunctions _create(params object[] values)
|
||||
{
|
||||
instance ??=
|
||||
InteropFunctionsType is null
|
||||
? new NullInteropFunctions()
|
||||
: Activator.CreateInstance(InteropFunctionsType, values) as IInteropFunctions;
|
||||
|
||||
if (instance is null)
|
||||
throw new TypeLoadException();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -66,7 +71,7 @@ namespace LibationFileManager
|
||||
.GetTypes()
|
||||
.FirstOrDefault(type.IsAssignableFrom);
|
||||
}
|
||||
private static string getOSConfigApp()
|
||||
private static string? getOSConfigApp()
|
||||
{
|
||||
// find '*ConfigApp.dll' files
|
||||
var appName =
|
||||
@@ -76,8 +81,8 @@ namespace LibationFileManager
|
||||
return appName;
|
||||
}
|
||||
|
||||
private static Dictionary<string, Assembly> lowEffortCache { get; } = new();
|
||||
private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
|
||||
private static Dictionary<string, Assembly?> lowEffortCache { get; } = new();
|
||||
private static Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args)
|
||||
{
|
||||
var asmName = new AssemblyName(args.Name);
|
||||
var here = Configuration.ProcessDirectory;
|
||||
@@ -97,7 +102,7 @@ namespace LibationFileManager
|
||||
return assembly;
|
||||
}
|
||||
|
||||
private static Assembly CurrentDomain_AssemblyResolve_internal(AssemblyName asmName, string here)
|
||||
private static Assembly? CurrentDomain_AssemblyResolve_internal(AssemblyName asmName, string here)
|
||||
{
|
||||
/*
|
||||
* Find the requested assembly in the program files directory.
|
||||
|
||||
@@ -2,26 +2,27 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public class BookDto
|
||||
{
|
||||
public string AudibleProductId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Subtitle { get; set; }
|
||||
public string TitleWithSubtitle { get; set; }
|
||||
public string Locale { get; set; }
|
||||
public string? AudibleProductId { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Subtitle { get; set; }
|
||||
public string? TitleWithSubtitle { get; set; }
|
||||
public string? Locale { get; set; }
|
||||
public int? YearPublished { get; set; }
|
||||
|
||||
public IEnumerable<string> Authors { get; set; }
|
||||
public string AuthorNames => string.Join(", ", Authors);
|
||||
public string FirstAuthor => Authors.FirstOrDefault();
|
||||
public IEnumerable<string>? Authors { get; set; }
|
||||
public string? AuthorNames => Authors is null ? null : string.Join(", ", Authors);
|
||||
public string? FirstAuthor => Authors?.FirstOrDefault();
|
||||
|
||||
public IEnumerable<string> Narrators { get; set; }
|
||||
public string NarratorNames => string.Join(", ", Narrators);
|
||||
public string FirstNarrator => Narrators.FirstOrDefault();
|
||||
public IEnumerable<string>? Narrators { get; set; }
|
||||
public string? NarratorNames => Narrators is null? null: string.Join(", ", Narrators);
|
||||
public string? FirstNarrator => Narrators?.FirstOrDefault();
|
||||
|
||||
public string SeriesName { get; set; }
|
||||
public string? SeriesName { get; set; }
|
||||
public float? SeriesNumber { get; set; }
|
||||
public bool IsSeries => !string.IsNullOrEmpty(SeriesName);
|
||||
public bool IsPodcastParent { get; set; }
|
||||
@@ -32,13 +33,13 @@ namespace LibationFileManager
|
||||
public int Channels { get; set; }
|
||||
public DateTime FileDate { get; set; } = DateTime.Now;
|
||||
public DateTime? DatePublished { get; set; }
|
||||
public string Language { get; set; }
|
||||
public string? Language { get; set; }
|
||||
}
|
||||
|
||||
public class LibraryBookDto : BookDto
|
||||
{
|
||||
public DateTime? DateAdded { get; set; }
|
||||
public string Account { get; set; }
|
||||
public string AccountNickname { get; set; }
|
||||
public string? Account { get; set; }
|
||||
public string? AccountNickname { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,16 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
internal partial class NameListFormat
|
||||
{
|
||||
public static string Formatter(ITemplateTag _, IEnumerable<string> names, string formatString)
|
||||
public static string Formatter(ITemplateTag _, IEnumerable<string>? names, string formatString)
|
||||
{
|
||||
var humanNames = names.Select(n => new HumanName(RemoveSuffix(n), Prefer.FirstOverPrefix));
|
||||
if (names is null) return "";
|
||||
|
||||
var humanNames = names.Select(n => new HumanName(RemoveSuffix(n), Prefer.FirstOverPrefix));
|
||||
|
||||
var sortedNames = Sort(humanNames, formatString);
|
||||
var nameFormatString = Format(formatString, defaultValue: "{T} {F} {M} {L} {S}");
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public class NullInteropFunctions : IInteropFunctions
|
||||
|
||||
@@ -6,18 +6,25 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public enum PictureSize { Native, _80x80 = 80, _300x300 = 300, _500x500 = 500 }
|
||||
public class PictureCachedEventArgs : EventArgs
|
||||
{
|
||||
public PictureDefinition Definition { get; internal set; }
|
||||
public byte[] Picture { get; internal set; }
|
||||
public PictureDefinition Definition { get; }
|
||||
public byte[] Picture { get; }
|
||||
|
||||
internal PictureCachedEventArgs(PictureDefinition definition, byte[] picture)
|
||||
{
|
||||
Definition = definition;
|
||||
Picture = picture;
|
||||
}
|
||||
}
|
||||
public struct PictureDefinition : IEquatable<PictureDefinition>
|
||||
{
|
||||
public string PictureId { get; }
|
||||
public PictureSize Size { get; }
|
||||
public string PictureId { get; init; }
|
||||
public PictureSize Size { get; init; }
|
||||
|
||||
public PictureDefinition(string pictureId, PictureSize pictureSize)
|
||||
{
|
||||
@@ -45,7 +52,7 @@ namespace LibationFileManager
|
||||
.Start();
|
||||
}
|
||||
|
||||
public static event EventHandler<PictureCachedEventArgs> PictureCached;
|
||||
public static event EventHandler<PictureCachedEventArgs>? PictureCached;
|
||||
|
||||
private static BlockingCollection<PictureDefinition> DownloadQueue { get; } = new BlockingCollection<PictureDefinition>();
|
||||
private static object cacheLocker { get; } = new object();
|
||||
@@ -112,7 +119,7 @@ namespace LibationFileManager
|
||||
lock (cacheLocker)
|
||||
cache[def] = bytes;
|
||||
|
||||
PictureCached?.Invoke(nameof(PictureStorage), new PictureCachedEventArgs { Definition = def, Picture = bytes });
|
||||
PictureCached?.Invoke(nameof(PictureStorage), new PictureCachedEventArgs(def, bytes));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,30 +5,30 @@ using System.Linq;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public static class QuickFilters
|
||||
{
|
||||
public static event EventHandler Updated;
|
||||
public static event EventHandler? Updated;
|
||||
|
||||
internal class FilterState
|
||||
public static event EventHandler? UseDefaultChanged;
|
||||
|
||||
internal class FilterState
|
||||
{
|
||||
public bool UseDefault { get; set; }
|
||||
public List<string> Filters { get; set; } = new List<string>();
|
||||
}
|
||||
|
||||
static FilterState inMemoryState { get; } = new FilterState();
|
||||
|
||||
public static string JsonFile => Path.Combine(Configuration.Instance.LibationFiles, "QuickFilters.json");
|
||||
|
||||
static QuickFilters()
|
||||
{
|
||||
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
|
||||
if (File.Exists(JsonFile))
|
||||
inMemoryState = JsonConvert.DeserializeObject<FilterState>(File.ReadAllText(JsonFile));
|
||||
}
|
||||
|
||||
public static event EventHandler UseDefaultChanged;
|
||||
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
|
||||
static FilterState inMemoryState { get; }
|
||||
= File.Exists(JsonFile) && JsonConvert.DeserializeObject<FilterState>(File.ReadAllText(JsonFile)) is FilterState inMemState
|
||||
? inMemState
|
||||
: new FilterState();
|
||||
|
||||
public static bool UseDefault
|
||||
{
|
||||
get => inMemoryState.UseDefault;
|
||||
@@ -43,7 +43,7 @@ namespace LibationFileManager
|
||||
save(false);
|
||||
}
|
||||
|
||||
UseDefaultChanged?.Invoke(null, null);
|
||||
UseDefaultChanged?.Invoke(null, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ namespace LibationFileManager
|
||||
}
|
||||
|
||||
if (invokeUpdatedEvent)
|
||||
Updated?.Invoke(null, null);
|
||||
Updated?.Invoke(null, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public interface ITemplateEditor
|
||||
@@ -14,14 +15,11 @@ namespace LibationFileManager
|
||||
string DefaultTemplate { get; }
|
||||
string TemplateName { get; }
|
||||
string TemplateDescription { get; }
|
||||
Templates Folder { get; }
|
||||
Templates File { get; }
|
||||
Templates Name { get; }
|
||||
Templates EditingTemplate { get; }
|
||||
void SetTemplateText(string templateText);
|
||||
string GetFolderName();
|
||||
string GetFileName();
|
||||
string GetName();
|
||||
bool SetTemplateText(string templateText);
|
||||
string? GetFolderName();
|
||||
string? GetFileName();
|
||||
string? GetName();
|
||||
}
|
||||
|
||||
public class TemplateEditor<T> : ITemplateEditor where T : Templates, ITemplate, new()
|
||||
@@ -32,9 +30,9 @@ namespace LibationFileManager
|
||||
public string DefaultTemplate { get; private init; }
|
||||
public string TemplateName { get; private init; }
|
||||
public string TemplateDescription { get; private init; }
|
||||
public Templates Folder { get; private set; }
|
||||
public Templates File { get; private set; }
|
||||
public Templates Name { get; private set; }
|
||||
private Templates? Folder { get; set; }
|
||||
private Templates? File { get; set; }
|
||||
private Templates? Name { get; set; }
|
||||
public Templates EditingTemplate
|
||||
{
|
||||
get => _editingTemplate;
|
||||
@@ -43,10 +41,14 @@ namespace LibationFileManager
|
||||
|
||||
private Templates _editingTemplate;
|
||||
|
||||
public void SetTemplateText(string templateText)
|
||||
public bool SetTemplateText(string templateText)
|
||||
{
|
||||
Templates.TryGetTemplate<T>(templateText, out var template);
|
||||
EditingTemplate = template;
|
||||
if (Templates.TryGetTemplate<T>(templateText, out var template))
|
||||
{
|
||||
EditingTemplate = template;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static readonly LibraryBookDto libraryBookDto
|
||||
@@ -80,7 +82,7 @@ namespace LibationFileManager
|
||||
Title = "A Flight for Life"
|
||||
};
|
||||
|
||||
public string GetFolderName()
|
||||
public string? GetFolderName()
|
||||
{
|
||||
/*
|
||||
* Path must be rooted for windows to allow long file paths. This is
|
||||
@@ -88,49 +90,49 @@ namespace LibationFileManager
|
||||
* subdirectories. Without rooting, we won't be allowed to create a
|
||||
* relative path longer than MAX_PATH.
|
||||
*/
|
||||
var dir = Folder.GetFilename(libraryBookDto, BaseDirectory, "");
|
||||
var dir = Folder?.GetFilename(libraryBookDto, BaseDirectory, "");
|
||||
if (dir is null) return null;
|
||||
return Path.GetRelativePath(BaseDirectory, dir);
|
||||
}
|
||||
|
||||
public string GetFileName()
|
||||
=> File.GetFilename(libraryBookDto, partFileProperties, "", "");
|
||||
public string GetName()
|
||||
=> Name.GetName(libraryBookDto, partFileProperties);
|
||||
public string? GetFileName()
|
||||
=> File?.GetFilename(libraryBookDto, partFileProperties, "", "");
|
||||
public string? GetName()
|
||||
=> Name?.GetName(libraryBookDto, partFileProperties);
|
||||
|
||||
private TemplateEditor(
|
||||
Templates editingTemplate,
|
||||
LongPath baseDirectory,
|
||||
string defaultTemplate,
|
||||
string templateName,
|
||||
string templateDescription)
|
||||
{
|
||||
_editingTemplate = editingTemplate;
|
||||
BaseDirectory = baseDirectory;
|
||||
DefaultTemplate = defaultTemplate;
|
||||
TemplateName = templateName;
|
||||
TemplateDescription = templateDescription;
|
||||
}
|
||||
|
||||
public static ITemplateEditor CreateFilenameEditor(LongPath baseDir, string templateText)
|
||||
{
|
||||
Templates.TryGetTemplate<T>(templateText, out var template);
|
||||
if (!Templates.TryGetTemplate<T>(templateText, out var template))
|
||||
throw new ArgumentException($"Failed to parse {nameof(templateText)}");
|
||||
|
||||
var templateEditor = new TemplateEditor<T>
|
||||
{
|
||||
_editingTemplate = template,
|
||||
BaseDirectory = baseDir,
|
||||
DefaultTemplate = T.DefaultTemplate,
|
||||
TemplateName = T.Name,
|
||||
TemplateDescription = T.Description
|
||||
|
||||
};
|
||||
var templateEditor = new TemplateEditor<T>(template, baseDir, T.DefaultTemplate, T.Name, T.Description);
|
||||
|
||||
if (!templateEditor.IsFolder && !templateEditor.IsFilePath)
|
||||
throw new InvalidOperationException($"This method is only for File and Folder templates. Use {nameof(CreateNameEditor)} for name templates");
|
||||
|
||||
templateEditor.Folder = templateEditor.IsFolder ? template : Templates.Folder;
|
||||
templateEditor.File = templateEditor.IsFolder ? Templates.File : template;
|
||||
|
||||
return templateEditor;
|
||||
}
|
||||
|
||||
public static ITemplateEditor CreateNameEditor(string templateText)
|
||||
{
|
||||
Templates.TryGetTemplate<T>(templateText, out var nameTemplate);
|
||||
if (!Templates.TryGetTemplate<T>(templateText, out var nameTemplate))
|
||||
throw new ArgumentException($"Failed to parse {nameof(templateText)}");
|
||||
|
||||
var templateEditor = new TemplateEditor<T>
|
||||
{
|
||||
_editingTemplate = nameTemplate,
|
||||
DefaultTemplate = T.DefaultTemplate,
|
||||
TemplateName = T.Name,
|
||||
TemplateDescription = T.Description
|
||||
};
|
||||
var templateEditor = new TemplateEditor<T>(nameTemplate, "", T.DefaultTemplate, T.Name, T.Description);
|
||||
|
||||
if (templateEditor.IsFolder || templateEditor.IsFilePath)
|
||||
throw new InvalidOperationException($"This method is only for name templates. Use {nameof(CreateFilenameEditor)} for file templates");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using FileManager.NamingTemplate;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public sealed class TemplateTags : ITemplateTag
|
||||
@@ -10,7 +11,7 @@ namespace LibationFileManager
|
||||
public string Description { get; }
|
||||
public string Display { get; }
|
||||
|
||||
private TemplateTags(string tagName, string description, string defaultValue = null, string display = null)
|
||||
private TemplateTags(string tagName, string description, string? defaultValue = null, string? display = null)
|
||||
{
|
||||
TagName = tagName;
|
||||
Description = description;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using AaxDecrypter;
|
||||
@@ -8,6 +9,7 @@ using FileManager;
|
||||
using FileManager.NamingTemplate;
|
||||
using NameParser;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public interface ITemplate
|
||||
@@ -26,10 +28,10 @@ namespace LibationFileManager
|
||||
//Assigning the properties in the static constructor will require all
|
||||
//Templates users to have a valid configuration file. To allow tests
|
||||
//to work without access to Configuration, only load templates on demand.
|
||||
private static FolderTemplate _folder;
|
||||
private static FileTemplate _file;
|
||||
private static ChapterFileTemplate _chapterFile;
|
||||
private static ChapterTitleTemplate _chapterTitle;
|
||||
private static FolderTemplate? _folder;
|
||||
private static FileTemplate? _file;
|
||||
private static ChapterFileTemplate? _chapterFile;
|
||||
private static ChapterTitleTemplate? _chapterTitle;
|
||||
|
||||
public static FolderTemplate Folder => _folder ??= GetTemplate<FolderTemplate>(Configuration.Instance.FolderTemplate);
|
||||
public static FileTemplate File => _file ??= GetTemplate<FileTemplate>(Configuration.Instance.FileTemplate);
|
||||
@@ -38,10 +40,10 @@ namespace LibationFileManager
|
||||
|
||||
#region Template Parsing
|
||||
|
||||
public static T GetTemplate<T>(string templateText) where T : Templates, ITemplate, new()
|
||||
=> TryGetTemplate<T>(templateText, out var template) ? template : GetDefaultTemplate<T>();
|
||||
public static T GetTemplate<T>(string? templateText) where T : Templates, ITemplate, new()
|
||||
=> TryGetTemplate<T>(templateText ?? "", out var template) ? template : GetDefaultTemplate<T>();
|
||||
|
||||
public static bool TryGetTemplate<T>(string templateText, out T template) where T : Templates, ITemplate, new()
|
||||
public static bool TryGetTemplate<T>(string templateText, [NotNullWhen(true)] out T? template) where T : Templates, ITemplate, new()
|
||||
{
|
||||
var namingTemplate = NamingTemplate.Parse(templateText, T.TagCollections);
|
||||
|
||||
@@ -56,19 +58,19 @@ namespace LibationFileManager
|
||||
{
|
||||
Configuration.Instance.PropertyChanged +=
|
||||
[PropertyChangeFilter(nameof(Configuration.FolderTemplate))]
|
||||
(_,e) => _folder = GetTemplate<FolderTemplate>((string)e.NewValue);
|
||||
(_,e) => _folder = GetTemplate<FolderTemplate>(e.NewValue as string);
|
||||
|
||||
Configuration.Instance.PropertyChanged +=
|
||||
[PropertyChangeFilter(nameof(Configuration.FileTemplate))]
|
||||
(_, e) => _file = GetTemplate<FileTemplate>((string)e.NewValue);
|
||||
(_, e) => _file = GetTemplate<FileTemplate>(e.NewValue as string);
|
||||
|
||||
Configuration.Instance.PropertyChanged +=
|
||||
[PropertyChangeFilter(nameof(Configuration.ChapterFileTemplate))]
|
||||
(_, e) => _chapterFile = GetTemplate<ChapterFileTemplate>((string)e.NewValue);
|
||||
(_, e) => _chapterFile = GetTemplate<ChapterFileTemplate>(e.NewValue as string);
|
||||
|
||||
Configuration.Instance.PropertyChanged +=
|
||||
[PropertyChangeFilter(nameof(Configuration.ChapterTitleTemplate))]
|
||||
(_, e) => _chapterTitle = GetTemplate<ChapterTitleTemplate>((string)e.NewValue);
|
||||
(_, e) => _chapterTitle = GetTemplate<ChapterTitleTemplate>(e.NewValue as string);
|
||||
|
||||
HumanName.Suffixes.Add("ret");
|
||||
HumanName.Titles.Add("professor");
|
||||
@@ -78,10 +80,18 @@ namespace LibationFileManager
|
||||
|
||||
#region Template Properties
|
||||
|
||||
public IEnumerable<TemplateTags> TagsRegistered => NamingTemplate.TagsRegistered.Cast<TemplateTags>();
|
||||
public IEnumerable<TemplateTags> TagsInUse => NamingTemplate.TagsInUse.Cast<TemplateTags>();
|
||||
public string TemplateText => NamingTemplate.TemplateText;
|
||||
protected NamingTemplate NamingTemplate { get; private set; }
|
||||
public IEnumerable<TemplateTags> TagsRegistered
|
||||
=> NamingTemplate?.TagsRegistered.Cast<TemplateTags>() ?? Enumerable.Empty<TemplateTags>();
|
||||
public IEnumerable<TemplateTags> TagsInUse
|
||||
=> NamingTemplate?.TagsInUse.Cast<TemplateTags>() ?? Enumerable.Empty<TemplateTags>();
|
||||
public string TemplateText => NamingTemplate?.TemplateText ?? "";
|
||||
|
||||
private readonly NamingTemplate? _namingTemplate;
|
||||
protected NamingTemplate NamingTemplate
|
||||
{
|
||||
get => _namingTemplate ?? throw new NullReferenceException(nameof(_namingTemplate));
|
||||
private init => _namingTemplate = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -104,7 +114,7 @@ namespace LibationFileManager
|
||||
return string.Concat(NamingTemplate.Evaluate(libraryBookDto, multiChapProps).Select(p => p.Value));
|
||||
}
|
||||
|
||||
public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, ReplacementCharacters replacements = null, bool returnFirstExisting = false)
|
||||
public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||
ArgumentValidator.EnsureNotNull(baseDir, nameof(baseDir));
|
||||
@@ -114,7 +124,7 @@ namespace LibationFileManager
|
||||
return GetFilename(baseDir, fileExtension,replacements, returnFirstExisting, libraryBookDto);
|
||||
}
|
||||
|
||||
public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir, string fileExtension, ReplacementCharacters replacements = null, bool returnFirstExisting = false)
|
||||
public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||
ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps));
|
||||
@@ -246,7 +256,7 @@ namespace LibationFileManager
|
||||
new(caseSensative: true, StringFormatter, DateTimeFormatter, IntegerFormatter, FloatFormatter)
|
||||
{
|
||||
//Don't allow formatting of Id
|
||||
{ TemplateTags.Id, lb => lb.AudibleProductId, v => v },
|
||||
{ TemplateTags.Id, lb => lb.AudibleProductId, v => v ?? "" },
|
||||
{ TemplateTags.Title, lb => lb.TitleWithSubtitle },
|
||||
{ TemplateTags.TitleShort, lb => getTitleShort(lb.Title) },
|
||||
{ TemplateTags.AudibleTitle, lb => lb.Title },
|
||||
@@ -308,13 +318,13 @@ namespace LibationFileManager
|
||||
|
||||
#region Tag Formatters
|
||||
|
||||
private static string getTitleShort(string title)
|
||||
private static string? getTitleShort(string? title)
|
||||
=> title?.IndexOf(':') > 0 ? title.Substring(0, title.IndexOf(':')) : title;
|
||||
|
||||
private static string getLanguageShort(string language)
|
||||
private static string getLanguageShort(string? language)
|
||||
{
|
||||
if (language is null)
|
||||
return null;
|
||||
return "";
|
||||
|
||||
language = language.Trim();
|
||||
if (language.Length <= 3)
|
||||
@@ -324,8 +334,9 @@ namespace LibationFileManager
|
||||
|
||||
private static string StringFormatter(ITemplateTag templateTag, string value, string formatString)
|
||||
{
|
||||
if (string.Compare(formatString, "u", ignoreCase: true) == 0) return value?.ToUpper();
|
||||
else if (string.Compare(formatString, "l", ignoreCase: true) == 0) return value?.ToLower();
|
||||
if (value is null) return "";
|
||||
else if (string.Compare(formatString, "u", ignoreCase: true) == 0) return value.ToUpper();
|
||||
else if (string.Compare(formatString, "l", ignoreCase: true) == 0) return value.ToLower();
|
||||
else return value;
|
||||
}
|
||||
|
||||
@@ -358,7 +369,7 @@ namespace LibationFileManager
|
||||
public class FolderTemplate : Templates, ITemplate
|
||||
{
|
||||
public static string Name { get; }= "Folder Template";
|
||||
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate));
|
||||
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? "";
|
||||
public static string DefaultTemplate { get; } = "<title short> [<id>]";
|
||||
public static IEnumerable<TagCollection> TagCollections
|
||||
=> new TagCollection[] { filePropertyTags, conditionalTags, folderConditionalTags };
|
||||
@@ -378,7 +389,7 @@ namespace LibationFileManager
|
||||
public class FileTemplate : Templates, ITemplate
|
||||
{
|
||||
public static string Name { get; } = "File Template";
|
||||
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate));
|
||||
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate)) ?? "";
|
||||
public static string DefaultTemplate { get; } = "<title> [<id>]";
|
||||
public static IEnumerable<TagCollection> TagCollections { get; } = new TagCollection[] { filePropertyTags, conditionalTags };
|
||||
}
|
||||
@@ -386,7 +397,7 @@ namespace LibationFileManager
|
||||
public class ChapterFileTemplate : Templates, ITemplate
|
||||
{
|
||||
public static string Name { get; } = "Chapter File Template";
|
||||
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
|
||||
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)) ?? "";
|
||||
public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
|
||||
public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags);
|
||||
|
||||
@@ -399,7 +410,7 @@ namespace LibationFileManager
|
||||
public class ChapterTitleTemplate : Templates, ITemplate
|
||||
{
|
||||
public static string Name { get; } = "Chapter Title Template";
|
||||
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate));
|
||||
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)) ?? "";
|
||||
public static string DefaultTemplate => "<ch#> - <title short>: <ch title>";
|
||||
public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(conditionalTags);
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
73
Source/LibationWinForms/ClearableTextBox.Designer.cs
generated
Normal file
73
Source/LibationWinForms/ClearableTextBox.Designer.cs
generated
Normal 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;
|
||||
}
|
||||
}
|
||||
44
Source/LibationWinForms/ClearableTextBox.cs
Normal file
44
Source/LibationWinForms/ClearableTextBox.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
120
Source/LibationWinForms/ClearableTextBox.resx
Normal file
120
Source/LibationWinForms/ClearableTextBox.resx
Normal 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>
|
||||
@@ -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}
|
||||
|
||||
@@ -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,30 +49,25 @@ 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(3, 58);
|
||||
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;
|
||||
//
|
||||
// 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(23, 56);
|
||||
customTb.Name = "customTb";
|
||||
customTb.Size = new System.Drawing.Size(1172, 39);
|
||||
customTb.Size = new System.Drawing.Size(606, 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(635, 55);
|
||||
customBtn.Name = "customBtn";
|
||||
customBtn.Size = new System.Drawing.Size(82, 54);
|
||||
customBtn.Size = new System.Drawing.Size(26, 23);
|
||||
customBtn.TabIndex = 4;
|
||||
customBtn.Text = "...";
|
||||
customBtn.UseVisualStyleBackColor = true;
|
||||
@@ -81,26 +75,22 @@ namespace LibationWinForms.Dialogs
|
||||
//
|
||||
// directorySelectControl
|
||||
//
|
||||
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.Name = "directorySelectControl";
|
||||
directorySelectControl.Size = new System.Drawing.Size(1270, 104);
|
||||
directorySelectControl.Size = new System.Drawing.Size(637, 50);
|
||||
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, 80);
|
||||
Load += DirectoryOrCustomSelectControl_Load;
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
|
||||
@@ -38,6 +38,17 @@ namespace LibationWinForms.Dialogs
|
||||
selectDir(directory, null);
|
||||
}
|
||||
|
||||
protected override void OnResize(EventArgs e)
|
||||
{
|
||||
base.OnResize(e);
|
||||
//Workaround for anchoring bug in user controls
|
||||
//https://github.com/dotnet/winforms/issues/6381
|
||||
customBtn.Location = new System.Drawing.Point(Width - customBtn.Width, customTb.Location.Y);
|
||||
customBtn.Height = customTb.Height;
|
||||
directorySelectControl.Width = Width - directorySelectControl.Location.X;
|
||||
customTb.Width = Width - customTb.Location.X - customBtn.Width - customTb.Margin.Left;
|
||||
}
|
||||
|
||||
/// <summary>set selection</summary>
|
||||
public void SelectDirectory(string directory)
|
||||
{
|
||||
|
||||
@@ -35,36 +35,32 @@ namespace LibationWinForms.Dialogs
|
||||
//
|
||||
// directoryComboBox
|
||||
//
|
||||
directoryComboBox.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
directoryComboBox.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
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.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
textBox1.Location = new System.Drawing.Point(0, 26);
|
||||
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, 49);
|
||||
Load += DirectorySelectControl_Load;
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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, 119);
|
||||
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(736, 119);
|
||||
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);
|
||||
libationFilesSelectControl.Name = "libationFilesSelectControl";
|
||||
libationFilesSelectControl.Size = new System.Drawing.Size(1818, 176);
|
||||
libationFilesSelectControl.Size = new System.Drawing.Size(906, 81);
|
||||
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, 158);
|
||||
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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user