mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-09 22:38:53 -05:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
9720a573c7 | ||
|
|
1cf01aa92a | ||
|
|
4df9e5abbf | ||
|
|
9243aa47e7 | ||
|
|
c69f41a2a6 | ||
|
|
27c74e52ca | ||
|
|
bfa7f5cca9 | ||
|
|
22a3dcbc1f | ||
|
|
ec9d11cf52 | ||
|
|
fbc29dfb0a |
1
.github/workflows/build-windows.yml
vendored
1
.github/workflows/build-windows.yml
vendored
@@ -75,6 +75,7 @@ jobs:
|
||||
LibationCli/LibationCli.csproj `
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
|
||||
-p:DefineConstants="${{ matrix.release_name }}" `
|
||||
-p:PublishProfile=LibationCli/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish `
|
||||
Hangover${{ matrix.ui }}/Hangover${{ matrix.ui }}.csproj `
|
||||
|
||||
@@ -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,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>10.5.0.1</Version>
|
||||
<Version>10.6.3.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="6.0.0" />
|
||||
|
||||
@@ -90,28 +90,18 @@ namespace AppScaffolding
|
||||
}
|
||||
|
||||
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
|
||||
public static void RunPostMigrationScaffolding(Configuration config)
|
||||
public static void RunPostMigrationScaffolding(Variety variety, Configuration config)
|
||||
{
|
||||
Variety = Enum.IsDefined(variety) ? variety : Variety.None;
|
||||
|
||||
var releaseID = (ReleaseIdentifier)((int)variety | (int)Configuration.OS | (int)RuntimeInformation.ProcessArchitecture);
|
||||
|
||||
ReleaseIdentifier = Enum.IsDefined(releaseID) ? releaseID : ReleaseIdentifier.None;
|
||||
|
||||
ensureSerilogConfig(config);
|
||||
configureLogging(config);
|
||||
logStartupState(config);
|
||||
|
||||
#region Determine Libation Variery and Release ID
|
||||
|
||||
Variety = File.Exists("System.Windows.Forms.dll") ? Variety.Classic : Variety.Chardonnay;
|
||||
|
||||
var releaseID = (ReleaseIdentifier)((int)Variety | (int)Configuration.OS | (int)RuntimeInformation.ProcessArchitecture);
|
||||
|
||||
if (Enum.IsDefined(releaseID))
|
||||
ReleaseIdentifier = releaseID;
|
||||
else
|
||||
{
|
||||
ReleaseIdentifier = ReleaseIdentifier.None;
|
||||
Serilog.Log.Logger.Warning("Unknown release identifier @{DebugInfo}", new { Variety, Configuration.OS, RuntimeInformation.ProcessArchitecture });
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// all else should occur after logging
|
||||
|
||||
wireUpSystemEvents(config);
|
||||
|
||||
@@ -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.2.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,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.2.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.3.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.1.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.2.1" />
|
||||
<PackageReference Include="Polly" Version="7.2.3" />
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.3.1" />
|
||||
<PackageReference Include="Polly" Version="7.2.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
||||
@@ -131,6 +131,35 @@ namespace FileManager
|
||||
writeFile(propertyName, parsedNewValue);
|
||||
}
|
||||
|
||||
public bool RemoveProperty(string propertyName)
|
||||
{
|
||||
if (IsReadOnly)
|
||||
return false;
|
||||
|
||||
var success = false;
|
||||
try
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
var jObject = readFile();
|
||||
|
||||
if (!jObject.ContainsKey(propertyName))
|
||||
return false;
|
||||
|
||||
jObject.Remove(propertyName);
|
||||
|
||||
var endContents = JsonConvert.SerializeObject(jObject, Formatting.Indented);
|
||||
|
||||
File.WriteAllText(Filepath, endContents);
|
||||
success = true;
|
||||
}
|
||||
Serilog.Log.Logger.Information("Removed property. {@DebugInfo}", propertyName);
|
||||
}
|
||||
catch { }
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private void writeFile(string propertyName, JToken newValue)
|
||||
{
|
||||
if (IsReadOnly)
|
||||
|
||||
@@ -67,13 +67,13 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
<PackageReference Include="Avalonia" Version="11.0.0-rc2.2" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-rc2.2" />
|
||||
<PackageReference Include="Avalonia" Version="11.0.0" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
|
||||
<!--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-rc2.2" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-rc2.2" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-rc2.2" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-rc2.2" />
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace HangoverAvalonia.Views
|
||||
|
||||
var config = LibationScaffolding.RunPreConfigMigrations();
|
||||
LibationScaffolding.RunPostConfigMigrations(config);
|
||||
LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
LibationScaffolding.RunPostMigrationScaffolding(Variety.Chardonnay, config);
|
||||
}
|
||||
|
||||
public void OnLoad()
|
||||
|
||||
4
Source/HangoverWinForms/Form1.Designer.cs
generated
4
Source/HangoverWinForms/Form1.Designer.cs
generated
@@ -219,8 +219,8 @@
|
||||
//
|
||||
// Form1
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.ClientSize = new System.Drawing.Size(800, 450);
|
||||
this.Controls.Add(this.tabControl1);
|
||||
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace HangoverWinForms
|
||||
|
||||
var config = LibationScaffolding.RunPreConfigMigrations();
|
||||
LibationScaffolding.RunPostConfigMigrations(config);
|
||||
LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
LibationScaffolding.RunPostMigrationScaffolding(Variety.Classic, config);
|
||||
|
||||
databaseTab.VisibleChanged += databaseTab_VisibleChanged;
|
||||
cliTab.VisibleChanged += cliTab_VisibleChanged;
|
||||
|
||||
@@ -141,7 +141,7 @@ namespace LibationAvalonia
|
||||
await MessageBox.VerboseLoggingWarning_ShowIfTrue();
|
||||
|
||||
// logging is init'd here
|
||||
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(AppScaffolding.Variety.Chardonnay, config);
|
||||
}
|
||||
|
||||
private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action<IClassicDesktopStyleApplicationLifetime, LibationFilesDialog, Configuration> OnClose)
|
||||
|
||||
@@ -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-rc2.2" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
||||
<PackageReference Include="Avalonia" Version="11.0.0-rc2.2" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-rc2.2" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-rc2.2" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-rc2.2" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-rc2.2" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-rc2.2" />
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using AppScaffolding;
|
||||
@@ -35,30 +34,45 @@ namespace LibationAvalonia
|
||||
$"\"{Configuration.ProcessDirectory}\"");
|
||||
return;
|
||||
}
|
||||
AppDomain.CurrentDomain.UnhandledException += (o, e) => LogError(e.ExceptionObject);
|
||||
|
||||
bool loggingEnabled = false;
|
||||
//***********************************************//
|
||||
// //
|
||||
// do not use Configuration before this line //
|
||||
// //
|
||||
//***********************************************//
|
||||
// Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration
|
||||
var config = LibationScaffolding.RunPreConfigMigrations();
|
||||
|
||||
//Start as much work in parallel as possible.
|
||||
var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime());
|
||||
var appBuilderTask = Task.Run(BuildAvaloniaApp);
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
try
|
||||
{
|
||||
if (!RunDbMigrations(config))
|
||||
return;
|
||||
var config = LibationScaffolding.RunPreConfigMigrations();
|
||||
|
||||
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
//Start as much work in parallel as possible.
|
||||
var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime());
|
||||
var appBuilderTask = Task.Run(BuildAvaloniaApp);
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
// 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));
|
||||
}
|
||||
|
||||
appBuilderTask.GetAwaiter().GetResult().SetupWithLifetime(classicLifetimeTask.GetAwaiter().GetResult());
|
||||
|
||||
classicLifetimeTask.Result.Start(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (loggingEnabled)
|
||||
Serilog.Log.Logger.Error(ex, "CRASH");
|
||||
else
|
||||
LogError(ex);
|
||||
}
|
||||
|
||||
appBuilderTask.GetAwaiter().GetResult().SetupWithLifetime(classicLifetimeTask.GetAwaiter().GetResult());
|
||||
|
||||
classicLifetimeTask.Result.Start(null);
|
||||
}
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
@@ -67,20 +81,35 @@ namespace LibationAvalonia
|
||||
.LogToTrace()
|
||||
.UseReactiveUI();
|
||||
|
||||
public static bool RunDbMigrations(Configuration config)
|
||||
private static void LogError(object exceptionObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
// most migrations go in here
|
||||
LibationScaffolding.RunPostConfigMigrations(config);
|
||||
LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
var logError = $"""
|
||||
{DateTime.Now} - Libation Crash
|
||||
OS {Configuration.OS}
|
||||
Version {LibationScaffolding.BuildVersion}
|
||||
ReleaseIdentifier {LibationScaffolding.ReleaseIdentifier}
|
||||
InteropFunctionsType {InteropFactory.InteropFunctionsType}
|
||||
LibationFiles {getConfigValue(c => c.LibationFiles)}
|
||||
Books Folder {getConfigValue(c => c.Books)}
|
||||
=== EXCEPTION ===
|
||||
{exceptionObject}
|
||||
""";
|
||||
|
||||
return LibationScaffolding.ReleaseIdentifier is not ReleaseIdentifier.None;
|
||||
}
|
||||
catch (Exception exDebug)
|
||||
var crashLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LibationCrash.log");
|
||||
|
||||
using var sw = new StreamWriter(crashLog, true);
|
||||
sw.WriteLine(logError);
|
||||
|
||||
static string getConfigValue(Func<Configuration, string> selector)
|
||||
{
|
||||
Serilog.Log.Logger.Debug(exDebug, "Silent failure");
|
||||
return false;
|
||||
try
|
||||
{
|
||||
return selector(Configuration.Instance);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ex.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -17,9 +17,13 @@ namespace LibationCli
|
||||
//***********************************************//
|
||||
var config = LibationScaffolding.RunPreConfigMigrations();
|
||||
|
||||
|
||||
LibationScaffolding.RunPostConfigMigrations(config);
|
||||
LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
|
||||
#if classic
|
||||
LibationScaffolding.RunPostMigrationScaffolding(Variety.Classic, config);
|
||||
#else
|
||||
LibationScaffolding.RunPostMigrationScaffolding(Variety.Chardonnay, config);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static Type[] LoadVerbs() => Assembly.GetExecutingAssembly()
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ namespace LibationFileManager
|
||||
|
||||
private PersistentDictionary persistentDictionary;
|
||||
|
||||
public bool RemoveProperty(string propertyName) => persistentDictionary.RemoveProperty(propertyName);
|
||||
|
||||
public T GetNonString<T>(T defaultValue, [CallerMemberName] string propertyName = "") => persistentDictionary.GetNonString(propertyName, defaultValue);
|
||||
public object GetObject([CallerMemberName] string propertyName = "") => persistentDictionary.GetObject(propertyName);
|
||||
public string GetString(string defaultValue = null, [CallerMemberName] string propertyName = "") => persistentDictionary.GetString(propertyName, defaultValue);
|
||||
@@ -73,9 +75,18 @@ namespace LibationFileManager
|
||||
|
||||
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
|
||||
|
||||
[Description("Set cover art as the folder's icon. (Windows and macOS only)")]
|
||||
[Description("Set cover art as the folder's icon.")]
|
||||
public bool UseCoverAsFolderIcon { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Save audiobook metadata to metadata.json")]
|
||||
public bool SaveMetadataToFile { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Book display grid size")]
|
||||
public float GridScaleFactor { get => float.Min(2, float.Max(0.5f, GetNonString(defaultValue: 1f))); set => SetNonString(value); }
|
||||
|
||||
[Description("Book display font size")]
|
||||
public float GridFontScaleFactor { get => float.Min(2, float.Max(0.5f, GetNonString(defaultValue: 1f))); set => SetNonString(value); }
|
||||
|
||||
[Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")]
|
||||
public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
@@ -164,6 +175,9 @@ namespace LibationFileManager
|
||||
[Description("Save cover image alongside audiobook?")]
|
||||
public bool DownloadCoverArt { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Combine nested chapter titles")]
|
||||
public bool CombineNestedChapterTitles { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Download clips and bookmarks?")]
|
||||
public bool DownloadClipsBookmarks { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
|
||||
@@ -56,11 +56,11 @@ namespace LibationFileManager
|
||||
?.FirstOrDefault()
|
||||
?.Path;
|
||||
|
||||
private static List<CacheEntry> getEntries(Func<CacheEntry, bool> predicate)
|
||||
private static IEnumerable<CacheEntry> getEntries(Func<CacheEntry, bool> predicate)
|
||||
{
|
||||
var entries = cache.Where(predicate).ToList();
|
||||
if (entries is null || !entries.Any())
|
||||
return null;
|
||||
return Enumerable.Empty<CacheEntry>();
|
||||
|
||||
remove(entries.Where(e => !File.Exists(e.Path)).ToList());
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
using Lucene.Net.Analysis.Tokenattributes;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace LibationSearchEngine
|
||||
{
|
||||
internal static class QuerySanitizer
|
||||
internal static partial class QuerySanitizer
|
||||
{
|
||||
private static readonly HashSet<string> idTerms
|
||||
= SearchEngine.FieldIndexRules.IdFieldNames
|
||||
@@ -23,11 +24,17 @@ namespace LibationSearchEngine
|
||||
.Select(n => n.ToLowerInvariant())
|
||||
.ToHashSet();
|
||||
|
||||
private static readonly Regex tagRegex = TagRegex();
|
||||
|
||||
internal static string Sanitize(string searchString, StandardAnalyzer analyzer)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchString))
|
||||
return SearchEngine.ALL_QUERY;
|
||||
|
||||
//Replace a block tags with tags with proper tag query syntax
|
||||
//eg: [foo] -> tags:foo
|
||||
searchString = tagRegex.Replace(searchString, $"{SearchEngine.TAGS}:$1 ");
|
||||
|
||||
// range operator " TO " and bool operators " AND " and " OR " must be uppercase
|
||||
searchString
|
||||
= searchString
|
||||
@@ -76,11 +83,6 @@ namespace LibationSearchEngine
|
||||
addUnalteredToken(offset);
|
||||
previousIsTags = false;
|
||||
}
|
||||
else if (tryParseBlockTag(offset, partList, searchString, out var tagName))
|
||||
{
|
||||
//The term is a block tag. add it to the part list
|
||||
partList.Add($"{SearchEngine.TAGS}:{tagName}");
|
||||
}
|
||||
else if (double.TryParse(term, out var num))
|
||||
{
|
||||
//Term is a number so pad it with zeros
|
||||
@@ -117,35 +119,7 @@ namespace LibationSearchEngine
|
||||
partList.Add(searchString.Substring(offset.StartOffset, offset.EndOffset - offset.StartOffset));
|
||||
}
|
||||
|
||||
private static bool tryParseBlockTag(IOffsetAttribute offset, List<string> partList, string searchString, out string tagName)
|
||||
{
|
||||
tagName = null;
|
||||
if (partList.Count == 0) return false;
|
||||
|
||||
var previous = partList[^1].TrimEnd();
|
||||
|
||||
//cannot be preceeded by an escaping \
|
||||
if (previous.Length == 0) return false;
|
||||
if (previous[^1] != '[' || (previous.Length > 1 && previous[^2] == '\\')) return false;
|
||||
|
||||
var next = searchString.Substring(offset.EndOffset);
|
||||
if (next.Length == 0 || !next.TrimStart().StartsWith(']')) return false;
|
||||
|
||||
tagName = searchString.Substring(offset.StartOffset, offset.EndOffset - offset.StartOffset);
|
||||
|
||||
//Only legal tag characters are letters, numbers and underscores
|
||||
//Per DataLayer.UserDefinedItem.IllegalCharacterRegex()
|
||||
foreach (var c in tagName)
|
||||
{
|
||||
if (!char.IsLetterOrDigit(c) && c != '_')
|
||||
return false;
|
||||
}
|
||||
|
||||
//Remove the leading '['
|
||||
partList[^1] = previous[..^1];
|
||||
//Ignore the trailing ']'
|
||||
offset.SetOffset(offset.StartOffset, searchString.IndexOf(']', offset.EndOffset) + 1);
|
||||
return true;
|
||||
}
|
||||
[GeneratedRegex(@"(?<!\\)\[\u0020*(\w+)\u0020*\]", RegexOptions.Compiled)]
|
||||
private static partial Regex TagRegex();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -286,8 +286,8 @@
|
||||
//
|
||||
// AboutDialog
|
||||
//
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
ClientSize = new System.Drawing.Size(434, 491);
|
||||
Controls.Add(groupBox1);
|
||||
Controls.Add(getLibationLbl);
|
||||
|
||||
@@ -143,8 +143,8 @@
|
||||
// AccountsDialog
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(933, 519);
|
||||
this.Controls.Add(this.dataGridView1);
|
||||
|
||||
@@ -202,8 +202,8 @@
|
||||
// BookDetailsDialog
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(594, 466);
|
||||
this.Controls.Add(this.audibleLink);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -201,8 +201,8 @@
|
||||
//
|
||||
// BookRecordsDialog
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.ClientSize = new System.Drawing.Size(491, 361);
|
||||
this.Controls.Add(this.reloadAllBtn);
|
||||
this.Controls.Add(this.exportCheckedBtn);
|
||||
|
||||
@@ -29,77 +29,76 @@ namespace LibationWinForms.Dialogs
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.knownDirectoryRb = new System.Windows.Forms.RadioButton();
|
||||
this.customDirectoryRb = new System.Windows.Forms.RadioButton();
|
||||
this.customTb = new System.Windows.Forms.TextBox();
|
||||
this.customBtn = new System.Windows.Forms.Button();
|
||||
this.directorySelectControl = new LibationWinForms.Dialogs.DirectorySelectControl();
|
||||
this.SuspendLayout();
|
||||
knownDirectoryRb = new System.Windows.Forms.RadioButton();
|
||||
customDirectoryRb = new System.Windows.Forms.RadioButton();
|
||||
customTb = new System.Windows.Forms.TextBox();
|
||||
customBtn = new System.Windows.Forms.Button();
|
||||
directorySelectControl = new DirectorySelectControl();
|
||||
SuspendLayout();
|
||||
//
|
||||
// knownDirectoryRb
|
||||
//
|
||||
this.knownDirectoryRb.AutoSize = true;
|
||||
this.knownDirectoryRb.Location = new System.Drawing.Point(3, 3);
|
||||
this.knownDirectoryRb.Name = "knownDirectoryRb";
|
||||
this.knownDirectoryRb.Size = new System.Drawing.Size(14, 13);
|
||||
this.knownDirectoryRb.TabIndex = 0;
|
||||
this.knownDirectoryRb.UseVisualStyleBackColor = true;
|
||||
this.knownDirectoryRb.CheckedChanged += new System.EventHandler(this.radioButton_CheckedChanged);
|
||||
knownDirectoryRb.AutoSize = true;
|
||||
knownDirectoryRb.Location = new System.Drawing.Point(3, 3);
|
||||
knownDirectoryRb.Name = "knownDirectoryRb";
|
||||
knownDirectoryRb.Size = new System.Drawing.Size(14, 13);
|
||||
knownDirectoryRb.TabIndex = 0;
|
||||
knownDirectoryRb.UseVisualStyleBackColor = true;
|
||||
knownDirectoryRb.CheckedChanged += radioButton_CheckedChanged;
|
||||
//
|
||||
// customDirectoryRb
|
||||
//
|
||||
this.customDirectoryRb.AutoSize = true;
|
||||
this.customDirectoryRb.Location = new System.Drawing.Point(2, 62);
|
||||
this.customDirectoryRb.Name = "customDirectoryRb";
|
||||
this.customDirectoryRb.Size = new System.Drawing.Size(14, 13);
|
||||
this.customDirectoryRb.TabIndex = 2;
|
||||
this.customDirectoryRb.UseVisualStyleBackColor = true;
|
||||
this.customDirectoryRb.CheckedChanged += new System.EventHandler(this.radioButton_CheckedChanged);
|
||||
customDirectoryRb.AutoSize = true;
|
||||
customDirectoryRb.Location = new System.Drawing.Point(2, 62);
|
||||
customDirectoryRb.Name = "customDirectoryRb";
|
||||
customDirectoryRb.Size = new System.Drawing.Size(14, 13);
|
||||
customDirectoryRb.TabIndex = 2;
|
||||
customDirectoryRb.UseVisualStyleBackColor = true;
|
||||
customDirectoryRb.CheckedChanged += radioButton_CheckedChanged;
|
||||
//
|
||||
// customTb
|
||||
//
|
||||
this.customTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.customTb.Location = new System.Drawing.Point(22, 58);
|
||||
this.customTb.Name = "customTb";
|
||||
this.customTb.Size = new System.Drawing.Size(588, 23);
|
||||
this.customTb.TabIndex = 3;
|
||||
customTb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
customTb.Location = new System.Drawing.Point(22, 58);
|
||||
customTb.Name = "customTb";
|
||||
customTb.Size = new System.Drawing.Size(588, 23);
|
||||
customTb.TabIndex = 3;
|
||||
//
|
||||
// customBtn
|
||||
//
|
||||
this.customBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.customBtn.Location = new System.Drawing.Point(616, 58);
|
||||
this.customBtn.Name = "customBtn";
|
||||
this.customBtn.Size = new System.Drawing.Size(41, 27);
|
||||
this.customBtn.TabIndex = 4;
|
||||
this.customBtn.Text = "...";
|
||||
this.customBtn.UseVisualStyleBackColor = true;
|
||||
this.customBtn.Click += new System.EventHandler(this.customBtn_Click);
|
||||
customBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
customBtn.Location = new System.Drawing.Point(616, 58);
|
||||
customBtn.Name = "customBtn";
|
||||
customBtn.Size = new System.Drawing.Size(41, 27);
|
||||
customBtn.TabIndex = 4;
|
||||
customBtn.Text = "...";
|
||||
customBtn.UseVisualStyleBackColor = true;
|
||||
customBtn.Click += customBtn_Click;
|
||||
//
|
||||
// directorySelectControl
|
||||
//
|
||||
this.directorySelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.directorySelectControl.Location = new System.Drawing.Point(23, 0);
|
||||
this.directorySelectControl.Name = "directorySelectControl";
|
||||
this.directorySelectControl.Size = new System.Drawing.Size(635, 52);
|
||||
this.directorySelectControl.TabIndex = 5;
|
||||
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(23, 0);
|
||||
directorySelectControl.Margin = new System.Windows.Forms.Padding(6, 6, 6, 6);
|
||||
directorySelectControl.Name = "directorySelectControl";
|
||||
directorySelectControl.Size = new System.Drawing.Size(635, 55);
|
||||
directorySelectControl.TabIndex = 5;
|
||||
//
|
||||
// DirectoryOrCustomSelectControl
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.Controls.Add(this.directorySelectControl);
|
||||
this.Controls.Add(this.customBtn);
|
||||
this.Controls.Add(this.customTb);
|
||||
this.Controls.Add(this.customDirectoryRb);
|
||||
this.Controls.Add(this.knownDirectoryRb);
|
||||
this.Name = "DirectoryOrCustomSelectControl";
|
||||
this.Size = new System.Drawing.Size(660, 87);
|
||||
this.Load += new System.EventHandler(this.DirectoryOrCustomSelectControl_Load);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
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);
|
||||
Name = "DirectoryOrCustomSelectControl";
|
||||
Size = new System.Drawing.Size(660, 88);
|
||||
Load += DirectoryOrCustomSelectControl_Load;
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -37,6 +37,12 @@ namespace LibationWinForms.Dialogs
|
||||
if (directory != Configuration.KnownDirectories.None)
|
||||
selectDir(directory, null);
|
||||
}
|
||||
protected override void OnResize(EventArgs e)
|
||||
{
|
||||
base.OnResize(e);
|
||||
//For some reason anchors don't work when the parent form scales up, even with AutoScale
|
||||
directorySelectControl.Width = customTb.Width = Width;
|
||||
}
|
||||
|
||||
/// <summary>set selection</summary>
|
||||
public void SelectDirectory(string directory)
|
||||
|
||||
@@ -1,5 +1,64 @@
|
||||
<?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">
|
||||
|
||||
@@ -29,44 +29,42 @@ namespace LibationWinForms.Dialogs
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.directoryComboBox = new System.Windows.Forms.ComboBox();
|
||||
this.textBox1 = new System.Windows.Forms.TextBox();
|
||||
this.SuspendLayout();
|
||||
directoryComboBox = new System.Windows.Forms.ComboBox();
|
||||
textBox1 = new System.Windows.Forms.TextBox();
|
||||
SuspendLayout();
|
||||
//
|
||||
// directoryComboBox
|
||||
//
|
||||
this.directoryComboBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.directoryComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.directoryComboBox.FormattingEnabled = true;
|
||||
this.directoryComboBox.Location = new System.Drawing.Point(0, 0);
|
||||
this.directoryComboBox.Name = "directoryComboBox";
|
||||
this.directoryComboBox.Size = new System.Drawing.Size(407, 23);
|
||||
this.directoryComboBox.TabIndex = 0;
|
||||
this.directoryComboBox.SelectedIndexChanged += new System.EventHandler(this.directoryComboBox_SelectedIndexChanged);
|
||||
directoryComboBox.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
directoryComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
directoryComboBox.FormattingEnabled = true;
|
||||
directoryComboBox.Location = new System.Drawing.Point(0, 0);
|
||||
directoryComboBox.Name = "directoryComboBox";
|
||||
directoryComboBox.Size = new System.Drawing.Size(814, 23);
|
||||
directoryComboBox.TabIndex = 0;
|
||||
directoryComboBox.SelectedIndexChanged += directoryComboBox_SelectedIndexChanged;
|
||||
//
|
||||
// textBox1
|
||||
//
|
||||
this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.textBox1.Location = new System.Drawing.Point(0, 29);
|
||||
this.textBox1.Name = "textBox1";
|
||||
this.textBox1.ReadOnly = true;
|
||||
this.textBox1.Size = new System.Drawing.Size(407, 23);
|
||||
this.textBox1.TabIndex = 1;
|
||||
textBox1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
textBox1.Location = new System.Drawing.Point(0, 29);
|
||||
textBox1.Name = "textBox1";
|
||||
textBox1.ReadOnly = true;
|
||||
textBox1.Size = new System.Drawing.Size(814, 23);
|
||||
textBox1.TabIndex = 1;
|
||||
//
|
||||
// DirectorySelectControl
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.Controls.Add(this.textBox1);
|
||||
this.Controls.Add(this.directoryComboBox);
|
||||
this.Name = "DirectorySelectControl";
|
||||
this.Size = new System.Drawing.Size(407, 52);
|
||||
this.Load += new System.EventHandler(this.DirectorySelectControl_Load);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
AutoSize = true;
|
||||
Controls.Add(textBox1);
|
||||
Controls.Add(directoryComboBox);
|
||||
Name = "DirectorySelectControl";
|
||||
Size = new System.Drawing.Size(814, 55);
|
||||
Load += DirectorySelectControl_Load;
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,4 +1,64 @@
|
||||
<root>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing"">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
|
||||
@@ -126,8 +126,8 @@
|
||||
// EditQuickFilters
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(800, 450);
|
||||
this.Controls.Add(this.dataGridView1);
|
||||
|
||||
@@ -144,8 +144,8 @@
|
||||
//
|
||||
// EditReplacementChars
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.ClientSize = new System.Drawing.Size(522, 467);
|
||||
this.Controls.Add(this.minDefaultBtn);
|
||||
this.Controls.Add(this.loFiDefaultsBtn);
|
||||
|
||||
@@ -160,8 +160,8 @@
|
||||
// EditTemplateDialog
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(933, 388);
|
||||
this.Controls.Add(this.exampleLbl);
|
||||
|
||||
@@ -28,74 +28,74 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.libationFilesDescLbl = new System.Windows.Forms.Label();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.libationFilesSelectControl = new LibationWinForms.Dialogs.DirectoryOrCustomSelectControl();
|
||||
this.SuspendLayout();
|
||||
libationFilesDescLbl = new System.Windows.Forms.Label();
|
||||
cancelBtn = new System.Windows.Forms.Button();
|
||||
saveBtn = new System.Windows.Forms.Button();
|
||||
libationFilesSelectControl = new DirectoryOrCustomSelectControl();
|
||||
SuspendLayout();
|
||||
//
|
||||
// libationFilesDescLbl
|
||||
//
|
||||
this.libationFilesDescLbl.AutoSize = true;
|
||||
this.libationFilesDescLbl.Location = new System.Drawing.Point(14, 10);
|
||||
this.libationFilesDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.libationFilesDescLbl.Name = "libationFilesDescLbl";
|
||||
this.libationFilesDescLbl.Size = new System.Drawing.Size(39, 15);
|
||||
this.libationFilesDescLbl.TabIndex = 0;
|
||||
this.libationFilesDescLbl.Text = "[desc]";
|
||||
libationFilesDescLbl.AutoSize = true;
|
||||
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(39, 15);
|
||||
libationFilesDescLbl.TabIndex = 0;
|
||||
libationFilesDescLbl.Text = "[desc]";
|
||||
//
|
||||
// cancelBtn
|
||||
//
|
||||
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.cancelBtn.Location = new System.Drawing.Point(832, 118);
|
||||
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.cancelBtn.Name = "cancelBtn";
|
||||
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.cancelBtn.TabIndex = 3;
|
||||
this.cancelBtn.Text = "Cancel";
|
||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
||||
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(832, 118);
|
||||
cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
cancelBtn.Name = "cancelBtn";
|
||||
cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
cancelBtn.TabIndex = 3;
|
||||
cancelBtn.Text = "Cancel";
|
||||
cancelBtn.UseVisualStyleBackColor = true;
|
||||
cancelBtn.Click += cancelBtn_Click;
|
||||
//
|
||||
// saveBtn
|
||||
//
|
||||
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.saveBtn.Location = new System.Drawing.Point(714, 118);
|
||||
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.saveBtn.Name = "saveBtn";
|
||||
this.saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.saveBtn.TabIndex = 2;
|
||||
this.saveBtn.Text = "Save";
|
||||
this.saveBtn.UseVisualStyleBackColor = true;
|
||||
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
||||
saveBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
saveBtn.Location = new System.Drawing.Point(714, 118);
|
||||
saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
saveBtn.Name = "saveBtn";
|
||||
saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
saveBtn.TabIndex = 2;
|
||||
saveBtn.Text = "Save";
|
||||
saveBtn.UseVisualStyleBackColor = true;
|
||||
saveBtn.Click += saveBtn_Click;
|
||||
//
|
||||
// libationFilesSelectControl
|
||||
//
|
||||
this.libationFilesSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.libationFilesSelectControl.Location = new System.Drawing.Point(14, 28);
|
||||
this.libationFilesSelectControl.Name = "libationFilesSelectControl";
|
||||
this.libationFilesSelectControl.Size = new System.Drawing.Size(909, 87);
|
||||
this.libationFilesSelectControl.TabIndex = 1;
|
||||
libationFilesSelectControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
libationFilesSelectControl.Location = new System.Drawing.Point(14, 28);
|
||||
libationFilesSelectControl.Margin = new System.Windows.Forms.Padding(6, 6, 6, 6);
|
||||
libationFilesSelectControl.Name = "libationFilesSelectControl";
|
||||
libationFilesSelectControl.Size = new System.Drawing.Size(906, 88);
|
||||
libationFilesSelectControl.TabIndex = 1;
|
||||
//
|
||||
// LibationFilesDialog
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(933, 164);
|
||||
this.Controls.Add(this.libationFilesSelectControl);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
this.Controls.Add(this.libationFilesDescLbl);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "LibationFilesDialog";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Libation Files location";
|
||||
this.Load += new System.EventHandler(this.LibationFilesDialog_Load);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
AutoSize = true;
|
||||
ClientSize = new System.Drawing.Size(933, 164);
|
||||
Controls.Add(libationFilesSelectControl);
|
||||
Controls.Add(cancelBtn);
|
||||
Controls.Add(saveBtn);
|
||||
Controls.Add(libationFilesDescLbl);
|
||||
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
|
||||
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
Name = "LibationFilesDialog";
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "Libation Files location";
|
||||
Load += LibationFilesDialog_Load;
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,5 +1,64 @@
|
||||
<?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">
|
||||
|
||||
@@ -78,8 +78,8 @@
|
||||
// LiberatedStatusBatchAutoDialog
|
||||
//
|
||||
this.AcceptButton = this.okBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(564, 118);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
|
||||
@@ -88,8 +88,8 @@
|
||||
// LiberatedStatusBatchManualDialog
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(564, 118);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
|
||||
@@ -82,8 +82,8 @@
|
||||
//
|
||||
// LocateAudiobooksDialog
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.ClientSize = new System.Drawing.Size(345, 306);
|
||||
this.Controls.Add(this.booksFoundLbl);
|
||||
this.Controls.Add(this.foundAudiobooksLV);
|
||||
|
||||
@@ -58,8 +58,8 @@
|
||||
// ApprovalNeededDialog
|
||||
//
|
||||
this.AcceptButton = this.approvedBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.ClientSize = new System.Drawing.Size(345, 115);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.approvedBtn);
|
||||
|
||||
@@ -97,8 +97,8 @@
|
||||
// CaptchaDialog
|
||||
//
|
||||
AcceptButton = submitBtn;
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
ClientSize = new System.Drawing.Size(261, 210);
|
||||
Controls.Add(passwordTb);
|
||||
Controls.Add(label1);
|
||||
|
||||
@@ -89,8 +89,8 @@
|
||||
// LoginCallbackDialog
|
||||
//
|
||||
this.AcceptButton = this.submitBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.ClientSize = new System.Drawing.Size(330, 114);
|
||||
this.Controls.Add(this.usernameLbl);
|
||||
this.Controls.Add(this.localeLbl);
|
||||
|
||||
@@ -121,8 +121,8 @@
|
||||
// LoginChoiceEagerDialog
|
||||
//
|
||||
AcceptButton = submitBtn;
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
ClientSize = new System.Drawing.Size(394, 216);
|
||||
Controls.Add(externalLoginLbl2);
|
||||
Controls.Add(externalLoginLbl1);
|
||||
|
||||
@@ -151,8 +151,8 @@
|
||||
// LoginExternalDialog
|
||||
//
|
||||
this.AcceptButton = this.submitBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.ClientSize = new System.Drawing.Size(766, 498);
|
||||
this.Controls.Add(this.tldrLbl);
|
||||
this.Controls.Add(this.responseUrlTb);
|
||||
|
||||
@@ -84,8 +84,8 @@ namespace LibationWinForms.Dialogs.Login
|
||||
// MfaDialog
|
||||
//
|
||||
this.AcceptButton = this.submitBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.ClientSize = new System.Drawing.Size(398, 129);
|
||||
this.Controls.Add(this.radioButton3);
|
||||
this.Controls.Add(this.radioButton2);
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
//
|
||||
// WebLoginDialog
|
||||
//
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
ClientSize = new System.Drawing.Size(484, 761);
|
||||
Name = "WebLoginDialog";
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
// _2faCodeDialog
|
||||
//
|
||||
AcceptButton = submitBtn;
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
ClientSize = new System.Drawing.Size(222, 147);
|
||||
Controls.Add(promptLbl);
|
||||
Controls.Add(label1);
|
||||
|
||||
@@ -118,8 +118,8 @@ namespace LibationWinForms.Dialogs
|
||||
// MessageBoxAlertAdminDialog
|
||||
//
|
||||
this.AcceptButton = this.okBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.ClientSize = new System.Drawing.Size(584, 382);
|
||||
this.Controls.Add(this.exceptionTb);
|
||||
this.Controls.Add(this.logsLink);
|
||||
|
||||
@@ -93,8 +93,8 @@
|
||||
// ScanAccountsDialog
|
||||
//
|
||||
this.AcceptButton = this.importBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(584, 160);
|
||||
this.Controls.Add(this.editBtn);
|
||||
|
||||
@@ -103,8 +103,8 @@
|
||||
// SearchSyntaxDialog
|
||||
//
|
||||
AcceptButton = closeBtn;
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
CancelButton = closeBtn;
|
||||
ClientSize = new System.Drawing.Size(1140, 577);
|
||||
Controls.Add(closeBtn);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user